Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c8e1bc4
update android to latest libxmtp and fix compile errors
cameronvoell Jan 21, 2026
9d1c863
added stream message deletions on android and typescript side
cameronvoell Jan 21, 2026
9231aeb
add message deletion on android and typescript side
cameronvoell Jan 23, 2026
e060fca
adds noSend to prepareMessage and publishMessage in typescript and an…
cameronvoell Jan 23, 2026
875340c
ios code for delete, prepare, send, subscribeToDeletions
cameronvoell Jan 28, 2026
129543a
Adds DecodedMessageV2 and enrichedMessage functionality
cameronvoell Jan 28, 2026
decd9e0
Adds sendSyncRequest and delete message content type decoding
cameronvoell Jan 29, 2026
271c9d1
adds function comments to Conversation base class to help with IDE us…
cameronvoell Jan 29, 2026
68233db
adds comment about js decode limitation with enriched messages
cameronvoell Jan 30, 2026
7ee826d
fix json custom type handling on ios
cameronvoell Jan 30, 2026
a789e15
lint fix
cameronvoell Jan 30, 2026
e13af0a
prevent json serialization error in swift
cameronvoell Jan 30, 2026
df5dc25
removed uploadDebugInformation
cameronvoell Jan 30, 2026
2e00272
fix group permission test
cameronvoell Jan 30, 2026
cc2c241
fix reaction v2 on android
cameronvoell Jan 30, 2026
57d55a3
fix reaction v2 on ios
cameronvoell Jan 30, 2026
68d2712
fix delete message content type id
cameronvoell Jan 30, 2026
7eed895
adds deleteMessage to ContentJsonV2 in swift module code
cameronvoell Jan 30, 2026
6da1e24
adds defensive check against null content type field
cameronvoell Jan 30, 2026
1f6cb9d
update package.json version
cameronvoell Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:4.6.1"
implementation "org.xmtp:android:4.9.0"
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.facebook.react:react-native:0.71.3'
implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1"
Expand All @@ -109,5 +109,5 @@ dependencies {
// implementation 'org.web3j:crypto:4.9.4'
// implementation "net.java.dev.jna:jna:5.17.0@aar"
// api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
// api 'org.xmtp:proto-kotlin:3.72.4'
// api 'org.xmtp:proto-kotlin:3.88.0'
}
151 changes: 131 additions & 20 deletions android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.DisappearingMessageSettingsWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DmWrapper
import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.EnrichedMessageQueryParamsWrapper
import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper
import expo.modules.xmtpreactnativesdk.wrappers.InboxStateWrapper
import expo.modules.xmtpreactnativesdk.wrappers.KeyPackageStatusWrapper
Expand Down Expand Up @@ -152,6 +153,10 @@ fun Conversation.cacheKey(installationId: String): String {
return "${installationId}:${topic}"
}

fun getMessageDeletionsCacheKey(installationId: String): String {
return "${installationId}:messageDeletions"
}

class XMTPModule : Module() {
private val PREFS_NAME = "XMTPModulePrefs"
private val LOG_WRITER_ACTIVE_KEY = "logWriterActive"
Expand Down Expand Up @@ -238,7 +243,6 @@ class XMTPModule : Module() {
dbDirectory = authOptions.dbDirectory,
historySyncUrl = historySyncUrl,
deviceSyncEnabled = authOptions.deviceSyncEnabled,
debugEventsEnabled = authOptions.debugEventsEnabled,
forkRecoveryOptions = authOptions.forkRecoveryOptions
)
}
Expand Down Expand Up @@ -273,12 +277,14 @@ class XMTPModule : Module() {
"conversationMessage",
"consent",
"preferences",
"messageDeletion",
// Streams Closed
"conversationClosed",
"messageClosed",
"conversationMessageClosed",
"consentClosed",
"preferencesClosed",
"messageDeletionClosed"
)

Function("inboxId") { installationId: String ->
Expand Down Expand Up @@ -584,7 +590,7 @@ class XMTPModule : Module() {
logV("ffiRevokeAllOtherInstallationsSignatureText")
val client = clients[installationId] ?: throw XMTPException("No client")
val sigRequest = client.ffiRevokeAllOtherInstallations()
sigRequest.let {
sigRequest?.let {
clientSignatureRequests[installationId] = it
it.signatureText()
}
Expand Down Expand Up @@ -850,6 +856,11 @@ class XMTPModule : Module() {
subscriptions[getConsentKey(installationId)]?.cancel()
}

Function("unsubscribeFromMessageDeletions") { installationId: String ->
logV("unsubscribeFromMessageDeletions")
subscriptions[getMessageDeletionsCacheKey(installationId)]?.cancel()
}

AsyncFunction("getOrCreateInboxId") Coroutine { publicIdentity: String, environment: String ->
withContext(Dispatchers.IO) {
try {
Expand Down Expand Up @@ -1034,6 +1045,43 @@ class XMTPModule : Module() {
}
}

AsyncFunction("conversationEnrichedMessages") Coroutine { installationId: String, conversationId: String, queryParamsJson: String? ->
withContext(Dispatchers.IO) {
logV("conversationEnrichedMessages")
val client = clients[installationId] ?: throw XMTPException("No client")
val conversation = client.conversations.findConversation(conversationId)
val queryParams = EnrichedMessageQueryParamsWrapper.fromJson(queryParamsJson ?: "")

// Convert deliveryStatus string to enum
val deliveryStatus = when (queryParams.deliveryStatus?.uppercase()) {
"PUBLISHED" -> DecodedMessage.MessageDeliveryStatus.PUBLISHED
"UNPUBLISHED" -> DecodedMessage.MessageDeliveryStatus.UNPUBLISHED
"FAILED" -> DecodedMessage.MessageDeliveryStatus.FAILED
else -> DecodedMessage.MessageDeliveryStatus.ALL
}

// Convert sortBy string to enum
val sortBy = when (queryParams.sortBy?.uppercase()) {
"INSERTED_TIME" -> DecodedMessage.SortBy.INSERTED_TIME
else -> DecodedMessage.SortBy.SENT_TIME
}

conversation?.enrichedMessages(
limit = queryParams.limit,
beforeNs = queryParams.beforeNs,
afterNs = queryParams.afterNs,
direction = DecodedMessage.SortDirection.valueOf(
queryParams.direction ?: "DESCENDING"
),
deliveryStatus = deliveryStatus,
excludeSenderInboxIds = queryParams.excludeSenderInboxIds,
insertedAfterNs = queryParams.insertedAfterNs,
insertedBeforeNs = queryParams.insertedBeforeNs,
sortBy = sortBy
)?.map { MessageWrapper.encode(it) }
}
}

AsyncFunction("findMessage") Coroutine { installationId: String, messageId: String ->
withContext(Dispatchers.IO) {
logV("findMessage")
Expand Down Expand Up @@ -1135,6 +1183,16 @@ class XMTPModule : Module() {
}
}

AsyncFunction("publishMessage") Coroutine { installationId: String, id: String, messageId: String ->
withContext(Dispatchers.IO) {
logV("publishMessage")
val client = clients[installationId] ?: throw XMTPException("No client")
val conversation = client.conversations.findConversation(id)
?: throw XMTPException("no conversation found for $id")
conversation.publishMessage(messageId)
}
}

AsyncFunction("publishPreparedMessages") Coroutine { installationId: String, id: String ->
withContext(Dispatchers.IO) {
logV("publishPreparedMessages")
Expand All @@ -1145,7 +1203,7 @@ class XMTPModule : Module() {
}
}

AsyncFunction("prepareMessage") Coroutine { installationId: String, id: String, contentJson: String ->
AsyncFunction("prepareMessage") Coroutine { installationId: String, id: String, contentJson: String, noSend: Boolean ->
withContext(Dispatchers.IO) {
logV("prepareMessage")
val client = clients[installationId] ?: throw XMTPException("No client")
Expand All @@ -1154,12 +1212,13 @@ class XMTPModule : Module() {
val sending = ContentJson.fromJson(contentJson)
conversation.prepareMessage(
content = sending.content,
options = SendOptions(contentType = sending.type)
options = SendOptions(contentType = sending.type),
noSend = noSend
)
}
}

AsyncFunction("prepareEncodedMessage") Coroutine { installationId: String, conversationId: String, encodedContentData: List<Int>, shouldPush: Boolean ->
AsyncFunction("prepareEncodedMessage") Coroutine { installationId: String, conversationId: String, encodedContentData: List<Int>, shouldPush: Boolean, noSend: Boolean ->
withContext(Dispatchers.IO) {
logV("prepareEncodedMessage")
val client = clients[installationId] ?: throw XMTPException("No client")
Expand All @@ -1175,7 +1234,7 @@ class XMTPModule : Module() {
}
}
val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes)
conversation.prepareMessage(encodedContent = encodedContent, opts = MessageVisibilityOptions(shouldPush))
conversation.prepareMessage(encodedContent = encodedContent, opts = MessageVisibilityOptions(shouldPush), noSend = noSend)
}
}

Expand Down Expand Up @@ -1224,7 +1283,8 @@ class XMTPModule : Module() {
createGroupParams.groupName,
createGroupParams.groupImageUrl,
createGroupParams.groupDescription,
createGroupParams.disappearingMessageSettings
createGroupParams.disappearingMessageSettings,
createGroupParams.appData
)
GroupWrapper.encode(client, group)
}
Expand All @@ -1246,7 +1306,8 @@ class XMTPModule : Module() {
createGroupParams.groupName,
createGroupParams.groupImageUrl,
createGroupParams.groupDescription,
createGroupParams.disappearingMessageSettings
createGroupParams.disappearingMessageSettings,
createGroupParams.appData
)
GroupWrapper.encode(client, group)
}
Expand All @@ -1270,7 +1331,8 @@ class XMTPModule : Module() {
createGroupParams.groupName,
createGroupParams.groupImageUrl,
createGroupParams.groupDescription,
createGroupParams.disappearingMessageSettings
createGroupParams.disappearingMessageSettings,
createGroupParams.appData
)
GroupWrapper.encode(client, group)
}
Expand All @@ -1294,7 +1356,8 @@ class XMTPModule : Module() {
createGroupParams.groupName,
createGroupParams.groupImageUrl,
createGroupParams.groupDescription,
createGroupParams.disappearingMessageSettings
createGroupParams.disappearingMessageSettings,
createGroupParams.appData
)
GroupWrapper.encode(client, group)
}
Expand All @@ -1315,7 +1378,8 @@ class XMTPModule : Module() {
createGroupParams.groupName,
createGroupParams.groupImageUrl,
createGroupParams.groupDescription,
createGroupParams.disappearingMessageSettings
createGroupParams.disappearingMessageSettings,
createGroupParams.appData
)
GroupWrapper.encode(client, group)
}
Expand Down Expand Up @@ -1360,6 +1424,14 @@ class XMTPModule : Module() {
}
}

AsyncFunction("sendSyncRequest") Coroutine { installationId: String ->
withContext(Dispatchers.IO) {
logV("sendSyncRequest")
val client = clients[installationId] ?: throw XMTPException("No client")
client.sendSyncRequest()
}
}

AsyncFunction("syncAllConversations") Coroutine { installationId: String, consentStringStates: List<String>? ->
withContext(Dispatchers.IO) {
logV("syncAllConversations")
Expand Down Expand Up @@ -1876,6 +1948,16 @@ class XMTPModule : Module() {
}
}

AsyncFunction("deleteMessage") Coroutine { clientInstallationId: String, conversationId: String, messageId: String ->
withContext(Dispatchers.IO) {
logV("deleteMessage")
val client = clients[clientInstallationId] ?: throw XMTPException("No client")
val conversation = client.conversations.findConversation(conversationId)
?: throw XMTPException("no conversation found for $conversationId")
conversation.deleteMessage(messageId)
}
}

Function("subscribeToPreferenceUpdates") { installationId: String ->
logV("subscribeToPreferenceUpdates")

Expand Down Expand Up @@ -1915,6 +1997,15 @@ class XMTPModule : Module() {
}
}

AsyncFunction("subscribeToMessageDeletions") Coroutine { installationId: String ->
withContext(Dispatchers.IO) {
logV("subscribeToMessageDeletions")
subscribeToMessageDeletions(
installationId = installationId,
)
}
}

Function("unsubscribeFromPreferenceUpdates") { installationId: String ->
logV("unsubscribeFromPreferenceUpdates")
subscriptions[getPreferenceUpdatesKey(installationId)]?.cancel()
Expand Down Expand Up @@ -2020,15 +2111,6 @@ class XMTPModule : Module() {
}
}

AsyncFunction("uploadDebugInformation") Coroutine { installationId: String, serverUrl: String? ->
withContext(Dispatchers.IO) {
val client = clients[installationId] ?: throw XMTPException("No client")
serverUrl.takeUnless { it.isNullOrBlank() }?.let {
client.debugInformation.uploadDebugInformation(it)
} ?: client.debugInformation.uploadDebugInformation()
}
}

AsyncFunction("createArchive") Coroutine { installationId: String, path: String, encryptionKey: List<Int>, startNs: Int?, endNs: Int?, archiveElements: List<String>? ->
withContext(Dispatchers.IO) {
val client = clients[installationId] ?: throw XMTPException("No client")
Expand Down Expand Up @@ -2317,6 +2399,35 @@ class XMTPModule : Module() {
}
}

private fun subscribeToMessageDeletions(installationId: String) {
val client = clients[installationId] ?: throw XMTPException("No client")
subscriptions[getMessageDeletionsCacheKey(installationId)]?.cancel()
subscriptions[getMessageDeletionsCacheKey(installationId)] =
CoroutineScope(Dispatchers.IO).launch {
try {
client.conversations.streamMessageDeletions(onClose = {
sendEvent(
"messageDeletionClosed", mapOf(
"installationId" to installationId,
)
)
}).collect { message ->
sendEvent(
"messageDeletion",
mapOf(
"installationId" to installationId,
"messageId" to message.id,
"conversationId" to message.conversationId,
)
)
}
} catch (e: Exception) {
Log.e("XMTPModule", "Error in message deletions subscription: $e")
subscriptions[getMessageDeletionsCacheKey(installationId)]?.cancel()
}
}
}

private fun getPreferenceUpdatesKey(installationId: String): String {
return "preferences:$installationId"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import org.xmtp.android.library.codecs.MultiRemoteAttachment
import org.xmtp.android.library.codecs.Reaction
import org.xmtp.android.library.codecs.ReactionCodec
import org.xmtp.android.library.codecs.ReactionV2Codec
import org.xmtp.android.library.codecs.ContentTypeDeleteMessageRequest
import org.xmtp.android.library.codecs.ContentTypeLeaveRequest
import org.xmtp.android.library.codecs.DeleteMessageCodec
import org.xmtp.android.library.codecs.DeleteMessageRequest
import org.xmtp.android.library.codecs.LeaveRequest
import org.xmtp.android.library.codecs.LeaveRequestCodec
import org.xmtp.android.library.codecs.ReadReceipt
import org.xmtp.android.library.codecs.ReadReceiptCodec
import org.xmtp.android.library.codecs.RemoteAttachment
Expand Down Expand Up @@ -71,6 +77,8 @@ class ContentJson(
Client.register(ReadReceiptCodec())
Client.register(GroupUpdatedCodec())
Client.register(ReactionV2Codec())
Client.register(LeaveRequestCodec())
Client.register(DeleteMessageCodec())
}

fun fromJsonObject(obj: JsonObject): ContentJson {
Expand Down Expand Up @@ -136,14 +144,14 @@ class ContentJson(
)
} else if (obj.has("reactionV2")) {
val reaction = obj.get("reactionV2").asJsonObject
// Use SDK Reaction type (not FfiReactionPayload); ReactionV2Codec encodes Reaction.
return ContentJson(
ContentTypeReactionV2, FfiReactionPayload(
ContentTypeReactionV2, Reaction(
reference = reaction.get("reference").asString,
action = getReactionV2Action(reaction.get("action").asString.lowercase()),
schema = getReactionV2Schema(reaction.get("schema").asString.lowercase()),
action = getReactionAction(reaction.get("action").asString.lowercase()),
schema = getReactionSchema(reaction.get("schema").asString.lowercase()),
content = reaction.get("content").asString,
// Update if we add referenceInboxId to ../src/lib/types/ContentCodec.ts#L19-L24
referenceInboxId = ""
referenceInboxId = reaction.get("referenceInboxId")?.takeIf { !it.isJsonNull }?.asString ?: ""
)
)
} else if (obj.has("reply")) {
Expand Down Expand Up @@ -282,6 +290,18 @@ class ContentJson(
)
)

ContentTypeLeaveRequest.id -> mapOf(
"leaveRequest" to mapOf(
"authenticatedNote" to ((content as? LeaveRequest)?.authenticatedNote ?: "")
)
)

ContentTypeDeleteMessageRequest.id -> mapOf(
"deleteMessage" to mapOf(
"messageId" to ((content as? DeleteMessageRequest)?.messageId ?: "")
)
)

else -> {
val json = JsonObject()
encodedContent?.let {
Expand Down
Loading
Loading