Skip to content

Commit 65be7d1

Browse files
authored
Reaction from rust and list messages with reactions (#363)
* 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 <cameronvoell@users.noreply.github.com>
1 parent 51c7b17 commit 65be7d1

File tree

16 files changed

+1174
-777
lines changed

16 files changed

+1174
-777
lines changed

library/src/androidTest/java/org/xmtp/android/library/GroupPermissionsTest.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ class GroupPermissionsTest {
434434
updateGroupDescriptionPolicy = PermissionOption.Allow,
435435
updateGroupImagePolicy = PermissionOption.Admin,
436436
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
437-
updateMessageExpirationPolicy = PermissionOption.Admin,
437+
updateMessageDisappearingPolicy = PermissionOption.Admin,
438438
)
439439
val boGroup = runBlocking {
440440
boClient.conversations.newGroupCustomPermissions(
@@ -469,7 +469,7 @@ class GroupPermissionsTest {
469469
updateGroupDescriptionPolicy = PermissionOption.Allow,
470470
updateGroupImagePolicy = PermissionOption.Admin,
471471
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
472-
updateMessageExpirationPolicy = PermissionOption.Admin,
472+
updateMessageDisappearingPolicy = PermissionOption.Admin,
473473
)
474474

475475
assertThrows(GenericException.GroupMutablePermissions::class.java) {
@@ -490,7 +490,7 @@ class GroupPermissionsTest {
490490
updateGroupDescriptionPolicy = PermissionOption.Allow,
491491
updateGroupImagePolicy = PermissionOption.Admin,
492492
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
493-
updateMessageExpirationPolicy = PermissionOption.Allow,
493+
updateMessageDisappearingPolicy = PermissionOption.Allow,
494494
)
495495

496496
// Valid custom policy works as expected
@@ -518,7 +518,7 @@ class GroupPermissionsTest {
518518
updateGroupDescriptionPolicy = PermissionOption.Allow,
519519
updateGroupImagePolicy = PermissionOption.Admin,
520520
updateGroupPinnedFrameUrlPolicy = PermissionOption.Deny,
521-
updateMessageExpirationPolicy = PermissionOption.Admin,
521+
updateMessageDisappearingPolicy = PermissionOption.Admin,
522522
)
523523
val boGroup = runBlocking {
524524
boClient.conversations.newGroupCustomPermissionsWithInboxIds(

library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt

+113
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ import org.junit.Assert.assertEquals
77
import org.junit.Test
88
import org.junit.runner.RunWith
99
import org.xmtp.android.library.codecs.ContentTypeReaction
10+
import org.xmtp.android.library.codecs.ContentTypeReactionV2
1011
import org.xmtp.android.library.codecs.EncodedContent
1112
import org.xmtp.android.library.codecs.Reaction
1213
import org.xmtp.android.library.codecs.ReactionAction
1314
import org.xmtp.android.library.codecs.ReactionCodec
1415
import org.xmtp.android.library.codecs.ReactionSchema
16+
import org.xmtp.android.library.codecs.ReactionV2Codec
17+
import org.xmtp.android.library.libxmtp.Message
1518
import org.xmtp.android.library.messages.walletAddress
19+
import uniffi.xmtpv3.FfiReaction
20+
import uniffi.xmtpv3.FfiReactionAction
21+
import uniffi.xmtpv3.FfiReactionSchema
1622

1723
@RunWith(AndroidJUnit4::class)
1824
class ReactionTest {
@@ -98,4 +104,111 @@ class ReactionTest {
98104
assertEquals(ReactionSchema.Unicode, content?.schema)
99105
}
100106
}
107+
108+
@Test
109+
fun testCanUseReactionV2Codec() {
110+
Client.register(codec = ReactionV2Codec())
111+
112+
val fixtures = fixtures()
113+
val aliceClient = fixtures.alixClient
114+
val aliceConversation = runBlocking {
115+
aliceClient.conversations.newConversation(fixtures.bo.walletAddress)
116+
}
117+
118+
runBlocking { aliceConversation.send(text = "hey alice 2 bob") }
119+
120+
val messageToReact = runBlocking { aliceConversation.messages()[0] }
121+
122+
val reaction = FfiReaction(
123+
reference = messageToReact.id,
124+
referenceInboxId = aliceClient.inboxId,
125+
action = FfiReactionAction.ADDED,
126+
content = "U+1F603",
127+
schema = FfiReactionSchema.UNICODE,
128+
)
129+
130+
runBlocking {
131+
aliceConversation.send(
132+
content = reaction,
133+
options = SendOptions(contentType = ContentTypeReactionV2),
134+
)
135+
}
136+
val messages = runBlocking { aliceConversation.messages() }
137+
assertEquals(messages.size, 2)
138+
if (messages.size == 2) {
139+
val content: FfiReaction? = messages.first().content()
140+
assertEquals("U+1F603", content?.content)
141+
assertEquals(messageToReact.id, content?.reference)
142+
assertEquals(FfiReactionAction.ADDED, content?.action)
143+
assertEquals(FfiReactionSchema.UNICODE, content?.schema)
144+
}
145+
146+
val messagesWithReactions: List<Message> = runBlocking {
147+
aliceConversation.messagesWithReactions()
148+
}
149+
assertEquals(messagesWithReactions.size, 1)
150+
assertEquals(messagesWithReactions[0].id, messageToReact.id)
151+
val reactionContent: FfiReaction? =
152+
messagesWithReactions[0]?.childMessages!![0]?.let { it?.content()!! }
153+
assertEquals(reactionContent?.reference, messageToReact.id)
154+
}
155+
156+
@Test
157+
fun testCanMixReactionTypes() = runBlocking {
158+
// Register both codecs
159+
Client.register(codec = ReactionV2Codec())
160+
Client.register(codec = ReactionCodec())
161+
162+
val fixtures = fixtures()
163+
val aliceClient = fixtures.alixClient
164+
val aliceConversation =
165+
aliceClient.conversations.newConversation(fixtures.bo.walletAddress)
166+
167+
// Send initial message
168+
aliceConversation.send(text = "hey alice 2 bob")
169+
val messageToReact = aliceConversation.messages()[0]
170+
171+
// Send V2 reaction
172+
val reactionV2 = FfiReaction(
173+
reference = messageToReact.id,
174+
referenceInboxId = aliceClient.inboxId,
175+
action = FfiReactionAction.ADDED,
176+
content = "U+1F603",
177+
schema = FfiReactionSchema.UNICODE,
178+
)
179+
aliceConversation.send(
180+
content = reactionV2,
181+
options = SendOptions(contentType = ContentTypeReactionV2),
182+
)
183+
184+
// Send V1 reaction
185+
val reactionV1 = Reaction(
186+
reference = messageToReact.id,
187+
action = ReactionAction.Added,
188+
content = "U+1F604", // Different emoji to distinguish
189+
schema = ReactionSchema.Unicode,
190+
)
191+
aliceConversation.send(
192+
content = reactionV1,
193+
options = SendOptions(contentType = ContentTypeReaction),
194+
)
195+
196+
// Verify both reactions appear in messagesWithReactions
197+
val messagesWithReactions =
198+
aliceConversation.messagesWithReactions()
199+
200+
assertEquals(1, messagesWithReactions.size)
201+
assertEquals(messageToReact.id, messagesWithReactions[0].id)
202+
assertEquals(2, messagesWithReactions[0].childMessages!!.size)
203+
204+
// Verify both reaction contents
205+
val childContents = messagesWithReactions[0].childMessages!!.mapNotNull {
206+
when (val content = it.content<Any>()) {
207+
is FfiReaction -> content.content
208+
is Reaction -> content.content
209+
else -> null
210+
}
211+
}.toSet()
212+
assertEquals(setOf("U+1F603", "U+1F604"), childContents)
213+
}
101214
}
+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Version: fc37819c
1+
Version: a48e9a79
22
Branch: main
3-
Date: 2025-01-17 01:38:53 +0000
3+
Date: 2025-01-29 21:04:07 +0000

library/src/main/java/org/xmtp/android/library/Conversation.kt

+13
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ sealed class Conversation {
127127
}
128128
}
129129

130+
suspend fun messagesWithReactions(
131+
limit: Int? = null,
132+
beforeNs: Long? = null,
133+
afterNs: Long? = null,
134+
direction: Message.SortDirection = Message.SortDirection.DESCENDING,
135+
deliveryStatus: Message.MessageDeliveryStatus = Message.MessageDeliveryStatus.ALL,
136+
): List<Message> {
137+
return when (this) {
138+
is Group -> group.messagesWithReactions(limit, beforeNs, afterNs, direction, deliveryStatus)
139+
is Dm -> dm.messagesWithReactions(limit, beforeNs, afterNs, direction, deliveryStatus)
140+
}
141+
}
142+
130143
suspend fun processMessage(messageBytes: ByteArray): Message? {
131144
return when (this) {
132145
is Group -> group.processMessage(messageBytes)

library/src/main/java/org/xmtp/android/library/Conversations.kt

+17-36
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import uniffi.xmtpv3.FfiGroupPermissionsOptions
2222
import uniffi.xmtpv3.FfiListConversationsOptions
2323
import uniffi.xmtpv3.FfiMessage
2424
import uniffi.xmtpv3.FfiMessageCallback
25+
import uniffi.xmtpv3.FfiMessageDisappearingSettings
2526
import uniffi.xmtpv3.FfiPermissionPolicySet
2627
import uniffi.xmtpv3.FfiSubscribeException
2728
import java.util.Date
@@ -54,8 +55,7 @@ data class Conversations(
5455
groupImageUrlSquare: String = "",
5556
groupDescription: String = "",
5657
groupPinnedFrameUrl: String = "",
57-
messageExpirationFromMs: Long? = null,
58-
messageExpirationMs: Long? = null,
58+
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
5959
): Group {
6060
return newGroupInternal(
6161
accountAddresses,
@@ -65,8 +65,7 @@ data class Conversations(
6565
groupDescription,
6666
groupPinnedFrameUrl,
6767
null,
68-
messageExpirationFromMs,
69-
messageExpirationMs,
68+
messageDisappearingSettings,
7069
)
7170
}
7271

@@ -77,8 +76,7 @@ data class Conversations(
7776
groupImageUrlSquare: String = "",
7877
groupDescription: String = "",
7978
groupPinnedFrameUrl: String = "",
80-
messageExpirationFromMs: Long? = null,
81-
messageExpirationMs: Long? = null,
79+
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
8280
): Group {
8381
return newGroupInternal(
8482
accountAddresses,
@@ -88,8 +86,7 @@ data class Conversations(
8886
groupDescription,
8987
groupPinnedFrameUrl,
9088
PermissionPolicySet.toFfiPermissionPolicySet(permissionPolicySet),
91-
messageExpirationFromMs,
92-
messageExpirationMs
89+
messageDisappearingSettings
9390
)
9491
}
9592

@@ -101,8 +98,7 @@ data class Conversations(
10198
groupDescription: String,
10299
groupPinnedFrameUrl: String,
103100
permissionsPolicySet: FfiPermissionPolicySet?,
104-
messageExpirationFromMs: Long?,
105-
messageExpirationMs: Long?,
101+
messageDisappearingSettings: FfiMessageDisappearingSettings?,
106102
): Group {
107103
if (accountAddresses.any { it.equals(client.address, ignoreCase = true) }) {
108104
throw XMTPException("Recipient is sender")
@@ -124,8 +120,7 @@ data class Conversations(
124120
groupDescription = groupDescription,
125121
groupPinnedFrameUrl = groupPinnedFrameUrl,
126122
customPermissionPolicySet = permissionsPolicySet,
127-
messageExpirationFromMs = messageExpirationFromMs,
128-
messageExpirationMs = messageExpirationMs,
123+
messageDisappearingSettings = messageDisappearingSettings
129124
)
130125
)
131126
return Group(client.inboxId, group)
@@ -138,8 +133,7 @@ data class Conversations(
138133
groupImageUrlSquare: String = "",
139134
groupDescription: String = "",
140135
groupPinnedFrameUrl: String = "",
141-
messageExpirationFromMs: Long? = null,
142-
messageExpirationMs: Long? = null,
136+
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
143137
): Group {
144138
return newGroupInternalWithInboxIds(
145139
inboxIds,
@@ -149,8 +143,7 @@ data class Conversations(
149143
groupDescription,
150144
groupPinnedFrameUrl,
151145
null,
152-
messageExpirationFromMs,
153-
messageExpirationMs,
146+
messageDisappearingSettings
154147
)
155148
}
156149

@@ -161,8 +154,7 @@ data class Conversations(
161154
groupImageUrlSquare: String = "",
162155
groupDescription: String = "",
163156
groupPinnedFrameUrl: String = "",
164-
messageExpirationFromMs: Long? = null,
165-
messageExpirationMs: Long? = null,
157+
messageDisappearingSettings: FfiMessageDisappearingSettings? = null,
166158
): Group {
167159
return newGroupInternalWithInboxIds(
168160
inboxIds,
@@ -172,8 +164,7 @@ data class Conversations(
172164
groupDescription,
173165
groupPinnedFrameUrl,
174166
PermissionPolicySet.toFfiPermissionPolicySet(permissionPolicySet),
175-
messageExpirationFromMs,
176-
messageExpirationMs
167+
messageDisappearingSettings
177168
)
178169
}
179170

@@ -185,8 +176,7 @@ data class Conversations(
185176
groupDescription: String,
186177
groupPinnedFrameUrl: String,
187178
permissionsPolicySet: FfiPermissionPolicySet?,
188-
messageExpirationFromMs: Long?,
189-
messageExpirationMs: Long?,
179+
messageDisappearingSettings: FfiMessageDisappearingSettings?,
190180
): Group {
191181
if (inboxIds.any { it.equals(client.inboxId, ignoreCase = true) }) {
192182
throw XMTPException("Recipient is sender")
@@ -202,8 +192,7 @@ data class Conversations(
202192
groupDescription = groupDescription,
203193
groupPinnedFrameUrl = groupPinnedFrameUrl,
204194
customPermissionPolicySet = permissionsPolicySet,
205-
messageExpirationFromMs = messageExpirationFromMs,
206-
messageExpirationMs = messageExpirationMs,
195+
messageDisappearingSettings = messageDisappearingSettings
207196
)
208197
)
209198
return Group(client.inboxId, group)
@@ -237,12 +226,8 @@ data class Conversations(
237226
if (falseAddresses.isNotEmpty()) {
238227
throw XMTPException("${falseAddresses.joinToString()} not on network")
239228
}
240-
var dm = client.findDmByAddress(peerAddress)
241-
if (dm == null) {
242-
val dmConversation = ffiConversations.createDm(peerAddress.lowercase())
243-
dm = Dm(client.inboxId, dmConversation)
244-
}
245-
return dm
229+
val dmConversation = ffiConversations.findOrCreateDm(peerAddress.lowercase())
230+
return Dm(client.inboxId, dmConversation)
246231
}
247232

248233
suspend fun newConversationWithInboxId(peerInboxId: String): Conversation {
@@ -254,12 +239,8 @@ data class Conversations(
254239
if (peerInboxId.lowercase() == client.inboxId.lowercase()) {
255240
throw XMTPException("Recipient is sender")
256241
}
257-
var dm = client.findDmByInboxId(peerInboxId)
258-
if (dm == null) {
259-
val dmConversation = ffiConversations.createDmWithInboxId(peerInboxId.lowercase())
260-
dm = Dm(client.inboxId, dmConversation)
261-
}
262-
return dm
242+
val dmConversation = ffiConversations.findOrCreateDmByInboxId(peerInboxId.lowercase())
243+
return Dm(client.inboxId, dmConversation)
263244
}
264245

265246
fun listGroups(

library/src/main/java/org/xmtp/android/library/Dm.kt

+31
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,37 @@ class Dm(private val clientInboxId: String, private val libXMTPGroup: FfiConvers
131131
}
132132
}
133133

134+
suspend fun messagesWithReactions(
135+
limit: Int? = null,
136+
beforeNs: Long? = null,
137+
afterNs: Long? = null,
138+
direction: SortDirection = SortDirection.DESCENDING,
139+
deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL,
140+
): List<Message> {
141+
val ffiMessageWithReactions = libXMTPGroup.findMessagesWithReactions(
142+
opts = FfiListMessagesOptions(
143+
sentBeforeNs = beforeNs,
144+
sentAfterNs = afterNs,
145+
limit = limit?.toLong(),
146+
deliveryStatus = when (deliveryStatus) {
147+
MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED
148+
MessageDeliveryStatus.UNPUBLISHED -> FfiDeliveryStatus.UNPUBLISHED
149+
MessageDeliveryStatus.FAILED -> FfiDeliveryStatus.FAILED
150+
else -> null
151+
},
152+
when (direction) {
153+
SortDirection.ASCENDING -> FfiDirection.ASCENDING
154+
else -> FfiDirection.DESCENDING
155+
},
156+
contentTypes = null
157+
)
158+
)
159+
160+
return ffiMessageWithReactions.mapNotNull { ffiMessageWithReactions ->
161+
Message.create(ffiMessageWithReactions)
162+
}
163+
}
164+
134165
suspend fun processMessage(messageBytes: ByteArray): Message? {
135166
val message = libXMTPGroup.processStreamedConversationMessage(messageBytes)
136167
return Message.create(message)

0 commit comments

Comments
 (0)