From 417fc6f6733f1759f39f8d76f8cdf682e213c46a Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 19 Nov 2024 11:29:21 +0700 Subject: [PATCH 01/12] add axios mock adapter --- packages/js-sdk/package.json | 1 + yarn.lock | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 785cbc41d..7258d8caf 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -27,6 +27,7 @@ "@babel/preset-env": "^7.19.4", "@babel/preset-typescript": "^7.18.6", "@types/supertest": "^2.0.12", + "axios-mock-adapter": "^2.1.0", "babel-cli": "^6.26.0", "babel-jest": "^29.2.2", "babel-preset-env": "^1.7.0", diff --git a/yarn.lock b/yarn.lock index 618697925..984a9c589 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2229,6 +2229,7 @@ __metadata: "@types/supertest": ^2.0.12 "@web3-name-sdk/core": ^0.2.0 axios: ^0.27.2 + axios-mock-adapter: ^2.1.0 babel-cli: ^6.26.0 babel-jest: ^29.2.2 babel-preset-env: ^1.7.0 @@ -10530,6 +10531,18 @@ __metadata: languageName: node linkType: hard +"axios-mock-adapter@npm:^2.1.0": + version: 2.1.0 + resolution: "axios-mock-adapter@npm:2.1.0" + dependencies: + fast-deep-equal: ^3.1.3 + is-buffer: ^2.0.5 + peerDependencies: + axios: ">= 0.17.0" + checksum: bdebba1adc7c812734b8a2b2c5e82619f4c84ce0a96712dcdd6881266495ef40e9d68d939b6a847093dbdd1bd88c3285d6a49bdfe8e7e7aa75f0878b24e4b6ad + languageName: node + linkType: hard + "axios@npm:^0.25.0": version: 0.25.0 resolution: "axios@npm:0.25.0" From b96727f1dc58685f89db26d953bd0c99ffa6e8b2 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 19 Nov 2024 11:29:52 +0700 Subject: [PATCH 02/12] add connector agnostic login --- packages/js-sdk/src/Dm3Sdk.ts | 44 +++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/js-sdk/src/Dm3Sdk.ts b/packages/js-sdk/src/Dm3Sdk.ts index 62f2fa226..ab04c1c15 100644 --- a/packages/js-sdk/src/Dm3Sdk.ts +++ b/packages/js-sdk/src/Dm3Sdk.ts @@ -78,29 +78,27 @@ export class Dm3Sdk { this.addressEnsSubdomain = config.addressEnsSubdomain; this.userEnsSubdomain = config.userEnsSubdomain; this.resolverBackendUrl = config.resolverBackendUrl; - //this.backendUrl = config.backendUrl; - this.lukso = config.lukso; + this.backendUrl = config.backendUrl; this.storageApi = config.storageApi; } - - public async universalProfileLogin() { - if (!this.lukso) { - throw new Error('Lukso provider not found'); - } + /** + * login can be used to login with a profile regardles the connector. Its also great for testing + */ + public async login({ + profileKeys, + profile, + accountAddress, + }: { + profileKeys: ProfileKeys; + profile: SignedUserProfile; + accountAddress: string; + }) { const tld = new Tld( this.mainnetProvider, this.addressEnsSubdomain, this.userEnsSubdomain, this.resolverBackendUrl, ); - const lc = await LuksoConnector._instance( - this.lukso, - this.nonce, - this.defaultDeliveryService, - ); - const loginResult = await lc.login(); - - const { profileKeys, profile, accountAddress } = loginResult as Success; this.profileKeys = profileKeys; this.profile = profile; @@ -142,6 +140,22 @@ export class Dm3Sdk { return new Dm3(conversations, tld); } + //TODO use type of injected lukso provider + public async universalProfileLogin(lukso: any) { + if (!lukso) { + throw new Error('Lukso provider not found'); + } + const lc = await LuksoConnector._instance( + lukso, + this.nonce, + this.defaultDeliveryService, + ); + const loginResult = await lc.login(); + + const { profileKeys, profile, accountAddress } = loginResult as Success; + return await this.login({ profileKeys, profile, accountAddress }); + } + private async initializeBackendConnector( accountAddress: string, profileKeys: ProfileKeys, From 7ef92a35fa288849fe8475105d24feac0c746842 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 19 Nov 2024 11:30:29 +0700 Subject: [PATCH 03/12] use mock adapter to mock BE login flow --- packages/js-sdk/src/Dm3Sdk.test.ts | 76 +++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index 8bbb75c05..a7fc9d892 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -1,34 +1,78 @@ +import { StorageAPI } from '@dm3-org/dm3-lib-storage'; +import { + MockedUserProfile, + mockUserProfile, +} from '@dm3-org/dm3-lib-test-helper'; +import axios from 'axios'; import { ethers } from 'ethers'; import { Dm3Sdk, Dm3SdkConfig } from './Dm3Sdk'; -import { StorageAPI } from '@dm3-org/dm3-lib-storage'; + +import MockAdapter from 'axios-mock-adapter'; +import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; describe('Dm3Sdk', () => { - let upController: ethers.Signer; + let alice: MockedUserProfile; + + //Axios mock to mock the http requests + let axiosMock; beforeEach(async () => { - upController = ethers.Wallet.createRandom(); + alice = await mockUserProfile( + ethers.Wallet.createRandom(), + 'alice.up', + ['test.io'], + ); }); it('test', async () => { - const luksoProvider = () => ({ - send: () => Promise.resolve([]), - getSigner: () => Promise.resolve(upController), - }); + axiosMock = new MockAdapter(axios); + //Mock BackendConnector HttpRequests + //Mock profileExistsOnDeliveryService + axiosMock + .onGet( + `http://localhost:4060/profile/${normalizeEnsName( + alice.address, + )}.addr.test`, + ) + .reply(200); + + axiosMock + .onGet( + `http://localhost:4060/auth/${normalizeEnsName( + alice.address, + )}.addr.test`, + ) + .reply(200, 'mock-challenge'); + + axiosMock + .onPost( + `http://localhost:4060/auth/${normalizeEnsName( + alice.address, + )}.addr.test`, + ) + .reply(200, 'mock-challenge'); + const mockConfig: Dm3SdkConfig = { mainnetProvider: {} as ethers.providers.JsonRpcProvider, - lukso: luksoProvider as any, + storageApi: {} as StorageAPI, nonce: '1', defaultDeliveryService: 'test.io', - addressEnsSubdomain: 'addr.test', - userEnsSubdomain: 'user.test', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', resolverBackendUrl: 'resolver.io', - backendUrl: 'backend.io', - storageApi: {} as StorageAPI, + backendUrl: 'http://localhost:4060', }; - // const dm3 = await new Dm3Sdk().universalProfileLogin(); - // await dm3.conversations.addConversation('karl.eth'); - // const c = dm3.conversations.list; - // const karl = c[0]; + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + console.log(dm3.conversations.conversations); + + /* await dm3.conversations.addConversation('karl.eth'); + const c = dm3.conversations.conversations; + const karl = c[0]; */ }); }); From dc84d86429a9fc06d2a0743ab3767e5c1ad6fc0c Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 19 Nov 2024 14:35:46 +0700 Subject: [PATCH 04/12] add test for Conversation class --- packages/js-sdk/src/Dm3.ts | 6 ++-- packages/js-sdk/src/Dm3Sdk.test.ts | 28 ++++++++++++----- packages/js-sdk/src/Dm3Sdk.ts | 23 +++++++++----- .../js-sdk/src/conversation/Conversations.ts | 31 ++++++++++++------- .../conversation/hydrate/fetchDsProfiles.ts | 4 +-- packages/js-sdk/src/message/Messages.ts | 4 +-- packages/js-sdk/src/tld/Tld.ts | 13 +++++++- 7 files changed, 75 insertions(+), 34 deletions(-) diff --git a/packages/js-sdk/src/Dm3.ts b/packages/js-sdk/src/Dm3.ts index 2e630e9b3..380f65eec 100644 --- a/packages/js-sdk/src/Dm3.ts +++ b/packages/js-sdk/src/Dm3.ts @@ -1,11 +1,11 @@ import { Conversations } from './conversation/Conversations'; -import { Tld } from './tld/Tld'; +import { ITLDResolver } from './tld/nameService/ITLDResolver'; export class Dm3 { public readonly conversations: Conversations; - public readonly tld: Tld; + public readonly tld: ITLDResolver; - constructor(conversations: Conversations, tld: Tld) { + constructor(conversations: Conversations, tld: ITLDResolver) { this.conversations = conversations; this.tld = tld; } diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index a7fc9d892..432dfc418 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -9,9 +9,11 @@ import { Dm3Sdk, Dm3SdkConfig } from './Dm3Sdk'; import MockAdapter from 'axios-mock-adapter'; import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; +import { ITLDResolver } from './tld/nameService/ITLDResolver'; describe('Dm3Sdk', () => { let alice: MockedUserProfile; + let bob: MockedUserProfile; //Axios mock to mock the http requests let axiosMock; @@ -22,9 +24,12 @@ describe('Dm3Sdk', () => { 'alice.up', ['test.io'], ); + bob = await mockUserProfile(ethers.Wallet.createRandom(), 'bob.up', [ + 'test.io', + ]); }); - it('test', async () => { + it('can add a conversaton to the contact list', async () => { axiosMock = new MockAdapter(axios); //Mock BackendConnector HttpRequests //Mock profileExistsOnDeliveryService @@ -52,15 +57,24 @@ describe('Dm3Sdk', () => { ) .reply(200, 'mock-challenge'); + const mockTldResolver = { + resolveTLDtoAlias: async () => + `${normalizeEnsName(bob.address)}.addr.test`, + resolveAliasToTLD: async () => 'bob.eth', + } as unknown as ITLDResolver; + const mockConfig: Dm3SdkConfig = { mainnetProvider: {} as ethers.providers.JsonRpcProvider, - storageApi: {} as StorageAPI, + storageApi: { + addConversation: async () => {}, + } as unknown as StorageAPI, nonce: '1', defaultDeliveryService: 'test.io', addressEnsSubdomain: '.addr.test', userEnsSubdomain: '.user.test', resolverBackendUrl: 'resolver.io', backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, }; const dm3 = await new Dm3Sdk(mockConfig).login({ @@ -69,10 +83,10 @@ describe('Dm3Sdk', () => { accountAddress: alice.address, }); - console.log(dm3.conversations.conversations); - - /* await dm3.conversations.addConversation('karl.eth'); - const c = dm3.conversations.conversations; - const karl = c[0]; */ + await dm3.conversations.addConversation('bob.eth'); + const c = dm3.conversations.list; + console.log(c); + expect(c.length).toBe(1); + expect(c[0].contact.name).toBe('bob.eth'); }); }); diff --git a/packages/js-sdk/src/Dm3Sdk.ts b/packages/js-sdk/src/Dm3Sdk.ts index ab04c1c15..3ce98b8a2 100644 --- a/packages/js-sdk/src/Dm3Sdk.ts +++ b/packages/js-sdk/src/Dm3Sdk.ts @@ -13,6 +13,7 @@ import { StorageAPI } from '@dm3-org/dm3-lib-storage'; import { ethers } from 'ethers'; import { Tld } from './tld/Tld'; import { Dm3 } from './Dm3'; +import { ITLDResolver } from './tld/nameService/ITLDResolver'; /** * DM3SDK @@ -36,7 +37,7 @@ export interface Dm3SdkConfig { userEnsSubdomain: string; resolverBackendUrl: string; backendUrl: string; - lukso?: ethers.providers.ExternalProvider; + _tld?: ITLDResolver; } export class Dm3Sdk { @@ -69,6 +70,11 @@ export class Dm3Sdk { */ public conversations: Conversations; + /** + * DM3 TLD + */ + private _tld?: ITLDResolver; + constructor(config: Dm3SdkConfig) { //TODO keep ethers v5 for know but extract into common interface later this.mainnetProvider = config.mainnetProvider; @@ -80,6 +86,7 @@ export class Dm3Sdk { this.resolverBackendUrl = config.resolverBackendUrl; this.backendUrl = config.backendUrl; this.storageApi = config.storageApi; + this._tld = config._tld; } /** * login can be used to login with a profile regardles the connector. Its also great for testing @@ -93,12 +100,14 @@ export class Dm3Sdk { profile: SignedUserProfile; accountAddress: string; }) { - const tld = new Tld( - this.mainnetProvider, - this.addressEnsSubdomain, - this.userEnsSubdomain, - this.resolverBackendUrl, - ); + const tld = + this._tld ?? + new Tld( + this.mainnetProvider, + this.addressEnsSubdomain, + this.userEnsSubdomain, + this.resolverBackendUrl, + ); this.profileKeys = profileKeys; this.profile = profile; diff --git a/packages/js-sdk/src/conversation/Conversations.ts b/packages/js-sdk/src/conversation/Conversations.ts index 5d232da93..1b1085ebb 100644 --- a/packages/js-sdk/src/conversation/Conversations.ts +++ b/packages/js-sdk/src/conversation/Conversations.ts @@ -13,19 +13,20 @@ import { Contact, Conversation, getEmptyContact } from './types'; import { Tld } from '../tld/Tld'; import { hydrateContract as hydrateContact } from './hydrate/hydrateContact'; import { ethers } from 'ethers'; +import { ITLDResolver } from '../tld/nameService/ITLDResolver'; export class Conversations { private readonly provider: ethers.providers.JsonRpcProvider; private readonly storageApi: StorageAPI; - private readonly tld: Tld; + private readonly tld: ITLDResolver; private readonly addressEnsSubdomain: string; private readonly account: Account; - public conversations: Conversation[]; + public list: Conversation[]; constructor( storageApi: StorageAPI, - tld: Tld, + tld: ITLDResolver, mainnetProvider: ethers.providers.JsonRpcProvider, account: Account, addressEnsSubdomain: string, @@ -35,7 +36,7 @@ export class Conversations { this.account = account; this.provider = mainnetProvider; this.addressEnsSubdomain = addressEnsSubdomain; - this.conversations = []; + this.list = []; } public async addConversation(_ensName: string) { @@ -49,6 +50,7 @@ export class Conversations { updatedAt: new Date().getTime(), previewMessage: undefined, }; + const conversationPreview = this._addConversation(newConversation); //Add the contact to the storage in the background this.storageApi.addConversation(aliasName, [contactTldName]); @@ -64,7 +66,7 @@ export class Conversations { if (isOwnContact) { return; } - const alreadyAddedContact = this.conversations.find( + const alreadyAddedContact = this.list.find( (existingContact) => existingContact.contact.account.ensName === ensName, ); @@ -101,7 +103,7 @@ export class Conversations { const hydratedContact = await hydrateContact( this.provider, conversation, - this.tld.resolveTLDtoAlias, + this.tld.resolveAliasToTLD, this.addressEnsSubdomain, ); @@ -109,8 +111,13 @@ export class Conversations { messages: undefined as any, contact: hydratedContact, }; - this.conversations.push(hydratedConversation); - + //find existing contact and replace it with the hydrated one + this.list = this.list.map((existingContact) => { + if (existingContact.contact.account.ensName === ensName) { + return hydratedConversation; + } + return existingContact; + }); //Return the new onhydrated contact return hydratedConversation; } @@ -126,14 +133,14 @@ export class Conversations { const hydratedContact = await hydrateContact( this.provider, conversation, - this.tld.resolveTLDtoAlias, + this.tld.resolveAliasToTLD, this.addressEnsSubdomain, ); const hydratedConversation: Conversation = { messages: undefined as any, contact: hydratedContact, }; - this.conversations.push(hydratedConversation); + this.list.push(hydratedConversation); return hydratedConversation; }; @@ -142,12 +149,12 @@ export class Conversations { //Dont add duplicates const uniqueContacts = newConversations.filter( (newContact) => - !this.conversations.some( + !this.list.some( (existingContact) => existingContact.contact.account.ensName === newContact.contact.account.ensName, ), ); - this.conversations = [...this.conversations, ...uniqueContacts]; + this.list = [...this.list, ...uniqueContacts]; } } diff --git a/packages/js-sdk/src/conversation/hydrate/fetchDsProfiles.ts b/packages/js-sdk/src/conversation/hydrate/fetchDsProfiles.ts index 852285f63..8540a57e6 100644 --- a/packages/js-sdk/src/conversation/hydrate/fetchDsProfiles.ts +++ b/packages/js-sdk/src/conversation/hydrate/fetchDsProfiles.ts @@ -18,9 +18,9 @@ export const fetchDsProfiles = async ( ): Promise => { const deliveryServiceEnsNames = account.profile?.deliveryServices ?? []; if (deliveryServiceEnsNames.length === 0) { - //If there is nop DS profile the message will be storaged at the client side until they recipient has createed an account + //If there is no DS profile the message will be stored at the client side until the recipient has created an account console.debug( - '[fetchDeliverServicePorfile] Cant resolve deliveryServiceEnsName', + '[fetchDeliveryServiceProfile] account has no delivery-service profile', ); return { account, diff --git a/packages/js-sdk/src/message/Messages.ts b/packages/js-sdk/src/message/Messages.ts index 8ea3a018d..5eebdaff0 100644 --- a/packages/js-sdk/src/message/Messages.ts +++ b/packages/js-sdk/src/message/Messages.ts @@ -77,7 +77,7 @@ export class Messages { } //Find the recipient of the message in the contact list - const recipient = this.conversations.conversations.find( + const recipient = this.conversations.list.find( (c) => c.contact.account.ensName === contact, ); /** @@ -99,7 +99,7 @@ export class Messages { //There are cases were a messages is already to be send even though the contract hydration is not finished yet. //This happens if a message has been picked up from the delivery service and the clients sends READ_RECEIVE or READ_OPENED acknowledgements //In that case we've to check again to the if the user is a DM3 user, before we decide to keep the message - const potentialReceiver = this.conversations.conversations.find( + const potentialReceiver = this.conversations.list.find( (c) => c.contact.account.ensName === contact, ); diff --git a/packages/js-sdk/src/tld/Tld.ts b/packages/js-sdk/src/tld/Tld.ts index d4fa1b70d..23097b4bb 100644 --- a/packages/js-sdk/src/tld/Tld.ts +++ b/packages/js-sdk/src/tld/Tld.ts @@ -27,7 +27,7 @@ const SUPPORTED_NAMESERVICES = ( new EthAddressResolver(addressEnsSubdomain), ]; -export class Tld { +export class Tld implements ITLDResolver { private aliasTldCache: { [ensName: string]: string }; private tldAliasCache: { [ensName: string]: string }; private readonly mainnetProvider: ethers.providers.JsonRpcProvider; @@ -48,6 +48,17 @@ export class Tld { this.userEnsSubdomain = userEnsSubdomain; this.resolverBackendUrl = resolverBackendUrl; } + isResolverForTldName(ensName: string): Promise { + //Since its the root resolver its always capable of resolving + return Promise.resolve(true); + } + isResolverForAliasName( + ensName: string, + foreignTldName?: string, + ): Promise { + //Since its the root resolver its always capable of resolving + return Promise.resolve(true); + } //e.g. 0x1234.gnosis.eth -> 0x1234.gno resolveAliasToTLD = async (ensName: string, foreignTldName?: string) => { if (this.aliasTldCache[ensName]) { From a147f288f34d45f9c81a129758e7ffe4bb9ad4c3 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 19 Nov 2024 15:27:23 +0700 Subject: [PATCH 05/12] add more conversation test --- packages/js-sdk/src/Dm3Sdk.test.ts | 103 +++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index 432dfc418..f8ebd0ca3 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -14,6 +14,7 @@ import { ITLDResolver } from './tld/nameService/ITLDResolver'; describe('Dm3Sdk', () => { let alice: MockedUserProfile; let bob: MockedUserProfile; + let karl: MockedUserProfile; //Axios mock to mock the http requests let axiosMock; @@ -27,10 +28,13 @@ describe('Dm3Sdk', () => { bob = await mockUserProfile(ethers.Wallet.createRandom(), 'bob.up', [ 'test.io', ]); - }); - it('can add a conversaton to the contact list', async () => { + karl = await mockUserProfile(ethers.Wallet.createRandom(), 'karl.up', [ + 'test.io', + ]); + axiosMock = new MockAdapter(axios); + //Mock BackendConnector HttpRequests //Mock profileExistsOnDeliveryService axiosMock @@ -40,7 +44,7 @@ describe('Dm3Sdk', () => { )}.addr.test`, ) .reply(200); - + //Mock getChallenge axiosMock .onGet( `http://localhost:4060/auth/${normalizeEnsName( @@ -49,14 +53,17 @@ describe('Dm3Sdk', () => { ) .reply(200, 'mock-challenge'); + //Mock getToken axiosMock .onPost( `http://localhost:4060/auth/${normalizeEnsName( alice.address, )}.addr.test`, ) - .reply(200, 'mock-challenge'); + .reply(200); + }); + it('can add a conversation to the contact list', async () => { const mockTldResolver = { resolveTLDtoAlias: async () => `${normalizeEnsName(bob.address)}.addr.test`, @@ -85,7 +92,95 @@ describe('Dm3Sdk', () => { await dm3.conversations.addConversation('bob.eth'); const c = dm3.conversations.list; + expect(c.length).toBe(1); + expect(c[0].contact.name).toBe('bob.eth'); + }); + it('can multiple conversations to the contact list', async () => { + const mockTldResolver = { + resolveTLDtoAlias: async (ensName: string) => { + if (ensName === 'alice.eth') { + return `${normalizeEnsName(alice.address)}.addr.test`; + } + if (ensName === 'bob.eth') { + return `${normalizeEnsName(bob.address)}.addr.test`; + } + return `${normalizeEnsName(karl.address)}.addr.test`; + }, + resolveAliasToTLD: async (ensName: string) => { + if ( + normalizeEnsName(ensName) === + normalizeEnsName(alice.address) + '.addr.test' + ) { + return 'alice.eth'; + } + if ( + normalizeEnsName(ensName) === + normalizeEnsName(bob.address) + '.addr.test' + ) { + return 'bob.eth'; + } + return 'karl.eth'; + }, + } as unknown as ITLDResolver; + + const mockConfig: Dm3SdkConfig = { + mainnetProvider: {} as ethers.providers.JsonRpcProvider, + storageApi: { + addConversation: async () => {}, + } as unknown as StorageAPI, + nonce: '1', + defaultDeliveryService: 'test.io', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', + resolverBackendUrl: 'resolver.io', + backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, + }; + + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + await dm3.conversations.addConversation('bob.eth'); + await dm3.conversations.addConversation('karl.eth'); + const c = dm3.conversations.list; console.log(c); + expect(c.length).toBe(2); + expect(c[0].contact.name).toBe('bob.eth'); + expect(c[1].contact.name).toBe('karl.eth'); + }); + it('dont add duplicate conversations', async () => { + const mockTldResolver = { + resolveTLDtoAlias: async () => + `${normalizeEnsName(bob.address)}.addr.test`, + resolveAliasToTLD: async () => 'bob.eth', + } as unknown as ITLDResolver; + + const mockConfig: Dm3SdkConfig = { + mainnetProvider: {} as ethers.providers.JsonRpcProvider, + storageApi: { + addConversation: async () => {}, + } as unknown as StorageAPI, + nonce: '1', + defaultDeliveryService: 'test.io', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', + resolverBackendUrl: 'resolver.io', + backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, + }; + + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + await dm3.conversations.addConversation('bob.eth'); + await dm3.conversations.addConversation('bob.eth'); + const c = dm3.conversations.list; expect(c.length).toBe(1); expect(c[0].contact.name).toBe('bob.eth'); }); From 4c8473463d87e68e47066d3fba4f075404941f4c Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 19 Nov 2024 17:39:34 +0700 Subject: [PATCH 06/12] clean up --- packages/js-sdk/package.json | 1 + packages/js-sdk/src/Dm3Sdk.test.ts | 1 + packages/js-sdk/src/conversation/Conversations.ts | 14 ++++---------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 7258d8caf..f867c31b6 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -1,3 +1,4 @@ + { "name": "@dm3-org/dm3-js-sdk", "license": "BSD-2-Clause", diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index f8ebd0ca3..bf7b3ee93 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -146,6 +146,7 @@ describe('Dm3Sdk', () => { await dm3.conversations.addConversation('bob.eth'); await dm3.conversations.addConversation('karl.eth'); const c = dm3.conversations.list; + console.log(c); expect(c.length).toBe(2); expect(c[0].contact.name).toBe('bob.eth'); diff --git a/packages/js-sdk/src/conversation/Conversations.ts b/packages/js-sdk/src/conversation/Conversations.ts index 1b1085ebb..e72067d3c 100644 --- a/packages/js-sdk/src/conversation/Conversations.ts +++ b/packages/js-sdk/src/conversation/Conversations.ts @@ -1,19 +1,13 @@ /* eslint-disable max-len */ +import { Account, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; import { - Account, - DeliveryServiceProfile, - getAccountDisplayName, - normalizeEnsName, -} from '@dm3-org/dm3-lib-profile'; -import { - StorageAPI, Conversation as ConversationDto, + StorageAPI, } from '@dm3-org/dm3-lib-storage'; -import { Contact, Conversation, getEmptyContact } from './types'; -import { Tld } from '../tld/Tld'; -import { hydrateContract as hydrateContact } from './hydrate/hydrateContact'; import { ethers } from 'ethers'; import { ITLDResolver } from '../tld/nameService/ITLDResolver'; +import { hydrateContract as hydrateContact } from './hydrate/hydrateContact'; +import { Contact, Conversation, getEmptyContact } from './types'; export class Conversations { private readonly provider: ethers.providers.JsonRpcProvider; From 14438b9e7ad904ad5467a05c32bd8944aa4d7a43 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 21 Nov 2024 11:40:28 +0700 Subject: [PATCH 07/12] move messageRenderer over --- .../renderer/messageTypes/renderDelete.ts | 62 +++++++++++++ .../renderer/messageTypes/renderDuplicates.ts | 14 +++ .../renderer/messageTypes/renderEdit.ts | 87 +++++++++++++++++++ .../renderer/messageTypes/renderReactions.ts | 45 ++++++++++ .../renderer/messageTypes/renderReadOpened.ts | 25 ++++++ .../messageTypes/renderReadReceived.ts | 27 ++++++ .../renderer/messageTypes/renderReply.ts | 19 ++++ .../src/message/renderer/renderMessage.ts | 36 ++++++++ 8 files changed, 315 insertions(+) create mode 100644 packages/js-sdk/src/message/renderer/messageTypes/renderDelete.ts create mode 100644 packages/js-sdk/src/message/renderer/messageTypes/renderDuplicates.ts create mode 100644 packages/js-sdk/src/message/renderer/messageTypes/renderEdit.ts create mode 100644 packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts create mode 100644 packages/js-sdk/src/message/renderer/messageTypes/renderReadOpened.ts create mode 100644 packages/js-sdk/src/message/renderer/messageTypes/renderReadReceived.ts create mode 100644 packages/js-sdk/src/message/renderer/messageTypes/renderReply.ts create mode 100644 packages/js-sdk/src/message/renderer/renderMessage.ts diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderDelete.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderDelete.ts new file mode 100644 index 000000000..9c6adc2d8 --- /dev/null +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderDelete.ts @@ -0,0 +1,62 @@ +import { MessageType } from '@dm3-org/dm3-lib-messaging'; +import { MessageActionType, MessageModel } from '../../types'; + +export const renderDelete = (messages: MessageModel[]) => { + //We filter out all messages that are deleted + const deleteMessages = messages.filter( + (message) => message.envelop.message.metadata.type === 'DELETE_REQUEST', + ); + + //We get the message hash of the messages that are to be deleted + const toBeDeletedByMessageHash = deleteMessages.map( + (deleteMessage) => + deleteMessage.envelop.message.metadata.referenceMessageHash, + ); + + //We return the messages with out messages that are to be deleted and the delete requests + return messages + .filter( + (message) => + message.envelop.message.metadata.type !== + MessageActionType.DELETE, + ) + .map((message) => { + if ( + toBeDeletedByMessageHash.includes( + message.envelop.metadata?.messageHash, + ) + ) { + return { + ...message, + envelop: { + ...message.envelop, + message: { + ...message.envelop.message, + message: '', + metadata: { + ...message.envelop.message.metadata, + // REACT messages are set, so that it can be filtered out + // to be not shown on UI + type: + message.envelop.message.metadata.type === + MessageActionType.REACT + ? (MessageActionType.REACT as MessageType) + : (MessageActionType.DELETE as MessageType), + }, + }, + }, + }; + } + return message; + // filter out all the reaction messages those are deleted + // its not to be shown on UI + }) + .filter( + (messageData) => + !( + messageData.envelop.message.message === '' && + messageData.envelop.message.metadata.type === + MessageActionType.REACT + ), + ); +}; diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderDuplicates.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderDuplicates.ts new file mode 100644 index 000000000..5f8452565 --- /dev/null +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderDuplicates.ts @@ -0,0 +1,14 @@ +import { MessageModel } from '../../types'; + +export const renderDuplicates = (messages: MessageModel[]) => { + //Return messages without duplicates + return messages.filter( + (message, index, self) => + index === + self.findIndex( + (t) => + t.envelop.metadata?.messageHash === + message.envelop.metadata?.messageHash, + ), + ); +}; diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderEdit.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderEdit.ts new file mode 100644 index 000000000..7779a85eb --- /dev/null +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderEdit.ts @@ -0,0 +1,87 @@ +import { MessageModel } from '../../types'; + +export const renderEdit = (_messages: MessageModel[]) => { + //Copy messages object to not mutate the original object + const messages: MessageModel[] = [..._messages]; + //Before processing the messages have to be sorted ASC by timestamp + messages.sort( + (a, b) => + a.envelop.message.metadata.timestamp - + b.envelop.message.metadata.timestamp, + ); + //To apply insertions we have to find every message that is an edit and find the original message + //A message can be edited multiple times so we always have to find the original message + //A path for a simple edit looks like [NEW, EDIT] + //A path of a message that has been edited multiple times looks like [NEW, EDIT, EDIT, EDIT,...] + //To display an edit message correctly we have to find the first and the last element of the path + //Putting the first element of the path in the last index of the array and removing the rest of the elements + const paths: number[][] = []; + + messages.forEach((msg, idx) => { + const newPath = []; + if (msg.envelop.message.metadata.type !== 'EDIT') { + return; + } + + //Idx is the beginning of the path. Now we have to go throuhg the messages and + //find the original message recursifly. + //Along the way we have to keep track of every index we pass. + let currentIdx = idx; + let currentMsg = msg; + newPath.push(currentIdx); + while (currentMsg.envelop.message.metadata.type === 'EDIT') { + const originalMsg = messages.find( + (m, i) => + m.envelop.metadata?.messageHash === + currentMsg.envelop.message.metadata.referenceMessageHash, + ); + if (!originalMsg) { + break; + } + currentIdx = messages.indexOf(originalMsg); + currentMsg = originalMsg; + newPath.push(currentIdx); + } + paths.push(newPath); + }); + + //When a message has been edited multiple times the path contains every subset of the path + //i.e [NEW, EDIT, EDIT, EDIT] contains [NEW, EDIT], [NEW, EDIT, EDIT], [NEW, EDIT, EDIT, EDIT] + //Hence we're using a SET to find the longest path + const uniquePaths: { [id: number]: Set } = {}; + + paths.forEach((p) => { + const originalMessageIndex = p[p.length - 1]; + + if (!uniquePaths[originalMessageIndex]) { + uniquePaths[originalMessageIndex] = new Set(); + } + p.forEach((e) => uniquePaths[originalMessageIndex].add(e)); + }); + + //We sort the SET to get the original message always at the first index and the last edit at the last index + //Afterwards we turn the SET into an array to traverse it + const uniquePathArray: number[][] = Object.values(uniquePaths).map( + (pathSet) => [...pathSet].sort((a, b) => a - b), + ); + + for (const path of uniquePathArray) { + //The message that is about to be edited + const originalMessageIndex = path.shift(); + + //The last edit that'll replace the first message + const lastEditIndex = path.pop(); + + //Swap the original message with the last edit + const editedMessage = messages[lastEditIndex!]; + messages[originalMessageIndex!] = editedMessage; + + //remove the rest of the messages in the path + path.forEach((idx) => { + messages[idx] = undefined!; + }); + messages[lastEditIndex!] = undefined!; + } + //Filter out all undefined messages + return messages.filter((m) => m !== undefined); +}; diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts new file mode 100644 index 000000000..c727cb15f --- /dev/null +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts @@ -0,0 +1,45 @@ +import { MessageModel } from '../../types'; + +export const renderReactions = (messages: MessageModel[]) => { + //We filter out all messages that are reactions + const reactions = messages + .filter( + (message) => message.envelop.message.metadata.type === 'REACTION', + ) + .map((reaction) => reaction.envelop); + + //add reactions to the messages + return ( + messages + .map((message) => { + const _reactions = [...message.reactions, ...reactions] + .filter( + (reaction) => + reaction.message.metadata.referenceMessageHash === + message.envelop.metadata?.messageHash, + ) + //Filter duplicates, we only want to display a reaction once + //I.e if there are ten messages from type ❤️ we only want to display one ❤️ + .filter((reaction, index, self) => { + return ( + index === + self.findIndex( + (r) => + r.message.message === + reaction.message.message, + ) + ); + }); + + return { + ...message, + reactions: _reactions, + }; + }) + //Get get rid of the reactions, as they are now part of the message + .filter( + (message) => + message.envelop.message.metadata.type !== 'REACTION', + ) + ); +}; diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderReadOpened.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderReadOpened.ts new file mode 100644 index 000000000..929b09284 --- /dev/null +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderReadOpened.ts @@ -0,0 +1,25 @@ +import { MessageIndicator, MessageModel } from '../../types'; + +export const renderReadOpened = (messages: MessageModel[]) => { + //We filter out all messages that are of type READ_OPENED + const readOpenedMsgs = messages.filter( + (message) => message.envelop.message.metadata.type === 'READ_OPENED', + ); + + const msgsWithoutReadType = messages.filter( + (data) => data.envelop.message.metadata.type !== 'READ_OPENED', + ); + + //update indicator to the messages + return msgsWithoutReadType.map((message) => { + const openedMsg = readOpenedMsgs.find( + (m) => + m.envelop.message.metadata.referenceMessageHash === + message.envelop.metadata?.messageHash, + ); + return { + ...message, + indicator: openedMsg ? MessageIndicator.READED : message.indicator, + }; + }); +}; diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderReadReceived.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderReadReceived.ts new file mode 100644 index 000000000..d7ad7b0ec --- /dev/null +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderReadReceived.ts @@ -0,0 +1,27 @@ +import { MessageModel, MessageIndicator } from '../../types'; + +export const renderReadReceived = (messages: MessageModel[]) => { + //We filter out all messages that are of type READ_RECEIVED + const readReceivedMsgs = messages.filter( + (message) => message.envelop.message.metadata.type === 'READ_RECEIVED', + ); + + const msgsWithoutReadType = messages.filter( + (data) => data.envelop.message.metadata.type !== 'READ_RECEIVED', + ); + + //update indicator to the messages + return msgsWithoutReadType.map((message) => { + const receivedMsg = readReceivedMsgs.find( + (m) => + m.envelop.message.metadata.referenceMessageHash === + message.envelop.metadata?.messageHash, + ); + return { + ...message, + indicator: receivedMsg + ? MessageIndicator.RECEIVED + : MessageIndicator.SENT, + }; + }); +}; diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderReply.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderReply.ts new file mode 100644 index 000000000..dc4660330 --- /dev/null +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderReply.ts @@ -0,0 +1,19 @@ +import { MessageModel } from '../../types'; + +export const renderReply = (messages: MessageModel[]) => { + return messages.map((message) => { + if (message.envelop.message.metadata.type === 'REPLY') { + //TODO we've to figure out how to get a message from a different chunk (Alex) + const replyToMessageEnvelop = messages.find( + (m) => + m.envelop.metadata?.messageHash === + message.envelop.message.metadata.referenceMessageHash, + ); + return { + ...message, + replyToMessageEnvelop: replyToMessageEnvelop?.envelop, + }; + } + return message; + }); +}; diff --git a/packages/js-sdk/src/message/renderer/renderMessage.ts b/packages/js-sdk/src/message/renderer/renderMessage.ts new file mode 100644 index 000000000..f16dabe96 --- /dev/null +++ b/packages/js-sdk/src/message/renderer/renderMessage.ts @@ -0,0 +1,36 @@ +import { MessageModel } from '../Messages'; +import { renderDelete } from './messageTypes/renderDelete'; +import { renderDuplicates } from './messageTypes/renderDuplicates'; +import { renderEdit } from './messageTypes/renderEdit'; +import { renderReactions } from './messageTypes/renderReactions'; +import { renderReadOpened } from './messageTypes/renderReadOpened'; +import { renderReadReceived } from './messageTypes/renderReadReceived'; +import { renderReply } from './messageTypes/renderReply'; + +/** + * The storage contains the messages in the order they were received. This contains every message types not just the + * ones that are displayed. To give the user the correct impression of the conversation we have to render the messages + * That means we deal with DELETE, EDIT, REACTIONS and REPLIES. + * Putting them to the right place in the conversation. + */ +export const renderMessage = (messages: MessageModel[]) => { + const withReadReceived = renderReadReceived(messages); + const withReadOpened = renderReadOpened(withReadReceived); + const withDeletes = renderDelete(withReadOpened); + const withReactions = renderReactions(withDeletes); + const withReply = renderReply(withReactions); + + const withoutEdited = renderEdit(withReply); + //Sort the messages by timestamp DESC to show them in the right order + // withoutEdited.sort( + // (a, b) => + // b.envelop.message.metadata.timestamp - + // a.envelop.message.metadata.timestamp, + // ); + + //There a several ways a message can added to the client. I.e via Websocket, multiple DS or from the storage. + //This leads occasionally to duplicates we don't want to display. + const withoutDuplicates = renderDuplicates(withoutEdited); + //We reverse the array to display the messages in the right order + return withoutDuplicates.reverse(); +}; From 6076931af7b21967b0ee14ae5c256744119ca8c1 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 21 Nov 2024 11:42:13 +0700 Subject: [PATCH 08/12] add Messages to conversation --- packages/js-sdk/src/Dm3Sdk.test.ts | 288 +++++++++++------- .../js-sdk/src/conversation/Conversations.ts | 7 +- packages/js-sdk/src/message/Messages.ts | 67 ++-- 3 files changed, 197 insertions(+), 165 deletions(-) diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index bf7b3ee93..c0f8a3480 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -1,6 +1,9 @@ import { StorageAPI } from '@dm3-org/dm3-lib-storage'; import { + getMockDeliveryServiceProfile, + MockDeliveryServiceProfile, MockedUserProfile, + MockMessageFactory, mockUserProfile, } from '@dm3-org/dm3-lib-test-helper'; import axios from 'axios'; @@ -19,6 +22,8 @@ describe('Dm3Sdk', () => { //Axios mock to mock the http requests let axiosMock; + let deliveryService: MockDeliveryServiceProfile; + beforeEach(async () => { alice = await mockUserProfile( ethers.Wallet.createRandom(), @@ -33,6 +38,11 @@ describe('Dm3Sdk', () => { 'test.io', ]); + deliveryService = await getMockDeliveryServiceProfile( + ethers.Wallet.createRandom(), + 'http://localhost:3000', + ); + axiosMock = new MockAdapter(axios); //Mock BackendConnector HttpRequests @@ -63,126 +73,170 @@ describe('Dm3Sdk', () => { .reply(200); }); - it('can add a conversation to the contact list', async () => { - const mockTldResolver = { - resolveTLDtoAlias: async () => - `${normalizeEnsName(bob.address)}.addr.test`, - resolveAliasToTLD: async () => 'bob.eth', - } as unknown as ITLDResolver; - - const mockConfig: Dm3SdkConfig = { - mainnetProvider: {} as ethers.providers.JsonRpcProvider, - storageApi: { - addConversation: async () => {}, - } as unknown as StorageAPI, - nonce: '1', - defaultDeliveryService: 'test.io', - addressEnsSubdomain: '.addr.test', - userEnsSubdomain: '.user.test', - resolverBackendUrl: 'resolver.io', - backendUrl: 'http://localhost:4060', - _tld: mockTldResolver, - }; - - const dm3 = await new Dm3Sdk(mockConfig).login({ - profileKeys: alice.profileKeys, - profile: alice.signedUserProfile, - accountAddress: alice.address, + describe('conversations', () => { + it('can add a conversation to the contact list', async () => { + const mockTldResolver = { + resolveTLDtoAlias: async () => + `${normalizeEnsName(bob.address)}.addr.test`, + resolveAliasToTLD: async () => 'bob.eth', + } as unknown as ITLDResolver; + + const mockConfig: Dm3SdkConfig = { + mainnetProvider: {} as ethers.providers.JsonRpcProvider, + storageApi: { + addConversation: async () => {}, + } as unknown as StorageAPI, + nonce: '1', + defaultDeliveryService: 'test.io', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', + resolverBackendUrl: 'resolver.io', + backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, + }; + + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + await dm3.conversations.addConversation('bob.eth'); + const c = dm3.conversations.list; + expect(c.length).toBe(1); + expect(c[0].contact.name).toBe('bob.eth'); }); - - await dm3.conversations.addConversation('bob.eth'); - const c = dm3.conversations.list; - expect(c.length).toBe(1); - expect(c[0].contact.name).toBe('bob.eth'); - }); - it('can multiple conversations to the contact list', async () => { - const mockTldResolver = { - resolveTLDtoAlias: async (ensName: string) => { - if (ensName === 'alice.eth') { - return `${normalizeEnsName(alice.address)}.addr.test`; - } - if (ensName === 'bob.eth') { - return `${normalizeEnsName(bob.address)}.addr.test`; - } - return `${normalizeEnsName(karl.address)}.addr.test`; - }, - resolveAliasToTLD: async (ensName: string) => { - if ( - normalizeEnsName(ensName) === - normalizeEnsName(alice.address) + '.addr.test' - ) { - return 'alice.eth'; - } - if ( - normalizeEnsName(ensName) === - normalizeEnsName(bob.address) + '.addr.test' - ) { - return 'bob.eth'; - } - return 'karl.eth'; - }, - } as unknown as ITLDResolver; - - const mockConfig: Dm3SdkConfig = { - mainnetProvider: {} as ethers.providers.JsonRpcProvider, - storageApi: { - addConversation: async () => {}, - } as unknown as StorageAPI, - nonce: '1', - defaultDeliveryService: 'test.io', - addressEnsSubdomain: '.addr.test', - userEnsSubdomain: '.user.test', - resolverBackendUrl: 'resolver.io', - backendUrl: 'http://localhost:4060', - _tld: mockTldResolver, - }; - - const dm3 = await new Dm3Sdk(mockConfig).login({ - profileKeys: alice.profileKeys, - profile: alice.signedUserProfile, - accountAddress: alice.address, + it('can multiple conversations to the contact list', async () => { + const mockTldResolver = { + resolveTLDtoAlias: async (ensName: string) => { + if (ensName === 'alice.eth') { + return `${normalizeEnsName(alice.address)}.addr.test`; + } + if (ensName === 'bob.eth') { + return `${normalizeEnsName(bob.address)}.addr.test`; + } + return `${normalizeEnsName(karl.address)}.addr.test`; + }, + resolveAliasToTLD: async (ensName: string) => { + if ( + normalizeEnsName(ensName) === + normalizeEnsName(alice.address) + '.addr.test' + ) { + return 'alice.eth'; + } + if ( + normalizeEnsName(ensName) === + normalizeEnsName(bob.address) + '.addr.test' + ) { + return 'bob.eth'; + } + return 'karl.eth'; + }, + } as unknown as ITLDResolver; + + const mockConfig: Dm3SdkConfig = { + mainnetProvider: {} as ethers.providers.JsonRpcProvider, + storageApi: { + addConversation: async () => {}, + } as unknown as StorageAPI, + nonce: '1', + defaultDeliveryService: 'test.io', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', + resolverBackendUrl: 'resolver.io', + backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, + }; + + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + await dm3.conversations.addConversation('bob.eth'); + await dm3.conversations.addConversation('karl.eth'); + const c = dm3.conversations.list; + dm3; + console.log(c); + expect(c.length).toBe(2); + expect(c[0].contact.name).toBe('bob.eth'); + expect(c[1].contact.name).toBe('karl.eth'); }); - - await dm3.conversations.addConversation('bob.eth'); - await dm3.conversations.addConversation('karl.eth'); - const c = dm3.conversations.list; - - console.log(c); - expect(c.length).toBe(2); - expect(c[0].contact.name).toBe('bob.eth'); - expect(c[1].contact.name).toBe('karl.eth'); - }); - it('dont add duplicate conversations', async () => { - const mockTldResolver = { - resolveTLDtoAlias: async () => - `${normalizeEnsName(bob.address)}.addr.test`, - resolveAliasToTLD: async () => 'bob.eth', - } as unknown as ITLDResolver; - - const mockConfig: Dm3SdkConfig = { - mainnetProvider: {} as ethers.providers.JsonRpcProvider, - storageApi: { - addConversation: async () => {}, - } as unknown as StorageAPI, - nonce: '1', - defaultDeliveryService: 'test.io', - addressEnsSubdomain: '.addr.test', - userEnsSubdomain: '.user.test', - resolverBackendUrl: 'resolver.io', - backendUrl: 'http://localhost:4060', - _tld: mockTldResolver, - }; - - const dm3 = await new Dm3Sdk(mockConfig).login({ - profileKeys: alice.profileKeys, - profile: alice.signedUserProfile, - accountAddress: alice.address, + it('dont add duplicate conversations', async () => { + const mockTldResolver = { + resolveTLDtoAlias: async () => + `${normalizeEnsName(bob.address)}.addr.test`, + resolveAliasToTLD: async () => 'bob.eth', + } as unknown as ITLDResolver; + + const mockConfig: Dm3SdkConfig = { + mainnetProvider: {} as ethers.providers.JsonRpcProvider, + storageApi: { + addConversation: async () => {}, + } as unknown as StorageAPI, + nonce: '1', + defaultDeliveryService: 'test.io', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', + resolverBackendUrl: 'resolver.io', + backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, + }; + + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + await dm3.conversations.addConversation('bob.eth'); + await dm3.conversations.addConversation('bob.eth'); + const c = dm3.conversations.list; + expect(c.length).toBe(1); + expect(c[0].contact.name).toBe('bob.eth'); }); + }); - await dm3.conversations.addConversation('bob.eth'); - await dm3.conversations.addConversation('bob.eth'); - const c = dm3.conversations.list; - expect(c.length).toBe(1); - expect(c[0].contact.name).toBe('bob.eth'); + describe('messages', () => { + it('can send a message', async () => { + const mockTldResolver = { + resolveTLDtoAlias: async () => + `${normalizeEnsName(bob.address)}.addr.test`, + resolveAliasToTLD: async () => 'bob.eth', + } as unknown as ITLDResolver; + + const mockConfig: Dm3SdkConfig = { + mainnetProvider: {} as ethers.providers.JsonRpcProvider, + storageApi: { + addConversation: async () => {}, + addMessage: async () => {}, + } as unknown as StorageAPI, + nonce: '1', + defaultDeliveryService: 'test.io', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', + resolverBackendUrl: 'resolver.io', + backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, + }; + + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + const msgFactory = MockMessageFactory(alice, bob, deliveryService); + + const msg1 = await msgFactory.createMessage('Hi'); + + const c = await dm3.conversations.addConversation('bob.eth'); + expect(c?.messages.list().length).toBe(0); + await c?.messages.addMessage('bob.eth', msg1); + + expect(c?.messages.list().length).toBe(1); + expect(c?.messages.list()[0].envelop.message.message).toBe('Hi'); + }); }); }); diff --git a/packages/js-sdk/src/conversation/Conversations.ts b/packages/js-sdk/src/conversation/Conversations.ts index e72067d3c..c283c21cc 100644 --- a/packages/js-sdk/src/conversation/Conversations.ts +++ b/packages/js-sdk/src/conversation/Conversations.ts @@ -8,6 +8,7 @@ import { ethers } from 'ethers'; import { ITLDResolver } from '../tld/nameService/ITLDResolver'; import { hydrateContract as hydrateContact } from './hydrate/hydrateContact'; import { Contact, Conversation, getEmptyContact } from './types'; +import { Messages } from '../message/Messages'; export class Conversations { private readonly provider: ethers.providers.JsonRpcProvider; @@ -88,7 +89,7 @@ export class Conversations { const newConversation: Conversation = { //TODO change that once Message class has been implemented - messages: undefined as any, + messages: new Messages(this.storageApi, this), contact: newContact, }; //Set the new contact to the list @@ -102,7 +103,7 @@ export class Conversations { ); const hydratedConversation: Conversation = { - messages: undefined as any, + messages: new Messages(this.storageApi, this), contact: hydratedContact, }; //find existing contact and replace it with the hydrated one @@ -131,7 +132,7 @@ export class Conversations { this.addressEnsSubdomain, ); const hydratedConversation: Conversation = { - messages: undefined as any, + messages: new Messages(this.storageApi, this), contact: hydratedContact, }; this.list.push(hydratedConversation); diff --git a/packages/js-sdk/src/message/Messages.ts b/packages/js-sdk/src/message/Messages.ts index 5eebdaff0..69ddfffed 100644 --- a/packages/js-sdk/src/message/Messages.ts +++ b/packages/js-sdk/src/message/Messages.ts @@ -20,37 +20,8 @@ import { import { submitEnvelopsToReceiversDs } from '../api/ds/submitEnvelopsToReceiversDs'; import { Conversations } from '../conversation/Conversations'; import { Contact } from '../conversation/types'; - -const DEFAULT_MESSAGE_PAGESIZE = 100; - -export enum MessageIndicator { - SENT = 'SENT', - RECEIVED = 'RECEIVED', - READED = 'READED', -} - -//Message source to identify where a message comes from. This is important to handle pagination of storage messages properly -export enum MessageSource { - //Messages added by the client via addMessage - Client, - //Messages fetched from the storage - Storage, - //Messages fetched from the deliveryService - DeliveryService, - //Messages received from the Websocket - WebSocket, -} - -export type MessageModel = StorageEnvelopContainerNew & { - reactions: Envelop[]; - replyToMessageEnvelop?: Envelop; - source: MessageSource; - indicator?: MessageIndicator; -}; - -export type MessageStorage = { - [contact: string]: MessageModel[]; -}; +import { renderMessage } from './renderer/renderMessage'; +import { MessageModel, MessageSource } from './types'; export class Messages { private readonly storageApi: StorageAPI; @@ -58,17 +29,23 @@ export class Messages { private readonly account: Account; private readonly profileKeys: ProfileKeys; - private messages: MessageStorage = {}; + private readonly _messages: MessageModel[]; constructor(storageApi: StorageAPI, conversations: Conversations) { this.storageApi = storageApi; this.conversations = conversations; + + this._messages = []; + } + + public list() { + return renderMessage(this._messages); } - addMessage = async ( + public async addMessage( _contactName: string, message: Message, - ): Promise<{ isSuccess: boolean; error?: string }> => { + ): Promise<{ isSuccess: boolean; error?: string }> { const contact = normalizeEnsName(_contactName); //If a message is empty it should not be added @@ -122,13 +99,13 @@ export class Messages { //If neither the recipient nor the potential recipient is a DM3 user we store the message in the storage return await this._haltMessage(contact, message); - }; + } - private _dispatchMessage = async ( + private async _dispatchMessage( contact: string, recipient: Contact, message: Message, - ) => { + ) { //Build the envelops based on the message and the users profileKeys. //For each deliveryServiceProfile a envelop is created that will be sent to the delivery service const envelops = await Promise.all( @@ -183,7 +160,7 @@ export class Messages { }; //Add the message to the state - this.messages[contact].push(messageStorageContainer); + this._messages.push(messageStorageContainer); //Storage the message in the storage async this.storageApi.addMessage(contact, messageStorageContainer, false); @@ -204,9 +181,9 @@ export class Messages { //Send the envelops to the delivery service await submitEnvelopsToReceiversDs(envelops); return { isSuccess: true }; - }; + } - private _haltMessage = async (contact: string, message: Message) => { + private _haltMessage(contact: string, message: Message) { //StorageEnvelopContainerNew to store the message in the storage const messageModel: MessageModel = { envelop: { @@ -225,21 +202,21 @@ export class Messages { source: MessageSource.Client, reactions: [], }; - this.messages[contact].push(messageModel); + this._messages.push(messageModel); //Store the message and mark it as halted this.storageApi.addMessage(contact, messageModel, true); return { isSuccess: true }; - }; + } //TODO migrate to real lukso name service isLuksoName = (input: string): boolean => { const regex = /^[a-zA-Z0-9]+#[a-zA-Z0-9]{4}\.up$/; return regex.test(input); }; - private checkIfEnvelopAreInSizeLimit = async ( + private async checkIfEnvelopAreInSizeLimit( encryptedEnvelops: EncryptionEnvelop[], receiversMessageSizeLimit: number, - ): Promise => { + ): Promise { try { const atLeastOneEnvelopIsToLarge = !!encryptedEnvelops //get the size of each envelop @@ -255,5 +232,5 @@ export class Messages { console.error('Error in checkIfEnvelopAreInSizeLimit', error); return false; } - }; + } } From 17dd08033ec0354ed1cbee31a7f779066f6f9918 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 21 Nov 2024 11:42:30 +0700 Subject: [PATCH 09/12] streamline message related types --- packages/js-sdk/src/conversation/types.ts | 6 +-- packages/js-sdk/src/message/Messages.ts | 6 +-- .../src/message/renderer/renderMessage.ts | 2 +- packages/js-sdk/src/message/types.ts | 37 +++++++++++++++++++ 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 packages/js-sdk/src/message/types.ts diff --git a/packages/js-sdk/src/conversation/types.ts b/packages/js-sdk/src/conversation/types.ts index e970d7cc4..469d223e9 100644 --- a/packages/js-sdk/src/conversation/types.ts +++ b/packages/js-sdk/src/conversation/types.ts @@ -3,11 +3,7 @@ import { DeliveryServiceProfile, getAccountDisplayName, } from '@dm3-org/dm3-lib-profile'; - -class Messages { - public readonly list: string[]; - public addMessage(message: string) {} -} +import { Messages } from '../message/Messages'; export interface Contact { name: string; diff --git a/packages/js-sdk/src/message/Messages.ts b/packages/js-sdk/src/message/Messages.ts index 69ddfffed..01fe0d428 100644 --- a/packages/js-sdk/src/message/Messages.ts +++ b/packages/js-sdk/src/message/Messages.ts @@ -2,7 +2,6 @@ import { encryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; import { buildEnvelop, EncryptionEnvelop, - Envelop, getEnvelopSize, Message, MessageState, @@ -13,10 +12,7 @@ import { ProfileKeys, } from '@dm3-org/dm3-lib-profile'; import { sha256, stringify } from '@dm3-org/dm3-lib-shared'; -import { - StorageAPI, - StorageEnvelopContainer as StorageEnvelopContainerNew, -} from '@dm3-org/dm3-lib-storage'; +import { StorageAPI } from '@dm3-org/dm3-lib-storage'; import { submitEnvelopsToReceiversDs } from '../api/ds/submitEnvelopsToReceiversDs'; import { Conversations } from '../conversation/Conversations'; import { Contact } from '../conversation/types'; diff --git a/packages/js-sdk/src/message/renderer/renderMessage.ts b/packages/js-sdk/src/message/renderer/renderMessage.ts index f16dabe96..01701c1de 100644 --- a/packages/js-sdk/src/message/renderer/renderMessage.ts +++ b/packages/js-sdk/src/message/renderer/renderMessage.ts @@ -1,4 +1,4 @@ -import { MessageModel } from '../Messages'; +import { MessageModel } from '../types'; import { renderDelete } from './messageTypes/renderDelete'; import { renderDuplicates } from './messageTypes/renderDuplicates'; import { renderEdit } from './messageTypes/renderEdit'; diff --git a/packages/js-sdk/src/message/types.ts b/packages/js-sdk/src/message/types.ts new file mode 100644 index 000000000..8bf7dabe6 --- /dev/null +++ b/packages/js-sdk/src/message/types.ts @@ -0,0 +1,37 @@ +import { Envelop } from '@dm3-org/dm3-lib-messaging'; +import { + StorageAPI, + StorageEnvelopContainer as StorageEnvelopContainerNew, +} from '@dm3-org/dm3-lib-storage'; +export enum MessageActionType { + NEW = 'NEW', + EDIT = 'EDIT', + DELETE = 'DELETE_REQUEST', + REPLY = 'REPLY', + REACT = 'REACTION', + NONE = 'NONE', +} + +export enum MessageIndicator { + SENT = 'SENT', + RECEIVED = 'RECEIVED', + READED = 'READED', +} +//Message source to identify where a message comes from. This is important to handle pagination of storage messages properly +export enum MessageSource { + //Messages added by the client via addMessage + Client, + //Messages fetched from the storage + Storage, + //Messages fetched from the deliveryService + DeliveryService, + //Messages received from the Websocket + WebSocket, +} + +export type MessageModel = StorageEnvelopContainerNew & { + reactions: Envelop[]; + replyToMessageEnvelop?: Envelop; + source: MessageSource; + indicator?: MessageIndicator; +}; From 0c838b38612e6a892578504efeae51e1a4a7ec05 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 21 Nov 2024 16:50:00 +0700 Subject: [PATCH 10/12] add Messages to SDK Api --- packages/js-sdk/src/Dm3Sdk.test.ts | 38 ++++++++++++++- packages/js-sdk/src/Dm3Sdk.ts | 1 + .../js-sdk/src/conversation/Conversations.ts | 34 ++++++++++++-- packages/js-sdk/src/message/Messages.ts | 46 ++++++++++++++++--- 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index c0f8a3480..8fd5ac861 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -198,7 +198,7 @@ describe('Dm3Sdk', () => { }); }); - describe('messages', () => { + describe('Messages', () => { it('can send a message', async () => { const mockTldResolver = { resolveTLDtoAlias: async () => @@ -235,6 +235,42 @@ describe('Dm3Sdk', () => { expect(c?.messages.list().length).toBe(0); await c?.messages.addMessage('bob.eth', msg1); + expect(c?.messages.list().length).toBe(1); + expect(c?.messages.list()[0].envelop.message.message).toBe('Hi'); + }); + it('can send a message', async () => { + const mockTldResolver = { + resolveTLDtoAlias: async () => + `${normalizeEnsName(bob.address)}.addr.test`, + resolveAliasToTLD: async () => 'bob.eth', + } as unknown as ITLDResolver; + + const mockConfig: Dm3SdkConfig = { + mainnetProvider: {} as ethers.providers.JsonRpcProvider, + storageApi: { + addConversation: async () => {}, + addMessage: async () => {}, + } as unknown as StorageAPI, + nonce: '1', + defaultDeliveryService: 'test.io', + addressEnsSubdomain: '.addr.test', + userEnsSubdomain: '.user.test', + resolverBackendUrl: 'resolver.io', + backendUrl: 'http://localhost:4060', + _tld: mockTldResolver, + }; + + const dm3 = await new Dm3Sdk(mockConfig).login({ + profileKeys: alice.profileKeys, + profile: alice.signedUserProfile, + accountAddress: alice.address, + }); + + const c = await dm3.conversations.addConversation('bob.eth'); + expect(c?.messages.list().length).toBe(0); + + await c?.messages.sendMessage('Hi'); + expect(c?.messages.list().length).toBe(1); expect(c?.messages.list()[0].envelop.message.message).toBe('Hi'); }); diff --git a/packages/js-sdk/src/Dm3Sdk.ts b/packages/js-sdk/src/Dm3Sdk.ts index 3ce98b8a2..37b0fbf2c 100644 --- a/packages/js-sdk/src/Dm3Sdk.ts +++ b/packages/js-sdk/src/Dm3Sdk.ts @@ -137,6 +137,7 @@ export class Dm3Sdk { tld, this.mainnetProvider, account, + profileKeys, this.addressEnsSubdomain, ); diff --git a/packages/js-sdk/src/conversation/Conversations.ts b/packages/js-sdk/src/conversation/Conversations.ts index c283c21cc..59d8b18e7 100644 --- a/packages/js-sdk/src/conversation/Conversations.ts +++ b/packages/js-sdk/src/conversation/Conversations.ts @@ -1,5 +1,9 @@ /* eslint-disable max-len */ -import { Account, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; +import { + Account, + normalizeEnsName, + ProfileKeys, +} from '@dm3-org/dm3-lib-profile'; import { Conversation as ConversationDto, StorageAPI, @@ -16,7 +20,7 @@ export class Conversations { private readonly tld: ITLDResolver; private readonly addressEnsSubdomain: string; private readonly account: Account; - + private readonly profileKeys: ProfileKeys; public list: Conversation[]; constructor( @@ -24,6 +28,7 @@ export class Conversations { tld: ITLDResolver, mainnetProvider: ethers.providers.JsonRpcProvider, account: Account, + profileKeys: ProfileKeys, addressEnsSubdomain: string, ) { this.storageApi = storageApi; @@ -31,6 +36,7 @@ export class Conversations { this.account = account; this.provider = mainnetProvider; this.addressEnsSubdomain = addressEnsSubdomain; + this.profileKeys = profileKeys; this.list = []; } @@ -89,7 +95,13 @@ export class Conversations { const newConversation: Conversation = { //TODO change that once Message class has been implemented - messages: new Messages(this.storageApi, this), + messages: new Messages( + this.storageApi, + this, + this.account, + this.profileKeys, + newContact, + ), contact: newContact, }; //Set the new contact to the list @@ -103,7 +115,13 @@ export class Conversations { ); const hydratedConversation: Conversation = { - messages: new Messages(this.storageApi, this), + messages: new Messages( + this.storageApi, + this, + this.account, + this.profileKeys, + hydratedContact, + ), contact: hydratedContact, }; //find existing contact and replace it with the hydrated one @@ -132,7 +150,13 @@ export class Conversations { this.addressEnsSubdomain, ); const hydratedConversation: Conversation = { - messages: new Messages(this.storageApi, this), + messages: new Messages( + this.storageApi, + this, + this.account, + this.profileKeys, + hydratedContact, + ), contact: hydratedContact, }; this.list.push(hydratedConversation); diff --git a/packages/js-sdk/src/message/Messages.ts b/packages/js-sdk/src/message/Messages.ts index 01fe0d428..3c739cb4e 100644 --- a/packages/js-sdk/src/message/Messages.ts +++ b/packages/js-sdk/src/message/Messages.ts @@ -1,4 +1,4 @@ -import { encryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; +import { encryptAsymmetric, sign } from '@dm3-org/dm3-lib-crypto'; import { buildEnvelop, EncryptionEnvelop, @@ -22,21 +22,53 @@ import { MessageModel, MessageSource } from './types'; export class Messages { private readonly storageApi: StorageAPI; private readonly conversations: Conversations; - private readonly account: Account; - private readonly profileKeys: ProfileKeys; private readonly _messages: MessageModel[]; - constructor(storageApi: StorageAPI, conversations: Conversations) { + private readonly senderAccount: Account; + private readonly senderProfileKeys: ProfileKeys; + private readonly receiver: Contact; + + constructor( + storageApi: StorageAPI, + conversations: Conversations, + senderAccount: Account, + senderProfileKeys: ProfileKeys, + receiver: Contact, + ) { this.storageApi = storageApi; this.conversations = conversations; - + this.senderAccount = senderAccount; + this.senderProfileKeys = senderProfileKeys; + this.receiver = receiver; this._messages = []; } public list() { return renderMessage(this._messages); } + public async sendMessage(msg: string) { + const messageWithoutSig: Omit = { + message: msg, + attachments: [], + metadata: { + referenceMessageHash: undefined, + type: 'NEW', + to: this.receiver.account.ensName, + from: this.senderAccount.ensName, + timestamp: new Date().getTime(), + }, + }; + + const message: Message = { + ...messageWithoutSig, + signature: await sign( + this.senderProfileKeys.signingKeyPair.privateKey, + stringify(messageWithoutSig), + ), + }; + return await this.addMessage(this.receiver.account.ensName, message); + } public async addMessage( _contactName: string, @@ -112,7 +144,7 @@ export class Messages { (publicKey: string, msg: string) => encryptAsymmetric(publicKey, msg), { - from: this.account!, + from: this.senderAccount!, to: { ...recipient!.account, //Cover edge case of lukso names. TODO discuss with the team and decide how to dela with non ENS names @@ -121,7 +153,7 @@ export class Messages { : recipient.name, }, deliverServiceProfile, - keys: this.profileKeys!, + keys: this.senderProfileKeys, }, ); }, From 7d14017f8df8f49d0462c5ef6729dbbb2ff85fae Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 21 Nov 2024 17:46:35 +0700 Subject: [PATCH 11/12] adress pr comments --- packages/js-sdk/package.json | 1 - packages/js-sdk/src/Dm3Sdk.test.ts | 74 ++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 5504a6c88..9f14bd40d 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -1,4 +1,3 @@ - { "name": "@dm3-org/dm3-js-sdk", "license": "BSD-2-Clause", diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index 8fd5ac861..eb2b7097e 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -42,7 +42,9 @@ describe('Dm3Sdk', () => { ethers.Wallet.createRandom(), 'http://localhost:3000', ); + }); + beforeAll(() => { axiosMock = new MockAdapter(axios); //Mock BackendConnector HttpRequests @@ -102,9 +104,8 @@ describe('Dm3Sdk', () => { }); await dm3.conversations.addConversation('bob.eth'); - const c = dm3.conversations.list; - expect(c.length).toBe(1); - expect(c[0].contact.name).toBe('bob.eth'); + expect(dm3.conversations.list.length).toBe(1); + expect(dm3.conversations.list[0].contact.name).toBe('bob.eth'); }); it('can multiple conversations to the contact list', async () => { const mockTldResolver = { @@ -156,12 +157,10 @@ describe('Dm3Sdk', () => { await dm3.conversations.addConversation('bob.eth'); await dm3.conversations.addConversation('karl.eth'); - const c = dm3.conversations.list; - dm3; - console.log(c); - expect(c.length).toBe(2); - expect(c[0].contact.name).toBe('bob.eth'); - expect(c[1].contact.name).toBe('karl.eth'); + + expect(dm3.conversations.list.length).toBe(2); + expect(dm3.conversations.list[0].contact.name).toBe('bob.eth'); + expect(dm3.conversations.list[1].contact.name).toBe('karl.eth'); }); it('dont add duplicate conversations', async () => { const mockTldResolver = { @@ -192,9 +191,8 @@ describe('Dm3Sdk', () => { await dm3.conversations.addConversation('bob.eth'); await dm3.conversations.addConversation('bob.eth'); - const c = dm3.conversations.list; - expect(c.length).toBe(1); - expect(c[0].contact.name).toBe('bob.eth'); + expect(dm3.conversations.list.length).toBe(1); + expect(dm3.conversations.list[0].contact.name).toBe('bob.eth'); }); }); @@ -231,12 +229,25 @@ describe('Dm3Sdk', () => { const msg1 = await msgFactory.createMessage('Hi'); - const c = await dm3.conversations.addConversation('bob.eth'); - expect(c?.messages.list().length).toBe(0); - await c?.messages.addMessage('bob.eth', msg1); - - expect(c?.messages.list().length).toBe(1); - expect(c?.messages.list()[0].envelop.message.message).toBe('Hi'); + expect( + ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.list().length, + ).toBe(0); + await ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.addMessage('bob.eth', msg1); + + expect( + ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.list().length, + ).toBe(1); + expect( + ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.list()[0].envelop.message.message, + ).toBe('Hi'); }); it('can send a message', async () => { const mockTldResolver = { @@ -266,13 +277,26 @@ describe('Dm3Sdk', () => { accountAddress: alice.address, }); - const c = await dm3.conversations.addConversation('bob.eth'); - expect(c?.messages.list().length).toBe(0); - - await c?.messages.sendMessage('Hi'); - - expect(c?.messages.list().length).toBe(1); - expect(c?.messages.list()[0].envelop.message.message).toBe('Hi'); + expect( + ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.list().length, + ).toBe(0); + + await ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.sendMessage('Hi'); + + expect( + ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.list().length, + ).toBe(1); + expect( + ( + await dm3.conversations.addConversation('bob.eth') + )?.messages.list()[0].envelop.message.message, + ).toBe('Hi'); }); }); }); From 0620b517a5acece83941e3581a057600ad542448 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Fri, 22 Nov 2024 14:47:06 +0700 Subject: [PATCH 12/12] adress pr comments --- packages/js-sdk/src/Dm3Sdk.test.ts | 77 ++----------------- packages/js-sdk/src/message/Messages.ts | 8 +- .../renderer/messageTypes/renderReactions.ts | 6 +- .../src/message/renderer/renderMessage.ts | 6 -- 4 files changed, 14 insertions(+), 83 deletions(-) diff --git a/packages/js-sdk/src/Dm3Sdk.test.ts b/packages/js-sdk/src/Dm3Sdk.test.ts index eb2b7097e..14bf478da 100644 --- a/packages/js-sdk/src/Dm3Sdk.test.ts +++ b/packages/js-sdk/src/Dm3Sdk.test.ts @@ -42,9 +42,6 @@ describe('Dm3Sdk', () => { ethers.Wallet.createRandom(), 'http://localhost:3000', ); - }); - - beforeAll(() => { axiosMock = new MockAdapter(axios); //Mock BackendConnector HttpRequests @@ -197,58 +194,6 @@ describe('Dm3Sdk', () => { }); describe('Messages', () => { - it('can send a message', async () => { - const mockTldResolver = { - resolveTLDtoAlias: async () => - `${normalizeEnsName(bob.address)}.addr.test`, - resolveAliasToTLD: async () => 'bob.eth', - } as unknown as ITLDResolver; - - const mockConfig: Dm3SdkConfig = { - mainnetProvider: {} as ethers.providers.JsonRpcProvider, - storageApi: { - addConversation: async () => {}, - addMessage: async () => {}, - } as unknown as StorageAPI, - nonce: '1', - defaultDeliveryService: 'test.io', - addressEnsSubdomain: '.addr.test', - userEnsSubdomain: '.user.test', - resolverBackendUrl: 'resolver.io', - backendUrl: 'http://localhost:4060', - _tld: mockTldResolver, - }; - - const dm3 = await new Dm3Sdk(mockConfig).login({ - profileKeys: alice.profileKeys, - profile: alice.signedUserProfile, - accountAddress: alice.address, - }); - - const msgFactory = MockMessageFactory(alice, bob, deliveryService); - - const msg1 = await msgFactory.createMessage('Hi'); - - expect( - ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.list().length, - ).toBe(0); - await ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.addMessage('bob.eth', msg1); - - expect( - ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.list().length, - ).toBe(1); - expect( - ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.list()[0].envelop.message.message, - ).toBe('Hi'); - }); it('can send a message', async () => { const mockTldResolver = { resolveTLDtoAlias: async () => @@ -278,25 +223,15 @@ describe('Dm3Sdk', () => { }); expect( - ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.list().length, + (await dm3.conversations.addConversation('bob.eth'))?.messages + .list.length, ).toBe(0); - await ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.sendMessage('Hi'); + const c = await dm3.conversations.addConversation('bob.eth'); - expect( - ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.list().length, - ).toBe(1); - expect( - ( - await dm3.conversations.addConversation('bob.eth') - )?.messages.list()[0].envelop.message.message, - ).toBe('Hi'); + await c?.messages.sendMessage('Hi'); + expect(c?.messages.list.length).toBe(1); + expect(c?.messages.list[0].envelop.message.message).toBe('Hi'); }); }); }); diff --git a/packages/js-sdk/src/message/Messages.ts b/packages/js-sdk/src/message/Messages.ts index 3c739cb4e..a59534c8c 100644 --- a/packages/js-sdk/src/message/Messages.ts +++ b/packages/js-sdk/src/message/Messages.ts @@ -44,7 +44,7 @@ export class Messages { this._messages = []; } - public list() { + public get list() { return renderMessage(this._messages); } public async sendMessage(msg: string) { @@ -56,7 +56,7 @@ export class Messages { type: 'NEW', to: this.receiver.account.ensName, from: this.senderAccount.ensName, - timestamp: new Date().getTime(), + timestamp: Date.now(), }, }; @@ -241,10 +241,10 @@ export class Messages { return regex.test(input); }; - private async checkIfEnvelopAreInSizeLimit( + private checkIfEnvelopAreInSizeLimit( encryptedEnvelops: EncryptionEnvelop[], receiversMessageSizeLimit: number, - ): Promise { + ): boolean { try { const atLeastOneEnvelopIsToLarge = !!encryptedEnvelops //get the size of each envelop diff --git a/packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts b/packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts index c727cb15f..6dea30543 100644 --- a/packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts +++ b/packages/js-sdk/src/message/renderer/messageTypes/renderReactions.ts @@ -1,10 +1,12 @@ -import { MessageModel } from '../../types'; +import { MessageActionType, MessageModel } from '../../types'; export const renderReactions = (messages: MessageModel[]) => { //We filter out all messages that are reactions const reactions = messages .filter( - (message) => message.envelop.message.metadata.type === 'REACTION', + (message) => + message.envelop.message.metadata.type === + MessageActionType.REACT, ) .map((reaction) => reaction.envelop); diff --git a/packages/js-sdk/src/message/renderer/renderMessage.ts b/packages/js-sdk/src/message/renderer/renderMessage.ts index 01701c1de..1d23a8222 100644 --- a/packages/js-sdk/src/message/renderer/renderMessage.ts +++ b/packages/js-sdk/src/message/renderer/renderMessage.ts @@ -21,12 +21,6 @@ export const renderMessage = (messages: MessageModel[]) => { const withReply = renderReply(withReactions); const withoutEdited = renderEdit(withReply); - //Sort the messages by timestamp DESC to show them in the right order - // withoutEdited.sort( - // (a, b) => - // b.envelop.message.metadata.timestamp - - // a.envelop.message.metadata.timestamp, - // ); //There a several ways a message can added to the client. I.e via Websocket, multiple DS or from the storage. //This leads occasionally to duplicates we don't want to display.