From 5bb7b9837214f17d22f4ac73dc2244f11bb11cf7 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 16 Jan 2025 22:53:42 -0800 Subject: [PATCH 01/10] consent filtering by array --- android/build.gradle | 18 ++++----- .../modules/xmtpreactnativesdk/XMTPModule.kt | 39 +++++++++++-------- .../wrappers/PermissionPolicySetWrapper.kt | 4 +- example/src/tests/groupTests.ts | 4 +- src/index.ts | 4 +- src/lib/Conversations.ts | 4 +- 6 files changed, 40 insertions(+), 33 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 2deea96b..c0016834 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,19 +98,19 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:3.0.21" + implementation "org.xmtp:android:3.0.22" 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" // xmtp-android local testing setup below (comment org.xmtp:android above) // implementation files('/xmtp-android/library/build/outputs/aar/library-debug.aar') // implementation 'com.google.crypto.tink:tink-android:1.8.0' - // implementation 'io.grpc:grpc-kotlin-stub:1.4.1' - // implementation 'io.grpc:grpc-okhttp:1.62.2' - // implementation 'io.grpc:grpc-protobuf-lite:1.62.2' - // implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0' - // implementation 'org.web3j:crypto:4.9.4' - // implementation "net.java.dev.jna:jna:5.14.0@aar" - // api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3' - // api 'org.xmtp:proto-kotlin:3.71.0' + // implementation 'io.grpc:grpc-kotlin-stub:1.4.1' + // implementation 'io.grpc:grpc-okhttp:1.62.2' + // implementation 'io.grpc:grpc-protobuf-lite:1.62.2' + // implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0' + // implementation 'org.web3j:crypto:4.9.4' + // implementation "net.java.dev.jna:jna:5.14.0@aar" + // api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3' + // api 'org.xmtp:proto-kotlin:3.72.4' } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 2e98beed..c678664d 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -176,11 +176,6 @@ class XMTPModule : Module() { a.apply { set(i, v.toByte()) } } val historySyncUrl = authOptions.historySyncUrl - ?: when (authOptions.environment) { - "production" -> "https://message-history.production.ephemera.network/" - "local" -> "http://10.0.2.2:5558" - else -> "https://message-history.dev.ephemera.network/" - } return ClientOptions( api = apiEnvironments(authOptions.environment, authOptions.appVersion), preAuthenticateToInboxCallback = preAuthenticateToInboxCallback, @@ -552,15 +547,15 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentState: String? -> + AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentStates: List? -> withContext(Dispatchers.IO) { logV("listGroups") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") - val consent = consentState?.let { getConsentState(it) } + val consentList = consentStates?.let { getConsentStates(it) } val groups = client.conversations.listGroups( limit = limit, - consentState = consent + consentStates = consentList ) groups.map { group -> GroupWrapper.encode(client, group, params) @@ -568,15 +563,15 @@ class XMTPModule : Module() { } } - AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentState: String? -> + AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentStates: List? -> withContext(Dispatchers.IO) { logV("listDms") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") - val consent = consentState?.let { getConsentState(it) } + val consentList = consentStates?.let { getConsentStates(it) } val dms = client.conversations.listDms( limit = limit, - consentState = consent + consentStates = consentList ) dms.map { dm -> DmWrapper.encode(client, dm, params) @@ -584,15 +579,15 @@ class XMTPModule : Module() { } } - AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, limit: Int?, consentState: String? -> + AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, limit: Int?, consentStates: List? -> withContext(Dispatchers.IO) { logV("listConversations") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "") - val consent = consentState?.let { getConsentState(it) } + val consentList = consentStates?.let { getConsentStates(it) } val conversations = - client.conversations.list(limit = limit, consentState = consent) + client.conversations.list(limit = limit, consentStates = consentList) conversations.map { conversation -> ConversationWrapper.encode(client, conversation, params) } @@ -860,13 +855,13 @@ class XMTPModule : Module() { } } - AsyncFunction("syncAllConversations") Coroutine { installationId: String, consentState: String? -> + AsyncFunction("syncAllConversations") Coroutine { installationId: String, consentStates: List? -> withContext(Dispatchers.IO) { logV("syncAllConversations") val client = clients[installationId] ?: throw XMTPException("No client") - val consent = consentState?.let { getConsentState(it) } + val consentList = consentStates?.let { getConsentStates(it) } val numGroupsSyncedInt: Int = - client.conversations.syncAllConversations(consent).toInt() + client.conversations.syncAllConversations(consentList).toInt() numGroupsSyncedInt } } @@ -1443,6 +1438,16 @@ class XMTPModule : Module() { } } + private fun getConsentStates(stateStrings: List): List { + return stateStrings.map { stateString -> + when (stateString) { + "allowed" -> ConsentState.ALLOWED + "denied" -> ConsentState.DENIED + else -> ConsentState.UNKNOWN + } + } + } + private fun getEntryType(entryString: String): EntryType { return when (entryString) { "address" -> EntryType.ADDRESS diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt index 176b86bb..d2a153c3 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt @@ -37,6 +37,7 @@ class PermissionPolicySetWrapper { "updateGroupDescriptionPolicy" to fromPermissionOption(policySet.updateGroupDescriptionPolicy), "updateGroupImagePolicy" to fromPermissionOption(policySet.updateGroupImagePolicy), "updateGroupPinnedFrameUrlPolicy" to fromPermissionOption(policySet.updateGroupPinnedFrameUrlPolicy), + "updateMessageExpirationPolicy" to fromPermissionOption(policySet.updateMessageExpirationPolicy), ) } @@ -50,7 +51,8 @@ class PermissionPolicySetWrapper { updateGroupNamePolicy = createPermissionOptionFromString(jsonObj.get("updateGroupNamePolicy").asString), updateGroupDescriptionPolicy = createPermissionOptionFromString(jsonObj.get("updateGroupDescriptionPolicy").asString), updateGroupImagePolicy = createPermissionOptionFromString(jsonObj.get("updateGroupImagePolicy").asString), - updateGroupPinnedFrameUrlPolicy = createPermissionOptionFromString(jsonObj.get("updateGroupPinnedFrameUrlPolicy").asString) + updateGroupPinnedFrameUrlPolicy = createPermissionOptionFromString(jsonObj.get("updateGroupPinnedFrameUrlPolicy").asString), + updateMessageExpirationPolicy = createPermissionOptionFromString(jsonObj.get("updateMessageExpirationPolicy").asString), ) } diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 0dc934bf..df218e9e 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -974,12 +974,12 @@ test('can filter groups by consent', async () => { const boConvosFilteredAllowed = await boClient.conversations.listGroups( {}, undefined, - 'allowed' + ['allowed'] ) const boConvosFilteredUnknown = await boClient.conversations.listGroups( {}, undefined, - 'unknown' + ['unknown'] ) assert( diff --git a/src/index.ts b/src/index.ts index 138befef..f7987ff1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -352,14 +352,14 @@ export async function listGroups< clientInstallationId: InstallationId, opts?: ConversationOptions | undefined, limit?: number | undefined, - consentState?: ConsentState | undefined + consentStates?: ConsentState[] | undefined ): Promise[]> { return ( await XMTPModule.listGroups( clientInstallationId, JSON.stringify(opts), limit, - consentState + consentStates ) ).map((json: string) => { const group = JSON.parse(json) diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 15f0651e..2b716795 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -219,13 +219,13 @@ export default class Conversations< async listGroups( opts?: ConversationOptions | undefined, limit?: number | undefined, - consentState?: ConsentState | undefined + consentStates?: ConsentState[] | undefined ): Promise[]> { return await XMTPModule.listGroups( this.client.installationId, opts, limit, - consentState + consentStates ) } From 16783dbdf6f2e3cdad836aa74e8acf32e4d02994 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 16 Jan 2025 23:31:40 -0800 Subject: [PATCH 02/10] ios side of consent filtering by array --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 24 ++++---- example/ios/Podfile.lock | 16 ++--- example/src/tests/groupPermissionsTests.ts | 1 + ios/Wrappers/PermissionPolicySetWrapper.swift | 6 +- ios/XMTPModule.swift | 61 +++++++++++-------- ios/XMTPReactNative.podspec | 2 +- src/lib/types/PermissionPolicySet.ts | 1 + 7 files changed, 64 insertions(+), 47 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index c678664d..d8691e38 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -547,15 +547,15 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentStates: List? -> + AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentStringStates: List? -> withContext(Dispatchers.IO) { logV("listGroups") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") - val consentList = consentStates?.let { getConsentStates(it) } + val consentStates = consentStringStates?.let { getConsentStates(it) } val groups = client.conversations.listGroups( limit = limit, - consentStates = consentList + consentStates = consentStates ) groups.map { group -> GroupWrapper.encode(client, group, params) @@ -563,15 +563,15 @@ class XMTPModule : Module() { } } - AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentStates: List? -> + AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentStringStates: List? -> withContext(Dispatchers.IO) { logV("listDms") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") - val consentList = consentStates?.let { getConsentStates(it) } + val consentStates = consentStringStates?.let { getConsentStates(it) } val dms = client.conversations.listDms( limit = limit, - consentStates = consentList + consentStates = consentStates ) dms.map { dm -> DmWrapper.encode(client, dm, params) @@ -579,15 +579,15 @@ class XMTPModule : Module() { } } - AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, limit: Int?, consentStates: List? -> + AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, limit: Int?, consentStringStates: List? -> withContext(Dispatchers.IO) { logV("listConversations") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "") - val consentList = consentStates?.let { getConsentStates(it) } + val consentStates = consentStringStates?.let { getConsentStates(it) } val conversations = - client.conversations.list(limit = limit, consentStates = consentList) + client.conversations.list(limit = limit, consentStates = consentStates) conversations.map { conversation -> ConversationWrapper.encode(client, conversation, params) } @@ -855,13 +855,13 @@ class XMTPModule : Module() { } } - AsyncFunction("syncAllConversations") Coroutine { installationId: String, consentStates: List? -> + AsyncFunction("syncAllConversations") Coroutine { installationId: String, consentStringStates: List? -> withContext(Dispatchers.IO) { logV("syncAllConversations") val client = clients[installationId] ?: throw XMTPException("No client") - val consentList = consentStates?.let { getConsentStates(it) } + val consentStates = consentStringStates?.let { getConsentStates(it) } val numGroupsSyncedInt: Int = - client.conversations.syncAllConversations(consentList).toInt() + client.conversations.syncAllConversations(consentStates).toInt() numGroupsSyncedInt } } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 13ba46d0..71ba272e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -55,7 +55,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (3.0.18) + - LibXMTP (3.0.20) - MessagePacker (0.4.7) - MMKV (2.0.2): - MMKVCore (~> 2.0.2) @@ -448,18 +448,18 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.28.2) - - XMTP (3.0.23): + - XMTP (3.0.24): - Connect-Swift (= 1.0.0) - CryptoSwift (= 1.8.3) - CSecp256k1 (~> 0.2) - - LibXMTP (= 3.0.18) + - LibXMTP (= 3.0.20) - SQLCipher (= 4.5.7) - - XMTPReactNative (3.1.6): + - XMTPReactNative (3.1.7): - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 3.0.23) + - XMTP (= 3.0.24) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 09d80c1ef92a6fd195a13b2e0911259bf87005e3 + LibXMTP: 5c8a858675af142e4a5336e27bb2d6fa6d633b18 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 3eacda84cd1c4fc95cf848d3ecb69d85ed56006c MMKVCore: 508b4d3a8ce031f1b5c8bd235f0517fb3f4c73a9 @@ -762,8 +762,8 @@ SPEC CHECKSUMS: RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d - XMTP: 645cb707d5d779b896d00b5f547227325a7379a7 - XMTPReactNative: 9b25afc1fb008aa92d83d957263e8a14507efefc + XMTP: 3d82fe8598fe043a67ad93276084ab46a30a1fa3 + XMTPReactNative: cd9144370b01abf420c74dd773a82ca0c1a8633b Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index b3fa476c..983b8157 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -531,6 +531,7 @@ test('can create a group with custom permissions', async () => { updateGroupDescriptionPolicy: 'allow', updateGroupImagePolicy: 'admin', updateGroupPinnedFrameUrlPolicy: 'deny', + updateMessageExpirationPolicy: 'deny', } // Bo creates a group with Alix and Caro with custom permissions diff --git a/ios/Wrappers/PermissionPolicySetWrapper.swift b/ios/Wrappers/PermissionPolicySetWrapper.swift index f3d81cd7..b4ff5dac 100644 --- a/ios/Wrappers/PermissionPolicySetWrapper.swift +++ b/ios/Wrappers/PermissionPolicySetWrapper.swift @@ -50,7 +50,8 @@ class PermissionPolicySetWrapper { "updateGroupNamePolicy": fromPermissionOption(policySet.updateGroupNamePolicy), "updateGroupDescriptionPolicy": fromPermissionOption(policySet.updateGroupDescriptionPolicy), "updateGroupImagePolicy": fromPermissionOption(policySet.updateGroupImagePolicy), - "updateGroupPinnedFrameUrlPolicy": fromPermissionOption(policySet.updateGroupPinnedFrameUrlPolicy) + "updateGroupPinnedFrameUrlPolicy": fromPermissionOption(policySet.updateGroupPinnedFrameUrlPolicy), + "updateMessageExpirationPolicy": fromPermissionOption(policySet.updateMessageExpirationPolicy) ] } public static func createPermissionPolicySet(from json: String) throws -> PermissionPolicySet { @@ -71,7 +72,8 @@ class PermissionPolicySetWrapper { updateGroupNamePolicy: createPermissionOption(from: jsonDict["updateGroupNamePolicy"] as? String ?? ""), updateGroupDescriptionPolicy: createPermissionOption(from: jsonDict["updateGroupDescriptionPolicy"] as? String ?? ""), updateGroupImagePolicy: createPermissionOption(from: jsonDict["updateGroupImagePolicy"] as? String ?? ""), - updateGroupPinnedFrameUrlPolicy: createPermissionOption(from: jsonDict["updateGroupPinnedFrameUrlPolicy"] as? String ?? "") + updateGroupPinnedFrameUrlPolicy: createPermissionOption(from: jsonDict["updateGroupPinnedFrameUrlPolicy"] as? String ?? ""), + updateMessageExpirationPolicy: createPermissionOption(from: jsonDict["updateMessageExpirationPolicy"] as? String ?? "") ) } diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 7f8ff56d..f1702ad8 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -516,7 +516,7 @@ public class XMTPModule: Module { AsyncFunction("listGroups") { ( installationId: String, groupParams: String?, - limit: Int?, consentState: String? + limit: Int?, consentStringStates: [String]? ) -> [String] in guard let client = await clientsManager.getClient(key: installationId) @@ -526,14 +526,14 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") - let consent: ConsentState? - if let state = consentState { - consent = try getConsentState(state: state) + let consentStates: [ConsentState]? + if let states = consentStringStates { + consentStates = try getConsentStates(states: states) } else { - consent = nil + consentStates = nil } var groupList: [Group] = try await client.conversations.listGroups( - limit: limit, consentState: consent) + limit: limit, consentStates: consentStates) var results: [String] = [] for group in groupList { @@ -547,7 +547,7 @@ public class XMTPModule: Module { AsyncFunction("listDms") { ( installationId: String, groupParams: String?, - limit: Int?, consentState: String? + limit: Int?, consentStringStates: [String]? ) -> [String] in guard let client = await clientsManager.getClient(key: installationId) @@ -557,14 +557,14 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") - let consent: ConsentState? - if let state = consentState { - consent = try getConsentState(state: state) + let consentStates: [ConsentState]? + if let states = consentStringStates { + consentStates = try getConsentStates(states: states) } else { - consent = nil + consentStates = nil } var dmList: [Dm] = try await client.conversations.listDms( - limit: limit, consentState: consent) + limit: limit, consentStates: consentStates) var results: [String] = [] for dm in dmList { @@ -578,7 +578,7 @@ public class XMTPModule: Module { AsyncFunction("listConversations") { ( installationId: String, conversationParams: String?, - limit: Int?, consentState: String? + limit: Int?, consentStringStates: [String]? ) -> [String] in guard let client = await clientsManager.getClient(key: installationId) @@ -588,14 +588,14 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( conversationParams ?? "") - let consent: ConsentState? - if let state = consentState { - consent = try getConsentState(state: state) + let consentStates: [ConsentState]? + if let states = consentStringStates { + consentStates = try getConsentStates(states: states) } else { - consent = nil + consentStates = nil } let conversations = try await client.conversations.list( - limit: limit, consentState: consent) + limit: limit, consentStates: consentStates) var results: [String] = [] for conversation in conversations { @@ -1021,20 +1021,20 @@ public class XMTPModule: Module { } AsyncFunction("syncAllConversations") { - (installationId: String, consentState: String?) -> UInt32 in + (installationId: String, consentStringStates: [String]?) -> UInt32 in guard let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } - let consent: ConsentState? - if let state = consentState { - consent = try getConsentState(state: state) + let consentStates: [ConsentState]? + if let states = consentStringStates { + consentStates = try getConsentStates(states: states) } else { - consent = nil + consentStates = nil } return try await client.conversations.syncAllConversations( - consentState: consent) + consentStates: consentStates) } AsyncFunction("syncConversation") { @@ -1911,6 +1911,19 @@ public class XMTPModule: Module { } } + private func getConsentStates(states: [String]) throws -> [ConsentState] { + return states.map { state in + switch state { + case "allowed": + return .allowed + case "denied": + return .denied + default: + return .unknown + } + } + } + private func getEntryType(type: String) throws -> EntryType { switch type { case "inbox_id": diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 5b5c516b..a90ee985 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.23" + s.dependency "XMTP", "= 3.0.24" s.dependency 'CSecp256k1', '~> 0.2' s.dependency "SQLCipher", "= 4.5.7" end diff --git a/src/lib/types/PermissionPolicySet.ts b/src/lib/types/PermissionPolicySet.ts index 55811c80..01a21e54 100644 --- a/src/lib/types/PermissionPolicySet.ts +++ b/src/lib/types/PermissionPolicySet.ts @@ -15,4 +15,5 @@ export type PermissionPolicySet = { updateGroupDescriptionPolicy: PermissionOption updateGroupImagePolicy: PermissionOption updateGroupPinnedFrameUrlPolicy: PermissionOption + updateMessageExpirationPolicy: PermissionOption } From 1807b972126868a81f857cf00749f41fd6152590 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 17 Jan 2025 08:38:08 -0800 Subject: [PATCH 03/10] do android side of inboxId work --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index d8691e38..7cf694df 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -771,6 +771,15 @@ class XMTPModule : Module() { } } + AsyncFunction("findOrCreateDmWithInboxId") Coroutine { installationId: String, peerInboxId: String -> + withContext(Dispatchers.IO) { + logV("findOrCreateDmWithInboxId") + val client = clients[installationId] ?: throw XMTPException("No client") + val dm = client.conversations.findOrCreateDmWithInboxId(peerInboxId) + DmWrapper.encode(client, dm) + } + } + AsyncFunction("createGroup") Coroutine { installationId: String, peerAddresses: List, permission: String, groupOptionsJson: String -> withContext(Dispatchers.IO) { logV("createGroup") @@ -815,6 +824,49 @@ class XMTPModule : Module() { } } + AsyncFunction("createGroupWithInboxIds") Coroutine { installationId: String, inboxIds: List, permission: String, groupOptionsJson: String -> + withContext(Dispatchers.IO) { + logV("createGroupWithInboxIds") + val client = clients[installationId] ?: throw XMTPException("No client") + val permissionLevel = when (permission) { + "admin_only" -> GroupPermissionPreconfiguration.ADMIN_ONLY + else -> GroupPermissionPreconfiguration.ALL_MEMBERS + } + val createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) + val group = client.conversations.newGroupWithInboxIds( + inboxIds, + permissionLevel, + createGroupParams.groupName, + createGroupParams.groupImageUrlSquare, + createGroupParams.groupDescription, + createGroupParams.groupPinnedFrameUrl + ) + GroupWrapper.encode(client, group) + } + } + + AsyncFunction("createGroupCustomPermissionsWithInboxIds") Coroutine { installationId: String, inboxIds: List, permissionPolicySetJson: String, groupOptionsJson: String -> + withContext(Dispatchers.IO) { + logV("createGroupCustomPermissionsWithInboxIds") + val client = clients[installationId] ?: throw XMTPException("No client") + val createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) + val permissionPolicySet = + PermissionPolicySetWrapper.createPermissionPolicySetFromJson( + permissionPolicySetJson + ) + val group = client.conversations.newGroupCustomPermissionsWithInboxIds( + inboxIds, + permissionPolicySet, + createGroupParams.groupName, + createGroupParams.groupImageUrlSquare, + createGroupParams.groupDescription, + createGroupParams.groupPinnedFrameUrl + ) + GroupWrapper.encode(client, group) + } + } AsyncFunction("listMemberInboxIds") Coroutine { installationId: String, id: String -> withContext(Dispatchers.IO) { @@ -1440,11 +1492,7 @@ class XMTPModule : Module() { private fun getConsentStates(stateStrings: List): List { return stateStrings.map { stateString -> - when (stateString) { - "allowed" -> ConsentState.ALLOWED - "denied" -> ConsentState.DENIED - else -> ConsentState.UNKNOWN - } + getConsentState(stateString) } } From e9308b48ce79b91dd0e7db8f90f099f6cee44873 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 17 Jan 2025 08:47:22 -0800 Subject: [PATCH 04/10] do the ios side --- ios/XMTPModule.swift | 100 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index f1702ad8..dfa85a06 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -877,6 +877,24 @@ public class XMTPModule: Module { throw error } } + + AsyncFunction("findOrCreateDmWithInboxId") { + (installationId: String, peerInboxId: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + + do { + let dm = try await client.conversations.findOrCreateDmWithInboxId( + with: peerInboxId) + return try await DmWrapper.encode(dm, client: client) + } catch { + print("ERRRO!: \(error.localizedDescription)") + throw error + } + } AsyncFunction("createGroup") { ( @@ -948,6 +966,77 @@ public class XMTPModule: Module { throw error } } + + AsyncFunction("createGroupWithInboxIds") { + ( + installationId: String, inboxIds: [String], + permission: String, + groupOptionsJson: String + ) -> String in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + let permissionLevel: GroupPermissionPreconfiguration = { + switch permission { + case "admin_only": + return .adminOnly + default: + return .allMembers + } + }() + do { + let createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson( + groupOptionsJson) + let group = try await client.conversations.newGroupWithInboxIds( + with: inboxIds, + permissions: permissionLevel, + name: createGroupParams.groupName, + imageUrlSquare: createGroupParams.groupImageUrlSquare, + description: createGroupParams.groupDescription, + pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl + ) + return try await GroupWrapper.encode(group, client: client) + } catch { + print("ERRRO!: \(error.localizedDescription)") + throw error + } + } + + AsyncFunction("createGroupCustomPermissionsWithInboxIds") { + ( + installationId: String, inboxIds: [String], + permissionPolicySetJson: String, groupOptionsJson: String + ) -> String in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + do { + let createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson( + groupOptionsJson) + let permissionPolicySet = + try PermissionPolicySetWrapper.createPermissionPolicySet( + from: permissionPolicySetJson) + let group = try await client.conversations + .newGroupCustomPermissionsWithInboxIds( + with: inboxIds, + permissionPolicySet: permissionPolicySet, + name: createGroupParams.groupName, + imageUrlSquare: createGroupParams.groupImageUrlSquare, + description: createGroupParams.groupDescription, + pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl + ) + return try await GroupWrapper.encode(group, client: client) + } catch { + print("ERRRO!: \(error.localizedDescription)") + throw error + } + } AsyncFunction("listMemberInboxIds") { (installationId: String, groupId: String) -> [String] in @@ -1912,15 +2001,8 @@ public class XMTPModule: Module { } private func getConsentStates(states: [String]) throws -> [ConsentState] { - return states.map { state in - switch state { - case "allowed": - return .allowed - case "denied": - return .denied - default: - return .unknown - } + return try states.map { state in + try getConsentState(state: state) } } From a9fd95494389548cdebcb15b512dbad795ec9a41 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 17 Jan 2025 08:54:05 -0800 Subject: [PATCH 05/10] do the RN side --- src/index.ts | 73 ++++++++++++++++++++++++++++++++++++++++ src/lib/Conversations.ts | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/src/index.ts b/src/index.ts index f7987ff1..5bca2be5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -653,6 +653,21 @@ export async function findOrCreateDm< return new Dm(clientInstallationId, dm) } +export async function findOrCreateDmWithInboxId< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + clientInstallationId: InstallationId, + peerInboxId: InboxId +): Promise> { + const dm = JSON.parse( + await XMTPModule.findOrCreateDmWithInboxId( + clientInstallationId, + peerInboxId + ) + ) + return new Dm(clientInstallationId, dm) +} + export async function createGroup< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( @@ -682,6 +697,64 @@ export async function createGroup< return new Group(clientInstallationId, group) } +export async function createGroupCustomPermissionsWithInboxIds< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + clientInstallationId: InstallationId, + inboxIds: InboxId[], + permissionPolicySet: PermissionPolicySet, + name: string = '', + imageUrlSquare: string = '', + description: string = '', + pinnedFrameUrl: string = '' +): Promise> { + const options: CreateGroupParams = { + name, + imageUrlSquare, + description, + pinnedFrameUrl, + } + const group = JSON.parse( + await XMTPModule.createGroupCustomPermissionsWithInboxIds( + clientInstallationId, + inboxIds, + JSON.stringify(permissionPolicySet), + JSON.stringify(options) + ) + ) + + return new Group(clientInstallationId, group) +} + +export async function createGroupWithInboxIds< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + clientInstallationId: InstallationId, + inboxIds: InboxId[], + permissionLevel: 'all_members' | 'admin_only' = 'all_members', + name: string = '', + imageUrlSquare: string = '', + description: string = '', + pinnedFrameUrl: string = '' +): Promise> { + const options: CreateGroupParams = { + name, + imageUrlSquare, + description, + pinnedFrameUrl, + } + const group = JSON.parse( + await XMTPModule.createGroupWithInboxIds( + clientInstallationId, + inboxIds, + permissionLevel, + JSON.stringify(options) + ) + ) + + return new Group(clientInstallationId, group) +} + export async function createGroupCustomPermissions< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 2b716795..023eb557 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -158,6 +158,21 @@ export default class Conversations< ) } + /** + * Creates a new conversation. + * + * This method creates a new conversation with the specified peer inboxId. + * + * @param {InboxId} peerInboxId - The inboxId of the peer to create a conversation with. + * @returns {Promise} A Promise that resolves to a Dm object. + */ + async findOrCreateDmWithInboxId(peerInboxId: InboxId): Promise> { + return await XMTPModule.findOrCreateDmWithInboxId( + this.client.installationId, + peerInboxId + ) + } + /** * Creates a new group. * @@ -208,6 +223,56 @@ export default class Conversations< ) } + /** + * Creates a new group. + * + * This method creates a new group with the specified peer inboxIds and options. + * + * @param {InboxId[]} peerInboxIds - The inboxIds of the peers to create a group with. + * @param {CreateGroupOptions} opts - The options to use for the group. + * @returns {Promise>} A Promise that resolves to a Group object. + */ + async newGroupWithInboxIds( + peerInboxIds: InboxId[], + opts?: CreateGroupOptions | undefined + ): Promise> { + return await XMTPModule.createGroupWithInboxIds( + this.client.installationId, + peerInboxIds, + opts?.permissionLevel, + opts?.name, + opts?.imageUrlSquare, + opts?.description, + opts?.pinnedFrameUrl + ) + } + + /** + * Creates a new group with custom permissions. + * + * This method creates a new group with the specified peer inboxIds and options. + * + * @param {InboxId[]} peerInboxIds - The inboxIds of the peers to create a group with. + * @param {PermissionPolicySet} permissionPolicySet - The permission policy set to use for the group. + * @param {CreateGroupOptions} opts - The options to use for the group. + * @returns {Promise>} A Promise that resolves to a Group object. + */ + async newGroupCustomPermissionsWithInboxIds( + peerInboxIds: InboxId[], + permissionPolicySet: PermissionPolicySet, + opts?: CreateGroupOptions | undefined + ): Promise> { + return await XMTPModule.createGroupCustomPermissionsWithInboxIds( + this.client.installationId, + peerInboxIds, + permissionPolicySet, + opts?.name, + opts?.imageUrlSquare, + opts?.description, + opts?.pinnedFrameUrl + ) + } + /** * 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. From d73e55d6dd18ccbea5150509a8858dfada1cd444 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 17 Jan 2025 08:56:46 -0800 Subject: [PATCH 06/10] update all list functions and sync functions --- src/index.ts | 12 ++++++------ src/lib/Conversations.ts | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5bca2be5..41c2d57d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -377,14 +377,14 @@ export async function listDms< clientInstallationId: InstallationId, opts?: ConversationOptions | undefined, limit?: number | undefined, - consentState?: ConsentState | undefined + consentStates?: ConsentState[] | undefined ): Promise[]> { return ( await XMTPModule.listDms( clientInstallationId, JSON.stringify(opts), limit, - consentState + consentStates ) ).map((json: string) => { const group = JSON.parse(json) @@ -402,14 +402,14 @@ export async function listConversations< clientInstallationId: InstallationId, opts?: ConversationOptions | undefined, limit?: number | undefined, - consentState?: ConsentState | undefined + consentStates?: ConsentState[] | undefined ): Promise[]> { return ( await XMTPModule.listConversations( clientInstallationId, JSON.stringify(opts), limit, - consentState + consentStates ) ).map((json: string) => { const jsonObj = JSON.parse(json) @@ -815,9 +815,9 @@ export async function syncConversations(installationId: InstallationId) { export async function syncAllConversations( installationId: InstallationId, - consentState?: ConsentState | undefined + consentStates?: ConsentState[] | undefined ): Promise { - return await XMTPModule.syncAllConversations(installationId, consentState) + return await XMTPModule.syncAllConversations(installationId, consentStates) } export async function syncConversation( diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 023eb557..a64273eb 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -305,13 +305,13 @@ export default class Conversations< async listDms( opts?: ConversationOptions | undefined, limit?: number | undefined, - consentState?: ConsentState | undefined + consentStates?: ConsentState[] | undefined ): Promise[]> { return await XMTPModule.listDms( this.client.installationId, opts, limit, - consentState + consentStates ) } @@ -324,7 +324,7 @@ export default class Conversations< async list( opts?: ConversationOptions | undefined, limit?: number | undefined, - consentState?: ConsentState | undefined + consentStates?: ConsentState[] | undefined ): Promise[]> { return await XMTPModule.listConversations( this.client.installationId, @@ -355,11 +355,11 @@ export default class Conversations< * @returns {Promise} A Promise that resolves to the number of conversations synced. */ async syncAllConversations( - consentState: ConsentState | undefined = undefined + consentStates?: ConsentState[] | undefined = undefined ): Promise { return await XMTPModule.syncAllConversations( this.client.installationId, - consentState + consentStates ) } From 1714a2845ea801f0006aa9b47f4063eb730e8065 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 17 Jan 2025 09:01:09 -0800 Subject: [PATCH 07/10] small tweaks to conversations --- src/lib/Conversations.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index a64273eb..4705c510 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -278,6 +278,7 @@ export default class Conversations< * 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 {number} limit - Limit the number of groups returned in the list. + * @param {consentStates} ConsentState[] - Filter the groups by a list of consent states. * * @returns {Promise} A Promise that resolves to an array of Group objects. */ @@ -299,6 +300,7 @@ export default class Conversations< * 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 {number} limit - Limit the number of dms returned in the list. + * @param {consentStates} ConsentState[] - Filter the dms by a list of consent states. * * @returns {Promise} A Promise that resolves to an array of Dms objects. */ @@ -316,8 +318,10 @@ export default class Conversations< } /** - * 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. + * This method returns a list of all conversations that the client is a member of. + * @param {ConversationOptions} opts - The options to specify what fields you want returned for the conversations in the list. + * @param {number} limit - Limit the number of conversations returned in the list. + * @param {consentStates} ConsentState[] - Filter the conversations by a list of consent states. * * @returns {Promise} A Promise that resolves to an array of Conversation objects. */ @@ -330,7 +334,7 @@ export default class Conversations< this.client.installationId, opts, limit, - consentState + consentStates ) } @@ -351,11 +355,12 @@ export default class Conversations< /** * Executes a network request to sync all active conversations associated with the client + * @param {consentStates} ConsentState[] - Filter the conversations to sync by a list of consent states. * * @returns {Promise} A Promise that resolves to the number of conversations synced. */ async syncAllConversations( - consentStates?: ConsentState[] | undefined = undefined + consentStates: ConsentState[] | undefined = undefined ): Promise { return await XMTPModule.syncAllConversations( this.client.installationId, From 6f26b5132482a8c40265aa933dc213196f8afffa Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Fri, 17 Jan 2025 11:55:26 -0800 Subject: [PATCH 08/10] Updated consent filter tests --- example/src/tests/conversationTests.ts | 95 +++++++++++++++++++------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 46353f09..72deb4e0 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -349,24 +349,36 @@ test('can find a dm by address', async () => { 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) + // Bo allowed + 1 + const boGroupWithAlixAllowed = await boClient.conversations.newGroup([alixClient.address]) + // Bo unknown + 1 + const alixGroupWithBo = await alixClient.conversations.newGroup([boClient.address]) + // Bo allowed + 1 + const boDmWithAlixAllowed = await boClient.conversations.findOrCreateDm(alixClient.address) + // Bo unknown + 1 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 boDmWithCaroUnknownThenDenied = await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boGroupWithAlixUnknown = await boClient.conversations.findGroup(alixGroupWithBo.id) + + // Bo denied + 1; Bo unknown - 1 + boDmWithCaroUnknownThenDenied?.updateConsent('denied') const boConvos = await boClient.conversations.list() const boConvosFilteredAllowed = await boClient.conversations.list( {}, undefined, - 'allowed' + ['allowed'] ) const boConvosFilteredUnknown = await boClient.conversations.list( {}, undefined, - 'unknown' + ['unknown'] + ) + const boConvosFilteredAllowedOrDenied = await boClient.conversations.list( + {}, + undefined, + ['allowed', 'denied'] ) assert( @@ -377,10 +389,10 @@ test('can filter conversations by consent', async () => { assert( boConvosFilteredAllowed .map((conversation: any) => conversation.id) - .toString() === [boGroup1.id, boDm1.id].toString(), + .toString() === [boGroupWithAlixAllowed.id, boDmWithAlixAllowed.id].toString(), `Conversation allowed should be ${[ - boGroup1.id, - boDm1.id, + boGroupWithAlixAllowed.id, + boDmWithAlixAllowed.id, ].toString()} but was ${boConvosFilteredAllowed .map((convo: any) => convo.id) .toString()}` @@ -389,43 +401,72 @@ test('can filter conversations by consent', async () => { assert( boConvosFilteredUnknown .map((conversation: any) => conversation.id) - .toString() === [boGroup2?.id, boDm2?.id].toString(), + .toString() === [boGroupWithAlixUnknown?.id].toString(), `Conversation unknown filter should be ${[ - boGroup2?.id, - boDm2?.id, + boGroupWithAlixUnknown?.id, ].toString()} but was ${boConvosFilteredUnknown .map((convo: any) => convo.id) .toString()}` ) + assert( + boConvosFilteredAllowedOrDenied + .map((conversation: any) => conversation.id) + .toString() === [boGroupWithAlixAllowed.id, boDmWithAlixAllowed.id, boDmWithCaroUnknownThenDenied?.id].toString(), + `Conversation allowed or denied filter should be ${[ + boGroupWithAlixAllowed.id, + boDmWithAlixAllowed.id, + boDmWithCaroUnknownThenDenied?.id, + ].toString()} but was ${boConvosFilteredAllowedOrDenied + .map((convo: any) => convo.id) + .toString()}` + ) + + return true }) test('can filter sync all by consent', async () => { const [alixClient, boClient, caroClient] = await createClients(3) + // Bo allowed + 1 await boClient.conversations.newGroup([alixClient.address]) + // Bo unknown + 1 const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + // Bo allowed + 1 await boClient.conversations.findOrCreateDm(alixClient.address) + // Bo unknown + 1 await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync() - await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boDmWithCaro =await boClient.conversations.findDmByInboxId(caroClient.inboxId) await boClient.conversations.findGroup(otherGroup.id) + + // Bo denied + 1; Bo unknown - 1 + boDmWithCaro?.updateConsent('denied') const boConvos = await boClient.conversations.syncAllConversations() const boConvosFilteredAllowed = - await boClient.conversations.syncAllConversations('allowed') + await boClient.conversations.syncAllConversations(['allowed']) const boConvosFilteredUnknown = - await boClient.conversations.syncAllConversations('unknown') + await boClient.conversations.syncAllConversations(['unknown']) + + const boConvosFilteredAllowedOrDenied = + await boClient.conversations.syncAllConversations(['allowed', 'denied']) - assert(boConvos === 5, `Conversation length should be 5 but was ${boConvos}`) + assert(boConvos === 4, `Conversation length should be 4 but was ${boConvos}`) assert( - boConvosFilteredAllowed === 3, - `Conversation length should be 3 but was ${boConvosFilteredAllowed}` + boConvosFilteredAllowed === 2, + `Conversation length should be 2 but was ${boConvosFilteredAllowed}` ) assert( - boConvosFilteredUnknown === 3, - `Conversation length should be 3 but was ${boConvosFilteredUnknown}` + boConvosFilteredUnknown === 1, + `Conversation length should be 1 but was ${boConvosFilteredUnknown}` + ) + + assert( + boConvosFilteredAllowedOrDenied === 3, + `Conversation length should be 3 but was ${boConvosFilteredAllowedOrDenied}` ) return true @@ -810,7 +851,7 @@ test('can streamAllMessages from multiple clients - swapped', async () => { return true }) -test('can sync consent', async () => { +test('can sync consent (expected to fail unless historySyncUrl is set)', 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, @@ -834,6 +875,7 @@ test('can sync consent', async () => { appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, dbDirectory: dbDirPath, + historySyncUrl: 'http://10.0.2.2:5558', }) // Create DM conversation @@ -850,6 +892,7 @@ test('can sync consent', async () => { appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, dbDirectory: dbDirPath2, + historySyncUrl: 'http://10.0.2.2:5558', }) const state = await alix2.inboxState(true) @@ -887,7 +930,7 @@ test('can sync consent', async () => { return true }) -test('can stream consent', async () => { +test('can stream consent (expected to fail unless historySyncUrl is set)', 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, @@ -911,6 +954,7 @@ test('can stream consent', async () => { appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, dbDirectory: dbDirPath, + historySyncUrl: 'http://10.0.2.2:5558', }) const alixGroup = await alix.conversations.newGroup([bo.address]) @@ -920,6 +964,7 @@ test('can stream consent', async () => { appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, dbDirectory: dbDirPath2, + historySyncUrl: 'http://10.0.2.2:5558', }) await alixGroup.send('Hello') @@ -959,7 +1004,7 @@ test('can stream consent', async () => { return true }) -test('can preference updates', async () => { +test('can preference updates (expected to fail unless historySyncUrl is set)', 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, @@ -982,6 +1027,7 @@ test('can preference updates', async () => { appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, dbDirectory: dbDirPath, + historySyncUrl: 'http://10.0.2.2:5558', }) const types = [] @@ -995,6 +1041,7 @@ test('can preference updates', async () => { appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, dbDirectory: dbDirPath2, + historySyncUrl: 'http://10.0.2.2:5558', }) await alix2.conversations.syncAllConversations() From 86f38c2854fba6409d7ac7d9feead24d5864a779 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Fri, 17 Jan 2025 12:25:20 -0800 Subject: [PATCH 09/10] added test for newGroupWithInboxIds; format fixes --- example/src/tests/conversationTests.ts | 46 +++++++++----- example/src/tests/groupTests.ts | 86 +++++++++++++++++++++++++- src/lib/Conversations.ts | 4 +- 3 files changed, 117 insertions(+), 19 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 72deb4e0..7f7c1fa0 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,6 +1,5 @@ import { content } from '@xmtp/proto' import { Wallet } from 'ethers' -import ReactNativeBlobUtil from 'react-native-blob-util' import RNFS from 'react-native-fs' import { PreferenceUpdates } from 'xmtp-react-native-sdk/lib/PrivatePreferences' @@ -32,8 +31,6 @@ function test(name: string, perform: () => Promise) { type EncodedContent = content.EncodedContent type ContentTypeId = content.ContentTypeId -const { fs } = ReactNativeBlobUtil - const ContentTypeNumber: ContentTypeId = { authorityId: 'org', typeId: 'number', @@ -350,19 +347,28 @@ test('can filter conversations by consent', async () => { const [alixClient, boClient, caroClient] = await createClients(3) // Bo allowed + 1 - const boGroupWithAlixAllowed = await boClient.conversations.newGroup([alixClient.address]) + const boGroupWithAlixAllowed = await boClient.conversations.newGroup([ + alixClient.address, + ]) // Bo unknown + 1 - const alixGroupWithBo = await alixClient.conversations.newGroup([boClient.address]) + const alixGroupWithBo = await alixClient.conversations.newGroup([ + boClient.address, + ]) // Bo allowed + 1 - const boDmWithAlixAllowed = await boClient.conversations.findOrCreateDm(alixClient.address) + const boDmWithAlixAllowed = await boClient.conversations.findOrCreateDm( + alixClient.address + ) // Bo unknown + 1 await caroClient.conversations.findOrCreateDm(boClient.address) await boClient.conversations.sync() - const boDmWithCaroUnknownThenDenied = await boClient.conversations.findDmByInboxId(caroClient.inboxId) - const boGroupWithAlixUnknown = await boClient.conversations.findGroup(alixGroupWithBo.id) + const boDmWithCaroUnknownThenDenied = + await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boGroupWithAlixUnknown = await boClient.conversations.findGroup( + alixGroupWithBo.id + ) // Bo denied + 1; Bo unknown - 1 - boDmWithCaroUnknownThenDenied?.updateConsent('denied') + await boDmWithCaroUnknownThenDenied?.updateConsent('denied') const boConvos = await boClient.conversations.list() const boConvosFilteredAllowed = await boClient.conversations.list( @@ -389,7 +395,8 @@ test('can filter conversations by consent', async () => { assert( boConvosFilteredAllowed .map((conversation: any) => conversation.id) - .toString() === [boGroupWithAlixAllowed.id, boDmWithAlixAllowed.id].toString(), + .toString() === + [boGroupWithAlixAllowed.id, boDmWithAlixAllowed.id].toString(), `Conversation allowed should be ${[ boGroupWithAlixAllowed.id, boDmWithAlixAllowed.id, @@ -412,7 +419,12 @@ test('can filter conversations by consent', async () => { assert( boConvosFilteredAllowedOrDenied .map((conversation: any) => conversation.id) - .toString() === [boGroupWithAlixAllowed.id, boDmWithAlixAllowed.id, boDmWithCaroUnknownThenDenied?.id].toString(), + .toString() === + [ + boGroupWithAlixAllowed.id, + boDmWithAlixAllowed.id, + boDmWithCaroUnknownThenDenied?.id, + ].toString(), `Conversation allowed or denied filter should be ${[ boGroupWithAlixAllowed.id, boDmWithAlixAllowed.id, @@ -422,7 +434,6 @@ test('can filter conversations by consent', async () => { .toString()}` ) - return true }) @@ -439,11 +450,13 @@ test('can filter sync all by consent', async () => { await caroClient.conversations.findOrCreateDm(boClient.address) await boClient.conversations.sync() - const boDmWithCaro =await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boDmWithCaro = await boClient.conversations.findDmByInboxId( + caroClient.inboxId + ) await boClient.conversations.findGroup(otherGroup.id) - + // Bo denied + 1; Bo unknown - 1 - boDmWithCaro?.updateConsent('denied') + await boDmWithCaro?.updateConsent('denied') const boConvos = await boClient.conversations.syncAllConversations() const boConvosFilteredAllowed = @@ -1034,7 +1047,8 @@ test('can preference updates (expected to fail unless historySyncUrl is set)', a await alix.preferences.streamPreferenceUpdates( async (entry: PreferenceUpdates) => { types.push(entry) - }) + } + ) const alix2 = await Client.create(adaptEthersWalletToSigner(alixWallet), { env: 'local', diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index df218e9e..bad324a3 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -25,6 +25,89 @@ function test(name: string, perform: () => Promise) { groupTests.push({ name: String(counter++) + '. ' + name, run: perform }) } +test('can create a group with inbox ids default permissions', async () => { + const [alix, bo, caro] = await createClients(3) + + // Create group with inbox ID + const boGroup = await bo.conversations.newGroupWithInboxIds([alix.inboxId]) + + await alix.conversations.sync() + await boGroup.sync() + + const alixGroups = await alix.conversations.listGroups() + const alixGroup = alixGroups[0] + + // Verify group IDs are not empty + assert(boGroup.id !== '', 'bo group ID should not be empty') + assert(alixGroup.id !== '', 'alix group ID should not be empty') + + // Add caro to group + await alixGroup.addMembers([caro.address]) + await boGroup.sync() + + // Verify member counts + assert( + (await alixGroup.members()).length === 3, + 'alix group should have 3 members' + ) + assert( + (await boGroup.members()).length === 3, + 'bo group should have 3 members' + ) + + // Verify remove members throws error (admin only) + try { + await alixGroup.removeMembers([caro.address]) + await boGroup.sync() + throw new Error('Should not be able to remove members') + } catch { + // Expected error + } + + // Verify member counts unchanged + assert( + (await alixGroup.members()).length === 3, + 'alix group should still have 3 members' + ) + assert( + (await boGroup.members()).length === 3, + 'bo group should still have 3 members' + ) + + // Check permissions + const boPermissions = await boGroup.permissionPolicySet() + const alixPermissions = await alixGroup.permissionPolicySet() + + assert( + boPermissions.addMemberPolicy === 'allow', + 'bo group should have allow add member policy' + ) + assert( + alixPermissions.addMemberPolicy === 'allow', + 'alix group should have allow add member policy' + ) + + // Check super admin status + assert( + await boGroup.isSuperAdmin(bo.inboxId), + 'bo should be super admin in bo group' + ) + assert( + !(await boGroup.isSuperAdmin(alix.inboxId)), + 'alix should not be super admin in bo group' + ) + assert( + await alixGroup.isSuperAdmin(bo.inboxId), + 'bo should be super admin in alix group' + ) + assert( + !(await alixGroup.isSuperAdmin(alix.inboxId)), + 'alix should not be super admin in alix group' + ) + + return true +}) + test('groups cannot fork', async () => { const [alix, bo, caro] = await createClients(3) // Create group with 3 users @@ -236,8 +319,7 @@ test('groups cannot fork short version', async () => { }) test('groups cannot fork short version - update metadata', async () => { - const [alix, bo, new_one, new_two, new_three, new_four] = - await createClients(6) + const [alix, bo, new_one, new_two] = await createClients(6) // Create group with 2 users const alixGroup = await alix.conversations.newGroup([ bo.address, diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 4705c510..3296e146 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -166,7 +166,9 @@ export default class Conversations< * @param {InboxId} peerInboxId - The inboxId of the peer to create a conversation with. * @returns {Promise} A Promise that resolves to a Dm object. */ - async findOrCreateDmWithInboxId(peerInboxId: InboxId): Promise> { + async findOrCreateDmWithInboxId( + peerInboxId: InboxId + ): Promise> { return await XMTPModule.findOrCreateDmWithInboxId( this.client.installationId, peerInboxId From 8c15ef6aaba24778eae2a71f17f456459cdfb98f Mon Sep 17 00:00:00 2001 From: Cameron Voell <1103838+cameronvoell@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:46:37 -0800 Subject: [PATCH 10/10] Create sweet-toes-join.md --- .changeset/sweet-toes-join.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/sweet-toes-join.md diff --git a/.changeset/sweet-toes-join.md b/.changeset/sweet-toes-join.md new file mode 100644 index 00000000..c4959526 --- /dev/null +++ b/.changeset/sweet-toes-join.md @@ -0,0 +1,8 @@ +--- +"@xmtp/react-native-sdk": patch +--- + +- Add ability to filter and sync by multiple consents +- And new inboxId methods for performance +- Conversation list sort by last "readable" msg +- Default msg history off