diff --git a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt index dc653c07..a30a1b16 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt @@ -12,7 +12,13 @@ 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.libxmtp.Message import org.xmtp.android.library.messages.walletAddress +import uniffi.xmtpv3.FfiReaction +import uniffi.xmtpv3.FfiReactionAction +import uniffi.xmtpv3.FfiReactionSchema +import uniffi.xmtpv3.org.xmtp.android.library.codecs.ContentTypeReactionV2 +import uniffi.xmtpv3.org.xmtp.android.library.codecs.ReactionV2Codec @RunWith(AndroidJUnit4::class) class ReactionTest { @@ -98,4 +104,113 @@ 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 = runBlocking { + aliceConversation.messagesWithReactions() + } + assertEquals(messagesWithReactions.size, 1) + assertEquals(messagesWithReactions[0].id, messageToReact.id) + val reactionContent: FfiReaction? = messagesWithReactions[0].childMessages[0].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()) { + is FfiReaction -> content.content + is Reaction -> content.content + else -> null + } + }.toSet() + assertEquals(setOf("U+1F603", "U+1F604"), childContents) + } } diff --git a/library/src/main/java/libxmtp-version.txt b/library/src/main/java/libxmtp-version.txt index 17747f07..6923f7f8 100644 --- a/library/src/main/java/libxmtp-version.txt +++ b/library/src/main/java/libxmtp-version.txt @@ -1,3 +1,3 @@ -Version: 13846991 -Branch: cv/01-02-save_parent_id_to_db_and_add_get_group_messages_with_reactions_function -Date: 2025-01-07 17:52:41 +0000 +Version: 76983ce7 +Branch: cv/01-10-json_deserialization_for_legacy_reactions +Date: 2025-01-11 00:19:35 +0000 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 b5bfbb56..a546be40 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -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 { + 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) 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 c849b87c..67cd486c 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -115,7 +115,9 @@ data class Conversations( groupImageUrlSquare = groupImageUrlSquare, groupDescription = groupDescription, groupPinnedFrameUrl = groupPinnedFrameUrl, - customPermissionPolicySet = permissionsPolicySet + customPermissionPolicySet = permissionsPolicySet, + messageExpirationFromMs = null, + messageExpirationMs = null ) ) return Group(client, group) 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 f947c3eb..f3ec3116 100644 --- a/library/src/main/java/org/xmtp/android/library/Dm.kt +++ b/library/src/main/java/org/xmtp/android/library/Dm.kt @@ -140,6 +140,42 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation, private } } + suspend fun messagesWithReactions( + limit: Int? = null, + beforeNs: Long? = null, + afterNs: Long? = null, + direction: SortDirection = SortDirection.DESCENDING, + deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, + ): List { + 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 -> + val parentMessage = Message.create(ffiMessageWithReactions.message)!! + val childMessages = ffiMessageWithReactions.reactions.mapNotNull { childMessage -> + Message.create(childMessage)!! + } + + Message.MessageWithChildMessages(parentMessage, childMessages) + } + } + suspend fun processMessage(messageBytes: ByteArray): Message? { val message = libXMTPGroup.processStreamedConversationMessage(messageBytes) return Message.create(message) 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 ea248fc6..39f5de9f 100644 --- a/library/src/main/java/org/xmtp/android/library/Group.kt +++ b/library/src/main/java/org/xmtp/android/library/Group.kt @@ -162,6 +162,42 @@ class Group( } } + suspend fun messagesWithReactions( + limit: Int? = null, + beforeNs: Long? = null, + afterNs: Long? = null, + direction: SortDirection = SortDirection.DESCENDING, + deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, + ): List { + 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 -> + val parentMessage = Message.create(ffiMessageWithReactions.message)!! + val childMessages = ffiMessageWithReactions.reactions.mapNotNull { childMessage -> + Message.create(childMessage)!! + } + + Message.MessageWithChildMessages(parentMessage, childMessages) + } + } + suspend fun processMessage(messageBytes: ByteArray): Message? { val message = libXMTPGroup.processStreamedConversationMessage(messageBytes) return Message.create(message) diff --git a/library/src/main/java/org/xmtp/android/library/codecs/ContentTypeId.kt b/library/src/main/java/org/xmtp/android/library/codecs/ContentTypeId.kt index 0a71b0ee..88dc1c46 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/ContentTypeId.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/ContentTypeId.kt @@ -21,7 +21,7 @@ class ContentTypeIdBuilder { } val ContentTypeId.id: String - get() = "$authorityId:$typeId" + get() = "$authorityId:$typeId:$versionMajor.$versionMinor" val ContentTypeId.description: String get() = "$authorityId/$typeId:$versionMajor.$versionMinor" diff --git a/library/src/main/java/org/xmtp/android/library/codecs/ReactionV2Codec.kt b/library/src/main/java/org/xmtp/android/library/codecs/ReactionV2Codec.kt new file mode 100644 index 00000000..78160a30 --- /dev/null +++ b/library/src/main/java/org/xmtp/android/library/codecs/ReactionV2Codec.kt @@ -0,0 +1,42 @@ +package uniffi.xmtpv3.org.xmtp.android.library.codecs + +import org.xmtp.android.library.codecs.ContentCodec +import org.xmtp.android.library.codecs.ContentTypeId +import org.xmtp.android.library.codecs.ContentTypeIdBuilder +import org.xmtp.android.library.codecs.EncodedContent +import uniffi.xmtpv3.FfiReaction +import uniffi.xmtpv3.FfiReactionAction +import uniffi.xmtpv3.decodeReaction +import uniffi.xmtpv3.encodeReaction + +val ContentTypeReactionV2 = ContentTypeIdBuilder.builderFromAuthorityId( + "xmtp.org", + "reaction", + versionMajor = 2, + versionMinor = 0, +) + +data class ReactionV2Codec(override var contentType: ContentTypeId = ContentTypeReactionV2) : + ContentCodec { + + override fun encode(content: FfiReaction): EncodedContent { + return EncodedContent.parseFrom(encodeReaction(content)) + } + + override fun decode(content: EncodedContent): FfiReaction { + return decodeReaction(content.toByteArray()) + } + + override fun fallback(content: FfiReaction): String? { + return when (content.action) { + FfiReactionAction.ADDED -> "Reacted “${content.content}” to an earlier message" + FfiReactionAction.REMOVED -> "Removed “${content.content}” from an earlier message" + else -> null + } + } + + override fun shouldPush(content: FfiReaction): Boolean = when (content.action) { + FfiReactionAction.ADDED -> true + else -> false + } +} diff --git a/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt index 73140b4b..c42e2366 100644 --- a/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt +++ b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt @@ -77,4 +77,41 @@ class Message private constructor( } } } + + data class MessageWithChildMessages( + var message: Message, + var childMessages: List + ) { + val id: String + get() = message.id + + val convoId: String + get() = message.convoId + + val senderInboxId: String + get() = message.senderInboxId + + val sentAt: Date + get() = message.sentAt + + val sentAtNs: Long + get() = message.sentAtNs + + val deliveryStatus: MessageDeliveryStatus + get() = message.deliveryStatus + + val topic: String + get() = message.topic + + @Suppress("UNCHECKED_CAST") + fun content(): T? = message.content() as? T + + val fallbackContent: String + get() = message.fallbackContent + + val body: String + get() { + return content() as? String ?: fallbackContent + } + } } diff --git a/library/src/main/java/org/xmtp/android/library/libxmtp/PermissionPolicySet.kt b/library/src/main/java/org/xmtp/android/library/libxmtp/PermissionPolicySet.kt index fe04dde4..4c990aca 100644 --- a/library/src/main/java/org/xmtp/android/library/libxmtp/PermissionPolicySet.kt +++ b/library/src/main/java/org/xmtp/android/library/libxmtp/PermissionPolicySet.kt @@ -39,7 +39,7 @@ enum class GroupPermissionPreconfiguration { companion object { fun toFfiGroupPermissionOptions(option: GroupPermissionPreconfiguration): FfiGroupPermissionsOptions { return when (option) { - ALL_MEMBERS -> FfiGroupPermissionsOptions.ALL_MEMBERS + ALL_MEMBERS -> FfiGroupPermissionsOptions.DEFAULT ADMIN_ONLY -> FfiGroupPermissionsOptions.ADMIN_ONLY } } @@ -66,7 +66,8 @@ data class PermissionPolicySet( updateGroupNamePolicy = PermissionOption.toFfiPermissionPolicy(permissionPolicySet.updateGroupNamePolicy), updateGroupDescriptionPolicy = PermissionOption.toFfiPermissionPolicy(permissionPolicySet.updateGroupDescriptionPolicy), updateGroupImageUrlSquarePolicy = PermissionOption.toFfiPermissionPolicy(permissionPolicySet.updateGroupImagePolicy), - updateGroupPinnedFrameUrlPolicy = PermissionOption.toFfiPermissionPolicy(permissionPolicySet.updateGroupPinnedFrameUrlPolicy) + updateGroupPinnedFrameUrlPolicy = PermissionOption.toFfiPermissionPolicy(permissionPolicySet.updateGroupPinnedFrameUrlPolicy), + updateMessageExpirationMsPolicy = PermissionOption.toFfiPermissionPolicy(permissionPolicySet.addAdminPolicy) ) } diff --git a/library/src/main/java/xmtpv3.kt b/library/src/main/java/xmtpv3.kt index e7c8a072..c477a8e5 100644 --- a/library/src/main/java/xmtpv3.kt +++ b/library/src/main/java/xmtpv3.kt @@ -10241,6 +10241,8 @@ data class FfiCreateGroupOptions( var `groupDescription`: kotlin.String?, var `groupPinnedFrameUrl`: kotlin.String?, var `customPermissionPolicySet`: FfiPermissionPolicySet?, + var `messageExpirationFromMs`: kotlin.Long?, + var `messageExpirationMs`: kotlin.Long?, ) { companion object @@ -10259,6 +10261,8 @@ public object FfiConverterTypeFfiCreateGroupOptions : FfiConverterOptionalString.read(buf), FfiConverterOptionalString.read(buf), FfiConverterOptionalTypeFfiPermissionPolicySet.read(buf), + FfiConverterOptionalLong.read(buf), + FfiConverterOptionalLong.read(buf), ) } @@ -10268,7 +10272,9 @@ public object FfiConverterTypeFfiCreateGroupOptions : FfiConverterOptionalString.allocationSize(value.`groupImageUrlSquare`) + FfiConverterOptionalString.allocationSize(value.`groupDescription`) + FfiConverterOptionalString.allocationSize(value.`groupPinnedFrameUrl`) + - FfiConverterOptionalTypeFfiPermissionPolicySet.allocationSize(value.`customPermissionPolicySet`) + FfiConverterOptionalTypeFfiPermissionPolicySet.allocationSize(value.`customPermissionPolicySet`) + + FfiConverterOptionalLong.allocationSize(value.`messageExpirationFromMs`) + + FfiConverterOptionalLong.allocationSize(value.`messageExpirationMs`) ) override fun write(value: FfiCreateGroupOptions, buf: ByteBuffer) { @@ -10278,6 +10284,8 @@ public object FfiConverterTypeFfiCreateGroupOptions : FfiConverterOptionalString.write(value.`groupDescription`, buf) FfiConverterOptionalString.write(value.`groupPinnedFrameUrl`, buf) FfiConverterOptionalTypeFfiPermissionPolicySet.write(value.`customPermissionPolicySet`, buf) + FfiConverterOptionalLong.write(value.`messageExpirationFromMs`, buf) + FfiConverterOptionalLong.write(value.`messageExpirationMs`, buf) } } @@ -10668,6 +10676,7 @@ data class FfiPermissionPolicySet( var `updateGroupDescriptionPolicy`: FfiPermissionPolicy, var `updateGroupImageUrlSquarePolicy`: FfiPermissionPolicy, var `updateGroupPinnedFrameUrlPolicy`: FfiPermissionPolicy, + var `updateMessageExpirationMsPolicy`: FfiPermissionPolicy, ) { companion object @@ -10688,6 +10697,7 @@ public object FfiConverterTypeFfiPermissionPolicySet : FfiConverterTypeFfiPermissionPolicy.read(buf), FfiConverterTypeFfiPermissionPolicy.read(buf), FfiConverterTypeFfiPermissionPolicy.read(buf), + FfiConverterTypeFfiPermissionPolicy.read(buf), ) } @@ -10699,7 +10709,8 @@ public object FfiConverterTypeFfiPermissionPolicySet : FfiConverterTypeFfiPermissionPolicy.allocationSize(value.`updateGroupNamePolicy`) + FfiConverterTypeFfiPermissionPolicy.allocationSize(value.`updateGroupDescriptionPolicy`) + FfiConverterTypeFfiPermissionPolicy.allocationSize(value.`updateGroupImageUrlSquarePolicy`) + - FfiConverterTypeFfiPermissionPolicy.allocationSize(value.`updateGroupPinnedFrameUrlPolicy`) + FfiConverterTypeFfiPermissionPolicy.allocationSize(value.`updateGroupPinnedFrameUrlPolicy`) + + FfiConverterTypeFfiPermissionPolicy.allocationSize(value.`updateMessageExpirationMsPolicy`) ) override fun write(value: FfiPermissionPolicySet, buf: ByteBuffer) { @@ -10711,6 +10722,7 @@ public object FfiConverterTypeFfiPermissionPolicySet : FfiConverterTypeFfiPermissionPolicy.write(value.`updateGroupDescriptionPolicy`, buf) FfiConverterTypeFfiPermissionPolicy.write(value.`updateGroupImageUrlSquarePolicy`, buf) FfiConverterTypeFfiPermissionPolicy.write(value.`updateGroupPinnedFrameUrlPolicy`, buf) + FfiConverterTypeFfiPermissionPolicy.write(value.`updateMessageExpirationMsPolicy`, buf) } } @@ -11170,7 +11182,7 @@ public object FfiConverterTypeFfiDirection : FfiConverterRustBuffer