Skip to content

Commit

Permalink
Reaction from rust and list messages with reactions (#363)
Browse files Browse the repository at this point in the history
* reaction v2 and list messages with reactions

* test json deserialization of reaction in rust

* adds child messages as a nullable list of messages to message class

* set libxmtp ref to latest main

* removed unneccessary helper functions

---------

Co-authored-by: cameronvoell <[email protected]>
  • Loading branch information
cameronvoell and cameronvoell authored Jan 31, 2025
1 parent 51c7b17 commit 65be7d1
Show file tree
Hide file tree
Showing 16 changed files with 1,174 additions and 777 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ class GroupPermissionsTest {
updateGroupDescriptionPolicy = PermissionOption.Allow,
updateGroupImagePolicy = PermissionOption.Admin,
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
updateMessageExpirationPolicy = PermissionOption.Admin,
updateMessageDisappearingPolicy = PermissionOption.Admin,
)
val boGroup = runBlocking {
boClient.conversations.newGroupCustomPermissions(
Expand Down Expand Up @@ -469,7 +469,7 @@ class GroupPermissionsTest {
updateGroupDescriptionPolicy = PermissionOption.Allow,
updateGroupImagePolicy = PermissionOption.Admin,
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
updateMessageExpirationPolicy = PermissionOption.Admin,
updateMessageDisappearingPolicy = PermissionOption.Admin,
)

assertThrows(GenericException.GroupMutablePermissions::class.java) {
Expand All @@ -490,7 +490,7 @@ class GroupPermissionsTest {
updateGroupDescriptionPolicy = PermissionOption.Allow,
updateGroupImagePolicy = PermissionOption.Admin,
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
updateMessageExpirationPolicy = PermissionOption.Allow,
updateMessageDisappearingPolicy = PermissionOption.Allow,
)

// Valid custom policy works as expected
Expand Down Expand Up @@ -518,7 +518,7 @@ class GroupPermissionsTest {
updateGroupDescriptionPolicy = PermissionOption.Allow,
updateGroupImagePolicy = PermissionOption.Admin,
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
updateMessageExpirationPolicy = PermissionOption.Admin,
updateMessageDisappearingPolicy = PermissionOption.Admin,
)
val boGroup = runBlocking {
boClient.conversations.newGroupCustomPermissionsWithInboxIds(
Expand Down
113 changes: 113 additions & 0 deletions library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.xmtp.android.library.codecs.ContentTypeReaction
import org.xmtp.android.library.codecs.ContentTypeReactionV2
import org.xmtp.android.library.codecs.EncodedContent
import org.xmtp.android.library.codecs.Reaction
import org.xmtp.android.library.codecs.ReactionAction
import org.xmtp.android.library.codecs.ReactionCodec
import org.xmtp.android.library.codecs.ReactionSchema
import org.xmtp.android.library.codecs.ReactionV2Codec
import org.xmtp.android.library.libxmtp.Message
import org.xmtp.android.library.messages.walletAddress
import uniffi.xmtpv3.FfiReaction
import uniffi.xmtpv3.FfiReactionAction
import uniffi.xmtpv3.FfiReactionSchema

@RunWith(AndroidJUnit4::class)
class ReactionTest {
Expand Down Expand Up @@ -98,4 +104,111 @@ class ReactionTest {
assertEquals(ReactionSchema.Unicode, content?.schema)
}
}

@Test
fun testCanUseReactionV2Codec() {
Client.register(codec = ReactionV2Codec())

val fixtures = fixtures()
val aliceClient = fixtures.alixClient
val aliceConversation = runBlocking {
aliceClient.conversations.newConversation(fixtures.bo.walletAddress)
}

runBlocking { aliceConversation.send(text = "hey alice 2 bob") }

val messageToReact = runBlocking { aliceConversation.messages()[0] }

val reaction = FfiReaction(
reference = messageToReact.id,
referenceInboxId = aliceClient.inboxId,
action = FfiReactionAction.ADDED,
content = "U+1F603",
schema = FfiReactionSchema.UNICODE,
)

runBlocking {
aliceConversation.send(
content = reaction,
options = SendOptions(contentType = ContentTypeReactionV2),
)
}
val messages = runBlocking { aliceConversation.messages() }
assertEquals(messages.size, 2)
if (messages.size == 2) {
val content: FfiReaction? = messages.first().content()
assertEquals("U+1F603", content?.content)
assertEquals(messageToReact.id, content?.reference)
assertEquals(FfiReactionAction.ADDED, content?.action)
assertEquals(FfiReactionSchema.UNICODE, content?.schema)
}

val messagesWithReactions: List<Message> = runBlocking {
aliceConversation.messagesWithReactions()
}
assertEquals(messagesWithReactions.size, 1)
assertEquals(messagesWithReactions[0].id, messageToReact.id)
val reactionContent: FfiReaction? =
messagesWithReactions[0]?.childMessages!![0]?.let { it?.content()!! }
assertEquals(reactionContent?.reference, messageToReact.id)
}

@Test
fun testCanMixReactionTypes() = runBlocking {
// Register both codecs
Client.register(codec = ReactionV2Codec())
Client.register(codec = ReactionCodec())

val fixtures = fixtures()
val aliceClient = fixtures.alixClient
val aliceConversation =
aliceClient.conversations.newConversation(fixtures.bo.walletAddress)

// Send initial message
aliceConversation.send(text = "hey alice 2 bob")
val messageToReact = aliceConversation.messages()[0]

// Send V2 reaction
val reactionV2 = FfiReaction(
reference = messageToReact.id,
referenceInboxId = aliceClient.inboxId,
action = FfiReactionAction.ADDED,
content = "U+1F603",
schema = FfiReactionSchema.UNICODE,
)
aliceConversation.send(
content = reactionV2,
options = SendOptions(contentType = ContentTypeReactionV2),
)

// Send V1 reaction
val reactionV1 = Reaction(
reference = messageToReact.id,
action = ReactionAction.Added,
content = "U+1F604", // Different emoji to distinguish
schema = ReactionSchema.Unicode,
)
aliceConversation.send(
content = reactionV1,
options = SendOptions(contentType = ContentTypeReaction),
)

// Verify both reactions appear in messagesWithReactions
val messagesWithReactions =
aliceConversation.messagesWithReactions()

assertEquals(1, messagesWithReactions.size)
assertEquals(messageToReact.id, messagesWithReactions[0].id)
assertEquals(2, messagesWithReactions[0].childMessages!!.size)

// Verify both reaction contents
val childContents = messagesWithReactions[0].childMessages!!.mapNotNull {
when (val content = it.content<Any>()) {
is FfiReaction -> content.content
is Reaction -> content.content
else -> null
}
}.toSet()
assertEquals(setOf("U+1F603", "U+1F604"), childContents)
}
}
4 changes: 2 additions & 2 deletions library/src/main/java/libxmtp-version.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Version: fc37819c
Version: a48e9a79
Branch: main
Date: 2025-01-17 01:38:53 +0000
Date: 2025-01-29 21:04:07 +0000
13 changes: 13 additions & 0 deletions library/src/main/java/org/xmtp/android/library/Conversation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ sealed class Conversation {
}
}

suspend fun messagesWithReactions(
limit: Int? = null,
beforeNs: Long? = null,
afterNs: Long? = null,
direction: Message.SortDirection = Message.SortDirection.DESCENDING,
deliveryStatus: Message.MessageDeliveryStatus = Message.MessageDeliveryStatus.ALL,
): List<Message> {
return when (this) {
is Group -> group.messagesWithReactions(limit, beforeNs, afterNs, direction, deliveryStatus)
is Dm -> dm.messagesWithReactions(limit, beforeNs, afterNs, direction, deliveryStatus)
}
}

suspend fun processMessage(messageBytes: ByteArray): Message? {
return when (this) {
is Group -> group.processMessage(messageBytes)
Expand Down
53 changes: 17 additions & 36 deletions library/src/main/java/org/xmtp/android/library/Conversations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import uniffi.xmtpv3.FfiGroupPermissionsOptions
import uniffi.xmtpv3.FfiListConversationsOptions
import uniffi.xmtpv3.FfiMessage
import uniffi.xmtpv3.FfiMessageCallback
import uniffi.xmtpv3.FfiMessageDisappearingSettings
import uniffi.xmtpv3.FfiPermissionPolicySet
import uniffi.xmtpv3.FfiSubscribeException
import java.util.Date
Expand Down Expand Up @@ -54,8 +55,7 @@ data class Conversations(
groupImageUrlSquare: String = "",
groupDescription: String = "",
groupPinnedFrameUrl: String = "",
messageExpirationFromMs: Long? = null,
messageExpirationMs: Long? = null,
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
): Group {
return newGroupInternal(
accountAddresses,
Expand All @@ -65,8 +65,7 @@ data class Conversations(
groupDescription,
groupPinnedFrameUrl,
null,
messageExpirationFromMs,
messageExpirationMs,
messageDisappearingSettings,
)
}

Expand All @@ -77,8 +76,7 @@ data class Conversations(
groupImageUrlSquare: String = "",
groupDescription: String = "",
groupPinnedFrameUrl: String = "",
messageExpirationFromMs: Long? = null,
messageExpirationMs: Long? = null,
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
): Group {
return newGroupInternal(
accountAddresses,
Expand All @@ -88,8 +86,7 @@ data class Conversations(
groupDescription,
groupPinnedFrameUrl,
PermissionPolicySet.toFfiPermissionPolicySet(permissionPolicySet),
messageExpirationFromMs,
messageExpirationMs
messageDisappearingSettings
)
}

Expand All @@ -101,8 +98,7 @@ data class Conversations(
groupDescription: String,
groupPinnedFrameUrl: String,
permissionsPolicySet: FfiPermissionPolicySet?,
messageExpirationFromMs: Long?,
messageExpirationMs: Long?,
messageDisappearingSettings: FfiMessageDisappearingSettings?,
): Group {
if (accountAddresses.any { it.equals(client.address, ignoreCase = true) }) {
throw XMTPException("Recipient is sender")
Expand All @@ -124,8 +120,7 @@ data class Conversations(
groupDescription = groupDescription,
groupPinnedFrameUrl = groupPinnedFrameUrl,
customPermissionPolicySet = permissionsPolicySet,
messageExpirationFromMs = messageExpirationFromMs,
messageExpirationMs = messageExpirationMs,
messageDisappearingSettings = messageDisappearingSettings
)
)
return Group(client.inboxId, group)
Expand All @@ -138,8 +133,7 @@ data class Conversations(
groupImageUrlSquare: String = "",
groupDescription: String = "",
groupPinnedFrameUrl: String = "",
messageExpirationFromMs: Long? = null,
messageExpirationMs: Long? = null,
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
): Group {
return newGroupInternalWithInboxIds(
inboxIds,
Expand All @@ -149,8 +143,7 @@ data class Conversations(
groupDescription,
groupPinnedFrameUrl,
null,
messageExpirationFromMs,
messageExpirationMs,
messageDisappearingSettings
)
}

Expand All @@ -161,8 +154,7 @@ data class Conversations(
groupImageUrlSquare: String = "",
groupDescription: String = "",
groupPinnedFrameUrl: String = "",
messageExpirationFromMs: Long? = null,
messageExpirationMs: Long? = null,
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
): Group {
return newGroupInternalWithInboxIds(
inboxIds,
Expand All @@ -172,8 +164,7 @@ data class Conversations(
groupDescription,
groupPinnedFrameUrl,
PermissionPolicySet.toFfiPermissionPolicySet(permissionPolicySet),
messageExpirationFromMs,
messageExpirationMs
messageDisappearingSettings
)
}

Expand All @@ -185,8 +176,7 @@ data class Conversations(
groupDescription: String,
groupPinnedFrameUrl: String,
permissionsPolicySet: FfiPermissionPolicySet?,
messageExpirationFromMs: Long?,
messageExpirationMs: Long?,
messageDisappearingSettings: FfiMessageDisappearingSettings?,
): Group {
if (inboxIds.any { it.equals(client.inboxId, ignoreCase = true) }) {
throw XMTPException("Recipient is sender")
Expand All @@ -202,8 +192,7 @@ data class Conversations(
groupDescription = groupDescription,
groupPinnedFrameUrl = groupPinnedFrameUrl,
customPermissionPolicySet = permissionsPolicySet,
messageExpirationFromMs = messageExpirationFromMs,
messageExpirationMs = messageExpirationMs,
messageDisappearingSettings = messageDisappearingSettings
)
)
return Group(client.inboxId, group)
Expand Down Expand Up @@ -237,12 +226,8 @@ data class Conversations(
if (falseAddresses.isNotEmpty()) {
throw XMTPException("${falseAddresses.joinToString()} not on network")
}
var dm = client.findDmByAddress(peerAddress)
if (dm == null) {
val dmConversation = ffiConversations.createDm(peerAddress.lowercase())
dm = Dm(client.inboxId, dmConversation)
}
return dm
val dmConversation = ffiConversations.findOrCreateDm(peerAddress.lowercase())
return Dm(client.inboxId, dmConversation)
}

suspend fun newConversationWithInboxId(peerInboxId: String): Conversation {
Expand All @@ -254,12 +239,8 @@ data class Conversations(
if (peerInboxId.lowercase() == client.inboxId.lowercase()) {
throw XMTPException("Recipient is sender")
}
var dm = client.findDmByInboxId(peerInboxId)
if (dm == null) {
val dmConversation = ffiConversations.createDmWithInboxId(peerInboxId.lowercase())
dm = Dm(client.inboxId, dmConversation)
}
return dm
val dmConversation = ffiConversations.findOrCreateDmByInboxId(peerInboxId.lowercase())
return Dm(client.inboxId, dmConversation)
}

fun listGroups(
Expand Down
31 changes: 31 additions & 0 deletions library/src/main/java/org/xmtp/android/library/Dm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,37 @@ class Dm(private val clientInboxId: String, private val libXMTPGroup: FfiConvers
}
}

suspend fun messagesWithReactions(
limit: Int? = null,
beforeNs: Long? = null,
afterNs: Long? = null,
direction: SortDirection = SortDirection.DESCENDING,
deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL,
): List<Message> {
val ffiMessageWithReactions = libXMTPGroup.findMessagesWithReactions(
opts = FfiListMessagesOptions(
sentBeforeNs = beforeNs,
sentAfterNs = afterNs,
limit = limit?.toLong(),
deliveryStatus = when (deliveryStatus) {
MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED
MessageDeliveryStatus.UNPUBLISHED -> FfiDeliveryStatus.UNPUBLISHED
MessageDeliveryStatus.FAILED -> FfiDeliveryStatus.FAILED
else -> null
},
when (direction) {
SortDirection.ASCENDING -> FfiDirection.ASCENDING
else -> FfiDirection.DESCENDING
},
contentTypes = null
)
)

return ffiMessageWithReactions.mapNotNull { ffiMessageWithReactions ->
Message.create(ffiMessageWithReactions)
}
}

suspend fun processMessage(messageBytes: ByteArray): Message? {
val message = libXMTPGroup.processStreamedConversationMessage(messageBytes)
return Message.create(message)
Expand Down
Loading

0 comments on commit 65be7d1

Please sign in to comment.