Skip to content

Commit 6c44637

Browse files
authored
Group Chat Push Notifications (#210)
* bump the libxmtp version to the latest * add the functions to group and convos * add topics for mls * bump to latest libxmtp again * correctly set the install ids * change all these places to the topic * update the notification service to handle groups and welcome messages * fix up linter issue * change the installation id work around * keep that stored * small rename * update the readme * update the bindings * add handling for different content types * didnt mean to commit those
1 parent 8a39578 commit 6c44637

File tree

16 files changed

+201
-48
lines changed

16 files changed

+201
-48
lines changed

example/src/main/java/org/xmtp/android/example/MainViewModel.kt

+12-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import org.xmtp.android.example.extension.stateFlow
2121
import org.xmtp.android.example.pushnotifications.PushNotificationTokenManager
2222
import org.xmtp.android.library.Conversation
2323
import org.xmtp.android.library.DecodedMessage
24+
import org.xmtp.android.library.messages.Topic
2425
import org.xmtp.android.library.push.Service
2526

2627
class MainViewModel : ViewModel() {
@@ -46,7 +47,7 @@ class MainViewModel : ViewModel() {
4647
try {
4748
val conversations = ClientManager.client.conversations.list(includeGroups = true)
4849
val hmacKeysResult = ClientManager.client.conversations.getHmacKeys()
49-
val subscriptions = conversations.map {
50+
val subscriptions: MutableList<Service.Subscription> = conversations.map {
5051
val hmacKeys = hmacKeysResult.hmacKeysMap
5152
val result = hmacKeys[it.topic]?.valuesList?.map { hmacKey ->
5253
Service.Subscription.HmacKey.newBuilder().also { sub_key ->
@@ -56,11 +57,19 @@ class MainViewModel : ViewModel() {
5657
}
5758

5859
Service.Subscription.newBuilder().also { sub ->
59-
sub.addAllHmacKeys(result)
60+
if (!result.isNullOrEmpty()) {
61+
sub.addAllHmacKeys(result)
62+
}
6063
sub.topic = it.topic
6164
sub.isSilent = it.version == Conversation.Version.V1
6265
}.build()
63-
}
66+
}.toMutableList()
67+
68+
val welcomeTopic = Service.Subscription.newBuilder().also { sub ->
69+
sub.topic = Topic.userWelcome(ClientManager.client.installationId).description
70+
sub.isSilent = false
71+
}.build()
72+
subscriptions.add(welcomeTopic)
6473

6574
PushNotificationTokenManager.xmtpPush.subscribeWithMetadata(subscriptions)
6675
listItems.addAll(

example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt

+71-30
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import org.xmtp.android.example.R
2020
import org.xmtp.android.example.conversation.ConversationDetailActivity
2121
import org.xmtp.android.example.extension.truncatedAddress
2222
import org.xmtp.android.example.utils.KeyUtil
23+
import org.xmtp.android.library.Conversation
2324
import org.xmtp.android.library.messages.EnvelopeBuilder
25+
import org.xmtp.android.library.messages.Topic
26+
import uniffi.xmtpv3.org.xmtp.android.library.codecs.GroupMembershipChanges
2427
import java.util.Date
2528

2629
class PushNotificationsService : FirebaseMessagingService() {
@@ -57,40 +60,78 @@ class PushNotificationsService : FirebaseMessagingService() {
5760
GlobalScope.launch(Dispatchers.Main) {
5861
ClientManager.createClient(keysData, applicationContext)
5962
}
60-
val conversation =
61-
runBlocking { ClientManager.client.fetchConversation(topic, includeGroups = true) }
62-
if (conversation == null) {
63-
Log.e(TAG, "No keys or conversation persisted")
64-
return
65-
}
66-
val envelope = EnvelopeBuilder.buildFromString(topic, Date(), encryptedMessageData)
67-
val peerAddress = conversation.peerAddress
68-
val decodedMessage = conversation.decode(envelope)
63+
val welcomeTopic = Topic.userWelcome(ClientManager.client.installationId).description
64+
val builder = if (welcomeTopic == topic) {
65+
val group = ClientManager.client.conversations.fromWelcome(encryptedMessageData)
66+
val pendingIntent = PendingIntent.getActivity(
67+
this,
68+
0,
69+
ConversationDetailActivity.intent(
70+
this,
71+
topic = group.topic,
72+
peerAddress = Conversation.Group(group).peerAddress
73+
),
74+
(PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
75+
)
76+
77+
NotificationCompat.Builder(this, CHANNEL_ID)
78+
.setSmallIcon(R.drawable.ic_xmtp_white)
79+
.setContentTitle(Conversation.Group(group).peerAddress.truncatedAddress())
80+
.setContentText("New Group Chat")
81+
.setAutoCancel(true)
82+
.setColor(ContextCompat.getColor(this, R.color.black))
83+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
84+
.setStyle(NotificationCompat.BigTextStyle().bigText("New Group Chat"))
85+
.setContentIntent(pendingIntent)
86+
} else {
87+
val conversation =
88+
runBlocking { ClientManager.client.fetchConversation(topic, includeGroups = true) }
89+
if (conversation == null) {
90+
Log.e(TAG, topic)
91+
Log.e(TAG, "No keys or conversation persisted")
92+
return
93+
}
94+
val decodedMessage = if (conversation is Conversation.Group) {
95+
runBlocking { conversation.group.processMessage(encryptedMessageData).decode() }
96+
} else {
97+
val envelope = EnvelopeBuilder.buildFromString(topic, Date(), encryptedMessageData)
98+
conversation.decode(envelope)
99+
}
100+
val peerAddress = conversation.peerAddress
69101

70-
val body = decodedMessage.body
71-
val title = peerAddress.truncatedAddress()
102+
val body: String = if (decodedMessage.content<Any>() is String) {
103+
decodedMessage.body
104+
} else if (decodedMessage.content<Any>() is GroupMembershipChanges) {
105+
val changes = decodedMessage.content() as? GroupMembershipChanges
106+
"Membership Changed ${
107+
changes?.membersAddedList?.mapNotNull { it.accountAddress }.toString()
108+
}"
109+
} else {
110+
""
111+
}
112+
val title = peerAddress.truncatedAddress()
72113

73-
val pendingIntent = PendingIntent.getActivity(
74-
this,
75-
0,
76-
ConversationDetailActivity.intent(
114+
val pendingIntent = PendingIntent.getActivity(
77115
this,
78-
topic = topic,
79-
peerAddress = peerAddress
80-
),
81-
(PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
82-
)
83-
84-
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
85-
.setSmallIcon(R.drawable.ic_xmtp_white)
86-
.setContentTitle(title)
87-
.setContentText(body)
88-
.setAutoCancel(true)
89-
.setColor(ContextCompat.getColor(this, R.color.black))
90-
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
91-
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
92-
.setContentIntent(pendingIntent)
116+
0,
117+
ConversationDetailActivity.intent(
118+
this,
119+
topic = topic,
120+
peerAddress = peerAddress
121+
),
122+
(PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
123+
)
93124

125+
NotificationCompat.Builder(this, CHANNEL_ID)
126+
.setSmallIcon(R.drawable.ic_xmtp_white)
127+
.setContentTitle(title)
128+
.setContentText(body)
129+
.setAutoCancel(true)
130+
.setColor(ContextCompat.getColor(this, R.color.black))
131+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
132+
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
133+
.setContentIntent(pendingIntent)
134+
}
94135
// Use the URL as the ID for now until one is passed back from the server.
95136
NotificationManagerCompat.from(this).apply {
96137
if (ActivityCompat.checkSelfPermission(

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

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class ClientTest {
123123
)
124124
)
125125
assert(client.canMessageV3(listOf(client.address)))
126+
assert(client.installationId.isNotEmpty())
126127
}
127128

128129
@Test

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ class GroupTest {
494494
boClient.conversations.streamAll().test {
495495
val group =
496496
caroClient.conversations.newGroup(listOf(bo.walletAddress))
497-
assertEquals(group.id.toHex(), awaitItem().topic)
497+
assertEquals(group.topic, awaitItem().topic)
498498
val conversation =
499499
alixClient.conversations.newConversation(bo.walletAddress)
500500
assertEquals(conversation.topic, awaitItem().topic)

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

+10-5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import org.xmtp.android.library.messages.walletAddress
4343
import org.xmtp.proto.message.api.v1.MessageApiOuterClass
4444
import org.xmtp.proto.message.api.v1.MessageApiOuterClass.BatchQueryResponse
4545
import org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryRequest
46-
import uniffi.xmtpv3.FfiV2ApiClient
4746
import uniffi.xmtpv3.FfiXmtpClient
4847
import uniffi.xmtpv3.LegacyIdentitySource
4948
import uniffi.xmtpv3.createClient
@@ -87,9 +86,9 @@ class Client() {
8786
lateinit var conversations: Conversations
8887
var logger: XMTPLogger = XMTPLogger()
8988
val libXMTPVersion: String = getVersionInfo()
89+
var installationId: String = ""
9090
private var libXMTPClient: FfiXmtpClient? = null
9191
private var dbPath: String = ""
92-
private lateinit var v2RustClient: FfiV2ApiClient
9392

9493
companion object {
9594
private const val TAG = "Client"
@@ -166,6 +165,7 @@ class Client() {
166165
apiClient: ApiClient,
167166
libXMTPClient: FfiXmtpClient? = null,
168167
dbPath: String = "",
168+
installationId: String = "",
169169
) : this() {
170170
this.address = address
171171
this.privateKeyBundleV1 = privateKeyBundleV1
@@ -175,6 +175,7 @@ class Client() {
175175
this.conversations =
176176
Conversations(client = this, libXMTPConversations = libXMTPClient?.conversations())
177177
this.dbPath = dbPath
178+
this.installationId = installationId
178179
}
179180

180181
fun buildFrom(
@@ -207,7 +208,8 @@ class Client() {
207208
privateKeyBundleV1 = bundle,
208209
apiClient = apiClient,
209210
libXMTPClient = v3Client,
210-
dbPath = dbPath
211+
dbPath = dbPath,
212+
installationId = v3Client?.installationId()?.toHex() ?: ""
211213
)
212214
}
213215

@@ -257,7 +259,8 @@ class Client() {
257259
privateKeyBundleV1,
258260
apiClient,
259261
libXMTPClient,
260-
dbPath
262+
dbPath,
263+
libXMTPClient?.installationId()?.toHex() ?: ""
261264
)
262265
client.ensureUserContactPublished()
263266
client
@@ -304,7 +307,8 @@ class Client() {
304307
privateKeyBundleV1 = v1Bundle,
305308
apiClient = apiClient,
306309
libXMTPClient = v3Client,
307-
dbPath = dbPath
310+
dbPath = dbPath,
311+
installationId = v3Client?.installationId()?.toHex() ?: ""
308312
)
309313
}
310314

@@ -502,6 +506,7 @@ class Client() {
502506
suspend fun subscribe(topics: List<String>): Flow<Envelope> {
503507
return subscribe2(flowOf(makeSubscribeRequest(topics)))
504508
}
509+
505510
suspend fun subscribe2(request: Flow<MessageApiOuterClass.SubscribeRequest>): Flow<Envelope> {
506511
return apiClient.subscribe(request = request)
507512
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ sealed class Conversation {
212212
return when (this) {
213213
is V1 -> conversationV1.topic.description
214214
is V2 -> conversationV2.topic
215-
is Group -> group.id.toHex()
215+
is Group -> group.topic
216216
}
217217
}
218218

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

+6
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ data class Conversations(
9898
)
9999
}
100100

101+
fun fromWelcome(envelopeBytes: ByteArray): Group {
102+
val group = libXMTPConversations?.processStreamedWelcomeMessage(envelopeBytes)
103+
?: throw XMTPException("Client does not support Groups")
104+
return Group(client, group)
105+
}
106+
101107
suspend fun newGroup(
102108
accountAddresses: List<String>,
103109
permissions: GroupPermissions = GroupPermissions.EVERYONE_IS_ADMIN,

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

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.xmtp.android.library.codecs.compress
99
import org.xmtp.android.library.libxmtp.Message
1010
import org.xmtp.android.library.messages.DecryptedMessage
1111
import org.xmtp.android.library.messages.PagingInfoSortDirection
12+
import org.xmtp.android.library.messages.Topic
1213
import org.xmtp.proto.message.api.v1.MessageApiOuterClass
1314
import uniffi.xmtpv3.FfiGroup
1415
import uniffi.xmtpv3.FfiGroupMetadata
@@ -24,6 +25,9 @@ class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
2425
val id: ByteArray
2526
get() = libXMTPGroup.id()
2627

28+
val topic: String
29+
get() = Topic.groupMessage(id.toHex()).description
30+
2731
val createdAt: Date
2832
get() = Date(libXMTPGroup.createdAtNs() / 1_000_000)
2933

@@ -119,6 +123,11 @@ class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
119123
}
120124
}
121125

126+
suspend fun processMessage(envelopeBytes: ByteArray): Message {
127+
val message = libXMTPGroup.processStreamedGroupMessage(envelopeBytes)
128+
return Message(client, message)
129+
}
130+
122131
fun isActive(): Boolean {
123132
return libXMTPGroup.isActive()
124133
}

library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.xmtp.android.library.DecodedMessage
55
import org.xmtp.android.library.XMTPException
66
import org.xmtp.android.library.codecs.EncodedContent
77
import org.xmtp.android.library.messages.DecryptedMessage
8+
import org.xmtp.android.library.messages.Topic
89
import org.xmtp.android.library.toHex
910
import uniffi.xmtpv3.FfiMessage
1011
import java.util.Date
@@ -27,7 +28,7 @@ data class Message(val client: Client, private val libXMTPMessage: FfiMessage) {
2728
return DecodedMessage(
2829
id = id.toHex(),
2930
client = client,
30-
topic = id.toHex(),
31+
topic = Topic.groupMessage(convoId.toHex()).description,
3132
encodedContent = EncodedContent.parseFrom(libXMTPMessage.content),
3233
senderAddress = senderAddress,
3334
sent = sentAt
@@ -40,7 +41,7 @@ data class Message(val client: Client, private val libXMTPMessage: FfiMessage) {
4041
fun decrypt(): DecryptedMessage {
4142
return DecryptedMessage(
4243
id = id.toHex(),
43-
topic = convoId.toHex(),
44+
topic = Topic.groupMessage(convoId.toHex()).description,
4445
encodedContent = decode().encodedContent,
4546
senderAddress = senderAddress,
4647
sentAt = Date()

library/src/main/java/org/xmtp/android/library/messages/Topic.kt

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ sealed class Topic {
88
data class directMessageV1(val address1: String?, val address2: String?) : Topic()
99
data class directMessageV2(val addresses: String?) : Topic()
1010
data class preferenceList(val identifier: String?) : Topic()
11+
data class userWelcome(val installationId: String?) : Topic()
12+
data class groupMessage(val groupId: String?) : Topic()
1113

1214
/**
1315
* Getting the [Topic] structured depending if is [userPrivateStoreKeyBundle], [contact],
@@ -29,10 +31,13 @@ sealed class Topic {
2931

3032
is directMessageV2 -> wrap("m-$addresses")
3133
is preferenceList -> wrap("userpreferences-$identifier")
34+
is groupMessage -> wrapMls("g-$groupId")
35+
is userWelcome -> wrapMls("w-$installationId")
3236
}
3337
}
3438

3539
private fun wrap(value: String): String = "/xmtp/0/$value/proto"
40+
private fun wrapMls(value: String): String = "/xmtp/mls/1/$value/proto"
3641

3742
companion object {
3843
/**

library/src/main/java/org/xmtp/android/library/push/README.md

+14-5
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ These files can serve as the basis for what you might want to provide for your o
8282
8383
```kotlin
8484
val hmacKeysResult = ClientManager.client.conversations.getHmacKeys()
85-
val subscriptions = conversations.map {
85+
val subscriptions: MutableList<Service.Subscription> = conversations.map {
8686
val hmacKeys = hmacKeysResult.hmacKeysMap
8787
val result = hmacKeys[it.topic]?.valuesList?.map { hmacKey ->
8888
Service.Subscription.HmacKey.newBuilder().also { sub_key ->
@@ -96,11 +96,20 @@ These files can serve as the basis for what you might want to provide for your o
9696
sub.topic = it.topic
9797
sub.isSilent = it.version == Conversation.Version.V1
9898
}.build()
99-
}
100-
101-
XMTPPush(context, "10.0.2.2:8080").subscribeWithMetadata(subscriptions)
102-
```
99+
}.toMutableList()
100+
101+
// To get pushes for New Group (WelcomeMessages)
102+
val welcomeTopic = Service.Subscription.newBuilder().also { sub ->
103+
sub.topic = Topic.userWelcome(ClientManager.client.installationId).description
104+
sub.isSilent = false
105+
}.build()
106+
subscriptions.add(welcomeTopic)
107+
108+
XMTPPush(context, "10.0.2.2:8080").subscribeWithMetadata(subscriptions)
109+
```
103110
104111
```kotlin
105112
XMTPPush(context, "10.0.2.2:8080").unsubscribe(conversations.map { it.topic })
106113
```
114+
115+
8. See example in [PushNotificationsService](https://github.com/xmtp/xmtp-android/blob/main/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt) for how to decrypt the different messages.

0 commit comments

Comments
 (0)