From 1ed67836711b99bfb2561741f02b3a682d37c6c5 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 2 Jan 2025 20:38:35 -0800 Subject: [PATCH 01/11] add hmac keys and push support for keys --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 24 ++++++++++++++++++- src/index.ts | 17 ++++++++++--- src/lib/Conversations.ts | 8 +++++++ src/lib/XMTPPush.ts | 2 +- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 4b33d7c5b..6e7c15808 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -572,6 +572,14 @@ class XMTPModule : Module() { } } + AsyncFunction("getHmacKeys") { inboxId: String -> + logV("getHmacKeys") + val client = clients[inboxId] ?: throw XMTPException("No client") + val hmacKeys = client.conversations.getHmacKeys() + logV("$hmacKeys") + hmacKeys.toByteArray().map { it.toInt() and 0xFF } + } + AsyncFunction("conversationMessages") Coroutine { installationId: String, conversationId: String, limit: Int?, beforeNs: Long?, afterNs: Long?, direction: String? -> withContext(Dispatchers.IO) { logV("conversationMessages") @@ -1315,15 +1323,29 @@ class XMTPModule : Module() { xmtpPush?.register(token) } - Function("subscribePushTopics") { topics: List -> + Function("subscribePushTopics") { installationId: String, topics: List -> logV("subscribePushTopics") if (topics.isNotEmpty()) { if (xmtpPush == null) { throw XMTPException("Push server not registered") } + val client = clients[installationId] ?: throw XMTPException("No client") + val hmacKeysResult = client.conversations.getHmacKeys() val subscriptions = topics.map { + val hmacKeys = hmacKeysResult.hmacKeysMap + val result = hmacKeys[it]?.valuesList?.map { hmacKey -> + Service.Subscription.HmacKey.newBuilder().also { sub_key -> + sub_key.key = hmacKey.hmacKey + sub_key.thirtyDayPeriodsSinceEpoch = hmacKey.thirtyDayPeriodsSinceEpoch + }.build() + } + Service.Subscription.newBuilder().also { sub -> + sub.addAllHmacKeys(result) + if (!result.isNullOrEmpty()) { + sub.addAllHmacKeys(result) + } sub.topic = it }.build() } diff --git a/src/index.ts b/src/index.ts index 03b1ac363..d97fb07cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { content } from '@xmtp/proto' +import { content, keystore } from '@xmtp/proto' import { EventEmitter, NativeModulesProxy } from 'expo-modules-core' import { Client } from '.' @@ -402,6 +402,14 @@ export async function listConversations< }) } +export async function getHmacKeys( + installationId: InstallationId +): Promise { + const hmacKeysArray = await XMTPModule.getHmacKeys(installationId) + const array = new Uint8Array(hmacKeysArray) + return keystore.GetConversationHmacKeysResponse.decode(array) +} + export async function conversationMessages< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( @@ -1152,8 +1160,11 @@ export function registerPushToken(pushServer: string, token: string) { return XMTPModule.registerPushToken(pushServer, token) } -export function subscribePushTopics(topics: ConversationTopic[]) { - return XMTPModule.subscribePushTopics(topics) +export function subscribePushTopics( + installationId: InstallationId, + topics: ConversationTopic[] +) { + return XMTPModule.subscribePushTopics(installationId, topics) } export async function exportNativeLogs() { diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index e8bfc2cd2..1d1ce518a 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -23,6 +23,7 @@ import { MessageId, } from '../index' import { getAddress } from '../utils/address' +import { keystore } from '@xmtp/proto' export default class Conversations< ContentTypes extends DefaultContentTypes = DefaultContentTypes, @@ -266,6 +267,13 @@ export default class Conversations< ) } + /** + * This method returns a list of hmac keys for the conversation to help filter self push notifications + */ + async getHmacKeys(): Promise { + return await XMTPModule.getHmacKeys(this.client.installationId) + } + /** * Executes a network request to fetch the latest list of conversations associated with the client * and save them to the local state. diff --git a/src/lib/XMTPPush.ts b/src/lib/XMTPPush.ts index ac0a3832d..5efeb3ba9 100644 --- a/src/lib/XMTPPush.ts +++ b/src/lib/XMTPPush.ts @@ -13,6 +13,6 @@ export class XMTPPush { } subscribe(topics: ConversationTopic[]) { - XMTPModule.subscribePushTopics(topics) + XMTPModule.subscribePushTopics(this.client.installationId, topics) } } From fb1f3510b3129652951c6f4300dce090b3fb9068 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 2 Jan 2025 20:41:47 -0800 Subject: [PATCH 02/11] Create hungry-onions-hear.md --- .changeset/hungry-onions-hear.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/hungry-onions-hear.md diff --git a/.changeset/hungry-onions-hear.md b/.changeset/hungry-onions-hear.md new file mode 100644 index 000000000..c3218a4f5 --- /dev/null +++ b/.changeset/hungry-onions-hear.md @@ -0,0 +1,6 @@ +--- +"@xmtp/react-native-sdk": patch +--- + +V3 HMAC key support for self push notifications +Streaming preference updates From 6653bf37aede6f806b9c1483cba45cce8dbb0d6e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 20:20:53 -0800 Subject: [PATCH 03/11] get on latest RN --- android/build.gradle | 2 +- .../modules/xmtpreactnativesdk/XMTPModule.kt | 38 +++++++------------ .../xmtpreactnativesdk/wrappers/DmWrapper.kt | 2 +- .../wrappers/GroupWrapper.kt | 2 +- ...dedMessageWrapper.kt => MessageWrapper.kt} | 10 ++--- ios/XMTPModule.swift | 33 +++++++++++++++- 6 files changed, 54 insertions(+), 33 deletions(-) rename android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/{DecodedMessageWrapper.kt => MessageWrapper.kt} (81%) diff --git a/android/build.gradle b/android/build.gradle index f94c57811..2bf9397f8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:3.0.19" + implementation "org.xmtp:android:3.0.20" 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" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 6e7c15808..1d32c42d9 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -18,7 +18,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.ContentJson import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.CreateGroupParamsWrapper -import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper +import expo.modules.xmtpreactnativesdk.wrappers.MessageWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.DmWrapper import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment @@ -54,13 +54,13 @@ import org.xmtp.android.library.codecs.EncryptedEncodedContent import org.xmtp.android.library.codecs.RemoteAttachment import org.xmtp.android.library.codecs.decoded import org.xmtp.android.library.hexToByteArray +import org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import org.xmtp.android.library.libxmtp.Message +import org.xmtp.android.library.libxmtp.PermissionOption import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.Signature import org.xmtp.android.library.push.Service import org.xmtp.android.library.push.XMTPPush -import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration -import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption import java.io.BufferedReader import java.io.File import java.io.InputStreamReader @@ -520,15 +520,13 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> + AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listGroups") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") - val order = getConversationSortOrder(sortOrder ?: "") val consent = consentState?.let { getConsentState(it) } val groups = client.conversations.listGroups( - order = order, limit = limit, consentState = consent ) @@ -538,15 +536,13 @@ class XMTPModule : Module() { } } - AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> + AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listDms") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") - val order = getConversationSortOrder(sortOrder ?: "") val consent = consentState?.let { getConsentState(it) } val dms = client.conversations.listDms( - order = order, limit = limit, consentState = consent ) @@ -556,16 +552,15 @@ class XMTPModule : Module() { } } - AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> + AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listConversations") val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "") - val order = getConversationSortOrder(sortOrder ?: "") val consent = consentState?.let { getConsentState(it) } val conversations = - client.conversations.list(order = order, limit = limit, consentState = consent) + client.conversations.list(limit = limit, consentState = consent) conversations.map { conversation -> ConversationWrapper.encode(client, conversation, params) } @@ -592,7 +587,7 @@ class XMTPModule : Module() { direction = Message.SortDirection.valueOf( direction ?: "DESCENDING" ) - )?.map { DecodedMessageWrapper.encode(it) } + )?.map { MessageWrapper.encode(it) } } } @@ -602,7 +597,7 @@ class XMTPModule : Module() { val client = clients[installationId] ?: throw XMTPException("No client") val message = client.findMessage(messageId) message?.let { - DecodedMessageWrapper.encode(it.decode()) + MessageWrapper.encode(it) } } } @@ -1182,7 +1177,9 @@ class XMTPModule : Module() { val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") val message = conversation.processMessage(Base64.decode(encryptedMessage, NO_WRAP)) - DecodedMessageWrapper.encode(message.decode()) + message?.let { + MessageWrapper.encode(it) + } } } @@ -1412,13 +1409,6 @@ class XMTPModule : Module() { } } - private fun getConversationSortOrder(order: String): ConversationOrder { - return when (order) { - "lastMessage" -> ConversationOrder.LAST_MESSAGE - else -> ConversationOrder.CREATED_AT - } - } - private fun consentStateToString(state: ConsentState): String { return when (state) { ConsentState.ALLOWED -> "allowed" @@ -1487,7 +1477,7 @@ class XMTPModule : Module() { "message", mapOf( "installationId" to installationId, - "message" to DecodedMessageWrapper.encodeMap(message), + "message" to MessageWrapper.encodeMap(message), ) ) } @@ -1511,7 +1501,7 @@ class XMTPModule : Module() { "conversationMessage", mapOf( "installationId" to installationId, - "message" to DecodedMessageWrapper.encodeMap(message), + "message" to MessageWrapper.encodeMap(message), "conversationId" to id, ) ) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt index e41d2eaeb..1e013a4dc 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt @@ -24,7 +24,7 @@ class DmWrapper { if (dmParams.lastMessage) { val lastMessage = dm.messages(limit = 1).firstOrNull() if (lastMessage != null) { - put("lastMessage", DecodedMessageWrapper.encode(lastMessage)) + put("lastMessage", MessageWrapper.encode(lastMessage)) } } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index 72a7bb36b..1d4d6beff 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -31,7 +31,7 @@ class GroupWrapper { if (groupParams.lastMessage) { val lastMessage = group.messages(limit = 1).firstOrNull() if (lastMessage != null) { - put("lastMessage", DecodedMessageWrapper.encode(lastMessage)) + put("lastMessage", MessageWrapper.encode(lastMessage)) } } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageWrapper.kt similarity index 81% rename from android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt rename to android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageWrapper.kt index ab5ac6835..a4f7220ce 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageWrapper.kt @@ -1,19 +1,19 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder -import org.xmtp.android.library.DecodedMessage import org.xmtp.android.library.codecs.description +import org.xmtp.android.library.libxmtp.Message -class DecodedMessageWrapper { +class MessageWrapper { companion object { - fun encode(model: DecodedMessage): String { + fun encode(model: Message): String { val gson = GsonBuilder().create() val message = encodeMap(model) return gson.toJson(message) } - fun encodeMap(model: DecodedMessage): Map { + fun encodeMap(model: Message): Map { // Kotlin/Java Protos don't support null values and will always put the default "" // Check if there is a fallback, if there is then make it the set fallback, if not null val fallback = if (model.encodedContent.hasFallback()) model.encodedContent.fallback else null @@ -23,7 +23,7 @@ class DecodedMessageWrapper { "contentTypeId" to model.encodedContent.type.description, "content" to ContentJson(model.encodedContent).toJsonMap(), "senderInboxId" to model.senderInboxId, - "sentNs" to model.sentNs, + "sentNs" to model.sentAtNs, "fallback" to fallback, "deliveryStatus" to model.deliveryStatus.toString() ) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index ec1512cb4..8f089c9d5 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -582,6 +582,17 @@ public class XMTPModule: Module { return results } + AsyncFunction("getHmacKeys") { (installationId: String) -> [UInt8] in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + let hmacKeys = await client.conversations.getHmacKeys() + + return try [UInt8](hmacKeys.serializedData()) + } + AsyncFunction("conversationMessages") { ( installationId: String, conversationId: String, limit: Int?, @@ -1742,11 +1753,31 @@ public class XMTPModule: Module { } } - AsyncFunction("subscribePushTopics") { (topics: [String]) in + AsyncFunction("subscribePushTopics") { + (installationId: String, topics: [String]) in do { + guard + let client = await clientsManager.getClient( + key: installationId) + else { + throw Error.noClient + } + let hmacKeysResult = await client.conversations.getHmacKeys() let subscriptions = topics.map { topic -> NotificationSubscription in + let hmacKeys = hmacKeysResult.hmacKeys + + let result = hmacKeys[topic]?.values.map { + hmacKey -> NotificationSubscriptionHmacKey in + NotificationSubscriptionHmacKey.with { sub_key in + sub_key.key = hmacKey.hmacKey + sub_key.thirtyDayPeriodsSinceEpoch = UInt32( + hmacKey.thirtyDayPeriodsSinceEpoch) + } + } + return NotificationSubscription.with { sub in + sub.hmacKeys = result ?? [] sub.topic = topic } } From 6d24e395ad7f8f19f1056a47c945b2e3deb9036d Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 20:26:22 -0800 Subject: [PATCH 04/11] add preference streaming --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 1d32c42d9..20c5afe27 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -42,6 +42,7 @@ import org.xmtp.android.library.Conversation import org.xmtp.android.library.Conversations.* import org.xmtp.android.library.EntryType import org.xmtp.android.library.PreEventCallback +import org.xmtp.android.library.PreferenceType import org.xmtp.android.library.SendOptions import org.xmtp.android.library.SigningKey import org.xmtp.android.library.WalletType @@ -208,6 +209,7 @@ class XMTPModule : Module() { "message", "conversationMessage", "consent", + "preferences", ) Function("address") { installationId: String -> @@ -1262,6 +1264,12 @@ class XMTPModule : Module() { } } + Function("subscribeToPreferenceUpdates") { installationId: String -> + logV("subscribeToPreferenceUpdates") + + subscribeToPreferenceUpdates(installationId = installationId) + } + Function("subscribeToConsent") { installationId: String -> logV("subscribeToConsent") @@ -1289,6 +1297,11 @@ class XMTPModule : Module() { } } + Function("unsubscribeFromPreferenceUpdates") { installationId: String -> + logV("unsubscribeFromPreferenceUpdates") + subscriptions[getPreferenceUpdatesKey(installationId)]?.cancel() + } + Function("unsubscribeFromConsent") { installationId: String -> logV("unsubscribeFromConsent") subscriptions[getConsentKey(installationId)]?.cancel() @@ -1417,6 +1430,35 @@ class XMTPModule : Module() { } } + private fun preferenceTypeToString(type: PreferenceType): String { + return when (type) { + PreferenceType.HMAC_KEYS -> "hmac_keys" + } + } + + private fun subscribeToPreferenceUpdates(installationId: String) { + val client = clients[installationId] ?: throw XMTPException("No client") + + subscriptions[getPreferenceUpdatesKey(installationId)]?.cancel() + subscriptions[getPreferenceUpdatesKey(installationId)] = + CoroutineScope(Dispatchers.IO).launch { + try { + client.preferences.streamPreferenceUpdates().collect { type -> + sendEvent( + "preferences", + mapOf( + "installationId" to installationId, + "preferenceType" to preferenceTypeToString(type) + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in preference subscription: $e") + subscriptions[getPreferenceUpdatesKey(installationId)]?.cancel() + } + } + } + private fun subscribeToConsent(installationId: String) { val client = clients[installationId] ?: throw XMTPException("No client") @@ -1434,7 +1476,7 @@ class XMTPModule : Module() { ) } } catch (e: Exception) { - Log.e("XMTPModule", "Error in group subscription: $e") + Log.e("XMTPModule", "Error in consent subscription: $e") subscriptions[getConsentKey(installationId)]?.cancel() } } @@ -1513,6 +1555,10 @@ class XMTPModule : Module() { } } + private fun getPreferenceUpdatesKey(installationId: String): String { + return "preferences:$installationId" + } + private fun getConsentKey(installationId: String): String { return "consent:$installationId" } From 24d63fb87969e52813092d52c8c548f95f721e08 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 20:27:48 -0800 Subject: [PATCH 05/11] update the last message returned --- .../java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt | 2 +- .../expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt index 1e013a4dc..a2616e11e 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt @@ -22,7 +22,7 @@ class DmWrapper { put("consentState", consentStateToString(dm.consentState())) } if (dmParams.lastMessage) { - val lastMessage = dm.messages(limit = 1).firstOrNull() + val lastMessage = dm.lastMessage() if (lastMessage != null) { put("lastMessage", MessageWrapper.encode(lastMessage)) } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index 1d4d6beff..d77008f3a 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -29,7 +29,7 @@ class GroupWrapper { put("consentState", consentStateToString(group.consentState())) } if (groupParams.lastMessage) { - val lastMessage = group.messages(limit = 1).firstOrNull() + val lastMessage = group.lastMessage() if (lastMessage != null) { put("lastMessage", MessageWrapper.encode(lastMessage)) } From 577d49e1f3237112fd7039766b39702b5a975455 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 20:31:17 -0800 Subject: [PATCH 06/11] remove order --- ios/XMTPModule.swift | 9 --------- src/index.ts | 18 +++++++++-------- src/lib/Conversations.ts | 30 +++++----------------------- src/lib/types/ConversationOptions.ts | 4 ---- 4 files changed, 15 insertions(+), 46 deletions(-) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 8f089c9d5..c64bfd70a 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -1879,15 +1879,6 @@ public class XMTPModule: Module { } } - private func getConversationSortOrder(order: String) -> ConversationOrder { - switch order { - case "lastMessage": - return .lastMessage - default: - return .createdAt - } - } - private func getSortDirection(direction: String) throws -> SortDirection { switch direction { case "ASCENDING": diff --git a/src/index.ts b/src/index.ts index d97fb07cf..f753ce528 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,6 @@ import { InboxState } from './lib/InboxState' import { Member } from './lib/Member' import { WalletType } from './lib/Signer' import { - ConversationOrder, ConversationOptions, ConversationType, ConversationId, @@ -321,7 +320,6 @@ export async function listGroups< >( client: Client, opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, limit?: number | undefined, consentState?: ConsentState | undefined ): Promise[]> { @@ -329,7 +327,6 @@ export async function listGroups< await XMTPModule.listGroups( client.installationId, JSON.stringify(opts), - order, limit, consentState ) @@ -348,7 +345,6 @@ export async function listDms< >( client: Client, opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, limit?: number | undefined, consentState?: ConsentState | undefined ): Promise[]> { @@ -356,7 +352,6 @@ export async function listDms< await XMTPModule.listDms( client.installationId, JSON.stringify(opts), - order, limit, consentState ) @@ -375,7 +370,6 @@ export async function listConversations< >( client: Client, opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, limit?: number | undefined, consentState?: ConsentState | undefined ): Promise[]> { @@ -383,7 +377,6 @@ export async function listConversations< await XMTPModule.listConversations( client.installationId, JSON.stringify(opts), - order, limit, consentState ) @@ -1112,6 +1105,10 @@ export async function updateConversationConsent( ) } +export function subscribeToPreferenceUpdates(installationId: InstallationId) { + return XMTPModule.subscribeToPreferenceUpdates(installationId) +} + export function subscribeToConsent(installationId: InstallationId) { return XMTPModule.subscribeToConsent(installationId) } @@ -1137,6 +1134,12 @@ export async function subscribeToMessages( return await XMTPModule.subscribeToMessages(installationId, id) } +export function unsubscribeFromPreferenceUpdates( + installationId: InstallationId +) { + return XMTPModule.unsubscribeFromPreferenceUpdates(installationId) +} + export function unsubscribeFromConsent(installationId: InstallationId) { return XMTPModule.unsubscribeFromConsent(installationId) } @@ -1204,7 +1207,6 @@ export { Member } from './lib/Member' export { Address, InboxId, XMTPEnvironment } from './lib/Client' export { ConversationOptions, - ConversationOrder, ConversationId, ConversationTopic, ConversationType, diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 1d1ce518a..a88fd5adc 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -1,12 +1,11 @@ +import { keystore } from '@xmtp/proto' + import { Client, InboxId } from './Client' import { ConversationVersion } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Dm, DmParams } from './Dm' import { Group, GroupParams } from './Group' -import { - ConversationOrder, - ConversationOptions, -} from './types/ConversationOptions' +import { ConversationOptions } from './types/ConversationOptions' import { CreateGroupOptions } from './types/CreateGroupOptions' import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' @@ -23,7 +22,6 @@ import { MessageId, } from '../index' import { getAddress } from '../utils/address' -import { keystore } from '@xmtp/proto' export default class Conversations< ContentTypes extends DefaultContentTypes = DefaultContentTypes, @@ -202,48 +200,32 @@ export default class Conversations< * This method returns a list of all groups that the client is a member of. * To get the latest list of groups from the network, call syncGroups() first. * @param {ConversationOptions} opts - The options to specify what fields you want returned for the groups in the list. - * @param {ConversationOrder} order - The order to specify if you want groups listed by last message or by created at. * @param {number} limit - Limit the number of groups returned in the list. * * @returns {Promise} A Promise that resolves to an array of Group objects. */ async listGroups( opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, limit?: number | undefined, consentState?: ConsentState | undefined ): Promise[]> { - return await XMTPModule.listGroups( - this.client, - opts, - order, - limit, - consentState - ) + return await XMTPModule.listGroups(this.client, opts, limit, consentState) } /** * This method returns a list of all dms that the client is a member of. * To get the latest list of dms from the network, call sync() first. * @param {ConversationOptions} opts - The options to specify what fields you want returned for the dms in the list. - * @param {ConversationOrder} order - The order to specify if you want dms listed by last message or by created at. * @param {number} limit - Limit the number of dms returned in the list. * * @returns {Promise} A Promise that resolves to an array of Dms objects. */ async listDms( opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, limit?: number | undefined, consentState?: ConsentState | undefined ): Promise[]> { - return await XMTPModule.listDms( - this.client, - opts, - order, - limit, - consentState - ) + return await XMTPModule.listDms(this.client, opts, limit, consentState) } /** @@ -254,14 +236,12 @@ export default class Conversations< */ async list( opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, limit?: number | undefined, consentState?: ConsentState | undefined ): Promise[]> { return await XMTPModule.listConversations( this.client, opts, - order, limit, consentState ) diff --git a/src/lib/types/ConversationOptions.ts b/src/lib/types/ConversationOptions.ts index b9bc4d89f..217a0a8a4 100644 --- a/src/lib/types/ConversationOptions.ts +++ b/src/lib/types/ConversationOptions.ts @@ -8,10 +8,6 @@ export type ConversationOptions = { lastMessage?: boolean } -export type ConversationOrder = - | 'lastMessage' // Ordered by the last message that was sent - | 'createdAt' // DEFAULT: Ordered by the date the conversation was created - export type ConversationType = 'all' | 'groups' | 'dms' export type ConversationId = string & { readonly brand: unique symbol } From 6b322474a83b26f57948ab33f79555f66a581ac7 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 20:35:54 -0800 Subject: [PATCH 07/11] add stream for preference updates --- src/lib/PrivatePreferences.ts | 39 +++++++++++++++++++++++++++++++++++ src/lib/types/EventTypes.ts | 4 ++++ 2 files changed, 43 insertions(+) diff --git a/src/lib/PrivatePreferences.ts b/src/lib/PrivatePreferences.ts index 3820fa5e7..043567548 100644 --- a/src/lib/PrivatePreferences.ts +++ b/src/lib/PrivatePreferences.ts @@ -5,6 +5,8 @@ import * as XMTPModule from '../index' import { ConversationId } from '../index' import { getAddress } from '../utils/address' +export type PreferenceUpdates = 'hmac_keys' + export default class PrivatePreferences { client: Client private subscriptions: { [key: string]: { remove: () => void } } = {} @@ -49,6 +51,32 @@ export default class PrivatePreferences { return await XMTPModule.syncConsent(this.client.installationId) } + /** + * This method streams private preference updates. + * @returns {Promise} A Promise that resolves to an array of PreferenceUpdates objects. + */ + async streamPreferenceUpdates( + callback: (preferenceUpdates: PreferenceUpdates) => Promise + ): Promise { + XMTPModule.subscribeToPreferenceUpdates(this.client.installationId) + const subscription = XMTPModule.emitter.addListener( + EventTypes.PreferenceUpdates, + async ({ + installationId, + type, + }: { + installationId: string + type: PreferenceUpdates + }) => { + if (installationId !== this.client.installationId) { + return + } + return await callback(type) + } + ) + this.subscriptions[EventTypes.PreferenceUpdates] = subscription + } + /** * This method streams consent. * @returns {Promise} A Promise that resolves to an array of ConsentRecord objects. @@ -87,4 +115,15 @@ export default class PrivatePreferences { } XMTPModule.unsubscribeFromConsent(this.client.installationId) } + + /** + * Cancels the stream for preference updates. + */ + cancelStreamPreferenceUpdates() { + if (this.subscriptions[EventTypes.PreferenceUpdates]) { + this.subscriptions[EventTypes.PreferenceUpdates].remove() + delete this.subscriptions[EventTypes.PreferenceUpdates] + } + XMTPModule.unsubscribeFromPreferenceUpdates(this.client.installationId) + } } diff --git a/src/lib/types/EventTypes.ts b/src/lib/types/EventTypes.ts index 9f2c0d3f3..a46fde5d4 100644 --- a/src/lib/types/EventTypes.ts +++ b/src/lib/types/EventTypes.ts @@ -21,4 +21,8 @@ export enum EventTypes { * A inboxId or conversation has been approved or denied */ Consent = 'consent', + /** + * A new installation has been added making new hmac keys + */ + PreferenceUpdates = 'preferences', } From bbc5b1ef2ed879c235dabb1b9f11cc53c62c7e57 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 21:38:55 -0800 Subject: [PATCH 08/11] write tests for it --- example/src/tests/conversationTests.ts | 105 +++++++++++++++++++------ example/src/tests/dmTests.ts | 24 +----- example/src/tests/groupTests.ts | 63 ++++++++------- ios/XMTPReactNative.podspec | 2 +- 4 files changed, 116 insertions(+), 78 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index ff4cf7a0e..c92f05551 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -12,6 +12,7 @@ import { ConversationVersion, JSContentCodec, } from '../../../src/index' +import { PreferenceUpdates } from 'xmtp-react-native-sdk/lib/PrivatePreferences' export const conversationTests: Test[] = [] let counter = 1 @@ -356,13 +357,11 @@ test('can filter conversations by consent', async () => { const boConvosFilteredAllowed = await boClient.conversations.list( {}, undefined, - undefined, 'allowed' ) const boConvosFilteredUnknown = await boClient.conversations.list( {}, undefined, - undefined, 'unknown' ) @@ -445,25 +444,10 @@ test('can list conversations with params', async () => { // Order should be [Dm1, Group2, Dm2, Group1] await boClient.conversations.syncAllConversations() - const boConvosOrderCreated = await boClient.conversations.list() - const boConvosOrderLastMessage = await boClient.conversations.list( - { lastMessage: true }, - 'lastMessage' - ) - const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) - - assert( - boConvosOrderCreated.map((group: any) => group.id).toString() === - [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), - `Conversation created at order should be ${[ - boGroup1.id, - boGroup2.id, - boDm1.id, - boDm2.id, - ].toString()} but was ${boConvosOrderCreated - .map((group: any) => group.id) - .toString()}` - ) + const boConvosOrderLastMessage = await boClient.conversations.list({ + lastMessage: true, + }) + const boGroupsLimit = await boClient.conversations.list({}, 1) assert( boConvosOrderLastMessage.map((group: any) => group.id).toString() === @@ -483,10 +467,10 @@ test('can list conversations with params', async () => { messages[0].content() === 'dm message', `last message 1 should be dm message ${messages[0].content()}` ) - // assert( - // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', - // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` - // ) + assert( + boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', + `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` + ) assert( boGroupsLimit.length === 1, `List length should be 1 but was ${boGroupsLimit.length}` @@ -970,3 +954,74 @@ test('can stream consent', async () => { return true }) + +test('can preference updates', async () => { + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` + + // Ensure the directories exist + if (!(await RNFS.exists(dbDirPath))) { + await RNFS.mkdir(dbDirPath) + } + if (!(await RNFS.exists(dbDirPath2))) { + await RNFS.mkdir(dbDirPath2) + } + + const alixWallet = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, + }) + + const types = [] + await alix.preferences.streamPreferenceUpdates(async (entry: PreferenceUpdates) => { + types.push(entry) + }) + + const alix2 = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath2, + }) + + await alix.conversations.syncAllConversations() + await alix2.conversations.syncAllConversations() + + assert( + types.length === 1, + `Expected 1 preference update, got ${types.length}` + ) + + alix.preferences.cancelStreamConsent() + + return true +}) + +test('get all HMAC keys', async () => { + const [alix] = await createClients(1) + + const conversations: Conversation[] = [] + + for (let i = 0; i < 5; i++) { + const [client] = await createClients(1) + const convo = await alix.conversations.newConversation(client.address) + conversations.push(convo) + } + + const { hmacKeys } = await alix.conversations.getHmacKeys() + + const topics = Object.keys(hmacKeys) + conversations.forEach((conversation) => { + assert(topics.includes(conversation.topic), 'topic not found') + }) + + return true +}) diff --git a/example/src/tests/dmTests.ts b/example/src/tests/dmTests.ts index 51fd0c367..7cf2cf9ef 100644 --- a/example/src/tests/dmTests.ts +++ b/example/src/tests/dmTests.ts @@ -25,13 +25,11 @@ test('can filter dms by consent', async () => { const boConvosFilteredAllowed = await boClient.conversations.listDms( {}, undefined, - undefined, 'allowed' ) const boConvosFilteredUnknown = await boClient.conversations.listDms( {}, undefined, - undefined, 'unknown' ) @@ -81,24 +79,10 @@ test('can list dms with params', async () => { await boDm1.send({ text: `dm message` }) await boClient.conversations.syncAllConversations() - const boConvosOrderCreated = await boClient.conversations.listDms() - const boConvosOrderLastMessage = await boClient.conversations.listDms( - { lastMessage: true }, - 'lastMessage' - ) - const boDmsLimit = await boClient.conversations.listDms({}, undefined, 1) - - assert( - boConvosOrderCreated - .map((conversation: any) => conversation.id) - .toString() === [boDm1.id, boDm2.id].toString(), - `Conversation created at order should be ${[ - boDm1.id, - boDm2.id, - ].toString()} but was ${boConvosOrderCreated - .map((convo: any) => convo.id) - .toString()}` - ) + const boConvosOrderLastMessage = await boClient.conversations.listDms({ + lastMessage: true, + }) + const boDmsLimit = await boClient.conversations.listDms({}, 1) assert( boConvosOrderLastMessage diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 95585c898..fd3c3c631 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -140,7 +140,7 @@ test('groups cannot fork', async () => { let forkCount = 0 const tryCount = 5 for (let i = 0; i < tryCount; i++) { - console.log(`Checking fork status ${i+1}/${tryCount}`) + console.log(`Checking fork status ${i + 1}/${tryCount}`) try { await syncClientAndGroup(alix) await syncClientAndGroup(bo) @@ -171,18 +171,25 @@ test('groups cannot fork short version', async () => { // sync clients await alix.conversations.sync() await bo.conversations.sync() - const boGroup: Group = (await bo.conversations.findGroup(alixGroup.id))! + const boGroup: Group = (await bo.conversations.findGroup( + alixGroup.id + ))! // Remove two members in parallel // NB => if we don't use Promise.all but a loop, we don't get a fork - console.log('*************libxmtp*********************: Removing members in parallel') + console.log( + '*************libxmtp*********************: Removing members in parallel' + ) await Promise.all([ alixGroup.removeMembers([new_one.address]), - alixGroup.removeMembers([new_two.address]) + alixGroup.removeMembers([new_two.address]), ]) // Helper to send a message from a bunch of senders and make sure it is received by all receivers - const testMessageSending = async (senderGroup: Group, receiverGroup: Group) => { + const testMessageSending = async ( + senderGroup: Group, + receiverGroup: Group + ) => { const messageContent = Math.random().toString(36) await senderGroup.sync() await alixGroup.send(messageContent) @@ -209,7 +216,7 @@ test('groups cannot fork short version', async () => { let forkCount = 0 const tryCount = 5 for (let i = 0; i < tryCount; i++) { - console.log(`Checking fork status ${i+1}/${tryCount}`) + console.log(`Checking fork status ${i + 1}/${tryCount}`) try { await alixGroup.sync() await boGroup.sync() @@ -228,7 +235,8 @@ test('groups cannot fork short version', async () => { }) test('groups cannot fork short version - update metadata', async () => { - const [alix, bo, new_one, new_two, new_three, new_four] = await createClients(6) + const [alix, bo, new_one, new_two, new_three, new_four] = + await createClients(6) // Create group with 2 users const alixGroup = await alix.conversations.newGroup([ bo.address, @@ -239,18 +247,25 @@ test('groups cannot fork short version - update metadata', async () => { // sync clients await alix.conversations.sync() await bo.conversations.sync() - const boGroup: Group = (await bo.conversations.findGroup(alixGroup.id))! + const boGroup: Group = (await bo.conversations.findGroup( + alixGroup.id + ))! // Remove two members in parallel // NB => if we don't use Promise.all but a loop, we don't get a fork - console.log('*************libxmtp*********************: Updating metadata in parallel') + console.log( + '*************libxmtp*********************: Updating metadata in parallel' + ) await Promise.all([ alixGroup.updateGroupName('new name'), - alixGroup.updateGroupName('new name 2') + alixGroup.updateGroupName('new name 2'), ]) // Helper to send a message from a bunch of senders and make sure it is received by all receivers - const testMessageSending = async (senderGroup: Group, receiverGroup: Group) => { + const testMessageSending = async ( + senderGroup: Group, + receiverGroup: Group + ) => { const messageContent = Math.random().toString(36) await senderGroup.sync() await alixGroup.send(messageContent) @@ -277,7 +292,7 @@ test('groups cannot fork short version - update metadata', async () => { let forkCount = 0 const tryCount = 5 for (let i = 0; i < tryCount; i++) { - console.log(`Checking fork status ${i+1}/${tryCount}`) + console.log(`Checking fork status ${i + 1}/${tryCount}`) try { await alixGroup.sync() await boGroup.sync() @@ -958,13 +973,11 @@ test('can filter groups by consent', async () => { const boConvosFilteredAllowed = await boClient.conversations.listGroups( {}, undefined, - undefined, 'allowed' ) const boConvosFilteredUnknown = await boClient.conversations.listGroups( {}, undefined, - undefined, 'unknown' ) @@ -1009,24 +1022,10 @@ test('can list groups with params', async () => { await boGroup1.send({ text: `third message` }) await boGroup2.send({ text: `first message` }) - const boGroupsOrderCreated = await boClient.conversations.listGroups() - const boGroupsOrderLastMessage = await boClient.conversations.listGroups( - { lastMessage: true }, - 'lastMessage' - ) - const boGroupsLimit = await boClient.conversations.listGroups( - {}, - undefined, - 1 - ) - - assert( - boGroupsOrderCreated.map((group: any) => group.id).toString() === - [boGroup1.id, boGroup2.id].toString(), - `Group order should be group1 then group2 but was ${boGroupsOrderCreated - .map((group: any) => group.id) - .toString()}` - ) + const boGroupsOrderLastMessage = await boClient.conversations.listGroups({ + lastMessage: true, + }) + const boGroupsLimit = await boClient.conversations.listGroups({}, 1) assert( boGroupsOrderLastMessage.map((group: any) => group.id).toString() === diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 660887a67..3a58d3977 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.21" + s.dependency "XMTP", "= 3.0.22" s.dependency 'CSecp256k1', '~> 0.2' s.dependency "SQLCipher", "= 4.5.7" end From a6511cfe9bc383fa3b61c81cd7413f2671b447f5 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 21:56:16 -0800 Subject: [PATCH 09/11] do the iOS side --- example/ios/Podfile.lock | 26 ++--- ios/Wrappers/DmWrapper.swift | 4 +- ios/Wrappers/GroupWrapper.swift | 4 +- ...sageWrapper.swift => MessageWrapper.swift} | 12 +-- ios/XMTPModule.swift | 94 +++++++++++++++---- 5 files changed, 101 insertions(+), 39 deletions(-) rename ios/Wrappers/{DecodedMessageWrapper.swift => MessageWrapper.swift} (96%) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 64f3da2b8..eed930bfe 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -55,11 +55,11 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (3.0.15) + - LibXMTP (3.0.18) - MessagePacker (0.4.7) - - MMKV (2.0.0): - - MMKVCore (~> 2.0.0) - - MMKVCore (2.0.0) + - MMKV (2.0.2): + - MMKVCore (~> 2.0.2) + - MMKVCore (2.0.2) - OpenSSL-Universal (1.1.2200) - RCT-Folly (2021.07.22.00): - boost @@ -448,18 +448,18 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.28.2) - - XMTP (3.0.21): + - XMTP (3.0.22): - Connect-Swift (= 1.0.0) - CryptoSwift (= 1.8.3) - CSecp256k1 (~> 0.2) - - LibXMTP (= 3.0.15) + - LibXMTP (= 3.0.18) - SQLCipher (= 4.5.7) - - XMTPReactNative (3.1.4): + - XMTPReactNative (3.1.5): - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 3.0.21) + - XMTP (= 3.0.22) - Yoga (1.14.0) DEPENDENCIES: @@ -711,10 +711,10 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: ad2c28778d7273c499b12dbf5493978c58d76858 + LibXMTP: 09d80c1ef92a6fd195a13b2e0911259bf87005e3 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 - MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 - MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 + MMKV: 3eacda84cd1c4fc95cf848d3ecb69d85ed56006c + MMKVCore: 508b4d3a8ce031f1b5c8bd235f0517fb3f4c73a9 OpenSSL-Universal: 6e1ae0555546e604dbc632a2b9a24a9c46c41ef6 RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: e9df143e880d0e879e7a498dc06923d728809c79 @@ -762,8 +762,8 @@ SPEC CHECKSUMS: RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d - XMTP: 2c5dd2116778d1b547ac99b5b2396318d02c24d1 - XMTPReactNative: 7745410f6367ed23198e546409d84987c6c5fea9 + XMTP: c2049f379dd2a799921466cb0720690b786a2958 + XMTPReactNative: f8a2a80d316d2b381604131448462eb56baffbfb Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/ios/Wrappers/DmWrapper.swift b/ios/Wrappers/DmWrapper.swift index e7ff41e54..b3d0c16cf 100644 --- a/ios/Wrappers/DmWrapper.swift +++ b/ios/Wrappers/DmWrapper.swift @@ -24,8 +24,8 @@ struct DmWrapper { result["consentState"] = ConsentWrapper.consentStateToString(state: try dm.consentState()) } if conversationParams.lastMessage { - if let lastMessage = try await dm.messages(limit: 1).first { - result["lastMessage"] = try DecodedMessageWrapper.encode(lastMessage, client: client) + if let lastMessage = try await dm.lastMessage() { + result["lastMessage"] = try MessageWrapper.encode(lastMessage, client: client) } } diff --git a/ios/Wrappers/GroupWrapper.swift b/ios/Wrappers/GroupWrapper.swift index bea29d63f..994881b88 100644 --- a/ios/Wrappers/GroupWrapper.swift +++ b/ios/Wrappers/GroupWrapper.swift @@ -38,8 +38,8 @@ struct GroupWrapper { result["consentState"] = ConsentWrapper.consentStateToString(state: try group.consentState()) } if conversationParams.lastMessage { - if let lastMessage = try await group.messages(limit: 1).first { - result["lastMessage"] = try DecodedMessageWrapper.encode(lastMessage, client: client) + if let lastMessage = try await group.lastMessage() { + result["lastMessage"] = try MessageWrapper.encode(lastMessage, client: client) } } diff --git a/ios/Wrappers/DecodedMessageWrapper.swift b/ios/Wrappers/MessageWrapper.swift similarity index 96% rename from ios/Wrappers/DecodedMessageWrapper.swift rename to ios/Wrappers/MessageWrapper.swift index f9e51e3bd..beb115315 100644 --- a/ios/Wrappers/DecodedMessageWrapper.swift +++ b/ios/Wrappers/MessageWrapper.swift @@ -3,24 +3,24 @@ import XMTP // Wrapper around XMTP.DecodedMessage to allow passing these objects back // into react native. -struct DecodedMessageWrapper { - static func encodeToObj(_ model: XMTP.DecodedMessage, client: Client) throws -> [String: Any] { +struct MessageWrapper { + static func encodeToObj(_ model: XMTP.Message, client: Client) throws -> [String: Any] { // Swift Protos don't support null values and will always put the default "" // Check if there is a fallback, if there is then make it the set fallback, if not null - let fallback = model.encodedContent.hasFallback ? model.encodedContent.fallback : nil + let fallback = try model.encodedContent.hasFallback ? model.encodedContent.fallback : nil return [ "id": model.id, "topic": model.topic, - "contentTypeId": model.encodedContent.type.description, + "contentTypeId": try model.encodedContent.type.description, "content": try ContentJson.fromEncoded(model.encodedContent, client: client).toJsonMap() as Any, "senderInboxId": model.senderInboxId, - "sentNs": model.sentNs, + "sentNs": model.sentAtNs, "fallback": fallback, "deliveryStatus": model.deliveryStatus.rawValue.uppercased(), ] } - static func encode(_ model: XMTP.DecodedMessage, client: Client) throws -> String { + static func encode(_ model: XMTP.Message, client: Client) throws -> String { let obj = try encodeToObj(model, client: client) return try obj.toJson() } diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index c64bfd70a..7d47c9d83 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -104,7 +104,8 @@ public class XMTPModule: Module { "conversation", "message", "conversationMessage", - "consent" + "consent", + "preferences" ) AsyncFunction("address") { (installationId: String) -> String in @@ -496,7 +497,6 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") - let order = getConversationSortOrder(order: sortOrder ?? "") let consent: ConsentState? if let state = consentState { consent = try getConsentState(state: state) @@ -504,7 +504,7 @@ public class XMTPModule: Module { consent = nil } var groupList: [Group] = try await client.conversations.listGroups( - limit: limit, order: order, consentState: consent) + limit: limit, consentState: consent) var results: [String] = [] for group in groupList { @@ -529,7 +529,6 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") - let order = getConversationSortOrder(order: sortOrder ?? "") let consent: ConsentState? if let state = consentState { consent = try getConsentState(state: state) @@ -537,7 +536,7 @@ public class XMTPModule: Module { consent = nil } var dmList: [Dm] = try await client.conversations.listDms( - limit: limit, order: order, consentState: consent) + limit: limit, consentState: consent) var results: [String] = [] for dm in dmList { @@ -561,7 +560,6 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( conversationParams ?? "") - let order = getConversationSortOrder(order: sortOrder ?? "") let consent: ConsentState? if let state = consentState { consent = try getConsentState(state: state) @@ -569,7 +567,7 @@ public class XMTPModule: Module { consent = nil } let conversations = try await client.conversations.list( - limit: limit, order: order, consentState: consent) + limit: limit, consentState: consent) var results: [String] = [] for conversation in conversations { @@ -621,7 +619,7 @@ public class XMTPModule: Module { return messages.compactMap { msg in do { - return try DecodedMessageWrapper.encode(msg, client: client) + return try MessageWrapper.encode(msg, client: client) } catch { print( "discarding message, unable to encode wrapper \(msg.id)" @@ -639,8 +637,8 @@ public class XMTPModule: Module { throw Error.noClient } if let message = try client.findMessage(messageId: messageId) { - return try DecodedMessageWrapper.encode( - message.decode(), client: client) + return try MessageWrapper.encode( + message, client: client) } else { return nil } @@ -1528,7 +1526,7 @@ public class XMTPModule: Module { AsyncFunction("processMessage") { (installationId: String, id: String, encryptedMessage: String) - -> String in + -> String? in guard let client = await clientsManager.getClient(key: installationId) else { @@ -1549,10 +1547,14 @@ public class XMTPModule: Module { else { throw Error.noMessage } - let decodedMessage = try await conversation.processMessage( + if let decodedMessage = try await conversation.processMessage( messageBytes: encryptedMessageData) - return try DecodedMessageWrapper.encode( - decodedMessage.decode(), client: client) + { + return try MessageWrapper.encode( + decodedMessage, client: client) + } else { + return nil + } } AsyncFunction("processWelcomeMessage") { @@ -1690,6 +1692,13 @@ public class XMTPModule: Module { state: getConsentState(state: state)) } + AsyncFunction("subscribeToPreferenceUpdates") { + (installationId: String) in + + try await subscribeToPreferenceUpdates( + installationId: installationId) + } + AsyncFunction("subscribeToConsent") { (installationId: String) in @@ -1718,6 +1727,13 @@ public class XMTPModule: Module { installationId: installationId, id: id) } + AsyncFunction("unsubscribeFromPreferenceUpdates") { + (installationId: String) in + await subscriptionsManager.get( + getPreferenceUpdatesKey(installationId: installationId))? + .cancel() + } + AsyncFunction("unsubscribeFromConsent") { (installationId: String) in await subscriptionsManager.get( getConsentKey(installationId: installationId))? @@ -1899,6 +1915,14 @@ public class XMTPModule: Module { } } + private func getPreferenceUpdatesType(type: PreferenceType) throws -> String + { + switch type { + case .hmac_keys: + return "hmac_keys" + } + } + func createApiClient(env: String, appVersion: String? = nil) -> XMTP.ClientOptions.Api { @@ -1940,6 +1964,40 @@ public class XMTPModule: Module { historySyncUrl: authOptions.historySyncUrl) } + func subscribeToPreferenceUpdates(installationId: String) + async throws + { + guard let client = await clientsManager.getClient(key: installationId) + else { + return + } + + await subscriptionsManager.get( + getPreferenceUpdatesKey(installationId: installationId))? + .cancel() + await subscriptionsManager.set( + getPreferenceUpdatesKey(installationId: installationId), + Task { + do { + for try await pref in await client.preferences + .streamPreferenceUpdates() + { + try sendEvent( + "consent", + [ + "installationId": installationId, + "type": getPreferenceUpdatesType(type: pref), + ]) + } + } catch { + print("Error in preference subscription: \(error)") + await subscriptionsManager.get( + getPreferenceUpdatesKey(installationId: installationId))? + .cancel() + } + }) + } + func subscribeToConsent(installationId: String) async throws { @@ -2033,7 +2091,7 @@ public class XMTPModule: Module { "message", [ "installationId": installationId, - "message": DecodedMessageWrapper.encodeToObj( + "message": MessageWrapper.encodeToObj( message, client: client), ]) } @@ -2072,7 +2130,7 @@ public class XMTPModule: Module { [ "installationId": installationId, "message": - DecodedMessageWrapper.encodeToObj( + MessageWrapper.encodeToObj( message, client: client), "conversationId": id, ]) @@ -2109,6 +2167,10 @@ public class XMTPModule: Module { .cancel() } + func getPreferenceUpdatesKey(installationId: String) -> String { + return "preferences:\(installationId)" + } + func getConsentKey(installationId: String) -> String { return "consent:\(installationId)" } From 26520773a9e320b4c91ba64d37650a9e70c905d4 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 7 Jan 2025 22:22:25 -0800 Subject: [PATCH 10/11] get android compiling --- .../wrappers/InboxStateWrapper.kt | 2 +- .../wrappers/PermissionPolicySetWrapper.kt | 4 ++-- example/src/tests/conversationTests.ts | 23 +++++++++++-------- ios/XMTPModule.swift | 8 +++---- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt index 609c273b9..092795804 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt @@ -2,7 +2,7 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.Gson import com.google.gson.GsonBuilder -import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState +import org.xmtp.android.library.libxmtp.InboxState class InboxStateWrapper { companion object { diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt index 1e5c10b38..176b86bb2 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt @@ -2,8 +2,8 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder import com.google.gson.JsonParser -import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption -import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet +import org.xmtp.android.library.libxmtp.PermissionOption +import org.xmtp.android.library.libxmtp.PermissionPolicySet class PermissionPolicySetWrapper { diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index c92f05551..43485a182 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -2,6 +2,7 @@ import { content } from '@xmtp/proto' import { Wallet } from 'ethers' import ReactNativeBlobUtil from 'react-native-blob-util' import RNFS from 'react-native-fs' +import { PreferenceUpdates } from 'xmtp-react-native-sdk/lib/PrivatePreferences' import { Test, assert, createClients, delayToPropogate } from './test-utils' import { @@ -12,7 +13,6 @@ import { ConversationVersion, JSContentCodec, } from '../../../src/index' -import { PreferenceUpdates } from 'xmtp-react-native-sdk/lib/PrivatePreferences' export const conversationTests: Test[] = [] let counter = 1 @@ -476,8 +476,8 @@ test('can list conversations with params', async () => { `List length should be 1 but was ${boGroupsLimit.length}` ) assert( - boGroupsLimit[0].id === boGroup1.id, - `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` + boGroupsLimit[0].id === boGroup2.id, + `Group should be ${boGroup2.id} but was ${boGroupsLimit[0].id}` ) return true @@ -509,10 +509,10 @@ test('can list groups', async () => { ) if ( - boConversations[0].topic !== boGroup.topic || - boConversations[0].version !== ConversationVersion.GROUP || - boConversations[2].version !== ConversationVersion.DM || - boConversations[2].createdAt !== boDm.createdAt + boConversations[2].topic !== boGroup.topic || + boConversations[2].version !== ConversationVersion.GROUP || + boConversations[0].version !== ConversationVersion.DM || + boConversations[0].createdAt !== boDm.createdAt ) { throw Error('Listed containers should match streamed containers') } @@ -981,8 +981,9 @@ test('can preference updates', async () => { }) const types = [] - await alix.preferences.streamPreferenceUpdates(async (entry: PreferenceUpdates) => { - types.push(entry) + await alix.preferences.streamPreferenceUpdates( + async (entry: PreferenceUpdates) => { + types.push(entry) }) const alix2 = await Client.create(alixWallet, { @@ -992,8 +993,10 @@ test('can preference updates', async () => { dbDirectory: dbDirPath2, }) - await alix.conversations.syncAllConversations() await alix2.conversations.syncAllConversations() + await delayToPropogate(2000) + await alix.conversations.syncAllConversations() + await delayToPropogate(2000) assert( types.length === 1, diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 7d47c9d83..762e7cd61 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -486,7 +486,6 @@ public class XMTPModule: Module { AsyncFunction("listGroups") { ( installationId: String, groupParams: String?, - sortOrder: String?, limit: Int?, consentState: String? ) -> [String] in guard @@ -518,7 +517,6 @@ public class XMTPModule: Module { AsyncFunction("listDms") { ( installationId: String, groupParams: String?, - sortOrder: String?, limit: Int?, consentState: String? ) -> [String] in guard @@ -550,7 +548,7 @@ public class XMTPModule: Module { AsyncFunction("listConversations") { ( installationId: String, conversationParams: String?, - sortOrder: String?, limit: Int?, consentState: String? + limit: Int?, consentState: String? ) -> [String] in guard let client = await clientsManager.getClient(key: installationId) @@ -586,7 +584,7 @@ public class XMTPModule: Module { else { throw Error.noClient } - let hmacKeys = await client.conversations.getHmacKeys() + let hmacKeys = try await client.conversations.getHmacKeys() return try [UInt8](hmacKeys.serializedData()) } @@ -1778,7 +1776,7 @@ public class XMTPModule: Module { else { throw Error.noClient } - let hmacKeysResult = await client.conversations.getHmacKeys() + let hmacKeysResult = try await client.conversations.getHmacKeys() let subscriptions = topics.map { topic -> NotificationSubscription in let hmacKeys = hmacKeysResult.hmacKeys From fc50cab1345a15c4a57fa3c9b284dfafa2310625 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 8 Jan 2025 08:02:01 -0800 Subject: [PATCH 11/11] get all the tests passing --- example/src/tests/conversationTests.ts | 16 ++++++++-------- ios/XMTPModule.swift | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 43485a182..15f66aa2b 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -476,8 +476,8 @@ test('can list conversations with params', async () => { `List length should be 1 but was ${boGroupsLimit.length}` ) assert( - boGroupsLimit[0].id === boGroup2.id, - `Group should be ${boGroup2.id} but was ${boGroupsLimit[0].id}` + boGroupsLimit[0].id === boDm1.id, + `Group should be ${boDm1.id} but was ${boGroupsLimit[0].id}` ) return true @@ -491,8 +491,8 @@ test('can list groups', async () => { caroClient.address, alixClient.address, ]) - const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) - await boClient.conversations.findOrCreateDm(alixClient.address) + await boClient.conversations.findOrCreateDm(caroClient.address) + const boDm = await boClient.conversations.findOrCreateDm(alixClient.address) const boConversations = await boClient.conversations.list() await alixClient.conversations.sync() @@ -509,8 +509,8 @@ test('can list groups', async () => { ) if ( - boConversations[2].topic !== boGroup.topic || - boConversations[2].version !== ConversationVersion.GROUP || + boConversations[3].topic !== boGroup.topic || + boConversations[3].version !== ConversationVersion.GROUP || boConversations[0].version !== ConversationVersion.DM || boConversations[0].createdAt !== boDm.createdAt ) { @@ -999,8 +999,8 @@ test('can preference updates', async () => { await delayToPropogate(2000) assert( - types.length === 1, - `Expected 1 preference update, got ${types.length}` + types.length === 2, + `Expected 2 preference update, got ${types.length}` ) alix.preferences.cancelStreamConsent() diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 762e7cd61..16605264c 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -1981,7 +1981,7 @@ public class XMTPModule: Module { .streamPreferenceUpdates() { try sendEvent( - "consent", + "preferences", [ "installationId": installationId, "type": getPreferenceUpdatesType(type: pref),