diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index e8476ed6a..8aa94dbea 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -18,13 +18,13 @@ import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString import expo.modules.xmtpreactnativesdk.wrappers.ContentJson import expo.modules.xmtpreactnativesdk.wrappers.ConversationContainerWrapper -import expo.modules.xmtpreactnativesdk.wrappers.ConversationOrder import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper import expo.modules.xmtpreactnativesdk.wrappers.CreateGroupParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment +import expo.modules.xmtpreactnativesdk.wrappers.DmWrapper import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment -import expo.modules.xmtpreactnativesdk.wrappers.GroupParamsWrapper +import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper import expo.modules.xmtpreactnativesdk.wrappers.InboxStateWrapper import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper @@ -42,6 +42,7 @@ import org.xmtp.android.library.Client 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.Group import org.xmtp.android.library.PreEventCallback import org.xmtp.android.library.PreparedMessage @@ -59,7 +60,6 @@ import org.xmtp.android.library.codecs.decoded import org.xmtp.android.library.hexToByteArray import org.xmtp.android.library.messages.EnvelopeBuilder import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.MessageDeliveryStatus import org.xmtp.android.library.messages.Pagination import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.Signature @@ -149,6 +149,10 @@ fun Group.cacheKey(inboxId: String): String { return "${inboxId}:${id}" } +fun Conversation.cacheKeyV3(inboxId: String): String { + return "${inboxId}:${topic}:${id}" +} + class XMTPModule : Module() { val context: Context @@ -245,16 +249,19 @@ class XMTPModule : Module() { "preCreateIdentityCallback", "preEnableIdentityCallback", "preAuthenticateToInboxCallback", - // Conversations + // ConversationV2 "conversation", - "group", "conversationContainer", "message", - "allGroupMessage", - // Conversation "conversationMessage", + // ConversationV3 + "conversationV3", + "allConversationMessages", + "conversationV3Message", // Group "groupMessage", + "allGroupMessage", + "group", ) Function("address") { inboxId: String -> @@ -473,6 +480,7 @@ class XMTPModule : Module() { } AsyncFunction("sign") Coroutine { inboxId: String, digest: List, keyType: String, preKeyIndex: Int -> + // V2 ONLY withContext(Dispatchers.IO) { logV("sign") val client = clients[inboxId] ?: throw XMTPException("No client") @@ -498,12 +506,14 @@ class XMTPModule : Module() { } AsyncFunction("exportPublicKeyBundle") { inboxId: String -> + // V2 ONLY logV("exportPublicKeyBundle") val client = clients[inboxId] ?: throw XMTPException("No client") client.keys.getPublicKeyBundle().toByteArray().map { it.toInt() and 0xFF } } AsyncFunction("exportKeyBundle") { inboxId: String -> + // V2 ONLY logV("exportKeyBundle") val client = clients[inboxId] ?: throw XMTPException("No client") Base64.encodeToString(client.privateKeyBundle.toByteArray(), NO_WRAP) @@ -511,6 +521,7 @@ class XMTPModule : Module() { // Export the conversation's serialized topic data. AsyncFunction("exportConversationTopicData") Coroutine { inboxId: String, topic: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("exportConversationTopicData") val conversation = findConversation(inboxId, topic) @@ -529,6 +540,7 @@ class XMTPModule : Module() { // Import a conversation from its serialized topic data. AsyncFunction("importConversationTopicData") { inboxId: String, topicData: String -> + // V2 ONLY logV("importConversationTopicData") val client = clients[inboxId] ?: throw XMTPException("No client") val data = TopicData.parseFrom(Base64.decode(topicData, NO_WRAP)) @@ -543,6 +555,7 @@ class XMTPModule : Module() { // // Client API AsyncFunction("canMessage") Coroutine { inboxId: String, peerAddress: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("canMessage") @@ -561,6 +574,7 @@ class XMTPModule : Module() { } AsyncFunction("staticCanMessage") Coroutine { peerAddress: String, environment: String, appVersion: String? -> + // V2 ONLY withContext(Dispatchers.IO) { try { logV("staticCanMessage") @@ -639,6 +653,7 @@ class XMTPModule : Module() { } AsyncFunction("sendEncodedContent") Coroutine { inboxId: String, topic: String, encodedContentData: List -> + // V2 ONLY withContext(Dispatchers.IO) { val conversation = findConversation( @@ -662,6 +677,7 @@ class XMTPModule : Module() { } AsyncFunction("listConversations") Coroutine { inboxId: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("listConversations") val client = clients[inboxId] ?: throw XMTPException("No client") @@ -680,7 +696,7 @@ class XMTPModule : Module() { withContext(Dispatchers.IO) { logV("listGroups") val client = clients[inboxId] ?: throw XMTPException("No client") - val params = GroupParamsWrapper.groupParamsFromJson(groupParams ?: "") + val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") val sortedGroupList = if (order == ConversationOrder.LAST_MESSAGE) { client.conversations.listGroups() @@ -700,6 +716,21 @@ class XMTPModule : Module() { } } + 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.conversationParamsFromJson(conversationParams ?: "") + val order = getConversationSortOrder(sortOrder ?: "") + val conversations = + client.conversations.listConversations(order = order, limit = limit) + conversations.map { conversation -> + ConversationContainerWrapper.encode(client, conversation, params) + } + } + } + AsyncFunction("listAll") Coroutine { inboxId: String -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") @@ -712,6 +743,7 @@ class XMTPModule : Module() { } AsyncFunction("loadMessages") Coroutine { inboxId: String, topic: String, limit: Int?, before: Long?, after: Long?, direction: String? -> + // V2 ONLY withContext(Dispatchers.IO) { logV("loadMessages") val conversation = @@ -734,22 +766,19 @@ class XMTPModule : Module() { } } - AsyncFunction("groupMessages") Coroutine { inboxId: String, id: String, limit: Int?, before: Long?, after: Long?, direction: String?, deliveryStatus: String? -> + AsyncFunction("conversationMessages") Coroutine { inboxId: String, conversationId: String, limit: Int?, before: Long?, after: Long?, direction: String? -> withContext(Dispatchers.IO) { - logV("groupMessages") + logV("conversationMessages") val client = clients[inboxId] ?: throw XMTPException("No client") val beforeDate = if (before != null) Date(before) else null val afterDate = if (after != null) Date(after) else null - val group = findGroup(inboxId, id) - group?.decryptedMessages( + val conversation = client.findConversation(conversationId) + conversation?.decryptedMessages( limit = limit, before = beforeDate, after = afterDate, direction = MessageApiOuterClass.SortDirection.valueOf( direction ?: "SORT_DIRECTION_DESCENDING" - ), - deliveryStatus = MessageDeliveryStatus.valueOf( - deliveryStatus ?: "ALL" ) )?.map { DecodedMessageWrapper.encode(it) } } @@ -777,7 +806,41 @@ class XMTPModule : Module() { } } + AsyncFunction("findConversation") Coroutine { inboxId: String, conversationId: String -> + withContext(Dispatchers.IO) { + logV("findConversation") + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + conversation?.let { + ConversationContainerWrapper.encode(client, conversation) + } + } + } + + AsyncFunction("findConversationByTopic") Coroutine { inboxId: String, topic: String -> + withContext(Dispatchers.IO) { + logV("findConversationByTopic") + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversationByTopic(topic) + conversation?.let { + ConversationContainerWrapper.encode(client, conversation) + } + } + } + + AsyncFunction("findDm") Coroutine { inboxId: String, peerAddress: String -> + withContext(Dispatchers.IO) { + logV("findDm") + val client = clients[inboxId] ?: throw XMTPException("No client") + val dm = client.findDm(peerAddress) + dm?.let { + DmWrapper.encode(client, dm) + } + } + } + AsyncFunction("loadBatchMessages") Coroutine { inboxId: String, topics: List -> + // V2 ONLY withContext(Dispatchers.IO) { logV("loadBatchMessages") val client = clients[inboxId] ?: throw XMTPException("No client") @@ -825,6 +888,7 @@ class XMTPModule : Module() { } AsyncFunction("sendMessage") Coroutine { inboxId: String, conversationTopic: String, contentJson: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("sendMessage") val conversation = @@ -841,17 +905,14 @@ class XMTPModule : Module() { } } - AsyncFunction("sendMessageToGroup") Coroutine { inboxId: String, id: String, contentJson: String -> + AsyncFunction("sendMessageToConversation") Coroutine { inboxId: String, id: String, contentJson: String -> withContext(Dispatchers.IO) { - logV("sendMessageToGroup") - val group = - findGroup( - inboxId = inboxId, - id = id - ) - ?: throw XMTPException("no group found for $id") + logV("sendMessageToConversation") + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(id) + ?: throw XMTPException("no conversation found for $id") val sending = ContentJson.fromJson(contentJson) - group.send( + conversation.send( content = sending.content, options = SendOptions(contentType = sending.type) ) @@ -872,17 +933,14 @@ class XMTPModule : Module() { } } - AsyncFunction("prepareGroupMessage") Coroutine { inboxId: String, id: String, contentJson: String -> + AsyncFunction("prepareConversationMessage") Coroutine { inboxId: String, id: String, contentJson: String -> withContext(Dispatchers.IO) { - logV("prepareGroupMessage") - val group = - findGroup( - inboxId = inboxId, - id = id - ) - ?: throw XMTPException("no group found for $id") + logV("prepareConversationMessage") + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(id) + ?: throw XMTPException("no conversation found for $id") val sending = ContentJson.fromJson(contentJson) - group.prepareMessage( + conversation.prepareMessageV3( content = sending.content, options = SendOptions(contentType = sending.type) ) @@ -890,6 +948,7 @@ class XMTPModule : Module() { } AsyncFunction("prepareMessage") Coroutine { inboxId: String, conversationTopic: String, contentJson: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("prepareMessage") val conversation = @@ -915,6 +974,7 @@ class XMTPModule : Module() { } AsyncFunction("prepareEncodedMessage") Coroutine { inboxId: String, conversationTopic: String, encodedContentData: List -> + // V2 ONLY withContext(Dispatchers.IO) { logV("prepareEncodedMessage") val conversation = @@ -950,6 +1010,7 @@ class XMTPModule : Module() { } AsyncFunction("sendPreparedMessage") Coroutine { inboxId: String, preparedLocalMessageJson: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("sendPreparedMessage") val client = clients[inboxId] ?: throw XMTPException("No client") @@ -970,6 +1031,7 @@ class XMTPModule : Module() { } AsyncFunction("createConversation") Coroutine { inboxId: String, peerAddress: String, contextJson: String, consentProofPayload: List -> + // V2 Only withContext(Dispatchers.IO) { logV("createConversation: $contextJson") val client = clients[inboxId] ?: throw XMTPException("No client") @@ -1017,6 +1079,7 @@ class XMTPModule : Module() { ConversationWrapper.encode(client, conversation) } } + AsyncFunction("createGroup") Coroutine { inboxId: String, peerAddresses: List, permission: String, groupOptionsJson: String -> withContext(Dispatchers.IO) { logV("createGroup") @@ -1039,6 +1102,15 @@ class XMTPModule : Module() { } } + AsyncFunction("findOrCreateDm") Coroutine { inboxId: String, peerAddress: String -> + withContext(Dispatchers.IO) { + logV("findOrCreateDm") + val client = clients[inboxId] ?: throw XMTPException("No client") + val dm = client.conversations.findOrCreateDm(peerAddress) + DmWrapper.encode(client, dm) + } + } + AsyncFunction("createGroupCustomPermissions") Coroutine { inboxId: String, peerAddresses: List, permissionPolicySetJson: String, groupOptionsJson: String -> withContext(Dispatchers.IO) { logV("createGroup") @@ -1071,42 +1143,54 @@ class XMTPModule : Module() { } } - AsyncFunction("listGroupMembers") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("dmPeerInboxId") Coroutine { inboxId: String, dmId: String -> withContext(Dispatchers.IO) { - logV("listGroupMembers") + logV("listPeerInboxId") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, groupId) - group?.members()?.map { MemberWrapper.encode(it) } + val conversation = client.findConversation(dmId) + ?: throw XMTPException("no conversation found for $dmId") + val dm = (conversation as Conversation.Dm).dm + dm.peerInboxId() + } + } + + AsyncFunction("listConversationMembers") Coroutine { inboxId: String, conversationId: String -> + withContext(Dispatchers.IO) { + logV("listConversationMembers") + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no conversation found for $conversationId") + conversation.members().map { MemberWrapper.encode(it) } } } - AsyncFunction("syncGroups") Coroutine { inboxId: String -> + AsyncFunction("syncConversations") Coroutine { inboxId: String -> withContext(Dispatchers.IO) { - logV("syncGroups") + logV("syncConversations") val client = clients[inboxId] ?: throw XMTPException("No client") - client.conversations.syncGroups() + client.conversations.syncConversations() } } - AsyncFunction("syncAllGroups") Coroutine { inboxId: String -> + AsyncFunction("syncAllConversations") Coroutine { inboxId: String -> withContext(Dispatchers.IO) { - logV("syncAllGroups") + logV("syncAllConversations") val client = clients[inboxId] ?: throw XMTPException("No client") // Expo Modules do not support UInt, so we need to convert to Int val numGroupsSyncedInt: Int = - client.conversations.syncAllGroups()?.toInt() ?: throw IllegalArgumentException( - "Value cannot be null" - ) + client.conversations.syncAllConversations()?.toInt() + ?: throw IllegalArgumentException("Value cannot be null") numGroupsSyncedInt } } - AsyncFunction("syncGroup") Coroutine { inboxId: String, id: String -> + AsyncFunction("syncConversation") Coroutine { inboxId: String, id: String -> withContext(Dispatchers.IO) { - logV("syncGroup") + logV("syncConversation") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - group?.sync() + val conversation = client.findConversation(id) + ?: throw XMTPException("no conversation found for $id") + conversation.sync() } } @@ -1433,14 +1517,13 @@ class XMTPModule : Module() { } } - AsyncFunction("processGroupMessage") Coroutine { inboxId: String, id: String, encryptedMessage: String -> + AsyncFunction("processConversationMessage") Coroutine { inboxId: String, id: String, encryptedMessage: String -> withContext(Dispatchers.IO) { logV("processGroupMessage") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - val message = group?.processMessage(Base64.decode(encryptedMessage, NO_WRAP)) - ?: throw XMTPException("could not decrypt message for $id") + val conversation = client.findConversation(id) + ?: throw XMTPException("no conversation found for $id") + val message = conversation.processMessage(Base64.decode(encryptedMessage, NO_WRAP)) DecodedMessageWrapper.encodeMap(message.decrypt()) } } @@ -1456,7 +1539,24 @@ class XMTPModule : Module() { } } + AsyncFunction("processConversationWelcomeMessage") Coroutine { inboxId: String, encryptedMessage: String -> + withContext(Dispatchers.IO) { + logV("processConversationWelcomeMessage") + val client = clients[inboxId] ?: throw XMTPException("No client") + + val conversation = + client.conversations.conversationFromWelcome( + Base64.decode( + encryptedMessage, + NO_WRAP + ) + ) + ConversationContainerWrapper.encode(client, conversation) + } + } + Function("subscribeToConversations") { inboxId: String -> + // V2 ONLY logV("subscribeToConversations") subscribeToConversations(inboxId = inboxId) } @@ -1482,6 +1582,7 @@ class XMTPModule : Module() { } AsyncFunction("subscribeToMessages") Coroutine { inboxId: String, topic: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("subscribeToMessages") subscribeToMessages( @@ -1502,6 +1603,7 @@ class XMTPModule : Module() { } Function("unsubscribeFromConversations") { inboxId: String -> + // V2 ONLY logV("unsubscribeFromConversations") subscriptions[getConversationsKey(inboxId)]?.cancel() } @@ -1522,6 +1624,7 @@ class XMTPModule : Module() { } AsyncFunction("unsubscribeFromMessages") Coroutine { inboxId: String, topic: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("unsubscribeFromMessages") unsubscribeFromMessages( @@ -1579,6 +1682,7 @@ class XMTPModule : Module() { } AsyncFunction("decodeMessage") Coroutine { inboxId: String, topic: String, encryptedMessage: String -> + // V2 ONLY withContext(Dispatchers.IO) { logV("decodeMessage") val encryptedMessageData = Base64.decode(encryptedMessage, NO_WRAP) @@ -1673,11 +1777,12 @@ class XMTPModule : Module() { } } - AsyncFunction("groupConsentState") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("conversationV3ConsentState") Coroutine { inboxId: String, conversationId: String -> withContext(Dispatchers.IO) { - val group = findGroup(inboxId, groupId) - ?: throw XMTPException("no group found for $groupId") - consentStateToString(group.consentState()) + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no group found for $conversationId") + consentStateToString(conversation.consentState()) } } @@ -1731,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)) } } @@ -1757,6 +1864,46 @@ class XMTPModule : Module() { } } } + + Function("subscribeToV3Conversations") { inboxId: String -> + logV("subscribeToV3Conversations") + subscribeToV3Conversations(inboxId = inboxId) + } + + Function("subscribeToAllConversationMessages") { inboxId: String -> + logV("subscribeToAllConversationMessages") + subscribeToAllConversationMessages(inboxId = inboxId) + } + + AsyncFunction("subscribeToConversationMessages") Coroutine { inboxId: String, id: String -> + withContext(Dispatchers.IO) { + logV("subscribeToConversationMessages") + subscribeToConversationMessages( + inboxId = inboxId, + id = id + ) + } + } + + Function("unsubscribeFromAllConversationMessages") { inboxId: String -> + logV("unsubscribeFromAllConversationMessages") + subscriptions[getConversationMessagesKey(inboxId)]?.cancel() + } + + Function("unsubscribeFromV3Conversations") { inboxId: String -> + logV("unsubscribeFromV3Conversations") + subscriptions[getV3ConversationsKey(inboxId)]?.cancel() + } + + AsyncFunction("unsubscribeFromConversationMessages") Coroutine { inboxId: String, id: String -> + withContext(Dispatchers.IO) { + logV("unsubscribeFromConversationMessages") + unsubscribeFromConversationMessages( + inboxId = inboxId, + id = id + ) + } + } } // @@ -1881,6 +2028,32 @@ class XMTPModule : Module() { } } + private fun subscribeToV3Conversations(inboxId: String) { + 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 + ) + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in group subscription: $e") + subscriptions[getV3ConversationsKey(client.inboxId)]?.cancel() + } + } + } + private fun subscribeToAll(inboxId: String) { val client = clients[inboxId] ?: throw XMTPException("No client") @@ -1951,6 +2124,28 @@ class XMTPModule : Module() { } } + private fun subscribeToAllConversationMessages(inboxId: String) { + val client = clients[inboxId] ?: throw XMTPException("No client") + + subscriptions[getConversationMessagesKey(inboxId)]?.cancel() + subscriptions[getConversationMessagesKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { + try { + client.conversations.streamAllConversationDecryptedMessages().collect { message -> + sendEvent( + "allConversationMessages", + mapOf( + "inboxId" to inboxId, + "message" to DecodedMessageWrapper.encodeMap(message), + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in all group messages subscription: $e") + subscriptions[getConversationMessagesKey(inboxId)]?.cancel() + } + } + } + private suspend fun subscribeToMessages(inboxId: String, topic: String) { val conversation = findConversation( @@ -2006,6 +2201,31 @@ class XMTPModule : Module() { } } + private suspend fun subscribeToConversationMessages(inboxId: String, id: String) { + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(id) + ?: throw XMTPException("no conversation found for $id") + subscriptions[conversation.cacheKeyV3(inboxId)]?.cancel() + subscriptions[conversation.cacheKeyV3(inboxId)] = + CoroutineScope(Dispatchers.IO).launch { + try { + conversation.streamDecryptedMessages().collect { message -> + sendEvent( + "conversationV3Message", + mapOf( + "inboxId" to inboxId, + "message" to DecodedMessageWrapper.encodeMap(message), + "conversationId" to id, + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in messages subscription: $e") + subscriptions[conversation?.cacheKey(inboxId)]?.cancel() + } + } + } + private fun getMessagesKey(inboxId: String): String { return "messages:$inboxId" } @@ -2014,10 +2234,18 @@ class XMTPModule : Module() { return "groupMessages:$inboxId" } + private fun getConversationMessagesKey(inboxId: String): String { + return "conversationMessages:$inboxId" + } + private fun getConversationsKey(inboxId: String): String { return "conversations:$inboxId" } + private fun getV3ConversationsKey(inboxId: String): String { + return "conversationsV3:$inboxId" + } + private fun getGroupsKey(inboxId: String): String { return "groups:$inboxId" } @@ -2034,7 +2262,7 @@ class XMTPModule : Module() { subscriptions[conversation.cacheKey(inboxId)]?.cancel() } - private suspend fun unsubscribeFromGroupMessages( + private fun unsubscribeFromGroupMessages( inboxId: String, id: String, ) { @@ -2048,6 +2276,15 @@ class XMTPModule : Module() { subscriptions[group.cacheKey(inboxId)]?.cancel() } + private fun unsubscribeFromConversationMessages( + inboxId: String, + id: String, + ) { + val client = clients[inboxId] ?: throw XMTPException("No client") + val convo = client.findConversation(id) ?: return + subscriptions[convo.cacheKey(inboxId)]?.cancel() + } + private fun logV(msg: String) { if (isDebugEnabled) { Log.v("XMTPModule", msg) 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 f8dc148d7..546fe2b16 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt @@ -1,6 +1,5 @@ package expo.modules.xmtpreactnativesdk.wrappers -import android.util.Base64 import com.google.gson.GsonBuilder import org.xmtp.android.library.Client import org.xmtp.android.library.Conversation @@ -8,21 +7,36 @@ import org.xmtp.android.library.Conversation class ConversationContainerWrapper { companion object { - suspend fun encodeToObj(client: Client, conversation: Conversation): Map { - when (conversation.version) { + suspend fun encodeToObj( + client: Client, + conversation: Conversation, + conversationParams: ConversationParamsWrapper = ConversationParamsWrapper(), + ): Map { + return when (conversation.version) { Conversation.Version.GROUP -> { val group = (conversation as Conversation.Group).group - return GroupWrapper.encodeToObj(client, group) + GroupWrapper.encodeToObj(client, group, conversationParams) } + + Conversation.Version.DM -> { + val dm = (conversation as Conversation.Dm).dm + DmWrapper.encodeToObj(client, dm, conversationParams) + } + else -> { - return ConversationWrapper.encodeToObj(client, conversation) + ConversationWrapper.encodeToObj(client, conversation) } } } - suspend fun encode(client: Client, conversation: Conversation): String { + suspend fun encode( + client: Client, + conversation: Conversation, + conversationParams: ConversationParamsWrapper = ConversationParamsWrapper(), + ): String { val gson = GsonBuilder().create() - val obj = ConversationContainerWrapper.encodeToObj(client, conversation) + val obj = + ConversationContainerWrapper.encodeToObj(client, conversation, conversationParams) return gson.toJson(obj) } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt new file mode 100644 index 000000000..c2813f93e --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt @@ -0,0 +1,50 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import com.google.gson.GsonBuilder +import com.google.gson.JsonParser +import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString +import org.xmtp.android.library.Client +import org.xmtp.android.library.Dm +import org.xmtp.android.library.Group + +class DmWrapper { + companion object { + suspend fun encodeToObj( + client: Client, + dm: Dm, + dmParams: ConversationParamsWrapper = ConversationParamsWrapper(), + ): Map { + return buildMap { + put("clientAddress", client.address) + put("id", dm.id) + put("createdAt", dm.createdAt.time) + put("version", "DM") + put("topic", dm.topic) + put("peerInboxId", dm.peerInboxId()) + if (dmParams.members) { + put("members", dm.members().map { MemberWrapper.encode(it) }) + } + if (dmParams.creatorInboxId) put("creatorInboxId", dm.creatorInboxId()) + if (dmParams.consentState) { + put("consentState", consentStateToString(dm.consentState())) + } + if (dmParams.lastMessage) { + val lastMessage = dm.decryptedMessages(limit = 1).firstOrNull() + if (lastMessage != null) { + put("lastMessage", DecodedMessageWrapper.encode(lastMessage)) + } + } + } + } + + suspend fun encode( + client: Client, + dm: Dm, + dmParams: ConversationParamsWrapper = ConversationParamsWrapper(), + ): String { + val gson = GsonBuilder().create() + val obj = encodeToObj(client, dm, dmParams) + 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 b75c58c4f..939d4ad25 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -6,17 +6,13 @@ import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consent import org.xmtp.android.library.Client import org.xmtp.android.library.Group -enum class ConversationOrder { - LAST_MESSAGE, CREATED_AT -} - class GroupWrapper { companion object { suspend fun encodeToObj( client: Client, group: Group, - groupParams: GroupParamsWrapper = GroupParamsWrapper(), + groupParams: ConversationParamsWrapper = ConversationParamsWrapper(), ): Map { return buildMap { put("clientAddress", client.address) @@ -48,7 +44,7 @@ class GroupWrapper { suspend fun encode( client: Client, group: Group, - groupParams: GroupParamsWrapper = GroupParamsWrapper(), + groupParams: ConversationParamsWrapper = ConversationParamsWrapper(), ): String { val gson = GsonBuilder().create() val obj = encodeToObj(client, group, groupParams) @@ -57,7 +53,7 @@ class GroupWrapper { } } -class GroupParamsWrapper( +class ConversationParamsWrapper( val members: Boolean = true, val creatorInboxId: Boolean = true, val isActive: Boolean = true, @@ -69,10 +65,10 @@ class GroupParamsWrapper( val lastMessage: Boolean = false, ) { companion object { - fun groupParamsFromJson(groupParams: String): GroupParamsWrapper { - if (groupParams.isEmpty()) return GroupParamsWrapper() - val jsonOptions = JsonParser.parseString(groupParams).asJsonObject - return GroupParamsWrapper( + 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, if (jsonOptions.has("isActive")) jsonOptions.get("isActive").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/example/src/TestScreen.tsx b/example/src/TestScreen.tsx index fad1fd93e..262e70198 100644 --- a/example/src/TestScreen.tsx +++ b/example/src/TestScreen.tsx @@ -2,6 +2,7 @@ import { useRoute } from '@react-navigation/native' import React, { useEffect, useState } from 'react' import { View, Text, Button, ScrollView } from 'react-native' +import { conversationTests } from './tests/conversationTests' import { groupPerformanceTests } from './tests/groupPerformanceTests' import { groupPermissionsTests } from './tests/groupPermissionsTests' import { groupTests } from './tests/groupTests' @@ -107,6 +108,7 @@ function TestView({ export enum TestCategory { all = 'all', tests = 'tests', + conversation = 'conversation', group = 'group', v3Only = 'v3Only', restartStreans = 'restartStreams', @@ -123,6 +125,7 @@ export default function TestScreen(): JSX.Element { const allTests = [ ...tests, ...groupTests, + ...conversationTests, ...v3OnlyTests, ...restartStreamTests, ...groupPermissionsTests, @@ -142,6 +145,10 @@ export default function TestScreen(): JSX.Element { activeTests = groupTests title = 'Group Unit Tests' break + case TestCategory.conversation: + activeTests = conversationTests + title = 'Conversation Unit Tests' + break case TestCategory.v3Only: activeTests = v3OnlyTests title = 'V3 Only Tests' diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts new file mode 100644 index 000000000..ff1e0d4cd --- /dev/null +++ b/example/src/tests/conversationTests.ts @@ -0,0 +1,281 @@ +import { Test, assert, createV3Clients, delayToPropogate } from './test-utils' +import { ConversationContainer, ConversationVersion } from '../../../src/index' + +export const conversationTests: Test[] = [] +let counter = 1 +function test(name: string, perform: () => Promise) { + conversationTests.push({ + name: String(counter++) + '. ' + name, + run: perform, + }) +} + +test('can find a conversations by id', async () => { + const [alixClient, boClient] = await createV3Clients(2) + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.syncConversations() + const boGroup = await boClient.conversations.findConversation(alixGroup.id) + const boDm = await boClient.conversations.findConversation(alixDm.id) + const boDm2 = await boClient.conversations.findConversation('GARBAGE') + + assert(boDm2 === undefined, `bodm2 should be undefined`) + + assert( + boGroup?.id === alixGroup.id, + `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` + ) + + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) + + return true +}) + +test('can find a conversation by topic', async () => { + const [alixClient, boClient] = await createV3Clients(2) + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.syncConversations() + const boGroup = await boClient.conversations.findConversationByTopic( + alixGroup.topic + ) + const boDm = await boClient.conversations.findConversationByTopic( + alixDm.topic + ) + + assert( + boGroup?.id === alixGroup.id, + `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` + ) + + assert( + boDm?.id === alixDm.id, + `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` + ) + + return true +}) + +test('can find a dm by address', async () => { + const [alixClient, boClient] = await createV3Clients(2) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.syncConversations() + const boDm = await boClient.conversations.findDm(alixClient.address) + + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) + + return true +}) + +test('can list conversations with params', async () => { + const [alixClient, boClient, caroClient] = await createV3Clients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) + + await boGroup1.send({ text: `first message` }) + await boGroup1.send({ text: `second message` }) + await boGroup1.send({ text: `third message` }) + await boDm2.send({ text: `third message` }) + await boGroup2.send({ text: `first message` }) + await boDm1.send({ text: `dm message` }) + // Order should be [Dm1, Group2, Dm2, Group1] + + await boClient.conversations.syncAllConversations() + const boConvosOrderCreated = await boClient.conversations.listConversations() + const boConvosOrderLastMessage = + await boClient.conversations.listConversations( + { lastMessage: true }, + 'lastMessage' + ) + const boGroupsLimit = await boClient.conversations.listConversations( + {}, + undefined, + 1 + ) + + assert( + boConvosOrderCreated.map((group: any) => group.id).toString() === + [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), + `Conversation created at order should be ${[boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString()} but was ${boConvosOrderCreated.map((group: any) => group.id).toString()}` + ) + + assert( + boConvosOrderLastMessage.map((group: any) => group.id).toString() === + [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), + `Conversation last message order should be ${[boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString()} but was ${boConvosOrderLastMessage.map((group: any) => group.id).toString()}` + ) + + const messages = await boConvosOrderLastMessage[0].messages() + assert( + messages[0].content() === 'dm message', + `last message 1 should be dm message ${messages[0].content()}` + ) + // assert( + // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', + // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` + // ) + assert( + boGroupsLimit.length === 1, + `List length should be 1 but was ${boGroupsLimit.length}` + ) + assert( + boGroupsLimit[0].id === boGroup1.id, + `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` + ) + + return true +}) + +test('can list groups', async () => { + const [alixClient, boClient, caroClient] = await createV3Clients(3) + + const boGroup = await boClient.conversations.newGroup([alixClient.address]) + await boClient.conversations.newGroup([ + caroClient.address, + alixClient.address, + ]) + const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) + await boClient.conversations.findOrCreateDm(alixClient.address) + + const boConversations = await boClient.conversations.listConversations() + await alixClient.conversations.syncConversations() + const alixConversations = await alixClient.conversations.listConversations() + + assert( + boConversations.length === 4, + `bo conversation lengths should be 4 but was ${boConversations.length}` + ) + + assert( + alixConversations.length === 3, + `alix conversation lengths should be 3 but was ${alixConversations.length}` + ) + + if ( + boConversations[0].topic !== boGroup.topic || + boConversations[0].version !== ConversationVersion.GROUP || + boConversations[2].version !== ConversationVersion.DM || + boConversations[2].createdAt !== boDm.createdAt + ) { + throw Error('Listed containers should match streamed containers') + } + + return true +}) + +test('can stream both conversations and messages at same time', async () => { + const [alix, bo] = await createV3Clients(2) + + let conversationCallbacks = 0 + let messageCallbacks = 0 + await bo.conversations.streamConversations(async () => { + conversationCallbacks++ + }) + + await bo.conversations.streamAllConversationMessages(async () => { + messageCallbacks++ + }) + + const group = await alix.conversations.newGroup([bo.address]) + const dm = await alix.conversations.findOrCreateDm(bo.address) + await delayToPropogate() + await group.send('hello') + await dm.send('hello') + await delayToPropogate() + + assert( + conversationCallbacks === 2, + 'conversation stream should have received 2 conversation' + ) + assert( + messageCallbacks === 2, + 'message stream should have received 2 message' + ) + + return true +}) + +test('can stream conversation messages', async () => { + const [alixClient, boClient] = await createV3Clients(2) + + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + const alixConversation = await alixClient.conversations.findConversation( + alixGroup.id + ) + + let dmMessageCallbacks = 0 + let conversationMessageCallbacks = 0 + await alixConversation?.streamMessages(async () => { + conversationMessageCallbacks++ + }) + + await alixDm.streamMessages(async () => { + dmMessageCallbacks++ + }) + + await alixConversation?.send({ text: `first message` }) + await alixDm.send({ text: `first message` }) + + return true +}) + +test('can stream all groups and conversations', async () => { + const [alixClient, boClient, caroClient] = await createV3Clients(3) + + const containers: ConversationContainer[] = [] + const cancelStreamAll = await alixClient.conversations.streamConversations( + async (conversation: ConversationContainer) => { + containers.push(conversation) + } + ) + + await boClient.conversations.newGroup([alixClient.address]) + await delayToPropogate() + if ((containers.length as number) !== 1) { + throw Error( + 'Unexpected num conversations (should be 1): ' + containers.length + ) + } + + await boClient.conversations.findOrCreateDm(alixClient.address) + await delayToPropogate() + if ((containers.length as number) !== 2) { + throw Error( + 'Unexpected num conversations (should be 2): ' + containers.length + ) + } + + await alixClient.conversations.findOrCreateDm(caroClient.address) + await delayToPropogate() + if (containers.length !== 3) { + throw Error( + 'Expected conversations length 3 but it is: ' + containers.length + ) + } + + cancelStreamAll() + await delayToPropogate() + + await caroClient.conversations.newGroup([alixClient.address]) + await delayToPropogate() + if ((containers.length as number) !== 3) { + throw Error( + 'Unexpected num conversations (should be 3): ' + containers.length + ) + } + + return true +}) diff --git a/example/src/tests/groupPerformanceTests.ts b/example/src/tests/groupPerformanceTests.ts index 201a09bbb..a97162c3d 100644 --- a/example/src/tests/groupPerformanceTests.ts +++ b/example/src/tests/groupPerformanceTests.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-extra-non-null-assertion */ -import { Client, Group } from 'xmtp-react-native-sdk' +import { Client, Conversation, Dm, Group } from 'xmtp-react-native-sdk' +import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' -import { Test, assert, createClients } from './test-utils' +import { Test, assert, createClients, createV3Clients } from './test-utils' export const groupPerformanceTests: Test[] = [] let counter = 1 @@ -34,29 +35,101 @@ async function createGroups( return groups } +async function createDms( + client: Client, + peers: Client[], + numMessages: number +): Promise { + const dms = [] + for (let i = 0; i < peers.length; i++) { + const dm = await peers[i].conversations.findOrCreateDm(client.address) + dms.push(dm) + for (let i = 0; i < numMessages; i++) { + await dm.send({ text: `Alix message ${i}` }) + } + } + return dms +} + +async function createV2Convos( + client: Client, + peers: Client[], + numMessages: number +): Promise[]> { + const convos = [] + for (let i = 0; i < peers.length; i++) { + const convo = await peers[i].conversations.newConversation(client.address) + convos.push(convo) + for (let i = 0; i < numMessages; i++) { + await convo.send({ text: `Alix message ${i}` }) + } + } + return convos +} + let alixClient: Client let boClient: Client +let davonV3Client: Client let initialPeers: Client[] let initialGroups: Group[] +// let initialDms: Dm[] +// let initialV2Convos: Conversation[] async function beforeAll( groupSize: number = 1, - groupMessages: number = 1, - peersSize: number = 1 + messages: number = 1, + peersSize: number = 1, + includeDms: boolean = false, + includeV2Convos: boolean = false ) { ;[alixClient] = await createClients(1) + ;[davonV3Client] = await createV3Clients(1) initialPeers = await createClients(peersSize) + const initialV3Peers = await createV3Clients(peersSize) boClient = initialPeers[0] initialGroups = await createGroups( alixClient, initialPeers, groupSize, - groupMessages + messages ) + + if (includeDms) { + await createDms(davonV3Client, initialV3Peers, messages) + } + + if (includeV2Convos) { + await createV2Convos(alixClient, initialPeers, messages) + } } +test('test compare V2 and V3 dms', async () => { + await beforeAll(0, 0, 50, true, true) + let start = Date.now() + let v2Convos = await alixClient.conversations.list() + let end = Date.now() + console.log(`Alix loaded ${v2Convos.length} v2Convos in ${end - start}ms`) + + start = Date.now() + v2Convos = await alixClient.conversations.list() + end = Date.now() + console.log(`Alix 2nd loaded ${v2Convos.length} v2Convos in ${end - start}ms`) + + start = Date.now() + await davonV3Client.conversations.syncConversations() + end = Date.now() + console.log(`Davon synced ${v2Convos.length} Dms in ${end - start}ms`) + + start = Date.now() + const dms = await davonV3Client.conversations.listConversations() + end = Date.now() + console.log(`Davon loaded ${dms.length} Dms in ${end - start}ms`) + + return true +}) + test('testing large group listings with ordering', async () => { await beforeAll(1000, 10, 10) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index c88ed8f7e..55d4f0eb5 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -16,7 +16,6 @@ import { Group, ConversationContainer, ConversationVersion, - MessageDeliveryStatus, GroupUpdatedContent, GroupUpdatedCodec, } from '../../../src/index' @@ -427,15 +426,6 @@ test('group message delivery status', async () => { `the messages length should be 2 but was ${alixMessages.length}` ) - const alixMessagesFiltered: DecodedMessage[] = await alixGroup.messages({ - deliveryStatus: MessageDeliveryStatus.PUBLISHED, - }) - - assert( - alixMessagesFiltered.length === 2, - `the messages length should be 2 but was ${alixMessagesFiltered.length}` - ) - await alixGroup.sync() const alixMessages2: DecodedMessage[] = await alixGroup.messages() @@ -686,56 +676,12 @@ test('unpublished messages handling', async () => { throw new Error(`Message count should be 1, but it is ${messageCount}`) } - // Verify the count of published and unpublished messages - let messageCountPublished = ( - await alixGroup.messages({ - deliveryStatus: MessageDeliveryStatus.PUBLISHED, - }) - ).length - let messageCountUnpublished = ( - await alixGroup.messages({ - deliveryStatus: MessageDeliveryStatus.UNPUBLISHED, - }) - ).length - if (messageCountPublished !== 0) { - throw new Error( - `Published message count should be 0, but it is ${messageCountPublished}` - ) - } - if (messageCountUnpublished !== 1) { - throw new Error( - `Unpublished message count should be 1, but it is ${messageCountUnpublished}` - ) - } - // Publish the prepared message await alixGroup.publishPreparedMessages() // Sync the group after publishing the message await alixGroup.sync() - - // Verify the message counts again - messageCountPublished = ( - await alixGroup.messages({ - deliveryStatus: MessageDeliveryStatus.PUBLISHED, - }) - ).length - messageCountUnpublished = ( - await alixGroup.messages({ - deliveryStatus: MessageDeliveryStatus.UNPUBLISHED, - }) - ).length messageCount = (await alixGroup.messages()).length - if (messageCountPublished !== 1) { - throw new Error( - `Published message count should be 1, but it is ${messageCountPublished}` - ) - } - if (messageCountUnpublished !== 0) { - throw new Error( - `Unpublished message count should be 0, but it is ${messageCountUnpublished}` - ) - } if (messageCount !== 1) { throw new Error(`Message count should be 1, but it is ${messageCount}`) } @@ -1332,7 +1278,7 @@ test('can stream group messages', async () => { // Record message stream for this group const groupMessages: DecodedMessage[] = [] - const cancelGroupMessageStream = await alixGroup.streamGroupMessages( + const cancelGroupMessageStream = await alixGroup.streamMessages( async (message) => { groupMessages.push(message) } @@ -1825,10 +1771,10 @@ test('can stream all group Messages from multiple clients - swapped', async () = const alixGroup = await caro.conversations.newGroup([alix.address]) const boGroup = await caro.conversations.newGroup([bo.address]) - await boGroup.streamGroupMessages(async (message) => { + await boGroup.streamMessages(async (message) => { allBoMessages.push(message) }) - await alixGroup.streamGroupMessages(async (message) => { + await alixGroup.streamMessages(async (message) => { allAlixMessages.push(message) }) diff --git a/example/src/tests/test-utils.ts b/example/src/tests/test-utils.ts index 43d10b586..fc70a5887 100644 --- a/example/src/tests/test-utils.ts +++ b/example/src/tests/test-utils.ts @@ -40,6 +40,25 @@ export async function createClients(numClients: number): Promise { return clients } +export async function createV3Clients(numClients: number): Promise { + const clients = [] + for (let i = 0; i < numClients; i++) { + 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, + ]) + const client = await Client.createRandomV3({ + env: 'local', + enableV3: true, + dbEncryptionKey: keyBytes, + }) + client.register(new GroupUpdatedCodec()) + clients.push(client) + } + return clients +} + export async function createV3TestingClients(): Promise { const clients = [] const keyBytes = new Uint8Array([ diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts index 626e60ce6..b34301a95 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -203,6 +203,9 @@ export function convertPrivateKeyAccountToSigner( privateKeyAccount.signMessage({ message: typeof message === 'string' ? message : { raw: message }, }), + getChainId: () => undefined, + getBlockNumber: () => undefined, + walletType: () => undefined, } } @@ -219,10 +222,6 @@ test('can load a client from env "2k lens convos" private key', async () => { env: 'local', }) - assert( - xmtpClient.address === '0x209fAEc92D9B072f3E03d6115002d6652ef563cd', - 'Address: ' + xmtpClient.address - ) return true }) @@ -240,20 +239,12 @@ test('can load 1995 conversations from dev network "2k lens convos" account', as env: 'dev', }) - assert( - xmtpClient.address === '0x209fAEc92D9B072f3E03d6115002d6652ef563cd', - 'Address: ' + xmtpClient.address - ) const start = Date.now() const conversations = await xmtpClient.conversations.list() const end = Date.now() console.log( `Loaded ${conversations.length} conversations in ${end - start}ms` ) - assert( - conversations.length === 1995, - 'Conversations: ' + conversations.length - ) return true }) @@ -283,16 +274,14 @@ test('can pass a custom filter date and receive message objects with expected da const finalQueryDate = new Date('2025-01-01') // Show all messages before date in the past - const messages1: DecodedMessage[] = await aliceConversation.messages( - undefined, - initialQueryDate - ) + const messages1: DecodedMessage[] = await aliceConversation.messages({ + before: initialQueryDate, + }) // Show all messages before date in the future - const messages2: DecodedMessage[] = await aliceConversation.messages( - undefined, - finalQueryDate - ) + const messages2: DecodedMessage[] = await aliceConversation.messages({ + before: finalQueryDate, + }) const isAboutRightSendTime = Math.abs(messages2[0].sent - sentAt) < 1000 if (!isAboutRightSendTime) return false @@ -305,16 +294,14 @@ test('can pass a custom filter date and receive message objects with expected da // repeat the above test with a numeric date value // Show all messages before date in the past - const messages3: DecodedMessage[] = await aliceConversation.messages( - undefined, - initialQueryDate.getTime() - ) + const messages3: DecodedMessage[] = await aliceConversation.messages({ + before: initialQueryDate.getTime(), + }) // Show all messages before date in the future - const messages4: DecodedMessage[] = await aliceConversation.messages( - undefined, - finalQueryDate.getTime() - ) + const messages4: DecodedMessage[] = await aliceConversation.messages({ + before: finalQueryDate.getTime(), + }) const passingTimestampFieldSuccessful = !messages3.length && messages4.length === 1 diff --git a/example/src/tests/v3OnlyTests.ts b/example/src/tests/v3OnlyTests.ts index 8f3a03469..9e0004780 100644 --- a/example/src/tests/v3OnlyTests.ts +++ b/example/src/tests/v3OnlyTests.ts @@ -82,6 +82,22 @@ test('can create group', async () => { ) }) +test('can create dm', async () => { + const [alixV2, boV3, caroV2V3] = await createV3TestingClients() + const dm = await boV3.conversations.findOrCreateDm(caroV2V3.address) + assert(dm?.members?.length === 2, `dm should have 2 members`) + + try { + await boV3.conversations.findOrCreateDm(alixV2.address) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + return true + } + throw new Error( + 'should throw error when trying to add a V2 only client to a dm' + ) +}) + test('can send message', async () => { const [alixV2, boV3, caroV2V3] = await createV3TestingClients() const group = await boV3.conversations.newGroup([caroV2V3.address]) @@ -105,6 +121,29 @@ test('can send message', async () => { return true }) +test('can send messages to dm', async () => { + const [alixV2, boV3, caroV2V3] = await createV3TestingClients() + const dm = await boV3.conversations.findOrCreateDm(caroV2V3.address) + await dm.send('gm') + await dm.sync() + const dmMessages = await dm.messages() + assert( + dmMessages[0].content() === 'gm', + `first should be gm but was ${dmMessages[0].content()}` + ) + + await caroV2V3.conversations.syncConversations() + const sameDm = await caroV2V3.conversations.findConversation(dm.id) + await sameDm?.sync() + + const sameDmMessages = await sameDm!!.messages() + assert( + sameDmMessages[0].content() === 'gm', + `second should be gm but was ${sameDmMessages[0].content()}` + ) + return true +}) + test('can group consent', async () => { const [alixV2, boV3, caroV2V3] = await createV3TestingClients() const group = await boV3.conversations.newGroup([caroV2V3.address]) 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..1248fabc9 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", + "allConversationMessages", + "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 73177c589..051713834 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ import { Client } from '.' import { ConversationContext } from './XMTP.types' import XMTPModule from './XMTPModule' import { InboxId } from './lib/Client' -import { WalletType } from './lib/Signer' import { ConsentListEntry, ConsentState } from './lib/ConsentListEntry' import { ContentCodec, @@ -19,10 +18,12 @@ import { ConversationVersion, } from './lib/ConversationContainer' import { DecodedMessage, MessageDeliveryStatus } from './lib/DecodedMessage' +import { Dm } from './lib/Dm' import { Group, PermissionUpdateOption } from './lib/Group' import { InboxState } from './lib/InboxState' import { Member } from './lib/Member' import type { Query } from './lib/Query' +import { WalletType } from './lib/Signer' import { ConversationSendPayload } from './lib/types' import { DefaultContentTypes } from './lib/types/DefaultContentType' import { ConversationOrder, GroupOptions } from './lib/types/GroupOptions' @@ -122,7 +123,10 @@ export async function receiveSignature(requestID: string, signature: string) { return await XMTPModule.receiveSignature(requestID, signature) } -export async function receiveSCWSignature(requestID: string, signature: string) { +export async function receiveSCWSignature( + requestID: string, + signature: string +) { return await XMTPModule.receiveSCWSignature(requestID, signature) } @@ -285,6 +289,21 @@ export async function dropClient(inboxId: string) { return await XMTPModule.dropClient(inboxId) } +export async function findOrCreateDm< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + peerAddress: string +): Promise> { + const dm = JSON.parse( + await XMTPModule.findOrCreateDm(client.inboxId, peerAddress) + ) + const members = dm['members']?.map((mem: string) => { + return Member.from(mem) + }) + return new Dm(client, dm, members) +} + export async function createGroup< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( @@ -375,39 +394,81 @@ export async function listGroups< }) } +export async function listV3Conversations< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + opts?: GroupOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined +): Promise[]> { + return ( + await XMTPModule.listV3Conversations( + client.inboxId, + JSON.stringify(opts), + order, + limit + ) + ).map((json: string) => { + const jsonObj = JSON.parse(json) + const members = jsonObj.members.map((mem: string) => { + return Member.from(mem) + }) + if (jsonObj.version === ConversationVersion.GROUP) { + return new Group(client, jsonObj, members) + } else { + return new Dm(client, jsonObj, members) + } + }) +} + export async function listMemberInboxIds< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >(client: Client, id: string): Promise { return XMTPModule.listMemberInboxIds(client.inboxId, id) } -export async function listGroupMembers( +export async function listPeerInboxId< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>(client: Client, dmId: string): Promise { + return XMTPModule.listPeerInboxId(client.inboxId, dmId) +} + +export async function listConversationMembers( inboxId: string, id: string ): Promise { - const members = await XMTPModule.listGroupMembers(inboxId, id) + const members = await XMTPModule.listConversationMembers(inboxId, id) return members.map((json: string) => { return Member.from(json) }) } -export async function prepareGroupMessage( +export async function prepareConversationMessage( inboxId: string, - groupId: string, + conversationId: string, content: any ): Promise { const contentJson = JSON.stringify(content) - return await XMTPModule.prepareGroupMessage(inboxId, groupId, contentJson) + return await XMTPModule.prepareConversationMessage( + inboxId, + conversationId, + contentJson + ) } -export async function sendMessageToGroup( +export async function sendMessageToConversation( inboxId: string, - groupId: string, + conversationId: string, content: any ): Promise { const contentJson = JSON.stringify(content) - return await XMTPModule.sendMessageToGroup(inboxId, groupId, contentJson) + return await XMTPModule.sendMessageToConversation( + inboxId, + conversationId, + contentJson + ) } export async function publishPreparedGroupMessages( @@ -417,28 +478,26 @@ export async function publishPreparedGroupMessages( return await XMTPModule.publishPreparedGroupMessages(inboxId, groupId) } -export async function groupMessages< +export async function conversationMessages< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - id: string, + conversationId: string, limit?: number | undefined, before?: number | Date | undefined, after?: number | Date | undefined, direction?: | 'SORT_DIRECTION_ASCENDING' | 'SORT_DIRECTION_DESCENDING' - | undefined, - deliveryStatus?: MessageDeliveryStatus | undefined + | undefined ): Promise[]> { - const messages = await XMTPModule.groupMessages( + const messages = await XMTPModule.conversationMessages( client.inboxId, - id, + conversationId, limit, before, after, - direction, - deliveryStatus + direction ) return messages.map((json: string) => { return DecodedMessage.from(json, client) @@ -453,12 +512,80 @@ export async function findGroup< ): Promise | undefined> { const json = await XMTPModule.findGroup(client.inboxId, groupId) const group = JSON.parse(json) - const members = group['members']?.map((mem: string) => { - return Member.from(mem) - }) + if (!group || Object.keys(group).length === 0) { + return undefined + } + const members = + group['members']?.map((mem: string) => { + return Member.from(mem) + }) || [] return new Group(client, group, members) } +export async function findConversation< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + conversationId: string +): Promise | undefined> { + const json = await XMTPModule.findConversation(client.inboxId, conversationId) + const conversation = JSON.parse(json) + if (!conversation || Object.keys(conversation).length === 0) { + return undefined + } + const members = + conversation['members']?.map((mem: string) => { + return Member.from(mem) + }) || [] + + if (conversation.version === ConversationVersion.GROUP) { + return new Group(client, conversation, members) + } else { + return new Dm(client, conversation, members) + } +} + +export async function findConversationByTopic< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + topic: string +): Promise | undefined> { + const json = await XMTPModule.findConversationByTopic(client.inboxId, topic) + const conversation = JSON.parse(json) + if (!conversation || Object.keys(conversation).length === 0) { + return undefined + } + const members = + conversation['members']?.map((mem: string) => { + return Member.from(mem) + }) || [] + + if (conversation.version === ConversationVersion.GROUP) { + return new Group(client, conversation, members) + } else { + return new Dm(client, conversation, members) + } +} + +export async function findDm< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + address: string +): Promise | undefined> { + const json = await XMTPModule.findDm(client.inboxId, address) + const dm = JSON.parse(json) + if (!dm || Object.keys(dm).length === 0) { + return undefined + } + const members = + dm['members']?.map((mem: string) => { + return Member.from(mem) + }) || [] + return new Dm(client, dm, members) +} + export async function findV3Message< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( @@ -469,16 +596,16 @@ export async function findV3Message< return DecodedMessage.from(message, client) } -export async function syncGroups(inboxId: string) { - await XMTPModule.syncGroups(inboxId) +export async function syncConversations(inboxId: string) { + await XMTPModule.syncConversations(inboxId) } -export async function syncAllGroups(inboxId: string): Promise { - return await XMTPModule.syncAllGroups(inboxId) +export async function syncAllConversations(inboxId: string): Promise { + return await XMTPModule.syncAllConversations(inboxId) } -export async function syncGroup(inboxId: string, id: string) { - await XMTPModule.syncGroup(inboxId, id) +export async function syncConversation(inboxId: string, id: string) { + await XMTPModule.syncConversation(inboxId, id) } export async function subscribeToGroupMessages(inboxId: string, id: string) { @@ -937,6 +1064,36 @@ export async function unsubscribeFromMessages(inboxId: string, topic: string) { return await XMTPModule.unsubscribeFromMessages(inboxId, topic) } +export function subscribeToV3Conversations(inboxId: string) { + return XMTPModule.subscribeToV3Conversations(inboxId) +} + +export function subscribeToAllConversationMessages(inboxId: string) { + return XMTPModule.subscribeToAllConversationMessages(inboxId) +} + +export async function subscribeToConversationMessages( + inboxId: string, + id: string +) { + return await XMTPModule.subscribeToConversationMessages(inboxId, id) +} + +export function unsubscribeFromAllConversationMessages(inboxId: string) { + return XMTPModule.unsubscribeFromAllConversationMessages(inboxId) +} + +export function unsubscribeFromV3Conversations(inboxId: string) { + return XMTPModule.unsubscribeFromV3Conversations(inboxId) +} + +export async function unsubscribeFromConversationMessages( + inboxId: string, + id: string +) { + return await XMTPModule.unsubscribeFromConversationMessages(inboxId, id) +} + export function registerPushToken(pushServer: string, token: string) { return XMTPModule.registerPushToken(pushServer, token) } @@ -964,11 +1121,11 @@ export async function conversationConsentState( return await XMTPModule.conversationConsentState(inboxId, conversationTopic) } -export async function groupConsentState( +export async function conversationV3ConsentState( inboxId: string, - groupId: string + conversationId: string ): Promise { - return await XMTPModule.groupConsentState(inboxId, groupId) + return await XMTPModule.conversationV3ConsentState(inboxId, conversationId) } export async function isAllowed( @@ -1246,12 +1403,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( @@ -1282,14 +1439,14 @@ export async function isInboxDenied( return XMTPModule.isInboxDenied(clientInboxId, inboxId) } -export async function processGroupMessage< +export async function processConversationMessage< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, id: string, encryptedMessage: string ): Promise> { - const json = XMTPModule.processGroupMessage( + const json = XMTPModule.processConversationMessage( client.inboxId, id, encryptedMessage @@ -1314,6 +1471,28 @@ export async function processWelcomeMessage< return new Group(client, group, members) } +export async function processConversationWelcomeMessage< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + encryptedMessage: string +): Promise>> { + const json = await XMTPModule.processConversationWelcomeMessage( + client.inboxId, + encryptedMessage + ) + const conversation = JSON.parse(json) + const members = conversation['members']?.map((mem: string) => { + return Member.from(mem) + }) + + if (conversation.version === ConversationVersion.GROUP) { + return new Group(client, conversation, members) + } else { + return new Dm(client, conversation, members) + } +} + export async function exportNativeLogs() { return XMTPModule.exportNativeLogs() } @@ -1350,6 +1529,7 @@ export { Query } from './lib/Query' export { XMTPPush } from './lib/XMTPPush' export { ConsentListEntry, DecodedMessage, MessageDeliveryStatus } export { Group } from './lib/Group' +export { Dm } from './lib/Dm' export { Member } from './lib/Member' export { InboxId } from './lib/Client' export { GroupOptions, ConversationOrder } from './lib/types/GroupOptions' diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index d5eff4724..972ee5c7b 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,6 +1,7 @@ import { invitation } from '@xmtp/proto' import { Buffer } from 'buffer' +import { ConsentState } from './ConsentListEntry' import { ConversationVersion, ConversationContainer, @@ -12,6 +13,7 @@ import { EventTypes } from './types/EventTypes' import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' import { ConversationContext, PreparedLocalMessage } from '../index' +import { MessagesOptions } from './types' export interface ConversationParams { createdAt: number @@ -34,6 +36,9 @@ export class Conversation peerAddress: string version = ConversationVersion.DIRECT conversationID?: string | undefined + id: string + state: ConsentState + /** * Base64 encoded key material for the conversation. */ @@ -51,6 +56,8 @@ export class Conversation this.peerAddress = params.peerAddress ?? '' this.conversationID = params.conversationID this.keyMaterial = params.keyMaterial + this.id = params.topic + this.state = 'unknown' try { if (params?.consentProof) { this.consentProof = invitation.ConsentProofPayload.decode( @@ -59,6 +66,7 @@ export class Conversation } } catch {} } + lastMessage?: DecodedMessage | undefined async exportTopicData(): Promise { return await XMTP.exportConversationTopicData( @@ -80,22 +88,16 @@ export class Conversation * @todo Support pagination and conversation ID in future implementations. */ async messages( - limit?: number | undefined, - before?: number | Date | undefined, - after?: number | Date | undefined, - direction?: - | 'SORT_DIRECTION_ASCENDING' - | 'SORT_DIRECTION_DESCENDING' - | undefined + opts?: MessagesOptions ): Promise[]> { try { const messages = await XMTP.listMessages( this.client, this.topic, - limit, - before, - after, - direction + opts?.limit, + opts?.before, + opts?.after, + opts?.direction ) return messages @@ -316,4 +318,14 @@ export class Conversation await XMTP.unsubscribeFromMessages(this.client.inboxId, this.topic) } } + + sync() { + throw new Error('V3 only') + } + updateConsent(state: ConsentState): Promise { + throw new Error('V3 only') + } + processMessage(encryptedMessage: string): Promise> { + throw new Error('V3 only') + } } diff --git a/src/lib/ConversationContainer.ts b/src/lib/ConversationContainer.ts index d91dbed3f..a7037d78c 100644 --- a/src/lib/ConversationContainer.ts +++ b/src/lib/ConversationContainer.ts @@ -1,9 +1,13 @@ +import { ConsentState } from './ConsentListEntry' +import { ConversationSendPayload, MessagesOptions } from './types' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' +import { DecodedMessage } from '../index' export enum ConversationVersion { DIRECT = 'DIRECT', GROUP = 'GROUP', + DM = 'DM', } export interface ConversationContainer< @@ -13,4 +17,21 @@ export interface ConversationContainer< createdAt: number topic: string version: ConversationVersion + id: string + state: ConsentState + lastMessage?: DecodedMessage + + send( + content: ConversationSendPayload + ): Promise + sync() + messages(opts?: MessagesOptions): Promise[]> + streamMessages( + callback: (message: DecodedMessage) => Promise + ): Promise<() => void> + consentState(): Promise + updateConsent(state: ConsentState): Promise + processMessage( + encryptedMessage: string + ): Promise> } diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 5facaa2d3..90b62430b 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -7,6 +7,7 @@ import { ConversationContainer, } from './ConversationContainer' import { DecodedMessage } from './DecodedMessage' +import { Dm } from './Dm' import { Group, GroupParams } from './Group' import { Member } from './Member' import { CreateGroupOptions } from './types/CreateGroupOptions' @@ -81,6 +82,18 @@ export default class Conversations< ) } + /** + * Creates a new V3 conversation. + * + * This method creates a new conversation with the specified peer address. + * + * @param {string} peerAddress - The address of the peer to create a conversation with. + * @returns {Promise} A Promise that resolves to a Dm object. + */ + async findOrCreateDm(peerAddress: string): Promise> { + return await XMTPModule.findOrCreateDm(this.client, peerAddress) + } + /** * This method returns a list of all groups that the client is a member of. * To get the latest list of groups from the network, call syncGroups() first. @@ -114,6 +127,40 @@ export default class Conversations< return await XMTPModule.findGroup(this.client, groupId) } + /** + * This method returns a Dm by the address if that dm exists in the local database. + * To get the latest list of groups from the network, call syncConversations() first. + * + * @returns {Promise} A Promise that resolves to a Group or undefined if not found. + */ + async findDm(address: string): Promise | undefined> { + return await XMTPModule.findDm(this.client, address) + } + + /** + * This method returns a conversation by the topic if that conversation exists in the local database. + * To get the latest list of groups from the network, call syncConversations() first. + * + * @returns {Promise} A Promise that resolves to a Group or undefined if not found. + */ + async findConversationByTopic( + topic: string + ): Promise | undefined> { + return await XMTPModule.findConversationByTopic(this.client, topic) + } + + /** + * This method returns a conversation by the conversation id if that conversation exists in the local database. + * To get the latest list of groups from the network, call syncConversations() first. + * + * @returns {Promise} A Promise that resolves to a Group or undefined if not found. + */ + async findConversation( + conversationId: string + ): Promise | undefined> { + return await XMTPModule.findConversation(this.client, conversationId) + } + /** * This method returns a message by the message id if that message exists in the local database. * To get the latest list of messages from the network, call syncGroups() first. @@ -142,6 +189,20 @@ export default class Conversations< return result } + /** + * This method returns a list of all V3 conversations that the client is a member of. + * To include the latest groups from the network in the returned list, call syncGroups() first. + * + * @returns {Promise} A Promise that resolves to an array of ConversationContainer objects. + */ + async listConversations( + opts?: GroupOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined + ): Promise[]> { + return await XMTPModule.listV3Conversations(this.client, opts, order, limit) + } + /** * This method streams groups that the client is a member of. * @@ -171,6 +232,58 @@ export default class Conversations< } } + /** + * This method streams V3 conversations that the client is a member of. + * + * @returns {Promise} A Promise that resolves to an array of ConversationContainer objects. + */ + async streamConversations( + callback: ( + conversation: ConversationContainer + ) => Promise + ): Promise<() => void> { + XMTPModule.subscribeToV3Conversations(this.client.inboxId) + const subscription = XMTPModule.emitter.addListener( + EventTypes.ConversationV3, + async ({ + inboxId, + conversation, + }: { + inboxId: string + conversation: ConversationContainer + }) => { + if (inboxId !== this.client.inboxId) { + return + } + + this.known[conversation.topic] = true + if (conversation.version === ConversationVersion.GROUP) { + const members = conversation['members'].map((mem: string) => { + return Member.from(mem) + }) + return await callback( + new Group( + this.client, + conversation as unknown as GroupParams, + members + ) + ) + } else if (conversation.version === ConversationVersion.DM) { + const members = conversation['members'].map((mem: string) => { + return Member.from(mem) + }) + return await callback( + new Dm(this.client, conversation as unknown as GroupParams, members) + ) + } + } + ) + return () => { + subscription.remove() + XMTPModule.unsubscribeFromV3Conversations(this.client.inboxId) + } + } + /** * Creates a new group. * @@ -226,7 +339,11 @@ export default class Conversations< * and save them to the local state. */ async syncGroups() { - await XMTPModule.syncGroups(this.client.inboxId) + await XMTPModule.syncConversations(this.client.inboxId) + } + + async syncConversations() { + await XMTPModule.syncConversations(this.client.inboxId) } /** @@ -235,7 +352,11 @@ export default class Conversations< * @returns {Promise} A Promise that resolves to the number of groups synced. */ async syncAllGroups(): Promise { - return await XMTPModule.syncAllGroups(this.client.inboxId) + return await XMTPModule.syncAllConversations(this.client.inboxId) + } + + async syncAllConversations(): Promise { + return await XMTPModule.syncAllConversations(this.client.inboxId) } /** @@ -405,6 +526,40 @@ export default class Conversations< this.subscriptions[EventTypes.AllGroupMessage] = subscription } + /** + * Listen for new messages in all v3 conversations. + * + * This method subscribes to all groups in real-time and listens for incoming and outgoing messages. + * @param {Function} callback - A callback function that will be invoked when a message is sent or received. + * @returns {Promise} A Promise that resolves when the stream is set up. + */ + async streamAllConversationMessages( + callback: (message: DecodedMessage) => Promise + ): Promise { + XMTPModule.subscribeToAllConversationMessages(this.client.inboxId) + const subscription = XMTPModule.emitter.addListener( + EventTypes.AllConversationMessages, + async ({ + inboxId, + message, + }: { + inboxId: string + message: DecodedMessage + }) => { + if (inboxId !== this.client.inboxId) { + return + } + if (this.known[message.id]) { + return + } + + this.known[message.id] = true + await callback(DecodedMessage.fromObject(message, this.client)) + } + ) + this.subscriptions[EventTypes.AllConversationMessages] = subscription + } + async fromWelcome(encryptedMessage: string): Promise> { try { return await XMTPModule.processWelcomeMessage( @@ -417,6 +572,20 @@ export default class Conversations< } } + async conversationFromWelcome( + encryptedMessage: string + ): Promise> { + try { + return await XMTPModule.processConversationWelcomeMessage( + this.client, + encryptedMessage + ) + } catch (e) { + console.info('ERROR in processWelcomeMessage()', e) + throw e + } + } + /** * Cancels the stream for new conversations. */ @@ -439,6 +608,14 @@ export default class Conversations< XMTPModule.unsubscribeFromGroups(this.client.inboxId) } + cancelStreamConversations() { + if (this.subscriptions[EventTypes.ConversationV3]) { + this.subscriptions[EventTypes.ConversationV3].remove() + delete this.subscriptions[EventTypes.ConversationV3] + } + XMTPModule.unsubscribeFromV3Conversations(this.client.inboxId) + } + /** * Cancels the stream for new messages in all conversations. */ @@ -460,4 +637,12 @@ export default class Conversations< } XMTPModule.unsubscribeFromAllGroupMessages(this.client.inboxId) } + + cancelStreamAllConversations() { + if (this.subscriptions[EventTypes.AllConversationMessages]) { + this.subscriptions[EventTypes.AllConversationMessages].remove() + delete this.subscriptions[EventTypes.AllConversationMessages] + } + XMTPModule.unsubscribeFromAllConversationMessages(this.client.inboxId) + } } diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts new file mode 100644 index 000000000..dd9657148 --- /dev/null +++ b/src/lib/Dm.ts @@ -0,0 +1,258 @@ +import { InboxId } from './Client' +import { ConsentState } from './ConsentListEntry' +import { + ConversationVersion, + ConversationContainer, +} from './ConversationContainer' +import { DecodedMessage } from './DecodedMessage' +import { Member } from './Member' +import { ConversationSendPayload } from './types/ConversationCodecs' +import { DefaultContentTypes } from './types/DefaultContentType' +import { EventTypes } from './types/EventTypes' +import { MessagesOptions } from './types/MessagesOptions' +import * as XMTP from '../index' + +export interface DmParams { + id: string + createdAt: number + members: string[] + creatorInboxId: InboxId + topic: string + consentState: ConsentState + lastMessage?: DecodedMessage +} + +export class Dm + implements ConversationContainer +{ + client: XMTP.Client + id: string + createdAt: number + members: Member[] + version = ConversationVersion.DM + topic: string + state: ConsentState + lastMessage?: DecodedMessage + + constructor( + client: XMTP.Client, + params: DmParams, + members: Member[], + lastMessage?: DecodedMessage + ) { + this.client = client + this.id = params.id + this.createdAt = params.createdAt + this.members = members + this.topic = params.topic + this.state = params.consentState + this.lastMessage = lastMessage + } + + /** + * This method returns an array of inbox ids associated with the group. + * To get the latest member inbox ids from the network, call sync() first. + * @returns {Promise} A Promise that resolves to a InboxId. + */ + async peerInboxId(): Promise { + return XMTP.listPeerInboxId(this.client, this.id) + } + + /** + * Sends a message to the current group. + * + * @param {string | MessageContent} content - The content of the message. It can be either a string or a structured MessageContent object. + * @returns {Promise} A Promise that resolves to a string identifier for the sent message. + * @throws {Error} Throws an error if there is an issue with sending the message. + */ + async send( + content: ConversationSendPayload + ): Promise { + // TODO: Enable other content types + // if (opts && opts.contentType) { + // return await this._sendWithJSCodec(content, opts.contentType) + // } + + try { + if (typeof content === 'string') { + content = { text: content } + } + + return await XMTP.sendMessageToConversation( + this.client.inboxId, + this.id, + content + ) + } catch (e) { + console.info('ERROR in send()', e.message) + throw e + } + } + + /** + * Prepare a group message to be sent. + * + * @param {string | MessageContent} content - The content of the message. It can be either a string or a structured MessageContent object. + * @returns {Promise} A Promise that resolves to a string identifier for the prepared message to be sent. + * @throws {Error} Throws an error if there is an issue with sending the message. + */ + async prepareMessage< + SendContentTypes extends DefaultContentTypes = ContentTypes, + >(content: ConversationSendPayload): Promise { + // TODO: Enable other content types + // if (opts && opts.contentType) { + // return await this._sendWithJSCodec(content, opts.contentType) + // } + + try { + if (typeof content === 'string') { + content = { text: content } + } + + return await XMTP.prepareConversationMessage( + this.client.inboxId, + this.id, + content + ) + } catch (e) { + console.info('ERROR in prepareGroupMessage()', e.message) + throw e + } + } + + /** + * Publish all prepared messages. + * + * @throws {Error} Throws an error if there is an issue finding the unpublished message + */ + async publishPreparedMessages() { + try { + return await XMTP.publishPreparedGroupMessages( + this.client.inboxId, + this.id + ) + } catch (e) { + console.info('ERROR in publishPreparedMessages()', e.message) + throw e + } + } + + /** + * This method returns an array of messages associated with the group. + * To get the latest messages from the network, call sync() first. + * + * @param {number | undefined} limit - Optional maximum number of messages to return. + * @param {number | Date | undefined} before - Optional filter for specifying the maximum timestamp of messages to return. + * @param {number | Date | undefined} after - Optional filter for specifying the minimum timestamp of messages to return. + * @param direction - Optional parameter to specify the time ordering of the messages to return. + * @returns {Promise[]>} A Promise that resolves to an array of DecodedMessage objects. + */ + async messages( + opts?: MessagesOptions + ): Promise[]> { + return await XMTP.conversationMessages( + this.client, + this.id, + opts?.limit, + opts?.before, + opts?.after, + opts?.direction + ) + } + + /** + * Executes a network request to fetch the latest messages and membership changes + * associated with the group and saves them to the local state. + */ + async sync() { + await XMTP.syncConversation(this.client.inboxId, this.id) + } + + /** + * Sets up a real-time message stream for the current group. + * + * This method subscribes to incoming messages in real-time and listens for new message events. + * When a new message is detected, the provided callback function is invoked with the details of the message. + * Additionally, this method returns a function that can be called to unsubscribe and end the message stream. + * + * @param {Function} callback - A callback function that will be invoked with the new DecodedMessage when a message is received. + * @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates. + */ + async streamMessages( + callback: (message: DecodedMessage) => Promise + ): Promise<() => void> { + await XMTP.subscribeToConversationMessages(this.client.inboxId, this.id) + const hasSeen = {} + const messageSubscription = XMTP.emitter.addListener( + EventTypes.ConversationV3Message, + async ({ + inboxId, + message, + conversationId, + }: { + inboxId: string + message: DecodedMessage + conversationId: string + }) => { + // Long term these checks should be able to be done on the native layer as well, but additional checks in JS for safety + if (inboxId !== this.client.inboxId) { + return + } + if (conversationId !== this.id) { + return + } + if (hasSeen[message.id]) { + return + } + + hasSeen[message.id] = true + + message.client = this.client + await callback(DecodedMessage.fromObject(message, this.client)) + } + ) + return async () => { + messageSubscription.remove() + await XMTP.unsubscribeFromConversationMessages( + this.client.inboxId, + this.id + ) + } + } + + async processMessage( + encryptedMessage: string + ): Promise> { + try { + return await XMTP.processConversationMessage( + this.client, + this.id, + encryptedMessage + ) + } catch (e) { + console.info('ERROR in processConversationMessage()', e) + throw e + } + } + + async consentState(): Promise { + return await XMTP.conversationV3ConsentState(this.client.inboxId, this.id) + } + + async updateConsent(state: ConsentState): Promise { + return await XMTP.updateConversationConsent( + this.client.inboxId, + this.id, + state + ) + } + + /** + * + * @returns {Promise} A Promise that resolves to an array of Member objects. + * To get the latest member list from the network, call sync() first. + */ + async membersList(): Promise { + return await XMTP.listConversationMembers(this.client.inboxId, this.id) + } +} diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 4a25bf34d..96558ec6b 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -4,14 +4,13 @@ import { ConversationVersion, ConversationContainer, } from './ConversationContainer' -import { DecodedMessage, MessageDeliveryStatus } from './DecodedMessage' +import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessagesOptions } from './types/MessagesOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' -import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' export type PermissionUpdateOption = 'allow' | 'deny' | 'admin' | 'super_admin' @@ -90,8 +89,7 @@ export class Group< * @throws {Error} Throws an error if there is an issue with sending the message. */ async send( - content: ConversationSendPayload, - opts?: SendOptions + content: ConversationSendPayload ): Promise { // TODO: Enable other content types // if (opts && opts.contentType) { @@ -103,7 +101,7 @@ export class Group< content = { text: content } } - return await XMTP.sendMessageToGroup( + return await XMTP.sendMessageToConversation( this.client.inboxId, this.id, content @@ -123,10 +121,7 @@ export class Group< */ async prepareMessage< SendContentTypes extends DefaultContentTypes = ContentTypes, - >( - content: ConversationSendPayload, - opts?: SendOptions - ): Promise { + >(content: ConversationSendPayload): Promise { // TODO: Enable other content types // if (opts && opts.contentType) { // return await this._sendWithJSCodec(content, opts.contentType) @@ -137,7 +132,7 @@ export class Group< content = { text: content } } - return await XMTP.prepareGroupMessage( + return await XMTP.prepareConversationMessage( this.client.inboxId, this.id, content @@ -178,14 +173,13 @@ export class Group< async messages( opts?: MessagesOptions ): Promise[]> { - return await XMTP.groupMessages( + return await XMTP.conversationMessages( this.client, this.id, opts?.limit, opts?.before, opts?.after, - opts?.direction, - opts?.deliveryStatus ?? MessageDeliveryStatus.ALL + opts?.direction ) } @@ -194,7 +188,7 @@ export class Group< * associated with the group and saves them to the local state. */ async sync() { - await XMTP.syncGroup(this.client.inboxId, this.id) + await XMTP.syncConversation(this.client.inboxId, this.id) } /** @@ -207,7 +201,7 @@ export class Group< * @param {Function} callback - A callback function that will be invoked with the new DecodedMessage when a message is received. * @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates. */ - async streamGroupMessages( + async streamMessages( callback: (message: DecodedMessage) => Promise ): Promise<() => void> { await XMTP.subscribeToGroupMessages(this.client.inboxId, this.id) @@ -246,6 +240,12 @@ export class Group< } } + async streamGroupMessages( + callback: (message: DecodedMessage) => Promise + ): Promise<() => void> { + return this.streamMessages(callback) + } + /** * * @param addresses addresses to add to the group @@ -606,7 +606,7 @@ export class Group< encryptedMessage: string ): Promise> { try { - return await XMTP.processGroupMessage( + return await XMTP.processConversationMessage( this.client, this.id, encryptedMessage @@ -618,11 +618,15 @@ export class Group< } async consentState(): Promise { - return await XMTP.groupConsentState(this.client.inboxId, this.id) + return await XMTP.conversationV3ConsentState(this.client.inboxId, this.id) } async updateConsent(state: ConsentState): Promise { - return await XMTP.updateGroupConsent(this.client.inboxId, this.id, state) + return await XMTP.updateConversationConsent( + this.client.inboxId, + this.id, + state + ) } /** @@ -645,6 +649,6 @@ export class Group< * To get the latest member list from the network, call sync() first. */ async membersList(): Promise { - return await XMTP.listGroupMembers(this.client.inboxId, this.id) + return await XMTP.listConversationMembers(this.client.inboxId, this.id) } } diff --git a/src/lib/types/EventTypes.ts b/src/lib/types/EventTypes.ts index 26a41ab5d..d9730ba2b 100644 --- a/src/lib/types/EventTypes.ts +++ b/src/lib/types/EventTypes.ts @@ -38,4 +38,19 @@ export enum EventTypes { * A new message is sent to a specific group */ GroupMessage = 'groupMessage', + // Conversation Events + /** + * A new message is sent to a specific conversation + */ + ConversationV3 = 'conversationV3', + // All Conversation Message Events + /** + * A new message is sent to any V3 conversation + */ + AllConversationMessages = 'allConversationMessages', + // Conversation Events + /** + * A new V3 conversation is created + */ + ConversationV3Message = 'conversationV3Message', } diff --git a/src/lib/types/MessagesOptions.ts b/src/lib/types/MessagesOptions.ts index 089d098d1..91b2967ef 100644 --- a/src/lib/types/MessagesOptions.ts +++ b/src/lib/types/MessagesOptions.ts @@ -1,5 +1,3 @@ -import { MessageDeliveryStatus } from '../DecodedMessage' - export type MessagesOptions = { limit?: number | undefined before?: number | Date | undefined @@ -8,5 +6,4 @@ export type MessagesOptions = { | 'SORT_DIRECTION_ASCENDING' | 'SORT_DIRECTION_DESCENDING' | undefined - deliveryStatus?: MessageDeliveryStatus | undefined }