Skip to content

Commit 0976446

Browse files
authored
feat: improve PreparedMessage handling (#113)
1 parent b82ac6c commit 0976446

File tree

6 files changed

+113
-50
lines changed

6 files changed

+113
-50
lines changed

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

+18-2
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ class ConversationTest {
597597
assertEquals(conversation.version, Conversation.Version.V1)
598598
val preparedMessage = conversation.prepareMessage(content = "hi")
599599
val messageID = preparedMessage.messageId
600-
preparedMessage.send()
600+
conversation.send(prepared = preparedMessage)
601601
val messages = conversation.messages()
602602
val message = messages[0]
603603
assertEquals("hi", message.body)
@@ -609,7 +609,23 @@ class ConversationTest {
609609
val conversation = aliceClient.conversations.newConversation(bob.walletAddress)
610610
val preparedMessage = conversation.prepareMessage(content = "hi")
611611
val messageID = preparedMessage.messageId
612-
preparedMessage.send()
612+
conversation.send(prepared = preparedMessage)
613+
val messages = conversation.messages()
614+
val message = messages[0]
615+
assertEquals("hi", message.body)
616+
assertEquals(message.id, messageID)
617+
}
618+
619+
@Test
620+
fun testCanSendPreparedMessageWithoutConversation() {
621+
val conversation = aliceClient.conversations.newConversation(bob.walletAddress)
622+
val preparedMessage = conversation.prepareMessage(content = "hi")
623+
val messageID = preparedMessage.messageId
624+
625+
// This does not need the `conversation` to `.publish` the message.
626+
// This simulates a background task publishing all pending messages upon connection.
627+
aliceClient.publish(envelopes = preparedMessage.envelopes)
628+
613629
val messages = conversation.messages()
614630
val message = messages[0]
615631
assertEquals("hi", message.body)

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

+7
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ sealed class Conversation {
105105
}
106106
}
107107

108+
fun send(prepared: PreparedMessage) {
109+
when (this) {
110+
is V1 -> conversationV1.send(prepared = prepared)
111+
is V2 -> conversationV2.send(prepared = prepared)
112+
}
113+
}
114+
108115
fun <T> send(content: T, options: SendOptions? = null) {
109116
when (this) {
110117
is V1 -> conversationV1.send(content = content, options = options)

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

+23-30
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package org.xmtp.android.library
22

33
import android.util.Log
44
import kotlinx.coroutines.flow.Flow
5-
import kotlinx.coroutines.flow.collect
65
import kotlinx.coroutines.flow.flow
76
import kotlinx.coroutines.runBlocking
87
import org.web3j.crypto.Hash
@@ -89,20 +88,22 @@ data class ConversationV1(
8988
sentAt: Date? = null,
9089
): String {
9190
val preparedMessage = prepareMessage(content = text, options = sendOptions)
92-
preparedMessage.send()
93-
return preparedMessage.messageId
91+
return send(preparedMessage)
9492
}
9593

9694
fun <T> send(content: T, options: SendOptions? = null): String {
9795
val preparedMessage = prepareMessage(content = content, options = options)
98-
preparedMessage.send()
99-
return preparedMessage.messageId
96+
return send(preparedMessage)
10097
}
10198

10299
fun send(encodedContent: EncodedContent, options: SendOptions? = null): String {
103100
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
104-
preparedMessage.send()
105-
return preparedMessage.messageId
101+
return send(preparedMessage)
102+
}
103+
104+
fun send(prepared: PreparedMessage): String {
105+
client.publish(envelopes = prepared.envelopes)
106+
return prepared.messageId
106107
}
107108

108109
fun <T> prepareMessage(content: T, options: SendOptions?): PreparedMessage {
@@ -147,36 +148,28 @@ data class ConversationV1(
147148

148149
val isEphemeral: Boolean = options != null && options.ephemeral
149150

150-
val messageEnvelope =
151+
val env =
151152
EnvelopeBuilder.buildFromString(
152153
topic = if (isEphemeral) ephemeralTopic else topic.description,
153154
timestamp = date,
154155
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
155156
)
156-
return PreparedMessage(
157-
messageEnvelope = messageEnvelope,
158-
conversation = Conversation.V1(this)
159-
) {
160-
val envelopes = mutableListOf(messageEnvelope)
161-
if (client.contacts.needsIntroduction(peerAddress) && !isEphemeral) {
162-
envelopes.addAll(
163-
listOf(
164-
EnvelopeBuilder.buildFromTopic(
165-
topic = Topic.userIntro(peerAddress),
166-
timestamp = date,
167-
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
168-
),
169-
EnvelopeBuilder.buildFromTopic(
170-
topic = Topic.userIntro(client.address),
171-
timestamp = date,
172-
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
173-
)
174-
)
157+
158+
val envelopes = mutableListOf(env)
159+
if (client.contacts.needsIntroduction(peerAddress) && !isEphemeral) {
160+
envelopes.addAll(
161+
listOf(
162+
env.toBuilder().apply {
163+
contentTopic = Topic.userIntro(peerAddress).description
164+
}.build(),
165+
env.toBuilder().apply {
166+
contentTopic = Topic.userIntro(client.address).description
167+
}.build(),
175168
)
176-
client.contacts.hasIntroduced[peerAddress] = true
177-
}
178-
client.publish(envelopes = envelopes)
169+
)
170+
client.contacts.hasIntroduced[peerAddress] = true
179171
}
172+
return PreparedMessage(envelopes)
180173
}
181174

182175
private fun generateId(envelope: Envelope): String =

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

+9-9
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,22 @@ data class ConversationV2(
100100

101101
fun <T> send(content: T, options: SendOptions? = null): String {
102102
val preparedMessage = prepareMessage(content = content, options = options)
103-
preparedMessage.send()
104-
return preparedMessage.messageId
103+
return send(preparedMessage)
105104
}
106105

107106
fun send(text: String, options: SendOptions? = null, sentAt: Date? = null): String {
108107
val preparedMessage = prepareMessage(content = text, options = options)
109-
preparedMessage.send()
110-
return preparedMessage.messageId
108+
return send(preparedMessage)
111109
}
112110

113111
fun send(encodedContent: EncodedContent, options: SendOptions?): String {
114112
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
115-
preparedMessage.send()
116-
return preparedMessage.messageId
113+
return send(preparedMessage)
114+
}
115+
116+
fun send(prepared: PreparedMessage): String {
117+
client.publish(envelopes = prepared.envelopes)
118+
return prepared.messageId
117119
}
118120

119121
fun <Codec : ContentCodec<T>, T> encode(codec: Codec, content: T): ByteArray {
@@ -170,9 +172,7 @@ data class ConversationV2(
170172
timestamp = Date(),
171173
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray()
172174
)
173-
return PreparedMessage(messageEnvelope = envelope, conversation = Conversation.V2(this)) {
174-
client.publish(envelopes = listOf(envelope))
175-
}
175+
return PreparedMessage(listOf(envelope))
176176
}
177177

178178
private fun generateId(envelope: Envelope): String =

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

+26-9
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,37 @@ package org.xmtp.android.library
22

33
import org.web3j.crypto.Hash
44
import org.xmtp.android.library.messages.Envelope
5+
import org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishRequest
56

7+
// This houses a fully prepared message that can be published
8+
// as soon as the API client has connectivity.
9+
//
10+
// To support persistence layers that queue pending messages (e.g. while offline)
11+
// this struct supports serializing to/from bytes that can be written to disk or elsewhere.
12+
// See toSerializedData() and fromSerializedData()
613
data class PreparedMessage(
7-
var messageEnvelope: Envelope,
8-
var conversation: Conversation,
9-
var onSend: () -> Unit,
14+
// The first envelope should send the message to the conversation itself.
15+
// Any more are for required intros/invites etc.
16+
// A client can just publish these when it has connectivity.
17+
val envelopes: List<Envelope>
1018
) {
19+
companion object {
20+
fun fromSerializedData(data: ByteArray): PreparedMessage {
21+
val req = PublishRequest.parseFrom(data)
22+
return PreparedMessage(req.envelopesList)
23+
}
24+
}
1125

12-
fun decodedMessage(): DecodedMessage =
13-
conversation.decode(messageEnvelope)
14-
15-
fun send() {
16-
onSend()
26+
fun toSerializedData(): ByteArray {
27+
val req = PublishRequest.newBuilder()
28+
.addAllEnvelopes(envelopes)
29+
.build()
30+
return req.toByteArray()
1731
}
1832

1933
val messageId: String
20-
get() = Hash.sha256(messageEnvelope.message.toByteArray()).toHex()
34+
get() = Hash.sha256(envelopes.first().message.toByteArray()).toHex()
35+
36+
val conversationTopic: String
37+
get() = envelopes.first().contentTopic
2138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.xmtp.android.library
2+
3+
import com.google.protobuf.kotlin.toByteStringUtf8
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Test
6+
import org.xmtp.android.library.messages.Envelope
7+
8+
class PreparedMessageTest {
9+
10+
@Test
11+
fun testSerializing() {
12+
val original = PreparedMessage(
13+
listOf(
14+
Envelope.newBuilder().apply {
15+
contentTopic = "topic1"
16+
timestampNs = 1234
17+
message = "abc123".toByteStringUtf8()
18+
}.build(),
19+
Envelope.newBuilder().apply {
20+
contentTopic = "topic2"
21+
timestampNs = 5678
22+
message = "def456".toByteStringUtf8()
23+
}.build(),
24+
)
25+
)
26+
val serialized = original.toSerializedData()
27+
val unserialized = PreparedMessage.fromSerializedData(serialized)
28+
assertEquals(original, unserialized)
29+
}
30+
}

0 commit comments

Comments
 (0)