From 4fdbf733954eccf23974692640573d1723ea2abc Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 20:41:22 -0700 Subject: [PATCH] do the entire swift side --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 65 ++-- .../wrappers/GroupWrapper.kt | 6 +- example/ios/Podfile.lock | 8 +- .../ConversationContainerWrapper.swift | 2 + ios/Wrappers/DmWrapper.swift | 50 +++ ios/Wrappers/GroupWrapper.swift | 38 +- ios/XMTPModule.swift | 359 +++++++++++++++--- ios/XMTPReactNative.podspec | 2 +- src/index.ts | 6 +- 9 files changed, 426 insertions(+), 110 deletions(-) create mode 100644 ios/Wrappers/DmWrapper.swift diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 13b6e50b6..6ce5652f3 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -43,7 +43,6 @@ import org.xmtp.android.library.ClientOptions import org.xmtp.android.library.ConsentState import org.xmtp.android.library.Conversation import org.xmtp.android.library.Conversations.ConversationOrder -import org.xmtp.android.library.Dm import org.xmtp.android.library.Group import org.xmtp.android.library.PreEventCallback import org.xmtp.android.library.PreparedMessage @@ -263,7 +262,7 @@ class XMTPModule : Module() { "groupMessage", "allGroupMessage", "group", - ) + ) Function("address") { inboxId: String -> logV("address") @@ -697,7 +696,7 @@ class XMTPModule : Module() { withContext(Dispatchers.IO) { logV("listGroups") val client = clients[inboxId] ?: throw XMTPException("No client") - val params = ConversationParamsWrapper.groupParamsFromJson(groupParams ?: "") + val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") val sortedGroupList = if (order == ConversationOrder.LAST_MESSAGE) { client.conversations.listGroups() @@ -717,11 +716,12 @@ class XMTPModule : Module() { } } - AsyncFunction("listV3Conversations") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listV3Conversations") Coroutine { inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int? -> withContext(Dispatchers.IO) { logV("listV3Conversations") val client = clients[inboxId] ?: throw XMTPException("No client") - val params = ConversationParamsWrapper.groupParamsFromJson(groupParams ?: "") + val params = + ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") val conversations = client.conversations.listConversations(order = order, limit = limit) @@ -940,7 +940,7 @@ class XMTPModule : Module() { val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") val sending = ContentJson.fromJson(contentJson) - conversation.prepareMessage( + conversation.prepareMessageV3( content = sending.content, options = SendOptions(contentType = sending.type) ) @@ -1147,7 +1147,9 @@ class XMTPModule : Module() { withContext(Dispatchers.IO) { logV("listPeerInboxId") val client = clients[inboxId] ?: throw XMTPException("No client") - val dm = (findConversation(inboxId, dmId) as Conversation.Dm).dm + val conversation = client.findConversation(dmId) + ?: throw XMTPException("no conversation found for $dmId") + val dm = (conversation as Conversation.Dm).dm dm.peerInboxId() } } @@ -1543,7 +1545,12 @@ class XMTPModule : Module() { val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = - client.conversations.conversationFromWelcome(Base64.decode(encryptedMessage, NO_WRAP)) + client.conversations.conversationFromWelcome( + Base64.decode( + encryptedMessage, + NO_WRAP + ) + ) ConversationContainerWrapper.encode(client, conversation) } } @@ -1829,12 +1836,14 @@ class XMTPModule : Module() { client.contacts.isGroupDenied(groupId) } } - AsyncFunction("updateGroupConsent") Coroutine { inboxId: String, groupId: String, state: String -> + AsyncFunction("updateConversationConsent") Coroutine { inboxId: String, conversationId: String, state: String -> withContext(Dispatchers.IO) { - logV("updateGroupConsent") - val group = findGroup(inboxId, groupId) + logV("updateConversationConsent") + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no group found for $conversationId") - group?.updateConsentState(getConsentState(state)) + conversation.updateConsentState(getConsentState(state)) } } @@ -2023,22 +2032,26 @@ class XMTPModule : Module() { val client = clients[inboxId] ?: throw XMTPException("No client") subscriptions[getV3ConversationsKey(client.inboxId)]?.cancel() - subscriptions[getV3ConversationsKey(client.inboxId)] = CoroutineScope(Dispatchers.IO).launch { - try { - client.conversations.streamConversations().collect { conversation -> - sendEvent( - "conversationV3", - mapOf( - "inboxId" to inboxId, - "conversation" to ConversationContainerWrapper.encodeToObj(client, conversation) + subscriptions[getV3ConversationsKey(client.inboxId)] = + CoroutineScope(Dispatchers.IO).launch { + try { + client.conversations.streamConversations().collect { conversation -> + sendEvent( + "conversationV3", + mapOf( + "inboxId" to inboxId, + "conversation" to ConversationContainerWrapper.encodeToObj( + client, + conversation + ) + ) ) - ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in group subscription: $e") + subscriptions[getV3ConversationsKey(client.inboxId)]?.cancel() } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in group subscription: $e") - subscriptions[getV3ConversationsKey(client.inboxId)]?.cancel() } - } } private fun subscribeToAll(inboxId: String) { @@ -2117,7 +2130,7 @@ class XMTPModule : Module() { subscriptions[getConversationMessagesKey(inboxId)]?.cancel() subscriptions[getConversationMessagesKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { try { - client.conversations.streamAllGroupDecryptedMessages().collect { message -> + client.conversations.streamAllConversationDecryptedMessages().collect { message -> sendEvent( "allConversationMessages", mapOf( 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 2fb641a19..939d4ad25 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -65,9 +65,9 @@ class ConversationParamsWrapper( val lastMessage: Boolean = false, ) { companion object { - fun groupParamsFromJson(groupParams: String): ConversationParamsWrapper { - if (groupParams.isEmpty()) return ConversationParamsWrapper() - val jsonOptions = JsonParser.parseString(groupParams).asJsonObject + fun conversationParamsFromJson(conversationParams: String): ConversationParamsWrapper { + if (conversationParams.isEmpty()) return ConversationParamsWrapper() + val jsonOptions = JsonParser.parseString(conversationParams).asJsonObject return ConversationParamsWrapper( if (jsonOptions.has("members")) jsonOptions.get("members").asBoolean else true, if (jsonOptions.has("creatorInboxId")) jsonOptions.get("creatorInboxId").asBoolean else true, diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ee02ff527..36bec2102 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -449,7 +449,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.15.2): + - XMTP (0.16.0): - Connect-Swift (= 0.12.0) - GzipSwift - LibXMTP (= 0.5.10) @@ -458,7 +458,7 @@ PODS: - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.15.2) + - XMTP (= 0.16.0) - Yoga (1.14.0) DEPENDENCIES: @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 7d47e6bc507db66dd01116ce2b4ed04dd3560a4f - XMTPReactNative: 1a946cd697598fb4bc560a637094e63c4d553df3 + XMTP: 18d555dbf5afd3dcafa11b108042f9673da3c6b9 + XMTPReactNative: cd8be3d8547d116005f3d0f4f207f19c7b34d035 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/ios/Wrappers/ConversationContainerWrapper.swift b/ios/Wrappers/ConversationContainerWrapper.swift index 8bc185a7f..c670ae7d0 100644 --- a/ios/Wrappers/ConversationContainerWrapper.swift +++ b/ios/Wrappers/ConversationContainerWrapper.swift @@ -14,6 +14,8 @@ struct ConversationContainerWrapper { switch conversation { case .group(let group): return try await GroupWrapper.encodeToObj(group, client: client) + case .dm(let dm): + return try await DmWrapper.encodeToObj(dm, client: client) default: return try ConversationWrapper.encodeToObj(conversation, client: client) } diff --git a/ios/Wrappers/DmWrapper.swift b/ios/Wrappers/DmWrapper.swift new file mode 100644 index 000000000..72933339c --- /dev/null +++ b/ios/Wrappers/DmWrapper.swift @@ -0,0 +1,50 @@ +// +// DmWrapper.swift +// Pods +// +// Created by Naomi Plasterer on 10/24/24. +// + +import Foundation +import XMTP + +// Wrapper around XMTP.Dm to allow passing these objects back into react native. +struct DmWrapper { + static func encodeToObj(_ dm: XMTP.Dm, client: XMTP.Client, conversationParams: ConversationParamsWrapper = ConversationParamsWrapper()) async throws -> [String: Any] { + var result: [String: Any] = [ + "clientAddress": client.address, + "id": dm.id, + "createdAt": UInt64(dm.createdAt.timeIntervalSince1970 * 1000), + "version": "DM", + "topic": dm.topic, + "peerInboxId": try await dm.peerInboxId + ] + + if conversationParams.members { + result["members"] = try await dm.members.compactMap { member in return try MemberWrapper.encode(member) } + } + if conversationParams.creatorInboxId { + result["creatorInboxId"] = try dm.creatorInboxId() + } + if conversationParams.consentState { + result["consentState"] = ConsentWrapper.consentStateToString(state: try dm.consentState()) + } + if conversationParams.lastMessage { + if let lastMessage = try await dm.decryptedMessages(limit: 1).first { + result["lastMessage"] = try DecodedMessageWrapper.encode(lastMessage, client: client) + } + } + + return result + } + + static func encode(_ dm: XMTP.Dm, client: XMTP.Client, conversationParams: ConversationParamsWrapper = ConversationParamsWrapper()) async throws -> String { + let obj = try await encodeToObj(dm, client: client, conversationParams: conversationParams) + let data = try JSONSerialization.data(withJSONObject: obj) + guard let result = String(data: data, encoding: .utf8) else { + throw WrapperError.encodeError("could not encode dm") + } + return result + } +} + diff --git a/ios/Wrappers/GroupWrapper.swift b/ios/Wrappers/GroupWrapper.swift index 928fdd6d5..7460db33a 100644 --- a/ios/Wrappers/GroupWrapper.swift +++ b/ios/Wrappers/GroupWrapper.swift @@ -8,13 +8,9 @@ import Foundation import XMTP -enum ConversationOrder { - case lastMessage, createdAt -} - // Wrapper around XMTP.Group to allow passing these objects back into react native. struct GroupWrapper { - static func encodeToObj(_ group: XMTP.Group, client: XMTP.Client, groupParams: GroupParamsWrapper = GroupParamsWrapper()) async throws -> [String: Any] { + static func encodeToObj(_ group: XMTP.Group, client: XMTP.Client, conversationParams: ConversationParamsWrapper = ConversationParamsWrapper()) async throws -> [String: Any] { var result: [String: Any] = [ "clientAddress": client.address, "id": group.id, @@ -23,31 +19,31 @@ struct GroupWrapper { "topic": group.topic ] - if groupParams.members { + if conversationParams.members { result["members"] = try await group.members.compactMap { member in return try MemberWrapper.encode(member) } } - if groupParams.creatorInboxId { + if conversationParams.creatorInboxId { result["creatorInboxId"] = try group.creatorInboxId() } - if groupParams.isActive { + if conversationParams.isActive { result["isActive"] = try group.isActive() } - if groupParams.addedByInboxId { + if conversationParams.addedByInboxId { result["addedByInboxId"] = try group.addedByInboxId() } - if groupParams.name { + if conversationParams.name { result["name"] = try group.groupName() } - if groupParams.imageUrlSquare { + if conversationParams.imageUrlSquare { result["imageUrlSquare"] = try group.groupImageUrlSquare() } - if groupParams.description { + if conversationParams.description { result["description"] = try group.groupDescription() } - if groupParams.consentState { + if conversationParams.consentState { result["consentState"] = ConsentWrapper.consentStateToString(state: try group.consentState()) } - if groupParams.lastMessage { + if conversationParams.lastMessage { if let lastMessage = try await group.decryptedMessages(limit: 1).first { result["lastMessage"] = try DecodedMessageWrapper.encode(lastMessage, client: client) } @@ -56,8 +52,8 @@ struct GroupWrapper { return result } - static func encode(_ group: XMTP.Group, client: XMTP.Client, groupParams: GroupParamsWrapper = GroupParamsWrapper()) async throws -> String { - let obj = try await encodeToObj(group, client: client, groupParams: groupParams) + static func encode(_ group: XMTP.Group, client: XMTP.Client, conversationParams: ConversationParamsWrapper = ConversationParamsWrapper()) async throws -> String { + let obj = try await encodeToObj(group, client: client, conversationParams: conversationParams) let data = try JSONSerialization.data(withJSONObject: obj) guard let result = String(data: data, encoding: .utf8) else { throw WrapperError.encodeError("could not encode group") @@ -66,7 +62,7 @@ struct GroupWrapper { } } -struct GroupParamsWrapper { +struct ConversationParamsWrapper { let members: Bool let creatorInboxId: Bool let isActive: Bool @@ -99,14 +95,14 @@ struct GroupParamsWrapper { self.lastMessage = lastMessage } - static func groupParamsFromJson(_ groupParams: String) -> GroupParamsWrapper { - guard let jsonData = groupParams.data(using: .utf8), + static func conversationParamsFromJson(_ conversationParams: String) -> ConversationParamsWrapper { + guard let jsonData = conversationParams.data(using: .utf8), let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []), let jsonDict = jsonObject as? [String: Any] else { - return GroupParamsWrapper() + return ConversationParamsWrapper() } - return GroupParamsWrapper( + return ConversationParamsWrapper( members: jsonDict["members"] as? Bool ?? true, creatorInboxId: jsonDict["creatorInboxId"] as? Bool ?? true, isActive: jsonDict["isActive"] as? Bool ?? true, diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 5ec9831e9..e504f063e 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -11,6 +11,14 @@ extension Conversation { func cacheKey(_ inboxId: String) -> String { return Conversation.cacheKeyForTopic(inboxId: inboxId, topic: topic) } + + static func cacheKeyForV3(inboxId: String, topic: String, id: String) -> String { + return "\(inboxId):\(topic):\(id)" + } + + func cacheKeyV3(_ inboxId: String) throws -> String { + return try Conversation.cacheKeyForV3(inboxId: inboxId, topic: topic, id: id) + } } extension XMTP.Group { @@ -100,16 +108,19 @@ public class XMTPModule: Module { "preCreateIdentityCallback", "preEnableIdentityCallback", "preAuthenticateToInboxCallback", - // Conversations + // ConversationV2 "conversation", - "group", "conversationContainer", "message", - "allGroupMessage", - // Conversation - "conversationMessage", + "conversationMessage", + // ConversationV3 + "conversationV3", + "allConversationMessage", + "conversationV3Message", // Group - "groupMessage" + "group", + "groupMessage", + "allGroupMessage" ) AsyncFunction("address") { (inboxId: String) -> String in @@ -266,7 +277,7 @@ public class XMTPModule: Module { // Create a client using its serialized key bundle. AsyncFunction("createFromKeyBundle") { (keyBundle: String, dbEncryptionKey: [UInt8]?, authParams: String) -> [String: String] in - + // V2 ONLY do { guard let keyBundleData = Data(base64Encoded: keyBundle), let bundle = try? PrivateKeyBundle(serializedData: keyBundleData) @@ -382,6 +393,7 @@ public class XMTPModule: Module { } AsyncFunction("sign") { (inboxId: String, digest: [UInt8], keyType: String, preKeyIndex: Int) -> [UInt8] in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -395,6 +407,7 @@ public class XMTPModule: Module { } AsyncFunction("exportPublicKeyBundle") { (inboxId: String) -> [UInt8] in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -404,6 +417,7 @@ public class XMTPModule: Module { // Export the client's serialized key bundle. AsyncFunction("exportKeyBundle") { (inboxId: String) -> String in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -413,6 +427,7 @@ public class XMTPModule: Module { // Export the conversation's serialized topic data. AsyncFunction("exportConversationTopicData") { (inboxId: String, topic: String) -> String in + // V2 ONLY guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { throw Error.conversationNotFound(topic) } @@ -430,13 +445,14 @@ public class XMTPModule: Module { // Import a conversation from its serialized topic data. AsyncFunction("importConversationTopicData") { (inboxId: String, topicData: String) -> String in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } let data = try Xmtp_KeystoreApi_V1_TopicMap.TopicData( serializedData: Data(base64Encoded: Data(topicData.utf8))! ) - let conversation = await client.conversations.importTopicData(data: data) + let conversation = try await client.conversations.importTopicData(data: data) await conversationsManager.set(conversation.cacheKey(inboxId), conversation) return try ConversationWrapper.encode(conversation, client: client) } @@ -444,6 +460,7 @@ public class XMTPModule: Module { // // Client API AsyncFunction("canMessage") { (inboxId: String, peerAddress: String) -> Bool in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -460,6 +477,7 @@ public class XMTPModule: Module { } AsyncFunction("staticCanMessage") { (peerAddress: String, environment: String, appVersion: String?) -> Bool in + // V2 ONLY do { let options = createClientConfig(env: environment, appVersion: appVersion) return try await XMTP.Client.canMessage(peerAddress, options: options) @@ -530,6 +548,7 @@ public class XMTPModule: Module { } AsyncFunction("sendEncodedContent") { (inboxId: String, topic: String, encodedContentData: [UInt8]) -> String in + // V2 ONLY guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { throw Error.conversationNotFound("no conversation found for \(topic)") } @@ -540,6 +559,7 @@ public class XMTPModule: Module { } AsyncFunction("listConversations") { (inboxId: String) -> [String] in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -561,7 +581,7 @@ public class XMTPModule: Module { throw Error.noClient } - let params = GroupParamsWrapper.groupParamsFromJson(groupParams ?? "") + let params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") var groupList: [Group] = [] @@ -592,12 +612,30 @@ public class XMTPModule: Module { var results: [String] = [] for group in groupList { await self.groupsManager.set(group.cacheKey(inboxId), group) - let encodedGroup = try await GroupWrapper.encode(group, client: client, groupParams: params) + let encodedGroup = try await GroupWrapper.encode(group, client: client, conversationParams: params) results.append(encodedGroup) } return results } + AsyncFunction("listV3Conversations") { (inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int?) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + + let params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?? "") + let order = getConversationSortOrder(order: sortOrder ?? "") + let conversations = try await client.conversations.listConversations(limit: limit, order: order) + + var results: [String] = [] + for conversation in conversations { + let encodedConversationContainer = try await ConversationContainerWrapper.encode(conversation, client: client) + results.append(encodedConversationContainer) + } + + return results + } + AsyncFunction("listAll") { (inboxId: String) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient @@ -615,6 +653,7 @@ public class XMTPModule: Module { } AsyncFunction("loadMessages") { (inboxId: String, topic: String, limit: Int?, before: Double?, after: Double?, direction: String?) -> [String] in + // V2 ONLY let beforeDate = before != nil ? Date(timeIntervalSince1970: TimeInterval(before!) / 1000) : nil let afterDate = after != nil ? Date(timeIntervalSince1970: TimeInterval(after!) / 1000) : nil @@ -645,7 +684,7 @@ public class XMTPModule: Module { } } - AsyncFunction("groupMessages") { (inboxId: String, id: String, limit: Int?, before: Double?, after: Double?, direction: String?, deliveryStatus: String?) -> [String] in + AsyncFunction("conversationMessages") { (inboxId: String, conversationId: String, limit: Int?, before: Double?, after: Double?, direction: String?) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -654,18 +693,15 @@ public class XMTPModule: Module { let afterDate = after != nil ? Date(timeIntervalSince1970: TimeInterval(after!) / 1000) : nil let sortDirection: Int = (direction != nil && direction == "SORT_DIRECTION_ASCENDING") ? 1 : 2 - - let status: String = (deliveryStatus != nil) ? deliveryStatus!.lowercased() : "all" - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let conversation = try client.findConversation(conversationId: conversationId) else { + throw Error.conversationNotFound("no conversation found for \(conversationId)") } - let decryptedMessages = try await group.decryptedMessages( + let decryptedMessages = try await conversation.decryptedMessages( + limit: limit, before: beforeDate, after: afterDate, - limit: limit, - direction: PagingInfoSortDirection(rawValue: sortDirection), - deliveryStatus: MessageDeliveryStatus(rawValue: status) + direction: PagingInfoSortDirection(rawValue: sortDirection) ) return decryptedMessages.compactMap { msg in @@ -699,9 +735,43 @@ public class XMTPModule: Module { return nil } } + + AsyncFunction("findConversation") { (inboxId: String, conversationId: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + if let conversation = try client.findConversation(conversationId: conversationId) { + return try await ConversationContainerWrapper.encode(conversation, client: client) + } else { + return nil + } + } + + AsyncFunction("findConversationByTopic") { (inboxId: String, topic: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + if let conversation = try client.findConversationByTopic(topic: topic) { + return try await ConversationContainerWrapper.encode(conversation, client: client) + } else { + return nil + } + } + + AsyncFunction("findDm") { (inboxId: String, peerAddress: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + if let dm = try await client.findDm(address: peerAddress) { + return try await DmWrapper.encode(dm, client: client) + } else { + return nil + } + } AsyncFunction("loadBatchMessages") { (inboxId: String, topics: [String]) -> [String] in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -760,6 +830,7 @@ public class XMTPModule: Module { } AsyncFunction("sendMessage") { (inboxId: String, conversationTopic: String, contentJson: String) -> String in + // V2 ONLY guard let conversation = try await findConversation(inboxId: inboxId, topic: conversationTopic) else { throw Error.conversationNotFound("no conversation found for \(conversationTopic)") } @@ -771,13 +842,16 @@ public class XMTPModule: Module { ) } - AsyncFunction("sendMessageToGroup") { (inboxId: String, id: String, contentJson: String) -> String in - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + AsyncFunction("sendMessageToConversation") { (inboxId: String, id: String, contentJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + guard let conversation = try client.findConversation(conversationId: id) else { + throw Error.conversationNotFound("no conversation found for \(id)") } let sending = try ContentJson.fromJson(contentJson) - return try await group.send( + return try await conversation.send( content: sending.content, options: SendOptions(contentType: sending.type) ) @@ -791,13 +865,16 @@ public class XMTPModule: Module { try await group.publishMessages() } - AsyncFunction("prepareGroupMessage") { (inboxId: String, id: String, contentJson: String) -> String in - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + AsyncFunction("prepareConversationMessage") { (inboxId: String, id: String, contentJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + guard let conversation = try client.findConversation(conversationId: id) else { + throw Error.conversationNotFound("no conversation found for \(id)") } let sending = try ContentJson.fromJson(contentJson) - return try await group.prepareMessage( + return try await conversation.prepareMessageV3( content: sending.content, options: SendOptions(contentType: sending.type) ) @@ -808,6 +885,7 @@ public class XMTPModule: Module { conversationTopic: String, contentJson: String ) -> String in + // V2 ONLY guard let conversation = try await findConversation(inboxId: inboxId, topic: conversationTopic) else { throw Error.conversationNotFound("no conversation found for \(conversationTopic)") } @@ -832,6 +910,7 @@ public class XMTPModule: Module { conversationTopic: String, encodedContentData: [UInt8] ) -> String in + // V2 ONLY guard let conversation = try await findConversation(inboxId: inboxId, topic: conversationTopic) else { throw Error.conversationNotFound("no conversation found for \(conversationTopic)") } @@ -852,6 +931,7 @@ public class XMTPModule: Module { } AsyncFunction("sendPreparedMessage") { (inboxId: String, preparedLocalMessageJson: String) -> String in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -875,6 +955,7 @@ public class XMTPModule: Module { } AsyncFunction("createConversation") { (inboxId: String, peerAddress: String, contextJson: String, consentProofBytes: [UInt8]) -> String in + // V2 ONLY guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } @@ -906,6 +987,20 @@ public class XMTPModule: Module { } } + AsyncFunction("findOrCreateDm") { (inboxId: String, peerAddress: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + + do { + let dm = try await client.conversations.findOrCreateDm(with: peerAddress) + return try await DmWrapper.encode(dm, client: client) + } catch { + print("ERRRO!: \(error.localizedDescription)") + throw error + } + } + AsyncFunction("createGroup") { (inboxId: String, peerAddresses: [String], permission: String, groupOptionsJson: String) -> String in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient @@ -968,43 +1063,60 @@ public class XMTPModule: Module { return try await group.members.map(\.inboxId) } - AsyncFunction("listGroupMembers") { (inboxId: String, groupId: String) -> [String] in + AsyncFunction("dmPeerInboxId") { (inboxId: String, dmId: String) -> String in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } + guard let conversation = try client.findConversation(conversationId: dmId) else { + throw Error.conversationNotFound("no conversation found for \(dmId)") + } + if case let .dm(dm) = conversation { + return try await dm.peerInboxId + } else { + throw Error.conversationNotFound("no conversation found for \(dmId)") - guard let group = try await findGroup(inboxId: inboxId, id: groupId) else { - throw Error.conversationNotFound("no group found for \(groupId)") } - return try await group.members.compactMap { member in + } + + AsyncFunction("listConversationMembers") { (inboxId: String, conversationId: String) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + guard let conversation = try client.findConversation(conversationId: conversationId) else { + throw Error.conversationNotFound("no conversation found for \(conversationId)") + } + return try await conversation.members().compactMap { member in return try MemberWrapper.encode(member) } } - AsyncFunction("syncGroups") { (inboxId: String) in + AsyncFunction("syncConversations") { (inboxId: String) in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } try await client.conversations.sync() } - AsyncFunction("syncAllGroups") { (inboxId: String) -> UInt32 in + AsyncFunction("syncAllConversations") { (inboxId: String) -> UInt32 in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - return try await client.conversations.syncAllGroups() + return try await client.conversations.syncAllConversations() } - AsyncFunction("syncGroup") { (inboxId: String, id: String) in + AsyncFunction("syncConversation") { (inboxId: String, id: String) in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let conversation = try client.findConversation(conversationId: id) else { + throw Error.conversationNotFound("no conversation found for \(id)") } - try await group.sync() + try await conversation.sync() } AsyncFunction("addGroupMembers") { (inboxId: String, id: String, peerAddresses: [String]) in @@ -1356,20 +1468,20 @@ public class XMTPModule: Module { - AsyncFunction("processGroupMessage") { (inboxId: String, id: String, encryptedMessage: String) -> String in + AsyncFunction("processConversationMessage") { (inboxId: String, id: String, encryptedMessage: String) -> String in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let conversation = try client.findConversation(conversationId: id) else { + throw Error.conversationNotFound("no conversation found for \(id)") } guard let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) else { throw Error.noMessage } - let decodedMessage = try await group.processMessageDecrypted(envelopeBytes: encryptedMessageData) - return try DecodedMessageWrapper.encode(decodedMessage, client: client) + let decodedMessage = try await conversation.processMessage(envelopeBytes: encryptedMessageData) + return try DecodedMessageWrapper.encode(decodedMessage.decrypt(), client: client) } AsyncFunction("processWelcomeMessage") { (inboxId: String, encryptedMessage: String) -> String in @@ -1385,8 +1497,23 @@ public class XMTPModule: Module { return try await GroupWrapper.encode(group, client: client) } + + AsyncFunction("processConversationWelcomeMessage") { (inboxId: String, encryptedMessage: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + guard let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) else { + throw Error.noMessage + } + guard let conversation = try await client.conversations.conversationFromWelcome(envelopeBytes: encryptedMessageData) else { + throw Error.conversationNotFound("no group found") + } + + return try await ConversationContainerWrapper.encode(conversation, client: client) + } AsyncFunction("subscribeToConversations") { (inboxId: String) in + // V2 ONLY try await subscribeToConversations(inboxId: inboxId) } @@ -1399,6 +1526,7 @@ public class XMTPModule: Module { } AsyncFunction("subscribeToMessages") { (inboxId: String, topic: String) in + // V2 ONLY try await subscribeToMessages(inboxId: inboxId, topic: topic) } @@ -1415,6 +1543,7 @@ public class XMTPModule: Module { } AsyncFunction("unsubscribeFromConversations") { (inboxId: String) in + // V2 ONLY await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))?.cancel() } @@ -1428,6 +1557,7 @@ public class XMTPModule: Module { AsyncFunction("unsubscribeFromMessages") { (inboxId: String, topic: String) in + // V2 ONLY try await unsubscribeFromMessages(inboxId: inboxId, topic: topic) } @@ -1477,6 +1607,7 @@ public class XMTPModule: Module { } AsyncFunction("decodeMessage") { (inboxId: String, topic: String, encryptedMessage: String) -> String in + // V2 ONLY guard let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) else { throw Error.noMessage } @@ -1571,11 +1702,15 @@ public class XMTPModule: Module { return try ConsentWrapper.consentStateToString(state: await conversation.consentState()) } - AsyncFunction("groupConsentState") { (inboxId: String, groupId: String) -> String in - guard let group = try await findGroup(inboxId: inboxId, id: groupId) else { - throw Error.conversationNotFound("no group found for \(groupId)") + AsyncFunction("conversationV3ConsentState") { (inboxId: String, conversationId: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient } - return try ConsentWrapper.consentStateToString(state: await group.consentState()) + + guard let conversation = try client.findConversation(conversationId: conversationId) else { + throw Error.conversationNotFound("no conversation found for \(conversationId)") + } + return try ConsentWrapper.consentStateToString(state: await conversation.consentState()) } AsyncFunction("consentList") { (inboxId: String) -> [String] in @@ -1638,12 +1773,16 @@ public class XMTPModule: Module { return try await client.contacts.isGroupDenied(groupId: groupId) } - AsyncFunction("updateGroupConsent") { (inboxId: String, groupId: String, state: String) in - guard let group = try await findGroup(inboxId: inboxId, id: groupId) else { - throw Error.conversationNotFound(groupId) + AsyncFunction("updateConversationConsent") { (inboxId: String, conversationId: String, state: String) in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient } - try await group.updateConsentState(state: getConsentState(state: state)) + guard let conversation = try client.findConversation(conversationId: conversationId) else { + throw Error.conversationNotFound("no conversation found for \(conversationId)") + } + + try await conversation.updateConsentState(state: getConsentState(state: state)) } AsyncFunction("exportNativeLogs") { () -> String in @@ -1669,6 +1808,30 @@ public class XMTPModule: Module { return logOutput } + + AsyncFunction("subscribeToV3Conversations") { (inboxId: String) in + try await subscribeToV3Conversations(inboxId: inboxId) + } + + AsyncFunction("subscribeToAllConversationMessages") { (inboxId: String) in + try await subscribeToAllConversationMessages(inboxId: inboxId) + } + + AsyncFunction("subscribeToConversationMessages") { (inboxId: String, id: String) in + try await subscribeToConversationMessages(inboxId: inboxId, id: id) + } + + AsyncFunction("unsubscribeFromAllConversationMessages") { (inboxId: String) in + await subscriptionsManager.get(getConversationMessagesKey(inboxId: inboxId))?.cancel() + } + + AsyncFunction("unsubscribeFromV3Conversations") { (inboxId: String) in + await subscriptionsManager.get(getV3ConversationsKey(inboxId: inboxId))?.cancel() + } + + AsyncFunction("unsubscribeFromConversationMessages") { (inboxId: String, id: String) in + try await unsubscribeFromConversationMessages(inboxId: inboxId, id: id) + } OnAppBecomesActive { Task { @@ -1881,6 +2044,27 @@ public class XMTPModule: Module { }) } + func subscribeToV3Conversations(inboxId: String) async throws { + guard let client = await clientsManager.getClient(key: inboxId) else { + return + } + + await subscriptionsManager.get(getV3ConversationsKey(inboxId: inboxId))?.cancel() + await subscriptionsManager.set(getV3ConversationsKey(inboxId: inboxId), Task { + do { + for try await conversation in await client.conversations.streamConversations() { + try await sendEvent("conversationV3", [ + "inboxId": inboxId, + "conversation": ConversationContainerWrapper.encodeToObj(conversation, client: client), + ]) + } + } catch { + print("Error in all conversations subscription: \(error)") + await subscriptionsManager.get(getV3ConversationsKey(inboxId: inboxId))?.cancel() + } + }) + } + func subscribeToGroups(inboxId: String) async throws { guard let client = await clientsManager.getClient(key: inboxId) else { return @@ -1922,6 +2106,27 @@ public class XMTPModule: Module { }) } + func subscribeToAllConversationMessages(inboxId: String) async throws { + guard let client = await clientsManager.getClient(key: inboxId) else { + return + } + + await subscriptionsManager.get(getConversationMessagesKey(inboxId: inboxId))?.cancel() + await subscriptionsManager.set(getConversationMessagesKey(inboxId: inboxId), Task { + do { + for try await message in await client.conversations.streamAllDecryptedConversationMessages() { + try sendEvent("allConversationMessages", [ + "inboxId": inboxId, + "message": DecodedMessageWrapper.encodeToObj(message, client: client), + ]) + } + } catch { + print("Error in all conversations subscription: \(error)") + await subscriptionsManager.get(getConversationMessagesKey(inboxId: inboxId))?.cancel() + } + }) + } + func subscribeToGroupMessages(inboxId: String, id: String) async throws { guard let group = try await findGroup(inboxId: inboxId, id: id) else { return @@ -1952,6 +2157,36 @@ public class XMTPModule: Module { }) } + func subscribeToConversationMessages(inboxId: String, id: String) async throws { + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + + guard let converation = try client.findConversation(conversationId: id) else { + return + } + + await subscriptionsManager.get(try converation.cacheKeyV3(client.inboxID))?.cancel() + await subscriptionsManager.set(try converation.cacheKeyV3(client.inboxID), Task { + do { + for try await message in converation.streamDecryptedMessages() { + do { + try sendEvent("conversationV3Message", [ + "inboxId": inboxId, + "message": DecodedMessageWrapper.encodeToObj(message, client: client), + "conversationId": id, + ]) + } catch { + print("discarding message, unable to encode wrapper \(message.id)") + } + } + } catch { + print("Error in group messages subscription: \(error)") + await subscriptionsManager.get(converation.cacheKey(inboxId))?.cancel() + } + }) + } + func unsubscribeFromMessages(inboxId: String, topic: String) async throws { guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { @@ -1968,6 +2203,18 @@ public class XMTPModule: Module { await subscriptionsManager.get(group.cacheKey(inboxId))?.cancel() } + + func unsubscribeFromConversationMessages(inboxId: String, id: String) async throws { + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + + guard let converation = try client.findConversation(conversationId: id) else { + return + } + + await subscriptionsManager.get(try converation.cacheKeyV3(inboxId))?.cancel() + } func getMessagesKey(inboxId: String) -> String { return "messages:\(inboxId)" @@ -1981,6 +2228,14 @@ public class XMTPModule: Module { return "conversations:\(inboxId)" } + func getConversationMessagesKey(inboxId: String) -> String { + return "conversationMessages:\(inboxId)" + } + + func getV3ConversationsKey(inboxId: String) -> String { + return "conversationsV3:\(inboxId)" + } + func getGroupsKey(inboxId: String) -> String { return "groups:\(inboxId)" } diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index fd780cac2..d8cbfd215 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.15.2" + s.dependency "XMTP", "= 0.16.0" end diff --git a/src/index.ts b/src/index.ts index a14d5092a..dc9416336 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1387,12 +1387,12 @@ export async function isGroupDenied( return XMTPModule.isGroupDenied(inboxId, groupId) } -export async function updateGroupConsent( +export async function updateConversationConsent( inboxId: string, - groupId: string, + conversationId: string, state: string ): Promise { - return XMTPModule.updateGroupConsent(inboxId, groupId, state) + return XMTPModule.updateConversationConsent(inboxId, conversationId, state) } export async function allowInboxes(