diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt index c520796b7..c8b8a6afb 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt @@ -45,6 +45,8 @@ import org.xmtp.proto.message.contents.Invitation import org.xmtp.proto.message.contents.Invitation.InvitationV1.Context import java.nio.charset.StandardCharsets import java.util.Date +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.DurationUnit @RunWith(AndroidJUnit4::class) class ConversationTest { @@ -418,7 +420,12 @@ class ConversationTest { val messages = aliceConversation.messages(limit = 1) assertEquals(1, messages.size) assertEquals("hey alice 3", messages[0].body) - val messages2 = aliceConversation.messages(limit = 1, after = date) + val messages2 = aliceConversation.messages( + limit = 1, + afterNs = date.time.nanoseconds.toLong( + DurationUnit.NANOSECONDS + ) + ) assertEquals(1, messages2.size) assertEquals("hey alice 3", messages2[0].body) val messagesAsc = diff --git a/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt b/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt index d11ac79be..2da65bde9 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt @@ -75,6 +75,46 @@ class GroupTest { runBlocking { Client().createV3(account = davonV3Wallet, options = options) } } + @Test + fun testsCanDualSendConversations() { + val v2Convo = runBlocking { alixClient.conversations.newConversation(bo.walletAddress) } + runBlocking { + alixClient.conversations.syncConversations() + boClient.conversations.syncConversations() + } + val alixDm = runBlocking { alixClient.findDm(bo.walletAddress) } + val boDm = runBlocking { boClient.findDm(alix.walletAddress) } + + assertEquals(alixDm?.id, boDm?.id) + assertEquals(runBlocking { alixClient.conversations.list().size }, 1) + assertEquals(runBlocking { alixClient.conversations.listDms().size }, 1) + assertEquals(runBlocking { boClient.conversations.listDms().size }, 1) + assertEquals(runBlocking { boClient.conversations.list().size }, 1) + assertEquals(v2Convo.topic, runBlocking { boClient.conversations.list().first().topic }) + } + + @Test + fun testsCanDualSendMessages() { + val alixV2Convo = runBlocking { alixClient.conversations.newConversation(bo.walletAddress) } + val boV2Convo = runBlocking { boClient.conversations.list().first() } + runBlocking { boClient.conversations.syncConversations() } + val alixDm = runBlocking { alixClient.findDm(bo.walletAddress) } + val boDm = runBlocking { boClient.findDm(alix.walletAddress) } + + runBlocking { alixV2Convo.send("first") } + runBlocking { boV2Convo.send("second") } + + runBlocking { + alixDm?.sync() + boDm?.sync() + } + + assertEquals(runBlocking { alixV2Convo.messages().size }, 2) + assertEquals(runBlocking { alixV2Convo.messages().size }, runBlocking { boV2Convo.messages().size }) + assertEquals(boDm?.messages()?.size, 2) + assertEquals(alixDm?.messages()?.size, 3) // We send the group membership update to the dm + } + @Test fun testCanCreateAGroupWithDefaultPermissions() { val boGroup = runBlocking { @@ -545,6 +585,30 @@ class GroupTest { assertEquals(sameGroup.messages(deliveryStatus = MessageDeliveryStatus.PUBLISHED).size, 2) } + @Test + fun testCanListGroupMessagesAfter() { + val group = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress)) } + val messageId = runBlocking { + group.send("howdy") + group.send("gm") + } + val message = boClient.findMessage(messageId) + assertEquals(group.messages().size, 3) + assertEquals(group.messages(afterNs = message?.sentAtNs).size, 0) + runBlocking { + group.send("howdy") + group.send("gm") + } + assertEquals(group.messages().size, 5) + assertEquals(group.messages(afterNs = message?.sentAtNs).size, 2) + + runBlocking { alixClient.conversations.syncConversations() } + val sameGroup = runBlocking { alixClient.conversations.listGroups().last() } + runBlocking { sameGroup.sync() } + assertEquals(sameGroup.messages().size, 4) + assertEquals(sameGroup.messages(afterNs = message?.sentAtNs).size, 2) + } + @Test fun testCanSendContentTypesToGroup() { Client.register(codec = ReactionCodec()) diff --git a/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt b/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt index c636fde56..21c1aa060 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt @@ -29,6 +29,8 @@ import org.xmtp.proto.message.contents.PrivateKeyOuterClass import org.xmtp.proto.message.contents.PrivateKeyOuterClass.PrivateKeyBundle import uniffi.xmtpv3.createV2Client import java.util.Date +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.DurationUnit @RunWith(AndroidJUnit4::class) class LocalInstrumentedTest { @@ -163,10 +165,19 @@ class LocalInstrumentedTest { assertEquals(2, messagesLimit.size) val nowMessage = messages[0] assertEquals("now", nowMessage.body) - val messages2 = convo.messages(limit = 1, before = nowMessage.sent) + val messages2 = convo.messages( + limit = 1, + beforeNs = nowMessage.sent.time.nanoseconds.toLong( + DurationUnit.NANOSECONDS + ) + ) val tenSecondsAgoMessage = messages2[0] assertEquals("now first", tenSecondsAgoMessage.body) - val messages3 = convo.messages(after = tenSecondsAgoMessage.sent) + val messages3 = convo.messages( + afterNs = tenSecondsAgoMessage.sent.time.nanoseconds.toLong( + DurationUnit.NANOSECONDS + ) + ) val nowMessage2 = messages3[0] assertEquals("now", nowMessage2.body) val messagesAsc = diff --git a/library/src/main/java/org/xmtp/android/library/Conversation.kt b/library/src/main/java/org/xmtp/android/library/Conversation.kt index d75e3f44c..491e2d875 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -169,50 +169,71 @@ sealed class Conversation { */ suspend fun messages( limit: Int? = null, - before: Date? = null, - after: Date? = null, + beforeNs: Long? = null, + afterNs: Long? = null, direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, ): List { return when (this) { is V1 -> conversationV1.messages( limit = limit, - before = before, - after = after, + before = beforeNs?.let { Date(it / 1_000_000) }, + after = afterNs?.let { Date(it / 1_000_000) }, direction = direction, ) is V2 -> conversationV2.messages( limit = limit, - before = before, - after = after, + before = beforeNs?.let { Date(it / 1_000_000) }, + after = afterNs?.let { Date(it / 1_000_000) }, direction = direction, ) is Group -> { group.messages( limit = limit, - before = before, - after = after, + beforeNs = beforeNs, + afterNs = afterNs, direction = direction, ) } - is Dm -> dm.messages(limit, before, after, direction) + is Dm -> dm.messages(limit, beforeNs, afterNs, direction) } } suspend fun decryptedMessages( limit: Int? = null, - before: Date? = null, - after: Date? = null, + beforeNs: Long? = null, + afterNs: Long? = null, direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, ): List { return when (this) { - is V1 -> conversationV1.decryptedMessages(limit, before, after, direction) - is V2 -> conversationV2.decryptedMessages(limit, before, after, direction) - is Group -> group.decryptedMessages(limit, before, after, direction) - is Dm -> dm.decryptedMessages(limit, before, after, direction) + is V1 -> conversationV1.decryptedMessages( + limit = limit, + before = beforeNs?.let { Date(it / 1_000_000) }, + after = afterNs?.let { Date(it / 1_000_000) }, + direction = direction, + ) + + is V2 -> + conversationV2.decryptedMessages( + limit = limit, + before = beforeNs?.let { Date(it / 1_000_000) }, + after = afterNs?.let { Date(it / 1_000_000) }, + direction = direction, + ) + + is Group -> { + group.decryptedMessages( + limit = limit, + beforeNs = beforeNs, + afterNs = afterNs, + direction = direction, + ) + } + + is Dm -> dm.decryptedMessages(limit, beforeNs, afterNs, direction) } } diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt index 7c1660a9a..0874e6990 100644 --- a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt +++ b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt @@ -200,6 +200,16 @@ data class ConversationV1( } suspend fun send(prepared: PreparedMessage): String { + if (client.v3Client != null) { + try { + val dm = client.conversations.findOrCreateDm(peerAddress) + prepared.encodedContent?.let { + dm.send(it) + } + } catch (e: Exception) { + Log.e("ConversationV1 send", e.message.toString()) + } + } client.publish(envelopes = prepared.envelopes) if (client.contacts.consentList.state(address = peerAddress) == ConsentState.UNKNOWN) { client.contacts.allow(addresses = listOf(peerAddress)) @@ -274,7 +284,7 @@ data class ConversationV1( ) client.contacts.hasIntroduced[peerAddress] = true } - return PreparedMessage(envelopes) + return PreparedMessage(envelopes, encodedContent) } private fun generateId(envelope: Envelope): String = diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt index f7d254de4..3d74ddecf 100644 --- a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt +++ b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt @@ -205,6 +205,16 @@ data class ConversationV2( } suspend fun send(prepared: PreparedMessage): String { + if (client.v3Client != null) { + try { + val dm = client.conversations.findOrCreateDm(peerAddress) + prepared.encodedContent?.let { + dm.send(it) + } + } catch (e: Exception) { + Log.e("ConversationV1 send", e.message.toString()) + } + } client.publish(envelopes = prepared.envelopes) if (client.contacts.consentList.state(address = peerAddress) == ConsentState.UNKNOWN) { client.contacts.allow(addresses = listOf(peerAddress)) @@ -275,7 +285,7 @@ data class ConversationV2( timestamp = Date(), message = MessageBuilder.buildFromMessageV2(v2 = message.messageV2).toByteArray(), ) - return PreparedMessage(listOf(envelope)) + return PreparedMessage(listOf(envelope), encodedContent) } private fun generateId(envelope: Envelope): String = diff --git a/library/src/main/java/org/xmtp/android/library/Conversations.kt b/library/src/main/java/org/xmtp/android/library/Conversations.kt index 8ca9753b7..4b31bba0c 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -194,7 +194,6 @@ data class Conversations( } suspend fun findOrCreateDm(peerAddress: String): Dm { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") if (peerAddress.lowercase() == client.address.lowercase()) { throw XMTPException("Recipient is sender") } @@ -301,6 +300,13 @@ data class Conversations( client.contacts.allow(addresses = listOf(peerAddress)) val conversation = Conversation.V2(conversationV2) conversationsByTopic[conversation.topic] = conversation + if (client.v3Client != null) { + try { + client.conversations.findOrCreateDm(peerAddress) + } catch (e: Exception) { + Log.e("newConversation", e.message.toString()) + } + } return conversation } @@ -330,7 +336,6 @@ data class Conversations( limit: Int? = null, consentState: ConsentState? = null, ): List { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") val ffiDms = libXMTPConversations?.listDms( opts = FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), @@ -352,9 +357,6 @@ data class Conversations( order: ConversationOrder = ConversationOrder.CREATED_AT, consentState: ConsentState? = null, ): List { - if (client.hasV2Client) - throw XMTPException("Only supported for V3 only clients.") - val ffiConversations = libXMTPConversations?.list( FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), @@ -553,7 +555,6 @@ data class Conversations( } fun streamConversations(): Flow = callbackFlow { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") val conversationCallback = object : FfiConversationCallback { override fun onConversation(conversation: FfiConversation) { if (conversation.groupMetadata().conversationType() == "dm") { @@ -656,7 +657,6 @@ data class Conversations( } fun streamAllConversationMessages(): Flow = callbackFlow { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { val conversation = client.findConversation(message.convoId.toHex()) @@ -684,7 +684,6 @@ data class Conversations( } fun streamAllConversationDecryptedMessages(): Flow = callbackFlow { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { val conversation = client.findConversation(message.convoId.toHex()) diff --git a/library/src/main/java/org/xmtp/android/library/Dm.kt b/library/src/main/java/org/xmtp/android/library/Dm.kt index cc0a00d84..18dcb9e15 100644 --- a/library/src/main/java/org/xmtp/android/library/Dm.kt +++ b/library/src/main/java/org/xmtp/android/library/Dm.kt @@ -23,8 +23,6 @@ import uniffi.xmtpv3.FfiMessage import uniffi.xmtpv3.FfiMessageCallback import uniffi.xmtpv3.FfiSubscribeException import java.util.Date -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.DurationUnit class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { val id: String @@ -103,15 +101,15 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { fun messages( limit: Int? = null, - before: Date? = null, - after: Date? = null, + beforeNs: Long? = null, + afterNs: Long? = null, direction: PagingInfoSortDirection = SortDirection.SORT_DIRECTION_DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( opts = FfiListMessagesOptions( - sentBeforeNs = before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - sentAfterNs = after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), + sentBeforeNs = beforeNs, + sentAfterNs = afterNs, limit = limit?.toLong(), deliveryStatus = when (deliveryStatus) { MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED @@ -131,15 +129,15 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { fun decryptedMessages( limit: Int? = null, - before: Date? = null, - after: Date? = null, + beforeNs: Long? = null, + afterNs: Long? = null, direction: PagingInfoSortDirection = SortDirection.SORT_DIRECTION_DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( opts = FfiListMessagesOptions( - sentBeforeNs = before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - sentAfterNs = after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), + sentBeforeNs = beforeNs, + sentAfterNs = afterNs, limit = limit?.toLong(), deliveryStatus = when (deliveryStatus) { MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED diff --git a/library/src/main/java/org/xmtp/android/library/Group.kt b/library/src/main/java/org/xmtp/android/library/Group.kt index 372817884..a7ba9e3b1 100644 --- a/library/src/main/java/org/xmtp/android/library/Group.kt +++ b/library/src/main/java/org/xmtp/android/library/Group.kt @@ -29,8 +29,6 @@ import uniffi.xmtpv3.FfiSubscribeException import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet import java.util.Date -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.DurationUnit class Group(val client: Client, private val libXMTPGroup: FfiConversation) { val id: String @@ -121,15 +119,15 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { fun messages( limit: Int? = null, - before: Date? = null, - after: Date? = null, + beforeNs: Long? = null, + afterNs: Long? = null, direction: PagingInfoSortDirection = SORT_DIRECTION_DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( opts = FfiListMessagesOptions( - sentBeforeNs = before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - sentAfterNs = after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), + sentBeforeNs = beforeNs, + sentAfterNs = afterNs, limit = limit?.toLong(), deliveryStatus = when (deliveryStatus) { MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED @@ -149,15 +147,15 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { fun decryptedMessages( limit: Int? = null, - before: Date? = null, - after: Date? = null, + beforeNs: Long? = null, + afterNs: Long? = null, direction: PagingInfoSortDirection = SORT_DIRECTION_DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( opts = FfiListMessagesOptions( - sentBeforeNs = before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - sentAfterNs = after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), + sentBeforeNs = beforeNs, + sentAfterNs = afterNs, limit = limit?.toLong(), deliveryStatus = when (deliveryStatus) { MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED diff --git a/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt b/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt index 6bfc2cf48..e2f762dd1 100644 --- a/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt +++ b/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt @@ -3,6 +3,7 @@ package org.xmtp.android.library import org.web3j.crypto.Hash import org.xmtp.android.library.messages.Envelope import org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishRequest +import org.xmtp.proto.message.contents.Content.EncodedContent // This houses a fully prepared message that can be published // as soon as the API client has connectivity. @@ -14,7 +15,8 @@ data class PreparedMessage( // The first envelope should send the message to the conversation itself. // Any more are for required intros/invites etc. // A client can just publish these when it has connectivity. - val envelopes: List + val envelopes: List, + val encodedContent: EncodedContent? = null ) { companion object { fun fromSerializedData(data: ByteArray): PreparedMessage { diff --git a/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt b/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt index 631fd2e5f..f26ae2fd5 100644 --- a/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt +++ b/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt @@ -29,6 +29,9 @@ data class MessageV3(val client: Client, private val libXMTPMessage: FfiMessage) val sentAt: Date get() = Date(libXMTPMessage.sentAtNs / 1_000_000) + val sentAtNs: Long + get() = libXMTPMessage.sentAtNs + val deliveryStatus: MessageDeliveryStatus get() = when (libXMTPMessage.deliveryStatus) { FfiDeliveryStatus.UNPUBLISHED -> MessageDeliveryStatus.UNPUBLISHED