Skip to content

Commit 1183b26

Browse files
authored
Ephemeral topics (#105)
* add api client with grpc kotlin * add Ephemeral topic code to match swift * add tests for it * remove the flaky part of the test
1 parent 5cfaa52 commit 1183b26

File tree

5 files changed

+102
-16
lines changed

5 files changed

+102
-16
lines changed

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

+44
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,48 @@ class LocalInstrumentedTest {
345345
private fun delayToPropagate() {
346346
Thread.sleep(500)
347347
}
348+
349+
@Test
350+
fun testStreamEphemeralInV1Conversation() {
351+
val bob = PrivateKeyBuilder()
352+
val alice = PrivateKeyBuilder()
353+
val clientOptions =
354+
ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false))
355+
val bobClient = Client().create(bob, clientOptions)
356+
val aliceClient = Client().create(account = alice, options = clientOptions)
357+
aliceClient.publishUserContact(legacy = true)
358+
bobClient.publishUserContact(legacy = true)
359+
val convo = ConversationV1(client = bobClient, peerAddress = alice.address, sentAt = Date())
360+
convo.streamEphemeral().mapLatest {
361+
assertEquals("hi", it.message.toStringUtf8())
362+
}
363+
convo.send(content = "hi", options = SendOptions(ephemeral = true))
364+
val messages = convo.messages()
365+
assertEquals(0, messages.size)
366+
}
367+
368+
@Test
369+
fun testStreamEphemeralInV2Conversation() {
370+
val bob = PrivateKeyBuilder()
371+
val alice = PrivateKeyBuilder()
372+
val clientOptions =
373+
ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false))
374+
val bobClient = Client().create(bob, clientOptions)
375+
val aliceClient = Client().create(account = alice, options = clientOptions)
376+
val aliceConversation = aliceClient.conversations.newConversation(
377+
bob.address,
378+
context = InvitationV1ContextBuilder.buildFromConversation("https://example.com/3")
379+
)
380+
val bobConversation = bobClient.conversations.newConversation(
381+
alice.address,
382+
context = InvitationV1ContextBuilder.buildFromConversation("https://example.com/3")
383+
)
384+
385+
bobConversation.streamEphemeral().mapLatest {
386+
assertEquals("hi", it.message.toStringUtf8())
387+
}
388+
aliceConversation.send(content = "hi", options = SendOptions(ephemeral = true))
389+
val messages = aliceConversation.messages()
390+
assertEquals(0, messages.size)
391+
}
348392
}

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

+16-3
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,18 @@ sealed class Conversation {
127127
}
128128
}
129129

130-
fun send(encodedContent: EncodedContent): String {
130+
fun send(encodedContent: EncodedContent, options: SendOptions? = null): String {
131131
return when (this) {
132-
is V1 -> conversationV1.send(encodedContent = encodedContent)
133-
is V2 -> conversationV2.send(encodedContent = encodedContent)
132+
is V1 -> conversationV1.send(encodedContent = encodedContent, options = options)
133+
is V2 -> conversationV2.send(encodedContent = encodedContent, options = options)
134134
}
135135
}
136+
137+
val clientAddress: String
138+
get() {
139+
return client.address
140+
}
141+
136142
val topic: String
137143
get() {
138144
return when (this) {
@@ -176,4 +182,11 @@ sealed class Conversation {
176182
is V2 -> conversationV2.streamMessages()
177183
}
178184
}
185+
186+
fun streamEphemeral(): Flow<Envelope> {
187+
return when (this) {
188+
is V1 -> return conversationV1.streamEphemeral()
189+
is V2 -> return conversationV2.streamEphemeral()
190+
}
191+
}
179192
}

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

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

33
import android.util.Log
44
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.collect
56
import kotlinx.coroutines.flow.flow
67
import kotlinx.coroutines.runBlocking
78
import org.web3j.crypto.Hash
@@ -97,8 +98,8 @@ data class ConversationV1(
9798
return preparedMessage.messageId
9899
}
99100

100-
fun send(encodedContent: EncodedContent): String {
101-
val preparedMessage = prepareMessage(encodedContent = encodedContent)
101+
fun send(encodedContent: EncodedContent, options: SendOptions? = null): String {
102+
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
102103
preparedMessage.send()
103104
return preparedMessage.messageId
104105
}
@@ -123,10 +124,13 @@ data class ConversationV1(
123124
if (compression != null) {
124125
encoded = encoded.compress(compression)
125126
}
126-
return prepareMessage(encodedContent = encoded)
127+
return prepareMessage(encodedContent = encoded, options = options)
127128
}
128129

129-
fun prepareMessage(encodedContent: EncodedContent): PreparedMessage {
130+
fun prepareMessage(
131+
encodedContent: EncodedContent,
132+
options: SendOptions? = null,
133+
): PreparedMessage {
130134
val contact = client.contacts.find(peerAddress) ?: throw XMTPException("address not found")
131135
val recipient = contact.toPublicKeyBundle()
132136
if (!recipient.identityKey.hasSignature()) {
@@ -139,9 +143,12 @@ data class ConversationV1(
139143
message = encodedContent.toByteArray(),
140144
timestamp = date
141145
)
146+
147+
val isEphemeral: Boolean = options != null && options.ephemeral
148+
142149
val messageEnvelope =
143-
EnvelopeBuilder.buildFromTopic(
144-
topic = Topic.directMessageV1(client.address, peerAddress),
150+
EnvelopeBuilder.buildFromString(
151+
topic = if (isEphemeral) ephemeralTopic else topic.description,
145152
timestamp = date,
146153
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
147154
)
@@ -150,7 +157,7 @@ data class ConversationV1(
150157
conversation = Conversation.V1(this)
151158
) {
152159
val envelopes = mutableListOf(messageEnvelope)
153-
if (client.contacts.needsIntroduction(peerAddress)) {
160+
if (client.contacts.needsIntroduction(peerAddress) && !isEphemeral) {
154161
envelopes.addAll(
155162
listOf(
156163
EnvelopeBuilder.buildFromTopic(
@@ -173,4 +180,13 @@ data class ConversationV1(
173180

174181
private fun generateId(envelope: Envelope): String =
175182
Hash.sha256(envelope.message.toByteArray()).toHex()
183+
184+
val ephemeralTopic: String
185+
get() = topic.description.replace("/xmtp/0/dm-", "/xmtp/0/dmE-")
186+
187+
fun streamEphemeral(): Flow<Envelope> = flow {
188+
client.subscribe(topics = listOf(ephemeralTopic)).collect {
189+
emit(it)
190+
}
191+
}
176192
}

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

+17-5
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ data class ConversationV2(
113113
return preparedMessage.messageId
114114
}
115115

116-
fun send(encodedContent: EncodedContent): String {
117-
val preparedMessage = prepareMessage(encodedContent = encodedContent)
116+
fun send(encodedContent: EncodedContent, options: SendOptions?): String {
117+
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
118118
preparedMessage.send()
119119
return preparedMessage.messageId
120120
}
@@ -155,18 +155,21 @@ data class ConversationV2(
155155
if (compression != null) {
156156
encoded = encoded.compress(compression)
157157
}
158-
return prepareMessage(encoded)
158+
return prepareMessage(encoded, options = options)
159159
}
160160

161-
fun prepareMessage(encodedContent: EncodedContent): PreparedMessage {
161+
fun prepareMessage(encodedContent: EncodedContent, options: SendOptions?): PreparedMessage {
162162
val message = MessageV2Builder.buildEncode(
163163
client = client,
164164
encodedContent = encodedContent,
165165
topic = topic,
166166
keyMaterial = keyMaterial
167167
)
168+
169+
val newTopic = if (options?.ephemeral == true) ephemeralTopic else topic
170+
168171
val envelope = EnvelopeBuilder.buildFromString(
169-
topic = topic,
172+
topic = newTopic,
170173
timestamp = Date(),
171174
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray()
172175
)
@@ -177,4 +180,13 @@ data class ConversationV2(
177180

178181
private fun generateId(envelope: Envelope): String =
179182
Hash.sha256(envelope.message.toByteArray()).toHex()
183+
184+
val ephemeralTopic: String
185+
get() = topic.replace("/xmtp/0/m", "/xmtp/0/mE")
186+
187+
fun streamEphemeral(): Flow<Envelope> = flow {
188+
client.subscribe(topics = listOf(ephemeralTopic)).collect {
189+
emit(it)
190+
}
191+
}
180192
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ import org.xmtp.proto.message.contents.Content
55
data class SendOptions(
66
var compression: EncodedContentCompression? = null,
77
var contentType: Content.ContentTypeId? = null,
8-
var contentFallback: String? = null
8+
var contentFallback: String? = null,
9+
var ephemeral: Boolean = false
910
)

0 commit comments

Comments
 (0)