From 981700f229ab41a5f335305dfe629fb4b25c68c1 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 14 Nov 2024 20:27:54 -0800 Subject: [PATCH 1/7] add android consent functions --- android/build.gradle | 2 +- .../modules/xmtpreactnativesdk/XMTPModule.kt | 23 +++++++++++++------ ios/XMTPReactNative.podspec | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 7fcbe5e5..6ebd92fa 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:3.0.4" + implementation "org.xmtp:android:3.0.5" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 35ed22f0..23a88a82 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -438,41 +438,43 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("listGroups") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listGroups") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listGroups") val client = clients[inboxId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") - val groups = client.conversations.listGroups(order = order, limit = limit) + val consent = consentState?.let { getConsentState(it) } + val groups = client.conversations.listGroups(order = order, limit = limit, consentState = consent) groups.map { group -> GroupWrapper.encode(client, group, params) } } } - AsyncFunction("listDms") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listDms") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listDms") val client = clients[inboxId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") - val dms = client.conversations.listDms(order = order, limit = limit) + val consent = consentState?.let { getConsentState(it) } + val dms = client.conversations.listDms(order = order, limit = limit, consentState = consent) dms.map { dm -> DmWrapper.encode(client, dm, params) } } } - AsyncFunction("listConversations") Coroutine { inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listConversations") Coroutine { inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listConversations") val client = clients[inboxId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") - val conversations = - client.conversations.list(order = order, limit = limit) + val consent = consentState?.let { getConsentState(it) } + val conversations = client.conversations.list(order = order, limit = limit, consentState = consent) conversations.map { conversation -> ConversationWrapper.encode(client, conversation, params) } @@ -1060,6 +1062,13 @@ class XMTPModule : Module() { } } + AsyncFunction("syncConsent") Coroutine { inboxId: String -> + withContext(Dispatchers.IO) { + val client = clients[inboxId] ?: throw XMTPException("No client") + client.syncConsent() + } + } + AsyncFunction("setConsentState") Coroutine { inboxId: String, value: String, entryType: String, consentType: String -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 9bf755fc..d0b93865 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.4" + s.dependency "XMTP", "= 3.0.5" end From 30f7f32700611574cfc185ee9e466b1e851d80bf Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 14 Nov 2024 21:14:32 -0800 Subject: [PATCH 2/7] add the React native side of the code --- src/index.ts | 35 +++-- src/lib/Conversations.ts | 254 ++++++++++++++++++---------------- src/lib/PrivatePreferences.ts | 4 + 3 files changed, 169 insertions(+), 124 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1fae6317..25e48c9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -88,9 +88,13 @@ export async function getInboxState( export async function getInboxStates( inboxId: InboxId, refreshFromNetwork: boolean, - inboxIds: InboxId[], + inboxIds: InboxId[] ): Promise { - const inboxStates = await XMTPModule.getInboxStates(inboxId, refreshFromNetwork, inboxIds) + const inboxStates = await XMTPModule.getInboxStates( + inboxId, + refreshFromNetwork, + inboxIds + ) return inboxStates.map((json: string) => { return InboxState.from(json) }) @@ -230,14 +234,16 @@ export async function listGroups< client: Client, opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, - limit?: number | undefined + limit?: number | undefined, + consentState?: ConsentState | undefined ): Promise[]> { return ( await XMTPModule.listGroups( client.inboxId, JSON.stringify(opts), order, - limit + limit, + consentState ) ).map((json: string) => { const group = JSON.parse(json) @@ -255,10 +261,17 @@ export async function listDms< client: Client, opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, - limit?: number | undefined + limit?: number | undefined, + consentState?: ConsentState | undefined ): Promise[]> { return ( - await XMTPModule.listDms(client.inboxId, JSON.stringify(opts), order, limit) + await XMTPModule.listDms( + client.inboxId, + JSON.stringify(opts), + order, + limit, + consentState + ) ).map((json: string) => { const group = JSON.parse(json) @@ -275,14 +288,16 @@ export async function listConversations< client: Client, opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, - limit?: number | undefined + limit?: number | undefined, + consentState?: ConsentState | undefined ): Promise[]> { return ( await XMTPModule.listConversations( client.inboxId, JSON.stringify(opts), order, - limit + limit, + consentState ) ).map((json: string) => { const jsonObj = JSON.parse(json) @@ -854,6 +869,10 @@ export async function processWelcomeMessage< } } +export async function syncConsent(inboxId: InboxId): Promise { + return await XMTPModule.syncConsent(inboxId) +} + export async function setConsentState( inboxId: InboxId, value: string, diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index e585c124..d6e51924 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -13,6 +13,7 @@ import { PermissionPolicySet } from './types/PermissionPolicySet' import * as XMTPModule from '../index' import { Address, + ConsentState, ContentCodec, Conversation, ConversationId, @@ -32,67 +33,6 @@ export default class Conversations< this.client = client } - /** - * Creates a new conversation. - * - * This method creates a new conversation with the specified peer address and context. - * - * @param {Address} peerAddress - The address of the peer to create a conversation with. - * @returns {Promise} A Promise that resolves to a Conversation object. - */ - async newConversation( - peerAddress: Address - ): Promise> { - const checksumAddress = getAddress(peerAddress) - return await XMTPModule.findOrCreateDm(this.client, checksumAddress) - } - - /** - * Creates a new conversation. - * - * This method creates a new conversation with the specified peer address. - * - * @param {Address} peerAddress - The address of the peer to create a conversation with. - * @returns {Promise} A Promise that resolves to a Dm object. - */ - async findOrCreateDm(peerAddress: Address): Promise> { - return await XMTPModule.findOrCreateDm(this.client, peerAddress) - } - - /** - * This method returns a list of all groups that the client is a member of. - * To get the latest list of groups from the network, call syncGroups() first. - * @param {ConversationOptions} opts - The options to specify what fields you want returned for the groups in the list. - * @param {ConversationOrder} order - The order to specify if you want groups listed by last message or by created at. - * @param {number} limit - Limit the number of groups returned in the list. - * - * @returns {Promise} A Promise that resolves to an array of Group objects. - */ - async listGroups( - opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, - limit?: number | undefined - ): Promise[]> { - return await XMTPModule.listGroups(this.client, opts, order, limit) - } - - /** - * This method returns a list of all dms that the client is a member of. - * To get the latest list of dms from the network, call sync() first. - * @param {ConversationOptions} opts - The options to specify what fields you want returned for the dms in the list. - * @param {ConversationOrder} order - The order to specify if you want dms listed by last message or by created at. - * @param {number} limit - Limit the number of dms returned in the list. - * - * @returns {Promise} A Promise that resolves to an array of Dms objects. - */ - async listDms( - opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, - limit?: number | undefined - ): Promise[]> { - return await XMTPModule.listDms(this.client, opts, order, limit) - } - /** * This method returns a group by the group id if that group exists in the local database. * To get the latest list of groups from the network, call sync() first. @@ -165,54 +105,45 @@ export default class Conversations< return await XMTPModule.findMessage(this.client, messageId) } + async fromWelcome( + encryptedMessage: string + ): Promise> { + try { + return await XMTPModule.processWelcomeMessage( + this.client, + encryptedMessage + ) + } catch (e) { + console.info('ERROR in processWelcomeMessage()', e) + throw e + } + } + /** - * This method returns a list of all V3 conversations that the client is a member of. - * To include the latest conversations from the network in the returned list, call sync() first. + * Creates a new conversation. * - * @returns {Promise} A Promise that resolves to an array of Conversation objects. + * This method creates a new conversation with the specified peer address and context. + * + * @param {Address} peerAddress - The address of the peer to create a conversation with. + * @returns {Promise} A Promise that resolves to a Conversation object. */ - async list( - opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, - limit?: number | undefined - ): Promise[]> { - return await XMTPModule.listConversations(this.client, opts, order, limit) + async newConversation( + peerAddress: Address + ): Promise> { + const checksumAddress = getAddress(peerAddress) + return await XMTPModule.findOrCreateDm(this.client, checksumAddress) } /** - * This method streams conversations that the client is a member of. - * @param {type} ConversationType - Whether to stream groups, dms, or both - * @returns {Promise} A Promise that resolves to an array of Conversation objects. + * Creates a new conversation. + * + * This method creates a new conversation with the specified peer address. + * + * @param {Address} peerAddress - The address of the peer to create a conversation with. + * @returns {Promise} A Promise that resolves to a Dm object. */ - async stream( - callback: (conversation: Conversation) => Promise, - type: ConversationType = 'all' - ): Promise { - XMTPModule.subscribeToConversations(this.client.inboxId, type) - const subscription = XMTPModule.emitter.addListener( - EventTypes.Conversation, - async ({ - inboxId, - conversation, - }: { - inboxId: string - conversation: Conversation - }) => { - if (inboxId !== this.client.inboxId) { - return - } - if (conversation.version === ConversationVersion.GROUP) { - return await callback( - new Group(this.client, conversation as unknown as GroupParams) - ) - } else if (conversation.version === ConversationVersion.DM) { - return await callback( - new Dm(this.client, conversation as unknown as DmParams) - ) - } - } - ) - this.subscriptions[EventTypes.Conversation] = subscription + async findOrCreateDm(peerAddress: Address): Promise> { + return await XMTPModule.findOrCreateDm(this.client, peerAddress) } /** @@ -265,6 +196,75 @@ export default class Conversations< ) } + /** + * This method returns a list of all groups that the client is a member of. + * To get the latest list of groups from the network, call syncGroups() first. + * @param {ConversationOptions} opts - The options to specify what fields you want returned for the groups in the list. + * @param {ConversationOrder} order - The order to specify if you want groups listed by last message or by created at. + * @param {number} limit - Limit the number of groups returned in the list. + * + * @returns {Promise} A Promise that resolves to an array of Group objects. + */ + async listGroups( + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined, + consentState?: ConsentState | undefined + ): Promise[]> { + return await XMTPModule.listGroups( + this.client, + opts, + order, + limit, + consentState + ) + } + + /** + * This method returns a list of all dms that the client is a member of. + * To get the latest list of dms from the network, call sync() first. + * @param {ConversationOptions} opts - The options to specify what fields you want returned for the dms in the list. + * @param {ConversationOrder} order - The order to specify if you want dms listed by last message or by created at. + * @param {number} limit - Limit the number of dms returned in the list. + * + * @returns {Promise} A Promise that resolves to an array of Dms objects. + */ + async listDms( + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined, + consentState?: ConsentState | undefined + ): Promise[]> { + return await XMTPModule.listDms( + this.client, + opts, + order, + limit, + consentState + ) + } + + /** + * This method returns a list of all V3 conversations that the client is a member of. + * To include the latest conversations from the network in the returned list, call sync() first. + * + * @returns {Promise} A Promise that resolves to an array of Conversation objects. + */ + async list( + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined, + consentState?: ConsentState | undefined + ): Promise[]> { + return await XMTPModule.listConversations( + this.client, + opts, + order, + limit, + consentState + ) + } + /** * Executes a network request to fetch the latest list of conversations associated with the client * and save them to the local state. @@ -282,6 +282,42 @@ export default class Conversations< return await XMTPModule.syncAllConversations(this.client.inboxId) } + /** + * This method streams conversations that the client is a member of. + * @param {type} ConversationType - Whether to stream groups, dms, or both + * @returns {Promise} A Promise that resolves to an array of Conversation objects. + */ + async stream( + callback: (conversation: Conversation) => Promise, + type: ConversationType = 'all' + ): Promise { + XMTPModule.subscribeToConversations(this.client.inboxId, type) + const subscription = XMTPModule.emitter.addListener( + EventTypes.Conversation, + async ({ + inboxId, + conversation, + }: { + inboxId: string + conversation: Conversation + }) => { + if (inboxId !== this.client.inboxId) { + return + } + if (conversation.version === ConversationVersion.GROUP) { + return await callback( + new Group(this.client, conversation as unknown as GroupParams) + ) + } else if (conversation.version === ConversationVersion.DM) { + return await callback( + new Dm(this.client, conversation as unknown as DmParams) + ) + } + } + ) + this.subscriptions[EventTypes.Conversation] = subscription + } + /** * Listen for new messages in all conversations. * @@ -313,20 +349,6 @@ export default class Conversations< this.subscriptions[EventTypes.Message] = subscription } - async fromWelcome( - encryptedMessage: string - ): Promise> { - try { - return await XMTPModule.processWelcomeMessage( - this.client, - encryptedMessage - ) - } catch (e) { - console.info('ERROR in processWelcomeMessage()', e) - throw e - } - } - /** * Cancels the stream for new conversations. */ diff --git a/src/lib/PrivatePreferences.ts b/src/lib/PrivatePreferences.ts index 04c8e782..f78baca7 100644 --- a/src/lib/PrivatePreferences.ts +++ b/src/lib/PrivatePreferences.ts @@ -39,4 +39,8 @@ export default class PrivatePreferences { consentEntry.permissionType ) } + + async syncConsent(): Promise { + return await XMTPModule.syncConsent(this.client.inboxId) + } } From fbd2da7948aa23018d6a8ea28f4137cffac5ca29 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 15 Nov 2024 07:32:49 -0800 Subject: [PATCH 3/7] do the iOS side --- example/ios/Podfile.lock | 14 ++++----- example/src/tests/conversationTests.ts | 42 ++++++++++++++++++++++++-- ios/XMTPModule.swift | 24 +++++++++++---- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a2405114..533c7ead 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -56,7 +56,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (3.0.0) + - LibXMTP (3.0.1) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (2.0.0): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (3.0.4): + - XMTP (3.0.5): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 3.0.0) + - LibXMTP (= 3.0.1) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 3.0.4) + - XMTP (= 3.0.5) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 4ef99026c3b353bd27195b48580e1bd34d083c3a + LibXMTP: b23a18d05d458fee72f0a96a114b1eb1e6d77d3b Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: dba23b4f3bcee464ca2f7569e1dc05fd9f4c0148 - XMTPReactNative: 117d8a00b063044029c11f50320a6b9c8abcdea7 + XMTP: 7b9105a3549427a294fb991c5892ebec73d2388d + XMTPReactNative: 0862a746eaddb7d643ad4cbdc2727d02863d9a18 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 6c2c5cf6..23731e72 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,5 +1,7 @@ import { Test, assert, createClients, delayToPropogate } from './test-utils' +import RNFS from 'react-native-fs' import { + Client, Conversation, ConversationId, ConversationVersion, @@ -124,13 +126,27 @@ test('can list conversations with params', async () => { assert( boConvosOrderCreated.map((group: any) => group.id).toString() === [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), - `Conversation created at order should be ${[boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString()} but was ${boConvosOrderCreated.map((group: any) => group.id).toString()}` + `Conversation created at order should be ${[ + boGroup1.id, + boGroup2.id, + boDm1.id, + boDm2.id, + ].toString()} but was ${boConvosOrderCreated + .map((group: any) => group.id) + .toString()}` ) assert( boConvosOrderLastMessage.map((group: any) => group.id).toString() === [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), - `Conversation last message order should be ${[boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString()} but was ${boConvosOrderLastMessage.map((group: any) => group.id).toString()}` + `Conversation last message order should be ${[ + boDm1.id, + boGroup2.id, + boDm2.id, + boGroup1.id, + ].toString()} but was ${boConvosOrderLastMessage + .map((group: any) => group.id) + .toString()}` ) const messages = await boConvosOrderLastMessage[0].messages() @@ -474,5 +490,27 @@ test('can streamAllMessages from multiple clients - swapped', async () => { ) } + return true +}) + +test('can sync consent', async () => { + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const directoryExists = await RNFS.exists(dbDirPath) + if (!directoryExists) { + await RNFS.mkdir(dbDirPath) + } + const alix = await Client.createRandom({ + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, + }) + + return true }) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 94f6910a..991998d7 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -385,7 +385,7 @@ public class XMTPModule: Module { AsyncFunction("listGroups") { ( inboxId: String, groupParams: String?, sortOrder: String?, - limit: Int? + limit: Int?, consentState: String? ) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { @@ -395,9 +395,10 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") + let consent = consentState.map { getConsentState($0) } var groupList: [Group] = try await client.conversations.listGroups( - limit: limit, order: order) + limit: limit, order: order, consentState: consent) var results: [String] = [] for group in groupList { @@ -411,7 +412,7 @@ public class XMTPModule: Module { AsyncFunction("listDms") { ( inboxId: String, groupParams: String?, sortOrder: String?, - limit: Int? + limit: Int?, consentState: String? ) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { @@ -421,9 +422,10 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") + let consent = consentState.map { getConsentState($0) } var dmList: [Dm] = try await client.conversations.listDms( - limit: limit, order: order) + limit: limit, order: order, consentState: consent) var results: [String] = [] for dm in dmList { @@ -437,7 +439,7 @@ public class XMTPModule: Module { AsyncFunction("listConversations") { ( inboxId: String, conversationParams: String?, - sortOrder: String?, limit: Int? + sortOrder: String?, limit: Int?, consentState: String? ) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { @@ -447,8 +449,9 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( conversationParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") + let consent = consentState.map { getConsentState($0) } let conversations = try await client.conversations.list( - limit: limit, order: order) + limit: limit, order: order, consentState: consent) var results: [String] = [] for conversation in conversations { @@ -1329,6 +1332,15 @@ public class XMTPModule: Module { conversation, client: client) } + AsyncFunction("syncConsent") { (inboxId: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + + try await client.syncConsent() + } + AsyncFunction("setConsentState") { ( inboxId: String, value: String, entryType: String, From b83f9344d1c1348b26d01e33a819d4a15ff13e63 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 15 Nov 2024 07:55:14 -0800 Subject: [PATCH 4/7] fix: add tests for filtering and consent state --- example/src/tests/conversationTests.ts | 119 ++++++++++++++++++++++++- example/src/tests/dmTests.ts | 69 +++++++++++++- example/src/tests/groupTests.ts | 55 ++++++++++++ 3 files changed, 239 insertions(+), 4 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 23731e72..1c3eb6b1 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,7 +1,9 @@ -import { Test, assert, createClients, delayToPropogate } from './test-utils' import RNFS from 'react-native-fs' + +import { Test, assert, createClients, delayToPropogate } from './test-utils' import { Client, + ConsentListEntry, Conversation, ConversationId, ConversationVersion, @@ -99,6 +101,63 @@ test('can find a dm by address', async () => { return true }) +test('can filter conversations by consent', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync + const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) + + const boConvos = await boClient.conversations.list() + const boConvosFilteredAllowed = await boClient.conversations.list( + {}, + undefined, + undefined, + 'allowed' + ) + const boConvosFilteredUnknown = await boClient.conversations.list( + {}, + undefined, + undefined, + 'unknown' + ) + + assert( + boConvos.length === 4, + `Conversation length should be 4 but was ${boConvos.length}` + ) + + assert( + boConvosFilteredAllowed + .map((conversation: any) => conversation.id) + .toString() === [boGroup1.id, boDm1.id].toString(), + `Conversation allowed should be ${[ + boGroup1.id, + boDm1.id, + ].toString()} but was ${boConvosFilteredAllowed + .map((convo: any) => convo.id) + .toString()}` + ) + + assert( + boConvosFilteredUnknown + .map((conversation: any) => conversation.id) + .toString() === [boGroup2?.id, boDm2?.id].toString(), + `Conversation unknown filter should be ${[ + boGroup2?.id, + boDm2?.id, + ].toString()} but was ${boConvosFilteredUnknown + .map((convo: any) => convo.id) + .toString()}` + ) + + return true +}) + test('can list conversations with params', async () => { const [alixClient, boClient, caroClient] = await createClients(3) @@ -494,16 +553,22 @@ test('can streamAllMessages from multiple clients - swapped', async () => { }) test('can sync consent', async () => { + const [bo] = await createClients(1) const keyBytes = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, ]) // eslint-disable-next-line @typescript-eslint/no-unused-vars const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` const directoryExists = await RNFS.exists(dbDirPath) if (!directoryExists) { await RNFS.mkdir(dbDirPath) } + const directoryExists2 = await RNFS.exists(dbDirPath2) + if (!directoryExists2) { + await RNFS.mkdir(dbDirPath2) + } const alix = await Client.createRandom({ env: 'local', appVersion: 'Testing/0.0.0', @@ -511,6 +576,56 @@ test('can sync consent', async () => { dbDirectory: dbDirPath, }) - + // Create DM conversation + const dm = await alix.conversations.findOrCreateDm(bo.address) + await dm.updateConsent('denied') + const consentState = await dm.consentState() + assert(consentState === 'denied', `Expected 'denied', got ${consentState}`) + + await bo.conversations.sync() + const boDm = await bo.conversations.findConversation(dm.id) + + const alix2 = await Client.createRandom({ + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath2, + }) + + const state = await alix2.inboxState(true) + assert( + state.installations.length === 2, + `Expected 2 installations, got ${state.installations.length}` + ) + + // Sync conversations + await bo.conversations.sync() + if (boDm) await boDm.sync() + await alix.conversations.sync() + await alix2.conversations.sync() + await alix2.preferences.syncConsent() + await alix.conversations.syncAllConversations() + + await delayToPropogate(2000) + await alix2.conversations.syncAllConversations() + await delayToPropogate(2000) + + const dm2 = await alix2.conversations.findConversation(dm.id) + const consentState2 = await dm2?.consentState() + assert(consentState2 === 'denied', `Expected 'denied', got ${consentState2}`) + + await alix2.preferences.setConsentState( + new ConsentListEntry(dm2!.id, 'conversation_id', 'allowed') + ) + + const convoState = await alix2.preferences.conversationConsentState(dm2!.id) + assert(convoState === 'allowed', `Expected 'allowed', got ${convoState}`) + + const updatedConsentState = await dm2?.consentState() + assert( + updatedConsentState === 'allowed', + `Expected 'allowed', got ${updatedConsentState}` + ) + return true }) diff --git a/example/src/tests/dmTests.ts b/example/src/tests/dmTests.ts index 0c10ca24..3182c0d0 100644 --- a/example/src/tests/dmTests.ts +++ b/example/src/tests/dmTests.ts @@ -10,6 +10,61 @@ function test(name: string, perform: () => Promise) { }) } +test('can filter dms by consent', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + await boClient.conversations.newGroup([alixClient.address]) + const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync + const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) + await boClient.conversations.findGroup(otherGroup.id) + + const boConvos = await boClient.conversations.listDms() + const boConvosFilteredAllowed = await boClient.conversations.listDms( + {}, + undefined, + undefined, + 'allowed' + ) + const boConvosFilteredUnknown = await boClient.conversations.listDms( + {}, + undefined, + undefined, + 'unknown' + ) + + assert( + boConvos.length === 2, + `Conversation length should be 2 but was ${boConvos.length}` + ) + + assert( + boConvosFilteredAllowed + .map((conversation: any) => conversation.id) + .toString() === [boDm1.id].toString(), + `Conversation allowed should be ${[ + boDm1.id, + ].toString()} but was ${boConvosFilteredAllowed + .map((convo: any) => convo.id) + .toString()}` + ) + + assert( + boConvosFilteredUnknown + .map((conversation: any) => conversation.id) + .toString() === [boDm2?.id].toString(), + `Conversation unknown filter should be ${[ + boDm2?.id, + ].toString()} but was ${boConvosFilteredUnknown + .map((convo: any) => convo.id) + .toString()}` + ) + + return true +}) + test('can list dms with params', async () => { const [alixClient, boClient, caroClient] = await createClients(3) @@ -37,14 +92,24 @@ test('can list dms with params', async () => { boConvosOrderCreated .map((conversation: any) => conversation.id) .toString() === [boDm1.id, boDm2.id].toString(), - `Conversation created at order should be ${[boDm1.id, boDm2.id].toString()} but was ${boConvosOrderCreated.map((convo: any) => convo.id).toString()}` + `Conversation created at order should be ${[ + boDm1.id, + boDm2.id, + ].toString()} but was ${boConvosOrderCreated + .map((convo: any) => convo.id) + .toString()}` ) assert( boConvosOrderLastMessage .map((conversation: any) => conversation.id) .toString() === [boDm1.id, boDm2.id].toString(), - `Conversation last message order should be ${[boDm1.id, boDm2.id].toString()} but was ${boConvosOrderLastMessage.map((convo: any) => convo.id).toString()}` + `Conversation last message order should be ${[ + boDm1.id, + boDm2.id, + ].toString()} but was ${boConvosOrderLastMessage + .map((convo: any) => convo.id) + .toString()}` ) const messages = await boConvosOrderLastMessage[0].messages() diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 267a96f7..667c3e37 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -671,6 +671,61 @@ test('can stream groups', async () => { return true }) +test('can filter groups by consent', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + await boClient.conversations.findOrCreateDm(alixClient.address) + await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync + await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) + + const boConvos = await boClient.conversations.listGroups() + const boConvosFilteredAllowed = await boClient.conversations.listGroups( + {}, + undefined, + undefined, + 'allowed' + ) + const boConvosFilteredUnknown = await boClient.conversations.listGroups( + {}, + undefined, + undefined, + 'unknown' + ) + + assert( + boConvos.length === 2, + `Conversation length should be 2 but was ${boConvos.length}` + ) + + assert( + boConvosFilteredAllowed + .map((conversation: any) => conversation.id) + .toString() === [boGroup1.id].toString(), + `Conversation allowed should be ${[ + boGroup1.id, + ].toString()} but was ${boConvosFilteredAllowed + .map((convo: any) => convo.id) + .toString()}` + ) + + assert( + boConvosFilteredUnknown + .map((conversation: any) => conversation.id) + .toString() === [boGroup2?.id].toString(), + `Conversation unknown filter should be ${[ + boGroup2?.id, + ].toString()} but was ${boConvosFilteredUnknown + .map((convo: any) => convo.id) + .toString()}` + ) + + return true +}) + test('can list groups with params', async () => { const [alixClient, boClient] = await createClients(2) From 761b111a973f0b56c86f9651cee5b3c26f38a15c Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 15 Nov 2024 08:18:12 -0800 Subject: [PATCH 5/7] fix up the tests --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 2 +- example/src/tests/conversationTests.ts | 16 +++++++------ example/src/tests/dmTests.ts | 2 +- example/src/tests/groupTests.ts | 2 +- ios/XMTPModule.swift | 23 +++++++++++++++---- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 23a88a82..6d980b01 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -175,7 +175,7 @@ class XMTPModule : Module() { val historySyncUrl = authOptions.historySyncUrl ?: when (authOptions.environment) { "production" -> "https://message-history.production.ephemera.network/" - "local" -> "http://0.0.0.0:5558" + "local" -> "http://10.0.2.2:5558" else -> "https://message-history.dev.ephemera.network/" } return ClientOptions( diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 1c3eb6b1..057bd49c 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -8,6 +8,7 @@ import { ConversationId, ConversationVersion, } from '../../../src/index' +import { Wallet } from 'ethers' export const conversationTests: Test[] = [] let counter = 1 @@ -108,7 +109,7 @@ test('can filter conversations by consent', async () => { const otherGroup = await alixClient.conversations.newGroup([boClient.address]) const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) await caroClient.conversations.findOrCreateDm(boClient.address) - await boClient.conversations.sync + await boClient.conversations.sync() const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) @@ -288,12 +289,12 @@ test('can list conversation messages', async () => { assert( boGroupMessages?.length === 3, - `bo conversation lengths should be 4 but was ${boGroupMessages?.length}` + `bo conversation lengths should be 3 but was ${boGroupMessages?.length}` ) assert( - boDmMessages?.length === 3, - `alix conversation lengths should be 3 but was ${boDmMessages?.length}` + boDmMessages?.length === 2, + `alix conversation lengths should be 2 but was ${boDmMessages?.length}` ) return true @@ -569,7 +570,9 @@ test('can sync consent', async () => { if (!directoryExists2) { await RNFS.mkdir(dbDirPath2) } - const alix = await Client.createRandom({ + const alixWallet = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { env: 'local', appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, @@ -585,7 +588,7 @@ test('can sync consent', async () => { await bo.conversations.sync() const boDm = await bo.conversations.findConversation(dm.id) - const alix2 = await Client.createRandom({ + const alix2 = await Client.create(alixWallet, { env: 'local', appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, @@ -605,7 +608,6 @@ test('can sync consent', async () => { await alix2.conversations.sync() await alix2.preferences.syncConsent() await alix.conversations.syncAllConversations() - await delayToPropogate(2000) await alix2.conversations.syncAllConversations() await delayToPropogate(2000) diff --git a/example/src/tests/dmTests.ts b/example/src/tests/dmTests.ts index 3182c0d0..51fd0c36 100644 --- a/example/src/tests/dmTests.ts +++ b/example/src/tests/dmTests.ts @@ -17,7 +17,7 @@ test('can filter dms by consent', async () => { const otherGroup = await alixClient.conversations.newGroup([boClient.address]) const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) await caroClient.conversations.findOrCreateDm(boClient.address) - await boClient.conversations.sync + await boClient.conversations.sync() const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) await boClient.conversations.findGroup(otherGroup.id) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 667c3e37..517e2f0d 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -678,7 +678,7 @@ test('can filter groups by consent', async () => { const otherGroup = await alixClient.conversations.newGroup([boClient.address]) await boClient.conversations.findOrCreateDm(alixClient.address) await caroClient.conversations.findOrCreateDm(boClient.address) - await boClient.conversations.sync + await boClient.conversations.sync() await boClient.conversations.findDmByInboxId(caroClient.inboxId) const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 991998d7..fdcfa1f9 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -395,8 +395,12 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") - let consent = consentState.map { getConsentState($0) } - + let consent: ConsentState? + if let state = consentState { + consent = try getConsentState(state: state) + } else { + consent = nil + } var groupList: [Group] = try await client.conversations.listGroups( limit: limit, order: order, consentState: consent) @@ -422,8 +426,12 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") - let consent = consentState.map { getConsentState($0) } - + let consent: ConsentState? + if let state = consentState { + consent = try getConsentState(state: state) + } else { + consent = nil + } var dmList: [Dm] = try await client.conversations.listDms( limit: limit, order: order, consentState: consent) @@ -449,7 +457,12 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( conversationParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") - let consent = consentState.map { getConsentState($0) } + let consent: ConsentState? + if let state = consentState { + consent = try getConsentState(state: state) + } else { + consent = nil + } let conversations = try await client.conversations.list( limit: limit, order: order, consentState: consent) From d1d696e28df5591b9a99d0b4c6e1c3fbe44d211c Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 15 Nov 2024 10:44:44 -0800 Subject: [PATCH 6/7] update to account for additional sync group in sync --- example/src/tests/conversationTests.ts | 83 -------------------------- example/src/tests/groupTests.ts | 22 ++++--- 2 files changed, 13 insertions(+), 92 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 057bd49c..34ff5231 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,14 +1,10 @@ -import RNFS from 'react-native-fs' import { Test, assert, createClients, delayToPropogate } from './test-utils' import { - Client, - ConsentListEntry, Conversation, ConversationId, ConversationVersion, } from '../../../src/index' -import { Wallet } from 'ethers' export const conversationTests: Test[] = [] let counter = 1 @@ -552,82 +548,3 @@ test('can streamAllMessages from multiple clients - swapped', async () => { return true }) - -test('can sync consent', async () => { - const [bo] = await createClients(1) - const keyBytes = new Uint8Array([ - 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, - 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, - ]) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` - const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` - const directoryExists = await RNFS.exists(dbDirPath) - if (!directoryExists) { - await RNFS.mkdir(dbDirPath) - } - const directoryExists2 = await RNFS.exists(dbDirPath2) - if (!directoryExists2) { - await RNFS.mkdir(dbDirPath2) - } - const alixWallet = Wallet.createRandom() - - const alix = await Client.create(alixWallet, { - env: 'local', - appVersion: 'Testing/0.0.0', - dbEncryptionKey: keyBytes, - dbDirectory: dbDirPath, - }) - - // Create DM conversation - const dm = await alix.conversations.findOrCreateDm(bo.address) - await dm.updateConsent('denied') - const consentState = await dm.consentState() - assert(consentState === 'denied', `Expected 'denied', got ${consentState}`) - - await bo.conversations.sync() - const boDm = await bo.conversations.findConversation(dm.id) - - const alix2 = await Client.create(alixWallet, { - env: 'local', - appVersion: 'Testing/0.0.0', - dbEncryptionKey: keyBytes, - dbDirectory: dbDirPath2, - }) - - const state = await alix2.inboxState(true) - assert( - state.installations.length === 2, - `Expected 2 installations, got ${state.installations.length}` - ) - - // Sync conversations - await bo.conversations.sync() - if (boDm) await boDm.sync() - await alix.conversations.sync() - await alix2.conversations.sync() - await alix2.preferences.syncConsent() - await alix.conversations.syncAllConversations() - await delayToPropogate(2000) - await alix2.conversations.syncAllConversations() - await delayToPropogate(2000) - - const dm2 = await alix2.conversations.findConversation(dm.id) - const consentState2 = await dm2?.consentState() - assert(consentState2 === 'denied', `Expected 'denied', got ${consentState2}`) - - await alix2.preferences.setConsentState( - new ConsentListEntry(dm2!.id, 'conversation_id', 'allowed') - ) - - const convoState = await alix2.preferences.conversationConsentState(dm2!.id) - assert(convoState === 'allowed', `Expected 'allowed', got ${convoState}`) - - const updatedConsentState = await dm2?.consentState() - assert( - updatedConsentState === 'allowed', - `Expected 'allowed', got ${updatedConsentState}` - ) - - return true -}) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 517e2f0d..e8b04e71 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -1,5 +1,4 @@ import { Wallet } from 'ethers' -import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' import { Test, @@ -15,6 +14,7 @@ import { GroupUpdatedContent, GroupUpdatedCodec, ConsentListEntry, + DecodedMessage, } from '../../../src/index' export const groupTests: Test[] = [] @@ -751,13 +751,17 @@ test('can list groups with params', async () => { assert( boGroupsOrderCreated.map((group: any) => group.id).toString() === [boGroup1.id, boGroup2.id].toString(), - `Group order should be group1 then group2 but was ${boGroupsOrderCreated.map((group: any) => group.id).toString()}` + `Group order should be group1 then group2 but was ${boGroupsOrderCreated + .map((group: any) => group.id) + .toString()}` ) assert( boGroupsOrderLastMessage.map((group: any) => group.id).toString() === [boGroup2.id, boGroup1.id].toString(), - `Group order should be group2 then group1 but was ${boGroupsOrderLastMessage.map((group: any) => group.id).toString()}` + `Group order should be group2 then group1 but was ${boGroupsOrderLastMessage + .map((group: any) => group.id) + .toString()}` ) const messages = await boGroupsOrderLastMessage[0].messages() @@ -1502,8 +1506,8 @@ test('can sync all groups', async () => { `messages should be 4 after sync but was ${boGroup?.messages?.length}` ) assert( - numGroupsSynced === 50, - `should have synced 50 groups but synced ${numGroupsSynced}` + numGroupsSynced === 51, + `should have synced 51 groups but synced ${numGroupsSynced}` ) for (const group of groups) { @@ -1513,15 +1517,15 @@ test('can sync all groups', async () => { // First syncAllConversations after removal will still sync each group to set group inactive const numGroupsSynced2 = await bo.conversations.syncAllConversations() assert( - numGroupsSynced2 === 50, - `should have synced 50 groups but synced ${numGroupsSynced2}` + numGroupsSynced2 === 51, + `should have synced 51 groups but synced ${numGroupsSynced2}` ) // Next syncAllConversations will not sync inactive groups const numGroupsSynced3 = await bo.conversations.syncAllConversations() assert( - numGroupsSynced3 === 0, - `should have synced 0 groups but synced ${numGroupsSynced3}` + numGroupsSynced3 === 1, + `should have synced 1 groups but synced ${numGroupsSynced3}` ) return true }) From b0fd04a5acd490497dd5c8fb605ce6babb80c73f Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 15 Nov 2024 11:07:50 -0800 Subject: [PATCH 7/7] fix up small lint issue --- example/src/tests/conversationTests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 34ff5231..5012c14c 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,4 +1,3 @@ - import { Test, assert, createClients, delayToPropogate } from './test-utils' import { Conversation,