diff --git a/android/build.gradle b/android/build.gradle index d20f103a4..5fb6e7abb 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:0.15.10" + implementation "org.xmtp:android:0.15.11" 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/wrappers/ConversationContainerWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt index c1b04c08c..f8dc148d7 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt @@ -8,7 +8,7 @@ import org.xmtp.android.library.Conversation class ConversationContainerWrapper { companion object { - fun encodeToObj(client: Client, conversation: Conversation): Map { + suspend fun encodeToObj(client: Client, conversation: Conversation): Map { when (conversation.version) { Conversation.Version.GROUP -> { val group = (conversation as Conversation.Group).group @@ -20,7 +20,7 @@ class ConversationContainerWrapper { } } - fun encode(client: Client, conversation: Conversation): String { + suspend fun encode(client: Client, conversation: Conversation): String { val gson = GsonBuilder().create() val obj = ConversationContainerWrapper.encodeToObj(client, conversation) return gson.toJson(obj) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index 78d82ba3a..daaaa6969 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -8,7 +8,7 @@ import org.xmtp.android.library.toHex class GroupWrapper { companion object { - fun encodeToObj(client: Client, group: Group): Map { + suspend fun encodeToObj(client: Client, group: Group): Map { return mapOf( "clientAddress" to client.address, "id" to group.id, @@ -26,7 +26,7 @@ class GroupWrapper { ) } - fun encode(client: Client, group: Group): String { + suspend fun encode(client: Client, group: Group): String { val gson = GsonBuilder().create() val obj = encodeToObj(client, group) return gson.toJson(obj) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt index ceba7eb79..0302b5180 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt @@ -1,21 +1,29 @@ package expo.modules.xmtpreactnativesdk.wrappers +import com.google.gson.Gson import com.google.gson.GsonBuilder import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState class InboxStateWrapper { companion object { + val gson: Gson = GsonBuilder().create() fun encodeToObj(inboxState: InboxState): Map { return mapOf( "inboxId" to inboxState.inboxId, "addresses" to inboxState.addresses, - "installationIds" to inboxState.installationIds, + "installations" to inboxState.installations.map { + gson.toJson( + mapOf( + "id" to it.installationId, + "createdAt" to it.createdAt?.time + ) + ) + }, "recoveryAddress" to inboxState.recoveryAddress ) } fun encode(inboxState: InboxState): String { - val gson = GsonBuilder().create() val obj = encodeToObj(inboxState) return gson.toJson(obj) } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5c0dfc6bc..7fda21ca3 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 (0.5.8-beta6) + - LibXMTP (0.5.8-beta7) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.9): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.14.14): + - XMTP (0.14.17): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.5.8-beta6) + - LibXMTP (= 0.5.8-beta7) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.14.14) + - XMTP (= 0.14.17) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: c7338cace222bed90f950579300725325a2c0bfd + LibXMTP: 693447f2c1242dd2f5b2146828c52dbb2bd92d6f Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 817ba1eea17421547e01e087285606eb270a8dcb @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 37621f1258b12629af305e6697414ccb2fbd4ea8 - XMTPReactNative: 7bec275ed26997e6a73f06a678c328e6ba852cd5 + XMTP: 64600a3216ef8bfb074a128cffe37458b23c52fd + XMTPReactNative: 39e85c5215efa8cb7900285dc95319402758d85b Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 494fb2327..4215ade47 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -3,7 +3,7 @@ import { Platform } from 'expo-modules-core' import RNFS from 'react-native-fs' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' -import { Test, assert, createClients, delayToPropogate } from './test-utils' +import { Test, assert, createClients, createGroups, delayToPropogate } from './test-utils' import { Client, Conversation, @@ -21,28 +21,6 @@ function test(name: string, perform: () => Promise) { groupTests.push({ name: String(counter++) + '. ' + name, run: perform }) } -async function createGroups( - client: Client, - peers: Client[], - numGroups: number, - numMessages: number -): Promise { - const groups = [] - const addresses: string[] = peers.map((client) => client.address) - for (let i = 0; i < numGroups; i++) { - const group = await client.conversations.newGroup(addresses, { - name: `group ${i}`, - imageUrlSquare: `www.group${i}.com`, - description: `group ${i}`, - }) - groups.push(group) - for (let i = 0; i < numMessages; i++) { - await group.send({ text: `Message ${i}` }) - } - } - return groups -} - test('can make a MLS V3 client', async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const keyBytes = new Uint8Array([ @@ -91,16 +69,21 @@ test('can revoke all other installations', async () => { const inboxState = await alix2.inboxState(true) assert( - inboxState.installationIds.length === 2, - `installationIds length should be 2 but was ${inboxState.installationIds.length}` + inboxState.installations.length === 2, + `installations length should be 2 but was ${inboxState.installations.length}` ) await alix2.revokeAllOtherInstallations(alixWallet) const inboxState2 = await alix2.inboxState(true) assert( - inboxState2.installationIds.length === 1, - `installationIds length should be 1 but was ${inboxState2.installationIds.length}` + inboxState2.installations.length === 1, + `installations length should be 1 but was ${inboxState2.installations.length}` + ) + + assert( + inboxState2.installations[0].createdAt !== undefined, + `installations createdAt should not be undefined` ) return true }) @@ -240,34 +223,6 @@ test('can make a MLS V3 client with encryption key and database directory', asyn return true }) -test('testing large group listing with metadata performance', async () => { - const [alixClient, boClient] = await createClients(2) - - await createGroups(alixClient, [boClient], 50, 10) - - let start = Date.now() - let groups = await alixClient.conversations.listGroups() - let end = Date.now() - console.log(`Alix loaded ${groups.length} groups in ${end - start}ms`) - - start = Date.now() - await alixClient.conversations.syncGroups() - end = Date.now() - console.log(`Alix synced ${groups.length} groups in ${end - start}ms`) - - start = Date.now() - await boClient.conversations.syncGroups() - end = Date.now() - console.log(`Bo synced ${groups.length} groups in ${end - start}ms`) - - start = Date.now() - groups = await boClient.conversations.listGroups() - end = Date.now() - console.log(`Bo loaded ${groups.length} groups in ${end - start}ms`) - - return true -}) - test('can drop a local database', async () => { const [client, anotherClient] = await createClients(2) @@ -405,6 +360,53 @@ test('production MLS V3 client creation does not error', async () => { return true }) +test('can cancel streams', async () => { + const [alix, bo] = await createClients(2) + let messageCallbacks = 0 + + await bo.conversations.streamAllMessages(async () => { + messageCallbacks++ + }, true) + + const group = await alix.conversations.newGroup([bo.address]) + await group.send('hello') + await delayToPropogate() + + assert( + messageCallbacks === 1, + `message stream should have received 1 message but recieved ${messageCallbacks}` + ) + + await bo.conversations.cancelStreamAllMessages() + await delayToPropogate() + + await group.send('hello') + await group.send('hello') + await group.send('hello') + + await delayToPropogate() + + assert( + messageCallbacks === 1, + `message stream should still only received 1 message but recieved ${messageCallbacks}` + ) + + await bo.conversations.streamAllMessages(async () => { + messageCallbacks++ + }, true) + + await delayToPropogate() + await group.send('hello') + await delayToPropogate() + + assert( + messageCallbacks === 2, + `message stream should have received 2 message but recieved ${messageCallbacks}` + ) + + return true +}) + test('group message delivery status', async () => { const [alixClient, boClient] = await createClients(2) const alixGroup = await alixClient.conversations.newGroup([boClient.address]) @@ -958,52 +960,6 @@ test('can remove and add members from a group by inbox id', async () => { return true }) -test('can cancel streams', async () => { - const [alix, bo] = await createClients(2) - let messageCallbacks = 0 - - await bo.conversations.streamAllMessages(async () => { - messageCallbacks++ - }, true) - - const group = await alix.conversations.newGroup([bo.address]) - await group.send('hello') - await delayToPropogate() - - assert( - messageCallbacks === 1, - 'message stream should have received 1 message' - ) - - await bo.conversations.cancelStreamAllMessages() - await delayToPropogate() - - await group.send('hello') - await group.send('hello') - await group.send('hello') - - await delayToPropogate() - - assert( - messageCallbacks === 1, - 'message stream should still only received 1 message' - ) - - await bo.conversations.streamAllMessages(async () => { - messageCallbacks++ - }, true) - - await group.send('hello') - await delayToPropogate() - - assert( - messageCallbacks === 2, - 'message stream should have received 2 message' - ) - - return true -}) - test('can stream both groups and messages at same time', async () => { const [alix, bo] = await createClients(2) @@ -2216,7 +2172,7 @@ test('can create new installation without breaking group', async () => { test('can list many groups members in parallel', async () => { const [alix, bo] = await createClients(2) - const groups: Group[] = await createGroups(alix, [bo], 20, 0) + const groups: Group[] = await createGroups(alix, [bo], 20) try { await Promise.all(groups.slice(0, 10).map((g) => g.membersList())) @@ -2235,7 +2191,7 @@ test('can list many groups members in parallel', async () => { test('can sync all groups', async () => { const [alix, bo] = await createClients(2) - const groups: Group[] = await createGroups(alix, [bo], 50, 0) + const groups: Group[] = await createGroups(alix, [bo], 50) const alixGroup = groups[0] await bo.conversations.syncGroups() diff --git a/example/src/tests/test-utils.ts b/example/src/tests/test-utils.ts index ce7c2209b..43d10b586 100644 --- a/example/src/tests/test-utils.ts +++ b/example/src/tests/test-utils.ts @@ -1,5 +1,5 @@ import { Platform } from 'expo-modules-core' -import { Client, GroupUpdatedCodec } from 'xmtp-react-native-sdk' +import { Client, GroupUpdatedCodec, Group } from 'xmtp-react-native-sdk' export type Test = { name: string @@ -65,3 +65,21 @@ export async function createV3TestingClients(): Promise { clients.push(alix, bo, caro) return clients } + +export async function createGroups( + client: Client, + peers: Client[], + numGroups: number +): Promise { + const groups = [] + const addresses: string[] = peers.map((client) => client.address) + for (let i = 0; i < numGroups; i++) { + const group = await client.conversations.newGroup(addresses, { + name: `group ${i}`, + imageUrlSquare: `www.group${i}.com`, + description: `group ${i}`, + }) + groups.push(group) + } + return groups +} diff --git a/ios/Wrappers/ConversationContainerWrapper.swift b/ios/Wrappers/ConversationContainerWrapper.swift index 171a49599..8bc185a7f 100644 --- a/ios/Wrappers/ConversationContainerWrapper.swift +++ b/ios/Wrappers/ConversationContainerWrapper.swift @@ -10,17 +10,17 @@ import XMTP // Wrapper around XMTP.ConversationContainer to allow passing these objects back into react native. struct ConversationContainerWrapper { - static func encodeToObj(_ conversation: XMTP.Conversation, client: XMTP.Client) throws -> [String: Any] { + static func encodeToObj(_ conversation: XMTP.Conversation, client: XMTP.Client) async throws -> [String: Any] { switch conversation { case .group(let group): - return try GroupWrapper.encodeToObj(group, client: client) + return try await GroupWrapper.encodeToObj(group, client: client) default: return try ConversationWrapper.encodeToObj(conversation, client: client) } } - static func encode(_ conversation: XMTP.Conversation, client: XMTP.Client) throws -> String { - let obj = try encodeToObj(conversation, client: client) + static func encode(_ conversation: XMTP.Conversation, client: XMTP.Client) async throws -> String { + let obj = try await encodeToObj(conversation, client: client) let data = try JSONSerialization.data(withJSONObject: obj) guard let result = String(data: data, encoding: .utf8) else { throw WrapperError.encodeError("could not encode conversation") diff --git a/ios/Wrappers/GroupWrapper.swift b/ios/Wrappers/GroupWrapper.swift index bab4d2082..76f827dd1 100644 --- a/ios/Wrappers/GroupWrapper.swift +++ b/ios/Wrappers/GroupWrapper.swift @@ -10,12 +10,12 @@ import XMTP // Wrapper around XMTP.Group to allow passing these objects back into react native. struct GroupWrapper { - static func encodeToObj(_ group: XMTP.Group, client: XMTP.Client) throws -> [String: Any] { + static func encodeToObj(_ group: XMTP.Group, client: XMTP.Client) async throws -> [String: Any] { return [ "clientAddress": client.address, "id": group.id, "createdAt": UInt64(group.createdAt.timeIntervalSince1970 * 1000), - "members": try group.members.compactMap { member in return try MemberWrapper.encode(member) }, + "members": try await group.members.compactMap { member in return try MemberWrapper.encode(member) }, "version": "GROUP", "topic": group.topic, "creatorInboxId": try group.creatorInboxId(), @@ -28,8 +28,8 @@ struct GroupWrapper { ] } - static func encode(_ group: XMTP.Group, client: XMTP.Client) throws -> String { - let obj = try encodeToObj(group, client: client) + static func encode(_ group: XMTP.Group, client: XMTP.Client) async throws -> String { + let obj = try await encodeToObj(group, client: client) let data = try JSONSerialization.data(withJSONObject: obj) guard let result = String(data: data, encoding: .utf8) else { throw WrapperError.encodeError("could not encode group") diff --git a/ios/Wrappers/InboxStateWrapper.swift b/ios/Wrappers/InboxStateWrapper.swift index 62f0f2290..024b5de28 100644 --- a/ios/Wrappers/InboxStateWrapper.swift +++ b/ios/Wrappers/InboxStateWrapper.swift @@ -14,7 +14,7 @@ struct InboxStateWrapper { return [ "inboxId": inboxState.inboxId, "addresses": inboxState.addresses, - "installationIds": inboxState.installationIds, + "installations": try inboxState.installations.map { try Installation.encodeInstallation(installation: $0) }, "recoveryAddress": inboxState.recoveryAddress ] } @@ -28,3 +28,17 @@ struct InboxStateWrapper { return result } } + +struct Installation { + static func encodeInstallation(installation: XMTP.Installation) throws -> String { + let obj: [String: Any] = [ + "id": installation.id, + "createdAt": installation.createdAt?.timeIntervalSince1970 ?? NSNull() + ] + let data = try JSONSerialization.data(withJSONObject: obj) + guard let result = String(data: data, encoding: .utf8) else { + throw WrapperError.encodeError("could not encode inboxState") + } + return result + } +} diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index d1c365eb4..3f2c9e365 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -541,7 +541,7 @@ public class XMTPModule: Module { var results: [String] = [] for group in groupList { await self.groupsManager.set(group.cacheKey(inboxId), group) - let encodedGroup = try GroupWrapper.encode(group, client: client) + let encodedGroup = try await GroupWrapper.encode(group, client: client) results.append(encodedGroup) } @@ -557,7 +557,7 @@ public class XMTPModule: Module { var results: [String] = [] for conversation in conversationContainerList { await self.conversationsManager.set(conversation.cacheKey(inboxId), conversation) - let encodedConversationContainer = try ConversationContainerWrapper.encode(conversation, client: client) + let encodedConversationContainer = try await ConversationContainerWrapper.encode(conversation, client: client) results.append(encodedConversationContainer) } @@ -644,7 +644,7 @@ public class XMTPModule: Module { throw Error.noClient } if let group = try client.findGroup(groupId: groupId) { - return try GroupWrapper.encode(group, client: client) + return try await GroupWrapper.encode(group, client: client) } else { return nil } @@ -878,7 +878,7 @@ public class XMTPModule: Module { description: createGroupParams.groupDescription, pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl ) - return try GroupWrapper.encode(group, client: client) + return try await GroupWrapper.encode(group, client: client) } catch { print("ERRRO!: \(error.localizedDescription)") throw error @@ -900,7 +900,7 @@ public class XMTPModule: Module { description: createGroupParams.groupDescription, pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl ) - return try GroupWrapper.encode(group, client: client) + return try await GroupWrapper.encode(group, client: client) } catch { print("ERRRO!: \(error.localizedDescription)") throw error @@ -915,7 +915,7 @@ public class XMTPModule: Module { guard let group = try await findGroup(inboxId: inboxId, id: groupId) else { throw Error.conversationNotFound("no group found for \(groupId)") } - return try group.members.map(\.inboxId) + return try await group.members.map(\.inboxId) } AsyncFunction("listGroupMembers") { (inboxId: String, groupId: String) -> [String] in @@ -926,7 +926,7 @@ public class XMTPModule: Module { guard let group = try await findGroup(inboxId: inboxId, id: groupId) else { throw Error.conversationNotFound("no group found for \(groupId)") } - return try group.members.compactMap { member in + return try await group.members.compactMap { member in return try MemberWrapper.encode(member) } } @@ -1333,7 +1333,7 @@ public class XMTPModule: Module { throw Error.conversationNotFound("no group found") } - return try GroupWrapper.encode(group, client: client) + return try await GroupWrapper.encode(group, client: client) } AsyncFunction("subscribeToConversations") { (inboxId: String) in @@ -1811,7 +1811,7 @@ public class XMTPModule: Module { await subscriptionsManager.set(getGroupsKey(inboxId: client.inboxID), Task { do { for try await group in try await client.conversations.streamGroups() { - try sendEvent("group", [ + try await sendEvent("group", [ "inboxId": inboxId, "group": GroupWrapper.encodeToObj(group, client: client), ]) @@ -1832,7 +1832,7 @@ public class XMTPModule: Module { await subscriptionsManager.set(getConversationsKey(inboxId: inboxId), Task { do { for try await conversation in await client.conversations.streamAll() { - try sendEvent("conversationContainer", [ + try await sendEvent("conversationContainer", [ "inboxId": inboxId, "conversationContainer": ConversationContainerWrapper.encodeToObj(conversation, client: client), ]) diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 5d012342e..dd72e3ee0 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", "= 0.14.14" + s.dependency "XMTP", "= 0.14.17" end diff --git a/src/lib/InboxState.ts b/src/lib/InboxState.ts index 89d5b8593..5181d83d1 100644 --- a/src/lib/InboxState.ts +++ b/src/lib/InboxState.ts @@ -3,18 +3,18 @@ import { InboxId } from './Client' export class InboxState { inboxId: InboxId addresses: string[] - installationIds: string[] + installations: Installation[] recoveryAddress: string constructor( inboxId: InboxId, addresses: string[], - installationIds: string[], + installations: Installation[], recoveryAddress: string ) { this.inboxId = inboxId this.addresses = addresses - this.installationIds = installationIds + this.installations = installations this.recoveryAddress = recoveryAddress } @@ -23,8 +23,25 @@ export class InboxState { return new InboxState( entry.inboxId, entry.addresses, - entry.installationIds, + entry.installations.map((inst: string) => { + return Installation.from(inst) + }), entry.recoveryAddress ) } } + +export class Installation { + id: string + createdAt: number | undefined // timestamp in milliseconds + + constructor(id: string, createdAt: number) { + this.id = id + this.createdAt = createdAt + } + + static from(json: string): Installation { + const installation = JSON.parse(json) + return new Installation(installation.id, installation.createdAt) + } +}