From c6978588022c84343cf07e888f36fdaf783a26ae Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 9 Apr 2024 17:07:43 -0700 Subject: [PATCH 001/201] bump to get message kind filtering --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 6a7b95668..cbc7729df 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -101,7 +101,7 @@ dependencies { 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" - implementation "org.xmtp:android:0.9.1" + implementation "org.xmtp:android:0.10.0" // xmtp-android local testing setup below (comment org.xmtp:android above) // implementation files('/xmtp-android/library/build/outputs/aar/library-debug.aar') // implementation 'com.google.crypto.tink:tink-android:1.7.0' From 670ba0a2cfee2391da6968814eeeef68b0c604e4 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Fri, 12 Apr 2024 01:16:05 -0700 Subject: [PATCH 002/201] group name functionality added --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 30 +++++++++++-- example/src/tests/groupTests.ts | 42 +++++++++++++++++++ ios/XMTPModule.swift | 24 +++++++++++ src/index.ts | 15 +++++++ src/lib/Group.ts | 12 ++++++ 5 files changed, 119 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index ca64d7822..32776785c 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -794,6 +794,26 @@ class XMTPModule : Module() { } } + AsyncFunction("groupName") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("groupName") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.name + } + } + + AsyncFunction("updateGroupName") Coroutine { clientAddress: String, id: String, groupName: String -> + withContext(Dispatchers.IO) { + logV("updateGroupName") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.updateGroupName(groupName) + } + } + AsyncFunction("isGroupActive") Coroutine { clientAddress: String, id: String -> withContext(Dispatchers.IO) { logV("isGroupActive") @@ -1002,10 +1022,12 @@ class XMTPModule : Module() { } } - AsyncFunction("refreshConsentList") { clientAddress: String -> - val client = clients[clientAddress] ?: throw XMTPException("No client") - val consentList = client.contacts.refreshConsentList() - consentList.entries.map { ConsentWrapper.encode(it.value) } + AsyncFunction("refreshConsentList") Coroutine { clientAddress: String -> + withContext(Dispatchers.IO) { + val client = clients[clientAddress] ?: throw XMTPException("No client") + val consentList = client.contacts.refreshConsentList() + consentList.entries.map { ConsentWrapper.encode(it.value) } + } } AsyncFunction("conversationConsentState") Coroutine { clientAddress: String, conversationTopic: String -> diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index e83e4ce67..e4c73c927 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -1265,6 +1265,48 @@ test('skipSync parameter behaves as expected', async () => { return true }) +test('can read and update group name', async () => { + const [alix, bo, caro] = await createClients(3) + const alixGroup = await alix.conversations.newGroup([bo.address]) + + let groupName = await alixGroup.groupName() + + assert(groupName === 'New Group', 'group name should be "New Group"') + + await alixGroup.updateGroupName('Test name update 1') + + groupName = await alixGroup.groupName() + + assert( + groupName === 'Test name update 1', + 'group name should be "Test name update 1"' + ) + + const boGroup = (await bo.conversations.listGroups())[0] + groupName = await boGroup.groupName(true) + + assert(groupName === 'New Group', 'group name should be "New Group"') + + await boGroup.sync() + + groupName = await boGroup.groupName(true) + + assert( + groupName === 'Test name update 1', + 'group name should be "Test name update 1"' + ) + + await alixGroup.addMembers([caro.address]) + const caroGroup = (await caro.conversations.listGroups())[0] + + groupName = await caroGroup.groupName(true) + assert( + groupName === 'Test name update 1', + 'group name should be "Test name update 1"' + ) + return true +}) + // Commenting this out so it doesn't block people, but nice to have? // test('can stream messages for a long time', async () => { // const bo = await Client.createRandom({ env: 'local', enableAlphaMls: true }) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index cdb90dd3d..3d3e5ad25 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -692,6 +692,30 @@ public class XMTPModule: Module { try await group.removeMembers(addresses: peerAddresses) } + + AsyncFunction("groupName") { (clientAddress: String, id: String) -> String in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + + return try group.groupName() + } + + AsyncFunction("updateGroupName") { (clientAddress: String, id: String, groupName: String) in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + + try await group.updateGroupName(groupName: groupName) + } AsyncFunction("isGroupActive") { (clientAddress: String, id: String) -> Bool in guard let client = await clientsManager.getClient(key: clientAddress) else { diff --git a/src/index.ts b/src/index.ts index 7fcb861f4..70c6d6087 100644 --- a/src/index.ts +++ b/src/index.ts @@ -218,6 +218,21 @@ export async function removeGroupMembers( return XMTPModule.removeGroupMembers(clientAddress, id, addresses) } +export function groupName( + address: string, + id: string +): string | PromiseLike { + return XMTPModule.groupName(address, id) +} + +export function updateGroupName( + address: string, + id: string, + groupName: string +): Promise { + return XMTPModule.updateGroupName(address, id, groupName) +} + export async function sign( clientAddress: string, digest: Uint8Array, diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 28a90a957..326754add 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -196,6 +196,18 @@ export class Group< return XMTP.removeGroupMembers(this.client.address, this.id, addresses) } + async groupName(skipSync = false): Promise { + if (!skipSync) { + await this.sync() + } + + return XMTP.groupName(this.client.address, this.id) + } + + async updateGroupName(groupName: string): Promise { + return XMTP.updateGroupName(this.client.address, this.id, groupName) + } + async isActive(skipSync = false): Promise { if (!skipSync) { await this.sync() From 839304b453df19b368c2d06db8464fe6080f923c Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 12 Apr 2024 10:23:36 -0700 Subject: [PATCH 003/201] bump pod --- example/ios/Podfile.lock | 16 ++++++++-------- ios/XMTPReactNative.podspec | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 31e6dfa8d..f40874768 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -56,7 +56,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (0.4.3-beta4) + - LibXMTP (0.4.3-beta5) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.4): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.9.6): + - XMTP (0.10.1): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.4.3-beta4) + - LibXMTP (= 0.4.3-beta5) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.9.6) + - XMTP (= 0.10.1) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 3813a1f6c0cc2b4cd57dc4805c80da92e4b4bcab + LibXMTP: 9580b03bff217acb4f173c08cd55cf5935d4f3f3 Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74 @@ -763,10 +763,10 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 603eecf511ce63f6390b2468fc78500373c1e786 - XMTPReactNative: 54fa7119379885089f695af928594d5720666ef6 + XMTP: 28a385a7390d5df8f39f02c0615dc2ccb11bbbc9 + XMTPReactNative: a0a01706e94fd35763204cbd6f4d3a43c1a244df Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.2 diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 41295693a..1df5163e0 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.9.6" + s.dependency "XMTP", "= 0.10.1" end From dc7e17d619c4571838e1548cdde3c91a23c9b6fd Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 12 Apr 2024 10:34:39 -0700 Subject: [PATCH 004/201] update pod lock --- example/ios/Podfile.lock | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c283c65f9..f40874768 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -449,13 +449,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) -<<<<<<< HEAD - XMTP (0.10.1): -||||||| 89b6e78 - - XMTP (0.9.6): -======= - - XMTP (0.10.0): ->>>>>>> 1ee9fccd269fa13fd9f9c276bbd2ffe207913c1d - Connect-Swift (= 0.12.0) - GzipSwift - LibXMTP (= 0.4.3-beta5) @@ -464,13 +458,7 @@ PODS: - ExpoModulesCore - MessagePacker - secp256k1.swift -<<<<<<< HEAD - XMTP (= 0.10.1) -||||||| 89b6e78 - - XMTP (= 0.9.6) -======= - - XMTP (= 0.10.0) ->>>>>>> 1ee9fccd269fa13fd9f9c276bbd2ffe207913c1d - Yoga (1.14.0) DEPENDENCIES: From 4f5433f6a49573458027734ef20e46876a00129f Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 18 Apr 2024 10:38:50 -0700 Subject: [PATCH 005/201] undo bad merge --- .eslintrc.js | 8 + .github/workflows/tsc.yml | 13 + .gitignore | 1 + README.md | 4 + .../modules/xmtpreactnativesdk/XMTPModule.kt | 508 ++++- .../wrappers/ContentJson.kt | 19 + .../wrappers/ConversationContainerWrapper.kt | 29 + .../wrappers/ConversationWrapper.kt | 2 +- .../wrappers/GroupWrapper.kt | 38 + example/App.tsx | 6 + example/ios/Podfile.lock | 30 +- example/package.json | 5 +- example/src/ConversationCreateScreen.tsx | 37 +- example/src/ConversationScreen.tsx | 45 +- example/src/GroupScreen.tsx | 1185 +++++++++++ example/src/HomeScreen.tsx | 166 +- example/src/LaunchScreen.tsx | 398 ++-- example/src/Navigation.tsx | 5 +- example/src/StreamScreen.tsx | 14 +- example/src/TestScreen.tsx | 82 +- example/src/contentTypes/contentTypes.ts | 17 + example/src/hooks.tsx | 236 ++- example/src/tests/createdAtTests.ts | 433 ++++ example/src/tests/groupTests.ts | 1305 ++++++++++++ example/src/tests/restartStreamsTests.ts | 212 ++ example/src/tests/test-utils.ts | 43 + example/src/tests/tests.ts | 1815 +++++++++++++++++ example/src/types/typeTests.ts | 159 ++ example/yarn.lock | 41 +- .../ConversationContainerWrapper.swift | 30 + ios/Wrappers/ConversationWrapper.swift | 2 +- ios/Wrappers/DecodedMessageWrapper.swift | 19 +- ios/Wrappers/GroupWrapper.swift | 40 + ios/XMTPModule.swift | 529 ++++- package.json | 2 + src/hooks/useClient.ts | 9 +- src/index.ts | 342 +++- src/lib/Client.ts | 139 +- src/lib/Contacts.ts | 18 +- src/lib/ContentCodec.ts | 115 +- src/lib/Conversation.ts | 68 +- src/lib/ConversationContainer.ts | 16 + src/lib/Conversations.ts | 261 ++- src/lib/DecodedMessage.ts | 30 +- src/lib/Group.ts | 224 ++ src/lib/NativeCodecs/GroupChangeCodec.ts | 30 + src/lib/NativeCodecs/ReactionCodec.ts | 2 +- src/lib/NativeCodecs/RemoteAttachmentCodec.ts | 2 +- src/lib/NativeCodecs/ReplyCodec.ts | 10 +- src/lib/NativeCodecs/StaticAttachmentCodec.ts | 2 +- src/lib/NativeCodecs/TextCodec.ts | 2 +- src/lib/XMTPPush.ts | 10 +- src/lib/types/ContentCodec.ts | 122 ++ src/lib/types/ConversationCodecs.ts | 15 + src/lib/types/DefaultContentType.ts | 3 + src/lib/types/EventTypes.ts | 40 + src/lib/types/ExtractDecodedType.ts | 3 + src/lib/types/SendOptions.ts | 5 + src/lib/types/index.ts | 4 + 59 files changed, 8370 insertions(+), 580 deletions(-) create mode 100644 .github/workflows/tsc.yml create mode 100644 android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt create mode 100644 android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt create mode 100644 example/src/GroupScreen.tsx create mode 100644 example/src/contentTypes/contentTypes.ts create mode 100644 example/src/tests/createdAtTests.ts create mode 100644 example/src/tests/groupTests.ts create mode 100644 example/src/tests/restartStreamsTests.ts create mode 100644 example/src/tests/test-utils.ts create mode 100644 example/src/tests/tests.ts create mode 100644 example/src/types/typeTests.ts create mode 100644 ios/Wrappers/ConversationContainerWrapper.swift create mode 100644 ios/Wrappers/GroupWrapper.swift create mode 100644 src/lib/ConversationContainer.ts create mode 100644 src/lib/Group.ts create mode 100644 src/lib/NativeCodecs/GroupChangeCodec.ts create mode 100644 src/lib/types/ContentCodec.ts create mode 100644 src/lib/types/ConversationCodecs.ts create mode 100644 src/lib/types/DefaultContentType.ts create mode 100644 src/lib/types/EventTypes.ts create mode 100644 src/lib/types/ExtractDecodedType.ts create mode 100644 src/lib/types/SendOptions.ts create mode 100644 src/lib/types/index.ts diff --git a/.eslintrc.js b/.eslintrc.js index c4b0b9ab3..e42a86b98 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,9 +1,17 @@ module.exports = { root: true, + parser: "@typescript-eslint/parser", + parserOptions: { + project: ["./tsconfig.json", "./example/tsconfig.json"] + }, + plugins: ["@typescript-eslint"], extends: ['universe/native', 'universe/web'], ignorePatterns: ['build'], plugins: ['prettier'], globals: { __dirname: true, }, + rules: { + "@typescript-eslint/no-floating-promises": ["error"], + }, } diff --git a/.github/workflows/tsc.yml b/.github/workflows/tsc.yml new file mode 100644 index 000000000..5d64a66f5 --- /dev/null +++ b/.github/workflows/tsc.yml @@ -0,0 +1,13 @@ +name: Typescript +on: + pull_request: +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + - run: yarn + - run: yarn tsc diff --git a/.gitignore b/.gitignore index 84068a05d..b9ec20389 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ android/keystores/debug.keystore # Typedocs docs/ +**/.yarn/* \ No newline at end of file diff --git a/README.md b/README.md index 6ff2146ce..53977260a 100644 --- a/README.md +++ b/README.md @@ -437,3 +437,7 @@ The `env` parameter accepts one of three valid values: `dev`, `production`, or ` - `local`: Use to have a client communicate with an XMTP node you are running locally. For example, an XMTP node developer can set `env` to `local` to generate client traffic to test a node running locally. The `production` network is configured to store messages indefinitely. XMTP may occasionally delete messages and keys from the `dev` network, and will provide advance notice in the [XMTP Discord community](https://discord.gg/xmtp). + +## Enabling group chat + +Coming soon... \ No newline at end of file diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index cf53ec4a4..856716b28 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -1,5 +1,6 @@ package expo.modules.xmtpreactnativesdk +import android.content.Context import android.net.Uri import android.util.Base64 import android.util.Base64.NO_WRAP @@ -7,6 +8,7 @@ import android.util.Log import androidx.core.net.toUri import com.google.gson.JsonParser import com.google.protobuf.kotlin.toByteString +import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.functions.Coroutine import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition @@ -17,6 +19,8 @@ import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment +import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper +import expo.modules.xmtpreactnativesdk.wrappers.ConversationContainerWrapper import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -30,6 +34,7 @@ import org.json.JSONObject import org.xmtp.android.library.Client import org.xmtp.android.library.ClientOptions import org.xmtp.android.library.Conversation +import org.xmtp.android.library.Group import org.xmtp.android.library.PreEventCallback import org.xmtp.android.library.PreparedMessage import org.xmtp.android.library.SendOptions @@ -49,15 +54,20 @@ import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.Signature import org.xmtp.android.library.messages.getPublicKeyBundle import org.xmtp.android.library.push.XMTPPush +import org.xmtp.android.library.toHex import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData import org.xmtp.proto.message.api.v1.MessageApiOuterClass import org.xmtp.proto.message.contents.PrivateKeyOuterClass +import uniffi.xmtpv3.GroupPermissions import java.io.File import java.util.Date import java.util.UUID import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException +import com.facebook.common.util.Hex +import org.xmtp.android.library.messages.Topic +import org.xmtp.android.library.push.Service class ReactNativeSigner(var module: XMTPModule, override var address: String) : SigningKey { private val continuations: MutableMap> = mutableMapOf() @@ -101,7 +111,15 @@ fun Conversation.cacheKey(clientAddress: String): String { return "${clientAddress}:${topic}" } +fun Group.cacheKey(clientAddress: String): String { + return "${clientAddress}:${id}" +} + class XMTPModule : Module() { + + val context: Context + get() = appContext.reactContext ?: throw Exceptions.ReactContextLost() + private fun apiEnvironments(env: String, appVersion: String?): ClientOptions.Api { return when (env) { "local" -> ClientOptions.Api( @@ -129,6 +147,7 @@ class XMTPModule : Module() { private var signer: ReactNativeSigner? = null private val isDebugEnabled = BuildConfig.DEBUG // TODO: consider making this configurable private val conversations: MutableMap = mutableMapOf() + private val groups: MutableMap = mutableMapOf() private val subscriptions: MutableMap = mutableMapOf() private var preEnableIdentityCallbackDeferred: CompletableDeferred? = null private var preCreateIdentityCallbackDeferred: CompletableDeferred? = null @@ -137,12 +156,22 @@ class XMTPModule : Module() { override fun definition() = ModuleDefinition { Name("XMTP") Events( + // Auth "sign", "authed", + "preCreateIdentityCallback", + "preEnableIdentityCallback", + // Conversations "conversation", + "group", + "conversationContainer", "message", - "preEnableIdentityCallback", - "preCreateIdentityCallback" + "allGroupMessage", + // Conversation + "conversationMessage", + // Group + "groupMessage" + ) Function("address") { clientAddress: String -> @@ -151,11 +180,17 @@ class XMTPModule : Module() { client?.address ?: "No Client." } + AsyncFunction("deleteLocalDatabase") { clientAddress: String -> + val client = clients[clientAddress] ?: throw XMTPException("No client") + client.deleteLocalDatabase() + } + // // Auth functions // - AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? -> + AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean?, dbEncryptionKey: List?, dbPath: String? -> logV("auth") + requireNotProductionEnvForAlphaMLS(enableAlphaMls, environment) val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) signer = reactSigner @@ -167,10 +202,20 @@ class XMTPModule : Module() { preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } val preEnableIdentityCallback: PreEventCallback? = preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } + val context = if (enableAlphaMls == true) context else null + val encryptionKeyBytes = + dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v -> + a.apply { set(i, v.toByte()) } + } + val options = ClientOptions( api = apiEnvironments(environment, appVersion), preCreateIdentityCallback = preCreateIdentityCallback, - preEnableIdentityCallback = preEnableIdentityCallback + preEnableIdentityCallback = preEnableIdentityCallback, + enableAlphaMls = enableAlphaMls == true, + appContext = context, + dbEncryptionKey = encryptionKeyBytes, + dbPath = dbPath ) clients[address] = Client().create(account = reactSigner, options = options) ContentJson.Companion @@ -184,8 +229,9 @@ class XMTPModule : Module() { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? -> + AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean?, dbEncryptionKey: List?, dbPath: String? -> logV("createRandom") + requireNotProductionEnvForAlphaMLS(enableAlphaMls, environment) val privateKey = PrivateKeyBuilder() if (hasCreateIdentityCallback == true) @@ -196,11 +242,20 @@ class XMTPModule : Module() { preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } val preEnableIdentityCallback: PreEventCallback? = preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } + val context = if (enableAlphaMls == true) context else null + val encryptionKeyBytes = + dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v -> + a.apply { set(i, v.toByte()) } + } val options = ClientOptions( api = apiEnvironments(environment, appVersion), preCreateIdentityCallback = preCreateIdentityCallback, - preEnableIdentityCallback = preEnableIdentityCallback + preEnableIdentityCallback = preEnableIdentityCallback, + enableAlphaMls = enableAlphaMls == true, + appContext = context, + dbEncryptionKey = encryptionKeyBytes, + dbPath = dbPath ) val randomClient = Client().create(account = privateKey, options = options) ContentJson.Companion @@ -208,10 +263,23 @@ class XMTPModule : Module() { randomClient.address } - AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String? -> + AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String?, enableAlphaMls: Boolean?, dbEncryptionKey: List?, dbPath: String? -> + logV("createFromKeyBundle") + requireNotProductionEnvForAlphaMLS(enableAlphaMls, environment) + try { - logV("createFromKeyBundle") - val options = ClientOptions(api = apiEnvironments(environment, appVersion)) + val context = if (enableAlphaMls == true) context else null + val encryptionKeyBytes = + dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v -> + a.apply { set(i, v.toByte()) } + } + val options = ClientOptions( + api = apiEnvironments(environment, appVersion), + enableAlphaMls = enableAlphaMls == true, + appContext = context, + dbEncryptionKey = encryptionKeyBytes, + dbPath = dbPath + ) val bundle = PrivateKeyOuterClass.PrivateKeyBundle.parseFrom( Base64.decode( @@ -305,6 +373,12 @@ class XMTPModule : Module() { client.canMessage(peerAddress) } + AsyncFunction("canGroupMessage") { clientAddress: String, peerAddresses: List -> + logV("canGroupMessage") + val client = clients[clientAddress] ?: throw XMTPException("No client") + client.canMessageV3(peerAddresses) + } + AsyncFunction("staticCanMessage") { peerAddress: String, environment: String, appVersion: String? -> try { logV("staticCanMessage") @@ -407,6 +481,29 @@ class XMTPModule : Module() { } } + AsyncFunction("listGroups") Coroutine { clientAddress: String -> + withContext(Dispatchers.IO) { + logV("listGroups") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val groupList = client.conversations.listGroups() + groupList.map { group -> + groups[group.cacheKey(clientAddress)] = group + GroupWrapper.encode(client, group) + } + } + } + + AsyncFunction("listAll") Coroutine { clientAddress: String -> + withContext(Dispatchers.IO) { + val client = clients[clientAddress] ?: throw XMTPException("No client") + val conversationContainerList = client.conversations.list(includeGroups = true) + conversationContainerList.map { conversation -> + conversations[conversation.cacheKey(clientAddress)] = conversation + ConversationContainerWrapper.encode(client, conversation) + } + } + } + AsyncFunction("loadMessages") Coroutine { clientAddress: String, topic: String, limit: Int?, before: Long?, after: Long?, direction: String? -> withContext(Dispatchers.IO) { logV("loadMessages") @@ -430,6 +527,24 @@ class XMTPModule : Module() { } } + AsyncFunction("groupMessages") Coroutine { clientAddress: String, id: String, limit: Int?, before: Long?, after: Long?, direction: String? -> + withContext(Dispatchers.IO) { + logV("groupMessages") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val beforeDate = if (before != null) Date(before) else null + val afterDate = if (after != null) Date(after) else null + val group = findGroup(clientAddress, id) + group?.decryptedMessages( + limit = limit, + before = beforeDate, + after = afterDate, + direction = MessageApiOuterClass.SortDirection.valueOf( + direction ?: "SORT_DIRECTION_DESCENDING" + ) + )?.map { DecodedMessageWrapper.encode(it) } + } + } + AsyncFunction("loadBatchMessages") Coroutine { clientAddress: String, topics: List -> withContext(Dispatchers.IO) { logV("loadBatchMessages") @@ -494,6 +609,23 @@ class XMTPModule : Module() { } } + AsyncFunction("sendMessageToGroup") Coroutine { clientAddress: String, id: String, contentJson: String -> + withContext(Dispatchers.IO) { + logV("sendMessageToGroup") + val group = + findGroup( + clientAddress = clientAddress, + id = id + ) + ?: throw XMTPException("no group found for $id") + val sending = ContentJson.fromJson(contentJson) + group.send( + content = sending.content, + options = SendOptions(contentType = sending.type) + ) + } + } + AsyncFunction("prepareMessage") Coroutine { clientAddress: String, conversationTopic: String, contentJson: String -> withContext(Dispatchers.IO) { logV("prepareMessage") @@ -603,15 +735,131 @@ class XMTPModule : Module() { ConversationWrapper.encode(client, conversation) } } + AsyncFunction("createGroup") Coroutine { clientAddress: String, peerAddresses: List, permission: String -> + withContext(Dispatchers.IO) { + logV("createGroup") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val permissionLevel = when (permission) { + "creator_admin" -> GroupPermissions.GROUP_CREATOR_IS_ADMIN + else -> GroupPermissions.EVERYONE_IS_ADMIN + } + val group = client.conversations.newGroup(peerAddresses, permissionLevel) + GroupWrapper.encode(client, group) + } + } + + AsyncFunction("listMemberAddresses") Coroutine { clientAddress: String, groupId: String -> + withContext(Dispatchers.IO) { + logV("listMembers") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, groupId) + group?.memberAddresses() + } + } + + AsyncFunction("syncGroups") Coroutine { clientAddress: String -> + withContext(Dispatchers.IO) { + logV("syncGroups") + val client = clients[clientAddress] ?: throw XMTPException("No client") + client.conversations.syncGroups() + } + } + + AsyncFunction("syncGroup") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("syncGroup") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + group?.sync() + } + } + + AsyncFunction("addGroupMembers") Coroutine { clientAddress: String, id: String, peerAddresses: List -> + withContext(Dispatchers.IO) { + logV("addGroupMembers") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.addMembers(peerAddresses) + } + } + + AsyncFunction("removeGroupMembers") Coroutine { clientAddress: String, id: String, peerAddresses: List -> + withContext(Dispatchers.IO) { + logV("removeGroupMembers") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.removeMembers(peerAddresses) + } + } + + AsyncFunction("isGroupActive") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("isGroupActive") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.isActive() + } + } + + AsyncFunction("isGroupAdmin") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("isGroupAdmin") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.isAdmin() + } + } + + AsyncFunction("processGroupMessage") Coroutine { clientAddress: String, id: String, encryptedMessage: String -> + withContext(Dispatchers.IO) { + logV("processGroupMessage") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + val message = group?.processMessage(Base64.decode(encryptedMessage, NO_WRAP)) + ?: throw XMTPException("could not decrypt message for $id") + DecodedMessageWrapper.encodeMap(message.decrypt()) + } + } + + AsyncFunction("processWelcomeMessage") Coroutine { clientAddress: String, encryptedMessage: String -> + withContext(Dispatchers.IO) { + logV("processWelcomeMessage") + val client = clients[clientAddress] ?: throw XMTPException("No client") + + val group = + client.conversations.fromWelcome(Base64.decode(encryptedMessage, NO_WRAP)) + GroupWrapper.encode(client, group) + } + } Function("subscribeToConversations") { clientAddress: String -> logV("subscribeToConversations") subscribeToConversations(clientAddress = clientAddress) } - Function("subscribeToAllMessages") { clientAddress: String -> + Function("subscribeToGroups") { clientAddress: String -> + logV("subscribeToGroups") + subscribeToGroups(clientAddress = clientAddress) + } + + Function("subscribeToAll") { clientAddress: String -> + logV("subscribeToAll") + subscribeToAll(clientAddress = clientAddress) + } + + Function("subscribeToAllMessages") { clientAddress: String, includeGroups: Boolean -> logV("subscribeToAllMessages") - subscribeToAllMessages(clientAddress = clientAddress) + subscribeToAllMessages(clientAddress = clientAddress, includeGroups = includeGroups) + } + + Function("subscribeToAllGroupMessages") { clientAddress: String -> + logV("subscribeToAllGroupMessages") + subscribeToAllGroupMessages(clientAddress = clientAddress) } AsyncFunction("subscribeToMessages") Coroutine { clientAddress: String, topic: String -> @@ -624,16 +872,36 @@ class XMTPModule : Module() { } } + AsyncFunction("subscribeToGroupMessages") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("subscribeToGroupMessages") + subscribeToGroupMessages( + clientAddress = clientAddress, + id = id + ) + } + } + Function("unsubscribeFromConversations") { clientAddress: String -> logV("unsubscribeFromConversations") subscriptions[getConversationsKey(clientAddress)]?.cancel() } + Function("unsubscribeFromGroups") { clientAddress: String -> + logV("unsubscribeFromGroups") + subscriptions[getGroupsKey(clientAddress)]?.cancel() + } + Function("unsubscribeFromAllMessages") { clientAddress: String -> logV("unsubscribeFromAllMessages") subscriptions[getMessagesKey(clientAddress)]?.cancel() } + Function("unsubscribeFromAllGroupMessages") { clientAddress: String -> + logV("unsubscribeFromAllGroupMessages") + subscriptions[getGroupMessagesKey(clientAddress)]?.cancel() + } + AsyncFunction("unsubscribeFromMessages") Coroutine { clientAddress: String, topic: String -> withContext(Dispatchers.IO) { logV("unsubscribeFromMessages") @@ -644,19 +912,50 @@ class XMTPModule : Module() { } } + AsyncFunction("unsubscribeFromGroupMessages") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("unsubscribeFromGroupMessages") + unsubscribeFromGroupMessages( + clientAddress = clientAddress, + id = id + ) + } + } + Function("registerPushToken") { pushServer: String, token: String -> logV("registerPushToken") xmtpPush = XMTPPush(appContext.reactContext!!, pushServer) xmtpPush?.register(token) } - Function("subscribePushTopics") { topics: List -> + Function("subscribePushTopics") { clientAddress: String, topics: List -> logV("subscribePushTopics") if (topics.isNotEmpty()) { if (xmtpPush == null) { throw XMTPException("Push server not registered") } - xmtpPush?.subscribe(topics) + val client = clients[clientAddress] ?: 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() + } + + xmtpPush?.subscribeWithMetadata(subscriptions) } } @@ -733,6 +1032,32 @@ class XMTPModule : Module() { logV("preEnableIdentityCallbackCompleted") preEnableIdentityCallbackDeferred?.complete(Unit) } + + AsyncFunction("allowGroups") Coroutine { clientAddress: String, groupIds: List -> + logV("allowGroups") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val groupDataIds = groupIds.mapNotNull { Hex.hexStringToByteArray(it) } + client.contacts.allowGroup(groupDataIds) + } + + AsyncFunction("denyGroups") Coroutine { clientAddress: String, groupIds: List -> + logV("denyGroups") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val groupDataIds = groupIds.mapNotNull { Hex.hexStringToByteArray(it) } + client.contacts.denyGroup(groupDataIds) + } + + AsyncFunction("isGroupAllowed") { clientAddress: String, groupId: String -> + logV("isGroupAllowed") + val client = clients[clientAddress] ?: throw XMTPException("No client") + client.contacts.isGroupAllowed(Hex.hexStringToByteArray(groupId)) + } + + AsyncFunction("isGroupDenied") { clientAddress: String, groupId: String -> + logV("isGroupDenied") + val client = clients[clientAddress] ?: throw XMTPException("No client") + client.contacts.isGroupDenied(Hex.hexStringToByteArray(groupId)) + } } // @@ -760,6 +1085,27 @@ class XMTPModule : Module() { return null } + private suspend fun findGroup( + clientAddress: String, + id: String, + ): Group? { + val client = clients[clientAddress] ?: throw XMTPException("No client") + + val cacheKey = "${clientAddress}:${id}" + val cacheGroup = groups[cacheKey] + if (cacheGroup != null) { + return cacheGroup + } else { + val group = client.conversations.listGroups() + .firstOrNull { it.id.toHex() == id } + if (group != null) { + groups[group.cacheKey(clientAddress)] = group + return group + } + } + return null + } + private fun subscribeToConversations(clientAddress: String) { val client = clients[clientAddress] ?: throw XMTPException("No client") @@ -790,15 +1136,85 @@ class XMTPModule : Module() { } } - private fun subscribeToAllMessages(clientAddress: String) { + private fun subscribeToGroups(clientAddress: String) { + val client = clients[clientAddress] ?: throw XMTPException("No client") + + subscriptions[getGroupsKey(clientAddress)]?.cancel() + subscriptions[getGroupsKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { + try { + client.conversations.streamGroups().collect { group -> + sendEvent( + "group", + mapOf( + "clientAddress" to clientAddress, + "group" to GroupWrapper.encodeToObj(client, group) + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in group subscription: $e") + subscriptions[getGroupsKey(clientAddress)]?.cancel() + } + } + } + + private fun subscribeToAll(clientAddress: String) { + val client = clients[clientAddress] ?: throw XMTPException("No client") + + subscriptions[getConversationsKey(clientAddress)]?.cancel() + subscriptions[getConversationsKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { + try { + client.conversations.streamAll().collect { conversation -> + sendEvent( + "conversationContainer", + mapOf( + "clientAddress" to clientAddress, + "conversationContainer" to ConversationContainerWrapper.encodeToObj( + client, + conversation + ) + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in subscription to groups + conversations: $e") + subscriptions[getConversationsKey(clientAddress)]?.cancel() + } + } + } + + private fun subscribeToAllMessages(clientAddress: String, includeGroups: Boolean = false) { val client = clients[clientAddress] ?: throw XMTPException("No client") subscriptions[getMessagesKey(clientAddress)]?.cancel() subscriptions[getMessagesKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { try { - client.conversations.streamAllDecryptedMessages().collect { message -> + client.conversations.streamAllDecryptedMessages(includeGroups = includeGroups) + .collect { message -> + sendEvent( + "message", + mapOf( + "clientAddress" to clientAddress, + "message" to DecodedMessageWrapper.encodeMap(message), + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in all messages subscription: $e") + subscriptions[getMessagesKey(clientAddress)]?.cancel() + } + } + } + + private fun subscribeToAllGroupMessages(clientAddress: String) { + val client = clients[clientAddress] ?: throw XMTPException("No client") + + subscriptions[getGroupMessagesKey(clientAddress)]?.cancel() + subscriptions[getGroupMessagesKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { + try { + client.conversations.streamAllGroupDecryptedMessages().collect { message -> sendEvent( - "message", + "allGroupMessage", mapOf( "clientAddress" to clientAddress, "message" to DecodedMessageWrapper.encodeMap(message), @@ -806,8 +1222,8 @@ class XMTPModule : Module() { ) } } catch (e: Exception) { - Log.e("XMTPModule", "Error in all messages subscription: $e") - subscriptions[getMessagesKey(clientAddress)]?.cancel() + Log.e("XMTPModule", "Error in all group messages subscription: $e") + subscriptions[getGroupMessagesKey(clientAddress)]?.cancel() } } } @@ -824,10 +1240,11 @@ class XMTPModule : Module() { try { conversation.streamDecryptedMessages().collect { message -> sendEvent( - "message", + "conversationMessage", mapOf( "clientAddress" to clientAddress, "message" to DecodedMessageWrapper.encodeMap(message), + "topic" to topic, ) ) } @@ -838,14 +1255,49 @@ class XMTPModule : Module() { } } + private suspend fun subscribeToGroupMessages(clientAddress: String, id: String) { + val group = + findGroup( + clientAddress = clientAddress, + id = id + ) ?: return + subscriptions[group.cacheKey(clientAddress)]?.cancel() + subscriptions[group.cacheKey(clientAddress)] = + CoroutineScope(Dispatchers.IO).launch { + try { + group.streamDecryptedMessages().collect { message -> + sendEvent( + "groupMessage", + mapOf( + "clientAddress" to clientAddress, + "message" to DecodedMessageWrapper.encodeMap(message), + "groupId" to id, + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in messages subscription: $e") + subscriptions[group.cacheKey(clientAddress)]?.cancel() + } + } + } + private fun getMessagesKey(clientAddress: String): String { return "messages:$clientAddress" } + private fun getGroupMessagesKey(clientAddress: String): String { + return "groupMessages:$clientAddress" + } + private fun getConversationsKey(clientAddress: String): String { return "conversations:$clientAddress" } + private fun getGroupsKey(clientAddress: String): String { + return "groups:$clientAddress" + } + private suspend fun unsubscribeFromMessages( clientAddress: String, topic: String, @@ -858,6 +1310,18 @@ class XMTPModule : Module() { subscriptions[conversation.cacheKey(clientAddress)]?.cancel() } + private suspend fun unsubscribeFromGroupMessages( + clientAddress: String, + id: String, + ) { + val conversation = + findGroup( + clientAddress = clientAddress, + id = id + ) ?: return + subscriptions[conversation.cacheKey(clientAddress)]?.cancel() + } + private fun logV(msg: String) { if (isDebugEnabled) { Log.v("XMTPModule", msg) @@ -875,6 +1339,12 @@ class XMTPModule : Module() { preCreateIdentityCallbackDeferred?.await() preCreateIdentityCallbackDeferred = null } + + private fun requireNotProductionEnvForAlphaMLS(enableAlphaMls: Boolean?, environment: String) { + if (enableAlphaMls == true && (environment == "production")) { + throw XMTPException("Environment must be \"local\" or \"dev\" to enable alpha MLS") + } + } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt index b46015697..ece84a14b 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt @@ -30,6 +30,9 @@ import org.xmtp.android.library.codecs.description import org.xmtp.android.library.codecs.getReactionAction import org.xmtp.android.library.codecs.getReactionSchema import org.xmtp.android.library.codecs.id +import uniffi.xmtpv3.org.xmtp.android.library.codecs.ContentTypeGroupMembershipChange +import uniffi.xmtpv3.org.xmtp.android.library.codecs.GroupMembershipChangeCodec +import uniffi.xmtpv3.org.xmtp.android.library.codecs.GroupMembershipChanges import java.net.URL class ContentJson( @@ -51,6 +54,7 @@ class ContentJson( Client.register(RemoteAttachmentCodec()) Client.register(ReplyCodec()) Client.register(ReadReceiptCodec()) + Client.register(GroupMembershipChangeCodec()) } fun fromJsonObject(obj: JsonObject): ContentJson { @@ -171,6 +175,21 @@ class ContentJson( "readReceipt" to "" ) + ContentTypeGroupMembershipChange.id -> mapOf( + "groupChange" to mapOf( + "membersAdded" to (content as GroupMembershipChanges).membersAddedList.map { + mapOf( + "address" to it.accountAddress, + "initiatedByAddress" to it.initiatedByAccountAddress + )}, + "membersRemoved" to content.membersRemovedList.map { + mapOf( + "address" to it.accountAddress, + "initiatedByAddress" to it.initiatedByAccountAddress + )}, + ) + ) + else -> { val json = JsonObject() encodedContent?.let { diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt new file mode 100644 index 000000000..1af77351b --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt @@ -0,0 +1,29 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import android.util.Base64 +import com.google.gson.GsonBuilder +import org.xmtp.android.library.Client +import org.xmtp.android.library.Conversation + +class ConversationContainerWrapper { + + companion object { + fun encodeToObj(client: Client, conversation: Conversation): Map { + when (conversation.version) { + Conversation.Version.GROUP -> { + val group = (conversation as Conversation.Group).group + return GroupWrapper.encodeToObj(client, group) + } + else -> { + return ConversationWrapper.encodeToObj(client, conversation) + } + } + } + + fun encode(client: Client, conversation: Conversation): String { + val gson = GsonBuilder().create() + val obj = ConversationContainerWrapper.encodeToObj(client, conversation) + return gson.toJson(obj) + } + } +} diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt index 6d31ccb8f..efb0e3a01 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt @@ -24,7 +24,7 @@ class ConversationWrapper { "context" to context, "topic" to conversation.topic, "peerAddress" to conversation.peerAddress, - "version" to if (conversation.version == Conversation.Version.V1) "v1" else "v2", + "version" to "DIRECT", "conversationID" to (conversation.conversationId ?: ""), "keyMaterial" to (conversation.keyMaterial?.let { Base64.encodeToString(it, Base64.NO_WRAP) } ?: "") ) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt new file mode 100644 index 000000000..0529773f0 --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -0,0 +1,38 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import android.util.Base64 +import android.util.Base64.NO_WRAP +import com.google.gson.GsonBuilder +import org.xmtp.android.library.Client +import org.xmtp.android.library.Conversation +import org.xmtp.android.library.Group +import org.xmtp.android.library.toHex +import uniffi.xmtpv3.GroupPermissions + +class GroupWrapper { + + companion object { + fun encodeToObj(client: Client, group: Group): Map { + val permissionString = when (group.permissionLevel()) { + GroupPermissions.EVERYONE_IS_ADMIN -> "everyone_admin" + GroupPermissions.GROUP_CREATOR_IS_ADMIN -> "creator_admin" + } + return mapOf( + "clientAddress" to client.address, + "id" to group.id.toHex(), + "createdAt" to group.createdAt.time, + "peerAddresses" to Conversation.Group(group).peerAddresses, + "version" to "GROUP", + "topic" to group.id.toHex(), + "permissionLevel" to permissionString, + "adminAddress" to group.adminAddress() + ) + } + + fun encode(client: Client, group: Group): String { + val gson = GsonBuilder().create() + val obj = encodeToObj(client, group) + return gson.toJson(obj) + } + } +} \ No newline at end of file diff --git a/example/App.tsx b/example/App.tsx index 2a7f8b531..f6c70c434 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -14,6 +14,7 @@ import { XmtpProvider } from 'xmtp-react-native-sdk' import ConversationCreateScreen from './src/ConversationCreateScreen' import ConversationScreen from './src/ConversationScreen' +import GroupScreen from './src/GroupScreen' import HomeScreen from './src/HomeScreen' import LaunchScreen from './src/LaunchScreen' import { Navigator } from './src/Navigation' @@ -90,6 +91,11 @@ export default function App() { options={{ title: 'Conversation' }} initialParams={{ topic: '' }} /> + >>>>>> 4c62517c62f6834c39018fb37c1fb0c24341d863 - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.4): @@ -350,6 +344,8 @@ PODS: - RCTTypeSafety - React-Core - ReactCommon/turbomodule/core + - react-native-sqlite-storage (6.0.1): + - React-Core - react-native-webview (13.8.1): - RCT-Folly (= 2021.07.22.00) - React-Core @@ -439,6 +435,8 @@ PODS: - React-perflogger (= 0.71.14) - RNCAsyncStorage (1.21.0): - React-Core + - RNFS (2.20.0): + - React-Core - RNScreens (3.20.0): - React-Core - React-RCTImage @@ -451,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.10.3): + - XMTP (0.10.1): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.4.4-beta2) + - LibXMTP (= 0.4.3-beta5) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.10.3) + - XMTP (= 0.10.1) - Yoga (1.14.0) DEPENDENCIES: @@ -511,6 +509,7 @@ DEPENDENCIES: - react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`) - react-native-randombytes (from `../node_modules/react-native-randombytes`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`) - react-native-webview (from `../node_modules/react-native-webview`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -526,6 +525,7 @@ DEPENDENCIES: - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - RNFS (from `../node_modules/react-native-fs`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) - XMTPReactNative (from `../../ios`) @@ -640,6 +640,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-randombytes" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" + react-native-sqlite-storage: + :path: "../node_modules/react-native-sqlite-storage" react-native-webview: :path: "../node_modules/react-native-webview" React-perflogger: @@ -670,6 +672,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" + RNFS: + :path: "../node_modules/react-native-fs" RNScreens: :path: "../node_modules/react-native-screens" RNSVG: @@ -707,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 0c073613451e3850bfcaaab5438b481fe887cd97 + LibXMTP: 9580b03bff217acb4f173c08cd55cf5935d4f3f3 Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74 @@ -737,6 +741,7 @@ SPEC CHECKSUMS: react-native-quick-crypto: 455c1b411db006dba1026a30681ececb19180187 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc + react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261 react-native-webview: bdc091de8cf7f8397653e30182efcd9f772e03b3 React-perflogger: 4987ad83731c23d11813c84263963b0d3028c966 React-RCTActionSheet: 5ad952b2a9740d87a5bd77280c4bc23f6f89ea0c @@ -752,13 +757,14 @@ SPEC CHECKSUMS: React-runtimeexecutor: ffe826b7b1cfbc32a35ed5b64d5886c0ff75f501 ReactCommon: 7f3dd5e98a9ec627c6b03d26c062bf37ea9fc888 RNCAsyncStorage: 618d03a5f52fbccb3d7010076bc54712844c18ef + RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: bf00ef58d4fbcc8ab740145a6303591adf3fb355 - XMTPReactNative: fae44562e5e457c66fde8e2613e1dfd3c1a71113 + XMTP: 28a385a7390d5df8f39f02c0615dc2ccb11bbbc9 + XMTPReactNative: a0a01706e94fd35763204cbd6f4d3a43c1a244df Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 diff --git a/example/package.json b/example/package.json index ed42a73b2..3391a849e 100644 --- a/example/package.json +++ b/example/package.json @@ -30,13 +30,16 @@ "react-native-config": "^1.5.1", "react-native-crypto": "^2.2.0", "react-native-encrypted-storage": "^4.0.3", - "react-native-get-random-values": "^1.10.0", + "react-native-fs": "^2.20.0", + "react-native-get-random-values": "^1.11.0", "react-native-mmkv": "^2.8.0", + "react-native-modal-selector": "^2.1.2", "react-native-quick-base64": "^2.0.8", "react-native-quick-crypto": "^0.6.1", "react-native-randombytes": "^3.6.1", "react-native-safe-area-context": "4.5.0", "react-native-screens": "~3.20.0", + "react-native-sqlite-storage": "^6.0.1", "react-native-svg": "^13.9.0", "react-native-url-polyfill": "^2.0.0", "react-native-webview": "^13.8.1", diff --git a/example/src/ConversationCreateScreen.tsx b/example/src/ConversationCreateScreen.tsx index 9f0676dc9..6973bb6ac 100644 --- a/example/src/ConversationCreateScreen.tsx +++ b/example/src/ConversationCreateScreen.tsx @@ -1,6 +1,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useState } from 'react' -import { Button, ScrollView, Text, TextInput } from 'react-native' +import { Button, ScrollView, Switch, Text, TextInput, View } from 'react-native' import { useXmtp } from 'xmtp-react-native-sdk' import { NavigationParamList } from './Navigation' @@ -13,19 +13,33 @@ export default function ConversationCreateScreen({ const [alert, setAlert] = useState('') const [isCreating, setCreating] = useState(false) const { client } = useXmtp() + const [groupsEnabled, setGroupsEnabled] = useState(false) + const startNewConversation = async (toAddress: string) => { if (!client) { setAlert('Client not initialized') return } - const canMessage = await client.canMessage(toAddress) - if (!canMessage) { - setAlert(`${toAddress} is not on the XMTP network yet`) - return + if (groupsEnabled) { + const toAddresses = toAddress.split(',') + const canMessage = await client.canGroupMessage(toAddresses) + if (!canMessage) { + setAlert(`${toAddress} cannot be added to a group conversation yet`) + return + } + const group = await client.conversations.newGroup(toAddresses) + navigation.navigate('group', { id: group.id }) + } else { + const canMessage = await client.canMessage(toAddress) + if (!canMessage) { + setAlert(`${toAddress} is not on the XMTP network yet`) + return + } + const convo = await client.conversations.newConversation(toAddress) + navigation.navigate('conversation', { topic: convo.topic }) } - const convo = await client.conversations.newConversation(toAddress) - navigation.navigate('conversation', { topic: convo.topic }) } + return ( <> @@ -48,6 +62,15 @@ export default function ConversationCreateScreen({ opacity: isCreating ? 0.5 : 1, }} /> + + + setGroupsEnabled((previousState) => !previousState) + } + /> + Create Group: {groupsEnabled ? 'ON' : 'OFF'} +