Skip to content

Commit b74f02f

Browse files
committed
implement all the conversation functionality
1 parent 5b676a5 commit b74f02f

File tree

6 files changed

+127
-37
lines changed

6 files changed

+127
-37
lines changed

example/src/main/java/org/xmtp/android/example/account/WalletConnectV2Account.kt

+6-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ data class WalletConnectV2Account(
1818
private val sendSessionRequestDeepLink: (Uri) -> Unit,
1919
) :
2020
SigningKey {
21+
override val address: String
22+
get() = Keys.toChecksumAddress(
23+
session.namespaces.getValue(chain).accounts[0].substringAfterLast(
24+
":"
25+
)
26+
)
2127

2228
override suspend fun sign(data: ByteArray): SignatureOuterClass.Signature? {
2329
return signLegacy(String(data))
@@ -69,12 +75,4 @@ data class WalletConnectV2Account(
6975

7076
return null
7177
}
72-
73-
override fun getAddress(): String {
74-
return Keys.toChecksumAddress(
75-
session.namespaces.getValue(chain).accounts[0].substringAfterLast(
76-
":"
77-
)
78-
)
79-
}
8078
}

example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic
9090
val wallet = PrivateKeyBuilder()
9191
val client = Client().create(wallet, ClientManager.clientOptions(getApplication()))
9292
_uiState.value = ConnectUiState.Success(
93-
wallet.getAddress(),
93+
wallet.address,
9494
PrivateKeyBundleV1Builder.encodeData(client.privateKeyBundleV1)
9595
)
9696
} catch (e: XMTPException) {
@@ -110,7 +110,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic
110110
val wallet = PrivateKeyBuilder(privateKey)
111111
val client = Client().create(wallet, ClientManager.clientOptions(getApplication()))
112112
_uiState.value = ConnectUiState.Success(
113-
wallet.getAddress(),
113+
wallet.address,
114114
PrivateKeyBundleV1Builder.encodeData(client.privateKeyBundleV1)
115115
)
116116
} catch (e: XMTPException) {
@@ -134,7 +134,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic
134134
}
135135
val client = Client().create(wallet, ClientManager.clientOptions(getApplication()))
136136
_uiState.value = ConnectUiState.Success(
137-
wallet.getAddress(),
137+
wallet.address,
138138
PrivateKeyBundleV1Builder.encodeData(client.privateKeyBundleV1)
139139
)
140140
} catch (e: Exception) {

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ class GroupTest {
3131
fun setUp() {
3232
val context = InstrumentationRegistry.getInstrumentation().targetContext
3333
fixtures =
34-
fixtures(clientOptions = ClientOptions(enableAlphaMls = true, appContext = context))
34+
fixtures(
35+
clientOptions = ClientOptions(
36+
ClientOptions.Api(XMTPEnvironment.LOCAL, false),
37+
enableAlphaMls = true,
38+
appContext = context
39+
)
40+
)
3541
alixWallet = fixtures.aliceAccount
3642
alix = fixtures.alice
3743
boWallet = fixtures.bobAccount
@@ -88,11 +94,11 @@ class GroupTest {
8894
group.send("howdy")
8995
group.send("gm")
9096
runBlocking { group.sync() }
91-
assertEquals(group.messages().last().body, "howdy")
97+
assertEquals(group.messages().first().body, "howdy")
9298
assertEquals(group.messages().size, 2)
9399

94100
val sameGroup = alixClient.conversations.listGroups().last()
95101
assertEquals(sameGroup.messages().size, 2)
96-
assertEquals(sameGroup.messages().last().body, "howdy")
102+
assertEquals(sameGroup.messages().first().body, "howdy")
97103
}
98104
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ data class Contacts(
188188
val contactBundle = ContactBundleBuilder.buildFromEnvelope(envelope)
189189
knownBundles[peerAddress] = contactBundle
190190
val address = contactBundle.walletAddress
191-
if (address == peerAddress) {
191+
if (address?.lowercase() == peerAddress.lowercase()) {
192192
return contactBundle
193193
}
194194
}

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

+34-17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.google.protobuf.kotlin.toByteString
55
import kotlinx.coroutines.flow.Flow
66
import kotlinx.coroutines.flow.flowOf
77
import org.xmtp.android.library.codecs.EncodedContent
8+
import org.xmtp.android.library.libxmtp.Message
89
import org.xmtp.android.library.messages.Envelope
910
import org.xmtp.android.library.messages.PagingInfoSortDirection
1011
import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData
@@ -59,6 +60,16 @@ sealed class Conversation {
5960
}
6061
}
6162

63+
val peerAddresses: List<String>
64+
get() {
65+
return when (this) {
66+
is V1 -> listOf(conversationV1.peerAddress)
67+
is V2 -> listOf(conversationV2.peerAddress)
68+
is Group -> group.memberAddresses()
69+
}
70+
}
71+
72+
6273
// This distinctly identifies between two addresses.
6374
// Note: this will be empty for older v1 conversations.
6475
val conversationId: String?
@@ -80,12 +91,11 @@ sealed class Conversation {
8091
}
8192

8293
fun consentState(): ConsentState {
83-
val client: Client = when (this) {
84-
is V1 -> conversationV1.client
85-
is V2 -> conversationV2.client
86-
is Group -> group.client
94+
return when (this) {
95+
is V1 -> conversationV1.client.contacts.consentList.state(address = peerAddress)
96+
is V2 -> conversationV2.client.contacts.consentList.state(address = peerAddress)
97+
is Group -> ConsentState.UNKNOWN // No such thing as consent for a group
8798
}
88-
return client.contacts.consentList.state(address = peerAddress)
8999
}
90100

91101
/**
@@ -109,15 +119,15 @@ sealed class Conversation {
109119
),
110120
).build()
111121

112-
is Group -> TODO()
122+
is Group -> throw XMTPException("Groups do not support topics")
113123
}
114124
}
115125

116-
fun decode(envelope: Envelope): DecodedMessage {
126+
fun decode(envelope: Envelope, message: Message? = null): DecodedMessage {
117127
return when (this) {
118128
is V1 -> conversationV1.decode(envelope)
119129
is V2 -> conversationV2.decodeEnvelope(envelope)
120-
is Group -> TODO()
130+
is Group -> message?.decode() ?: throw XMTPException("Groups require message be passed")
121131
}
122132
}
123133

@@ -140,7 +150,7 @@ sealed class Conversation {
140150
conversationV2.prepareMessage(content = content, options = options)
141151
}
142152

143-
is Group -> TODO()
153+
is Group -> throw XMTPException("Groups do not support prepared messages") // We return a encoded content not a preparedmessage which requires a envelope
144154
}
145155
}
146156

@@ -157,23 +167,23 @@ sealed class Conversation {
157167
conversationV2.prepareMessage(encodedContent = encodedContent, options = options)
158168
}
159169

160-
is Group -> TODO()
170+
is Group -> throw XMTPException("Groups do not support prepared messages") // We return a encoded content not a preparedmessage which requires a envelope
161171
}
162172
}
163173

164174
fun send(prepared: PreparedMessage): String {
165175
return when (this) {
166176
is V1 -> conversationV1.send(prepared = prepared)
167177
is V2 -> conversationV2.send(prepared = prepared)
168-
is Group -> TODO()
178+
is Group -> throw XMTPException("Groups do not support prepared messages") // We return a encoded content not a prepared Message which requires a envelope
169179
}
170180
}
171181

172182
fun <T> send(content: T, options: SendOptions? = null): String {
173183
return when (this) {
174184
is V1 -> conversationV1.send(content = content, options = options)
175185
is V2 -> conversationV2.send(content = content, options = options)
176-
is Group -> TODO()
186+
is Group -> group.send(content = content, options = options)
177187
}
178188
}
179189

@@ -189,7 +199,7 @@ sealed class Conversation {
189199
return when (this) {
190200
is V1 -> conversationV1.send(encodedContent = encodedContent, options = options)
191201
is V2 -> conversationV2.send(encodedContent = encodedContent, options = options)
192-
is Group -> TODO()
202+
is Group -> group.send(encodedContent = encodedContent)
193203
}
194204
}
195205

@@ -258,17 +268,24 @@ sealed class Conversation {
258268
return when (this) {
259269
is V1 -> conversationV1.decryptedMessages(limit, before, after, direction)
260270
is V2 -> conversationV2.decryptedMessages(limit, before, after, direction)
261-
is Group -> TODO()
271+
is Group -> group.decryptedMessages(limit, before, after, direction)
262272
}
263273
}
264274

265275
fun decrypt(
266276
envelope: Envelope,
277+
message: Message? = null,
267278
): DecryptedMessage {
268279
return when (this) {
269280
is V1 -> conversationV1.decrypt(envelope)
270281
is V2 -> conversationV2.decrypt(envelope)
271-
is Group -> TODO()
282+
is Group -> {
283+
if (message == null) {
284+
throw XMTPException("Groups require message be passed")
285+
} else {
286+
group.decrypt(message)
287+
}
288+
}
272289
}
273290
}
274291

@@ -298,15 +315,15 @@ sealed class Conversation {
298315
return when (this) {
299316
is V1 -> conversationV1.streamDecryptedMessages()
300317
is V2 -> conversationV2.streamDecryptedMessages()
301-
is Group -> TODO()
318+
is Group -> group.streamDecryptedMessages()
302319
}
303320
}
304321

305322
fun streamEphemeral(): Flow<Envelope> {
306323
return when (this) {
307324
is V1 -> return conversationV1.streamEphemeral()
308325
is V2 -> return conversationV2.streamEphemeral()
309-
is Group -> TODO()
326+
is Group -> throw XMTPException("Groups do not support ephemeral messages")
310327
}
311328
}
312329
}

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

+74-5
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@ import org.xmtp.android.library.codecs.EncodedContent
1010
import org.xmtp.android.library.codecs.compress
1111
import org.xmtp.android.library.libxmtp.Message
1212
import org.xmtp.android.library.libxmtp.MessageEmitter
13+
import org.xmtp.android.library.messages.DecryptedMessage
14+
import org.xmtp.android.library.messages.EnvelopeBuilder
15+
import org.xmtp.android.library.messages.MessageBuilder
16+
import org.xmtp.android.library.messages.MessageV2Builder
17+
import org.xmtp.android.library.messages.Pagination
18+
import org.xmtp.android.library.messages.PagingInfoSortDirection
19+
import org.xmtp.proto.message.api.v1.MessageApiOuterClass
1320
import uniffi.xmtpv3.FfiGroup
1421
import uniffi.xmtpv3.FfiListMessagesOptions
22+
import uniffi.xmtpv3.FfiMessage
23+
import java.security.SecureRandom
1524
import java.util.Date
25+
import kotlin.time.Duration.Companion.nanoseconds
26+
import kotlin.time.DurationUnit
1627

1728
class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
1829
val id: ByteArray
@@ -22,10 +33,17 @@ class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
2233
get() = Date(libXMTPGroup.createdAtNs() / 1_000_000)
2334

2435
fun send(text: String): String {
36+
return send(prepareMessage(content = text, options = null))
37+
}
38+
39+
fun <T> send(content: T, options: SendOptions? = null): String {
40+
val preparedMessage = prepareMessage(content = content, options = options)
41+
return send(preparedMessage)
42+
}
43+
44+
fun send(encodedContent: EncodedContent): String {
2545
runBlocking {
26-
libXMTPGroup.send(
27-
contentBytes = prepareMessage(content = text, options = null).toByteArray()
28-
)
46+
libXMTPGroup.send(contentBytes = encodedContent.toByteArray())
2947
}
3048
return id.toString()
3149
}
@@ -71,11 +89,44 @@ class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
7189
)
7290
).map {
7391
Message(client, it).decode()
74-
}.drop(1)
75-
.reversed()// The first message is something I can't decode because it's about adding members
92+
}.reversed()
7693
}
7794
}
7895

96+
fun decryptedMessages(
97+
limit: Int? = null,
98+
before: Date? = null,
99+
after: Date? = null,
100+
direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING,
101+
): List<DecryptedMessage> {
102+
return runBlocking {
103+
libXMTPGroup.sync()
104+
val messages = libXMTPGroup.findMessages(
105+
opts = FfiListMessagesOptions(
106+
sentBeforeNs = before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS),
107+
sentAfterNs = after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS),
108+
limit = limit?.toLong()
109+
)
110+
).map {
111+
decrypt(Message(client, it))
112+
}
113+
when (direction) {
114+
MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING -> messages
115+
else -> messages.reversed()
116+
}
117+
}
118+
}
119+
120+
fun decrypt(message: Message): DecryptedMessage {
121+
return DecryptedMessage(
122+
id = message.id.toHex(),
123+
topic = message.id.toHex(),
124+
encodedContent = message.decode().encodedContent,
125+
senderAddress = message.senderAddress,
126+
sentAt = Date()
127+
)
128+
}
129+
79130
fun streamMessages(): Flow<DecodedMessage> = flow {
80131
val messageEmitter = MessageEmitter()
81132

@@ -90,10 +141,28 @@ class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
90141
libXMTPGroup.stream(messageEmitter.callback)
91142
}
92143

144+
fun streamDecryptedMessages(): Flow<DecryptedMessage> = flow {
145+
val messageEmitter = MessageEmitter()
146+
147+
coroutineScope {
148+
launch {
149+
messageEmitter.messages.collect { message ->
150+
emit(decrypt(Message(client, message)))
151+
}
152+
}
153+
}
154+
155+
libXMTPGroup.stream(messageEmitter.callback)
156+
}
157+
93158
fun addMembers(addresses: List<String>) {
94159
runBlocking { libXMTPGroup.addMembers(addresses) }
95160
}
96161

162+
fun removeMembers(addresses: List<String>) {
163+
runBlocking { libXMTPGroup.removeMembers(addresses) }
164+
}
165+
97166
fun memberAddresses(): List<String> {
98167
return runBlocking {
99168
libXMTPGroup.listMembers().map { it.accountAddress }

0 commit comments

Comments
 (0)