diff --git a/android/build.gradle b/android/build.gradle index 782930a1b..15baf9138 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:0.11.3" + implementation "org.xmtp:android:0.12.0" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 820881c4b..60e6d3ccd 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -113,8 +113,8 @@ fun Conversation.cacheKey(clientAddress: String): String { return "${clientAddress}:${topic}" } -fun Group.cacheKey(clientAddress: String): String { - return "${clientAddress}:${id}" +fun Group.cacheKey(inboxId: String): String { + return "${inboxId}:${id}" } class XMTPModule : Module() { @@ -182,15 +182,33 @@ class XMTPModule : Module() { client?.address ?: "No Client." } + Function("inboxId") { clientAddress: String -> + logV("inboxId") + val client = clients[clientAddress] + client?.inboxId ?: "No Client." + } + AsyncFunction("deleteLocalDatabase") { clientAddress: String -> val client = clients[clientAddress] ?: throw XMTPException("No client") client.deleteLocalDatabase() } + Function("dropLocalDatabaseConnection") { clientAddress: String -> + val client = clients[clientAddress] ?: throw XMTPException("No client") + client.dropLocalDatabaseConnection() + } + + AsyncFunction("reconnectLocalDatabase") Coroutine { clientAddress: String -> + withContext(Dispatchers.IO) { + val client = clients[clientAddress] ?: throw XMTPException("No client") + client.reconnectLocalDatabase() + } + } + // // Auth functions // - AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean?, dbEncryptionKey: List?, dbPath: String? -> + AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean?, dbEncryptionKey: List? -> logV("auth") requireNotProductionEnvForAlphaMLS(enableAlphaMls, environment) val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) @@ -217,12 +235,12 @@ class XMTPModule : Module() { enableAlphaMls = enableAlphaMls == true, appContext = context, dbEncryptionKey = encryptionKeyBytes, - dbPath = dbPath ) - clients[address] = Client().create(account = reactSigner, options = options) + val client = Client().create(account = reactSigner, options = options) + clients[address] = client ContentJson.Companion signer = null - sendEvent("authed") + sendEvent("authed", mapOf("inboxId" to client.inboxId)) } Function("receiveSignature") { requestID: String, signature: String -> @@ -231,7 +249,7 @@ class XMTPModule : Module() { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean?, dbEncryptionKey: List?, dbPath: String? -> + AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean?, dbEncryptionKey: List? -> logV("createRandom") requireNotProductionEnvForAlphaMLS(enableAlphaMls, environment) val privateKey = PrivateKeyBuilder() @@ -257,15 +275,17 @@ class XMTPModule : Module() { enableAlphaMls = enableAlphaMls == true, appContext = context, dbEncryptionKey = encryptionKeyBytes, - dbPath = dbPath ) val randomClient = Client().create(account = privateKey, options = options) ContentJson.Companion clients[randomClient.address] = randomClient - randomClient.address + mapOf ( + "address" to randomClient.address, + "inboxId" to randomClient.inboxId + ) } - AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String?, enableAlphaMls: Boolean?, dbEncryptionKey: List?, dbPath: String? -> + AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String?, enableAlphaMls: Boolean?, dbEncryptionKey: List? -> logV("createFromKeyBundle") requireNotProductionEnvForAlphaMLS(enableAlphaMls, environment) @@ -280,7 +300,6 @@ class XMTPModule : Module() { enableAlphaMls = enableAlphaMls == true, appContext = context, dbEncryptionKey = encryptionKeyBytes, - dbPath = dbPath ) val bundle = PrivateKeyOuterClass.PrivateKeyBundle.parseFrom( @@ -292,7 +311,10 @@ class XMTPModule : Module() { val client = Client().buildFromBundle(bundle = bundle, options = options) ContentJson.Companion clients[client.address] = client - client.address + mapOf ( + "address" to client.address, + "inboxId" to client.inboxId + ) } catch (e: Exception) { throw XMTPException("Failed to create client: $e") } @@ -721,14 +743,15 @@ class XMTPModule : Module() { var consentProof: ConsentProofPayload? = null if (consentProofPayload.isNotEmpty()) { - val consentProofDataBytes = consentProofPayload.foldIndexed(ByteArray(consentProofPayload.size)) { i, a, v -> - a.apply { - set( - i, - v.toByte() - ) + val consentProofDataBytes = + consentProofPayload.foldIndexed(ByteArray(consentProofPayload.size)) { i, a, v -> + a.apply { + set( + i, + v.toByte() + ) + } } - } consentProof = ConsentProofPayload.parseFrom(consentProofDataBytes) } @@ -765,20 +788,20 @@ class XMTPModule : Module() { 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 + "admin_only" -> GroupPermissions.ADMIN_ONLY + else -> GroupPermissions.ALL_MEMBERS } val group = client.conversations.newGroup(peerAddresses, permissionLevel) GroupWrapper.encode(client, group) } } - AsyncFunction("listMemberAddresses") Coroutine { clientAddress: String, groupId: String -> + AsyncFunction("listMemberInboxIds") 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() + group?.members()?.map { it.inboxId } } } @@ -819,6 +842,26 @@ class XMTPModule : Module() { } } + AsyncFunction("addGroupMembersByInboxId") Coroutine { clientAddress: String, id: String, peerInboxIds: List -> + withContext(Dispatchers.IO) { + logV("addGroupMembersByInboxId") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.addMembersByInboxId(peerInboxIds) + } + } + + AsyncFunction("removeGroupMembersByInboxId") Coroutine { clientAddress: String, id: String, peerInboxIds: List -> + withContext(Dispatchers.IO) { + logV("removeGroupMembersByInboxId") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.removeMembersByInboxId(peerInboxIds) + } + } + AsyncFunction("groupName") Coroutine { clientAddress: String, id: String -> withContext(Dispatchers.IO) { logV("groupName") @@ -849,12 +892,12 @@ class XMTPModule : Module() { } } - AsyncFunction("addedByAddress") Coroutine { clientAddress: String, id: String -> + AsyncFunction("addedByInboxId") Coroutine { clientAddress: String, id: String -> withContext(Dispatchers.IO) { - logV("addedByAddress") + logV("addedByInboxId") val group = findGroup(clientAddress, id) ?: throw XMTPException("No group found") - group.addedByAddress() + group.addedByInboxId() } } @@ -864,7 +907,7 @@ class XMTPModule : Module() { val client = clients[clientAddress] ?: throw XMTPException("No client") val group = findGroup(clientAddress, id) - group?.isAdmin() + group?.isAdmin(client.inboxId) } } @@ -941,9 +984,9 @@ class XMTPModule : Module() { subscriptions[getConversationsKey(clientAddress)]?.cancel() } - Function("unsubscribeFromGroups") { clientAddress: String -> + Function("unsubscribeFromGroups") { inboxId: String -> logV("unsubscribeFromGroups") - subscriptions[getGroupsKey(clientAddress)]?.cancel() + subscriptions[getGroupsKey(inboxId)]?.cancel() } Function("unsubscribeFromAllMessages") { clientAddress: String -> @@ -951,9 +994,9 @@ class XMTPModule : Module() { subscriptions[getMessagesKey(clientAddress)]?.cancel() } - Function("unsubscribeFromAllGroupMessages") { clientAddress: String -> + Function("unsubscribeFromAllGroupMessages") { inboxId: String -> logV("unsubscribeFromAllGroupMessages") - subscriptions[getGroupMessagesKey(clientAddress)]?.cancel() + subscriptions[getGroupMessagesKey(inboxId)]?.cancel() } AsyncFunction("unsubscribeFromMessages") Coroutine { clientAddress: String, topic: String -> @@ -1153,7 +1196,7 @@ class XMTPModule : Module() { ): Group? { val client = clients[clientAddress] ?: throw XMTPException("No client") - val cacheKey = "${clientAddress}:${id}" + val cacheKey = "${client.inboxId}:${id}" val cacheGroup = groups[cacheKey] if (cacheGroup != null) { return cacheGroup @@ -1161,7 +1204,7 @@ class XMTPModule : Module() { val group = client.conversations.listGroups() .firstOrNull { it.id.toHex() == id } if (group != null) { - groups[group.cacheKey(clientAddress)] = group + groups[group.cacheKey(client.inboxId)] = group return group } } @@ -1201,8 +1244,8 @@ class XMTPModule : Module() { private fun subscribeToGroups(clientAddress: String) { val client = clients[clientAddress] ?: throw XMTPException("No client") - subscriptions[getGroupsKey(clientAddress)]?.cancel() - subscriptions[getGroupsKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { + subscriptions[getGroupsKey(client.inboxId)]?.cancel() + subscriptions[getGroupsKey(client.inboxId)] = CoroutineScope(Dispatchers.IO).launch { try { client.conversations.streamGroups().collect { group -> sendEvent( @@ -1215,7 +1258,7 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in group subscription: $e") - subscriptions[getGroupsKey(clientAddress)]?.cancel() + subscriptions[getGroupsKey(client.inboxId)]?.cancel() } } } @@ -1271,8 +1314,8 @@ class XMTPModule : Module() { private fun subscribeToAllGroupMessages(clientAddress: String) { val client = clients[clientAddress] ?: throw XMTPException("No client") - subscriptions[getGroupMessagesKey(clientAddress)]?.cancel() - subscriptions[getGroupMessagesKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { + subscriptions[getGroupMessagesKey(client.inboxId)]?.cancel() + subscriptions[getGroupMessagesKey(client.inboxId)] = CoroutineScope(Dispatchers.IO).launch { try { client.conversations.streamAllGroupDecryptedMessages().collect { message -> sendEvent( @@ -1285,7 +1328,7 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in all group messages subscription: $e") - subscriptions[getGroupMessagesKey(clientAddress)]?.cancel() + subscriptions[getGroupMessagesKey(client.inboxId)]?.cancel() } } } @@ -1318,13 +1361,14 @@ class XMTPModule : Module() { } private suspend fun subscribeToGroupMessages(clientAddress: String, id: String) { + val client = clients[clientAddress] ?: throw XMTPException("No client") val group = findGroup( clientAddress = clientAddress, id = id ) ?: return - subscriptions[group.cacheKey(clientAddress)]?.cancel() - subscriptions[group.cacheKey(clientAddress)] = + subscriptions[group.cacheKey(client.inboxId)]?.cancel() + subscriptions[group.cacheKey(client.inboxId)] = CoroutineScope(Dispatchers.IO).launch { try { group.streamDecryptedMessages().collect { message -> @@ -1339,7 +1383,7 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in messages subscription: $e") - subscriptions[group.cacheKey(clientAddress)]?.cancel() + subscriptions[group.cacheKey(client.inboxId)]?.cancel() } } } @@ -1348,16 +1392,16 @@ class XMTPModule : Module() { return "messages:$clientAddress" } - private fun getGroupMessagesKey(clientAddress: String): String { - return "groupMessages:$clientAddress" + private fun getGroupMessagesKey(inboxId: String): String { + return "groupMessages:$inboxId" } private fun getConversationsKey(clientAddress: String): String { return "conversations:$clientAddress" } - private fun getGroupsKey(clientAddress: String): String { - return "groups:$clientAddress" + private fun getGroupsKey(inboxId: String): String { + return "groups:$inboxId" } private suspend fun unsubscribeFromMessages( @@ -1376,12 +1420,14 @@ class XMTPModule : Module() { clientAddress: String, id: String, ) { - val conversation = + val client = clients[clientAddress] ?: throw XMTPException("No client") + + val group = findGroup( clientAddress = clientAddress, id = id ) ?: return - subscriptions[conversation.cacheKey(clientAddress)]?.cancel() + subscriptions[group.cacheKey(client.inboxId)]?.cancel() } private fun logV(msg: String) { 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 ece84a14b..45e6280f1 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt @@ -9,6 +9,7 @@ import org.xmtp.android.library.Client import org.xmtp.android.library.codecs.Attachment import org.xmtp.android.library.codecs.AttachmentCodec import org.xmtp.android.library.codecs.ContentTypeAttachment +import org.xmtp.android.library.codecs.ContentTypeGroupUpdated import org.xmtp.android.library.codecs.ContentTypeId import org.xmtp.android.library.codecs.ContentTypeReaction import org.xmtp.android.library.codecs.ContentTypeReadReceipt @@ -16,6 +17,8 @@ import org.xmtp.android.library.codecs.ContentTypeRemoteAttachment import org.xmtp.android.library.codecs.ContentTypeReply import org.xmtp.android.library.codecs.ContentTypeText import org.xmtp.android.library.codecs.EncodedContent +import org.xmtp.android.library.codecs.GroupUpdated +import org.xmtp.android.library.codecs.GroupUpdatedCodec import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReadReceipt @@ -30,9 +33,6 @@ 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( @@ -54,7 +54,7 @@ class ContentJson( Client.register(RemoteAttachmentCodec()) Client.register(ReplyCodec()) Client.register(ReadReceiptCodec()) - Client.register(GroupMembershipChangeCodec()) + Client.register(GroupUpdatedCodec()) } fun fromJsonObject(obj: JsonObject): ContentJson { @@ -175,17 +175,17 @@ class ContentJson( "readReceipt" to "" ) - ContentTypeGroupMembershipChange.id -> mapOf( - "groupChange" to mapOf( - "membersAdded" to (content as GroupMembershipChanges).membersAddedList.map { + ContentTypeGroupUpdated.id -> mapOf( + "groupUpdated" to mapOf( + "membersAdded" to (content as GroupUpdated).addedInboxesList.map { mapOf( - "address" to it.accountAddress, - "initiatedByAddress" to it.initiatedByAccountAddress + "inboxId" to it.inboxId, + "initiatedByInboxId" to content.initiatedByInboxId )}, - "membersRemoved" to content.membersRemovedList.map { + "membersRemoved" to content.removedInboxesList.map { mapOf( - "address" to it.accountAddress, - "initiatedByAddress" to it.initiatedByAccountAddress + "inboxId" to it.inboxId, + "initiatedByInboxId" to content.initiatedByInboxId )}, ) ) 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 778cacbe4..357ec1ac5 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -1,7 +1,5 @@ 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 @@ -14,18 +12,19 @@ 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" + GroupPermissions.ALL_MEMBERS -> "all_members" + GroupPermissions.ADMIN_ONLY -> "admin_only" } return mapOf( - "clientAddress" to client.address, "id" to group.id.toHex(), "createdAt" to group.createdAt.time, - "peerAddresses" to Conversation.Group(group).peerAddresses, + "peerInboxIds" to group.peerInboxIds(), "version" to "GROUP", "topic" to group.topic, "permissionLevel" to permissionString, - "adminAddress" to group.adminAddress() + "creatorInboxId" to group.creatorInboxId(), + "name" to group.name, + "isActive" to group.isActive() ) } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 041398a4f..cf4d1d6ee 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.4-beta5) + - LibXMTP (0.5.0-beta1) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.5): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.10.11): + - XMTP (0.11.0): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.4.4-beta5) + - LibXMTP (= 0.5.0-beta1) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.10.11) + - XMTP (= 0.11.0) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: e2fb601691981900099551ff3e05621bd73dccf1 + LibXMTP: 38800678de153b444c5a9b41a991080ee33e11ed Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 506311d0494023c2f7e0b62cc1f31b7370fa3cfb @@ -763,10 +763,10 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 1deb40ac712ba315dcfdecd590a9b924d8c2241a - XMTPReactNative: a7d7c609861e0b31ca7f624f58969dcb92c19990 + XMTP: 2fb42dccbf89949175e99309f337ec11d828b989 + XMTPReactNative: 53915a0488e11be25777aa0b96d7091fa3c3c05b Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.2 diff --git a/example/src/GroupScreen.tsx b/example/src/GroupScreen.tsx index c55116d26..38c2a5357 100644 --- a/example/src/GroupScreen.tsx +++ b/example/src/GroupScreen.tsx @@ -31,7 +31,7 @@ import { StaticAttachmentContent, ReplyContent, useClient, - GroupChangeContent, + GroupUpdatedContent, } from 'xmtp-react-native-sdk' import { ConversationSendPayload } from 'xmtp-react-native-sdk/lib/types' @@ -1074,16 +1074,16 @@ function formatAddress(address: string) { return `${address.slice(0, 6)}…${address.slice(-4)}` } -function GroupChangeContents({ content }: { content: GroupChangeContent }) { +function GroupUpdatedContents({ content }: { content: GroupUpdatedContent }) { return ( <> {content.membersAdded.length > 0 && (content.membersAdded.length === 1 ? ( {`${formatAddress( - content.membersAdded[0].address + content.membersAdded[0].inboxId )} has been added by ${formatAddress( - content.membersAdded[0].initiatedByAddress + content.membersAdded[0].initiatedByInboxId )}`} ) : ( @@ -1091,7 +1091,7 @@ function GroupChangeContents({ content }: { content: GroupChangeContent }) { {`${ content.membersAdded.length } members have been added by ${formatAddress( - content.membersAdded[0].initiatedByAddress + content.membersAdded[0].initiatedByInboxId )}`} ))} @@ -1099,9 +1099,9 @@ function GroupChangeContents({ content }: { content: GroupChangeContent }) { (content.membersRemoved.length === 1 ? ( {`${formatAddress( - content.membersRemoved[0].address + content.membersRemoved[0].inboxId )} has been removed by ${formatAddress( - content.membersRemoved[0].initiatedByAddress + content.membersRemoved[0].initiatedByInboxId )}`} ) : ( @@ -1109,7 +1109,7 @@ function GroupChangeContents({ content }: { content: GroupChangeContent }) { {`${ content.membersRemoved.length } members have been removed by ${formatAddress( - content.membersRemoved[0].initiatedByAddress + content.membersRemoved[0].initiatedByInboxId )}`} ))} @@ -1172,7 +1172,7 @@ function MessageContents({ ) } if (contentTypeId === 'xmtp.org/group_membership_change:1.0') { - return + return } return ( diff --git a/example/src/contentTypes/contentTypes.ts b/example/src/contentTypes/contentTypes.ts index 966ce8f90..51b3e1e74 100644 --- a/example/src/contentTypes/contentTypes.ts +++ b/example/src/contentTypes/contentTypes.ts @@ -1,5 +1,5 @@ import { - GroupChangeCodec, + GroupUpdatedCodec, ReactionCodec, ReplyCodec, RemoteAttachmentCodec, @@ -11,7 +11,7 @@ export const supportedCodecs = [ new ReplyCodec(), new RemoteAttachmentCodec(), new StaticAttachmentCodec(), - new GroupChangeCodec(), + new GroupUpdatedCodec(), ] export type SupportedContentTypes = typeof supportedCodecs diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 7736e62cd..1756c733f 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -1,4 +1,3 @@ -import RNFS from 'react-native-fs' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' import { @@ -68,36 +67,11 @@ test('can delete a local database', async () => { return true }) -test('can make a MLS V3 client with encryption key and database path', async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` - const directoryExists = await RNFS.exists(dbDirPath) - if (!directoryExists) { - await RNFS.mkdir(dbDirPath) - } - const timestamp = Date.now().toString() - const dbPath = `${dbDirPath}/myCoolApp${timestamp}.db3` - - const key = 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 client = await Client.createRandom({ - env: 'local', - appVersion: 'Testing/0.0.0', - enableAlphaMls: true, - dbEncryptionKey: key, - dbPath, - }) - - const anotherClient = await Client.createRandom({ - env: 'local', - appVersion: 'Testing/0.0.0', - enableAlphaMls: true, - dbEncryptionKey: key, - }) +test('can drop a local database', async () => { + const [client, anotherClient] = await createClients(2) - await client.conversations.newGroup([anotherClient.address]) + const group = await client.conversations.newGroup([anotherClient.address]) + await client.conversations.syncGroups() assert( (await client.conversations.listGroups()).length === 1, `should have a group size of 1 but was ${ @@ -105,27 +79,19 @@ test('can make a MLS V3 client with encryption key and database path', async () }` ) - const bundle = await client.exportKeyBundle() - const clientFromBundle = await Client.createFromKeyBundle(bundle, { - env: 'local', - appVersion: 'Testing/0.0.0', - enableAlphaMls: true, - dbEncryptionKey: key, - dbPath, - }) - - assert( - clientFromBundle.address === client.address, - `clients dont match ${client.address} and ${clientFromBundle.address}` - ) + await client.dropLocalDatabaseConnection() - assert( - (await clientFromBundle.conversations.listGroups()).length === 1, - `should have a group size of 1 but was ${ - (await clientFromBundle.conversations.listGroups()).length - }` + try { + await group.send('hi') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + await client.reconnectLocalDatabase() + await group.send('hi') + return true + } + throw new Error( + 'should throw when local database not connected' ) - return true }) test('can make a MLS V3 client from bundle', async () => { @@ -266,11 +232,11 @@ test('who added me to a group', async () => { await boClient.conversations.syncGroups() const boGroup = (await boClient.conversations.listGroups())[0] - const addedByAddress = await boGroup.addedByAddress() + const addedByInboxId = await boGroup.addedByInboxId() assert( - addedByAddress.toLocaleLowerCase === alixClient.address.toLocaleLowerCase, - `addedByAddress ${addedByAddress} does not match ${alixClient.address}` + addedByInboxId === alixClient.inboxId, + `addedByInboxId ${addedByInboxId} does not match ${alixClient.inboxId}` ) return true }) @@ -301,36 +267,30 @@ test('can message in a group', async () => { // alix group should match create time from list function assert(alixGroups[0].createdAt === alixGroup.createdAt, 'group create time') - // alix can confirm memberAddresses + // alix can confirm memberInboxIds await alixGroup.sync() - const memberAddresses = await alixGroup.memberAddresses() - if (memberAddresses.length !== 3) { + const memberInboxIds = await alixGroup.memberInboxIds() + if (memberInboxIds.length !== 3) { throw new Error('num group members should be 3') } - const peerAddresses = await alixGroup.peerAddresses - if (peerAddresses.length !== 2) { + const peerInboxIds = await alixGroup.peerInboxIds + if (peerInboxIds.length !== 2) { throw new Error('num peer group members should be 2') } - const lowercasedAddresses: string[] = memberAddresses.map((s) => - s.toLowerCase() - ) if ( !( - lowercasedAddresses.includes(alixClient.address.toLowerCase()) && - lowercasedAddresses.includes(boClient.address.toLowerCase()) && - lowercasedAddresses.includes(caroClient.address.toLowerCase()) + memberInboxIds.includes(alixClient.inboxId) && + memberInboxIds.includes(boClient.inboxId) && + memberInboxIds.includes(caroClient.inboxId) ) ) { throw new Error('missing address') } - const lowercasedPeerAddresses: string[] = peerAddresses.map((s) => - s.toLowerCase() - ) if ( !( - lowercasedPeerAddresses.includes(boClient.address.toLowerCase()) && - lowercasedPeerAddresses.includes(caroClient.address.toLowerCase()) + peerInboxIds.includes(boClient.inboxId) && + peerInboxIds.includes(caroClient.inboxId) ) ) { throw new Error('should include self') @@ -429,19 +389,16 @@ test('can add members to a group', async () => { throw new Error('num groups should be 1') } - // alix can confirm memberAddresses + // alix can confirm memberInboxIds await alixGroup.sync() - const memberAddresses = await alixGroup.memberAddresses() - if (memberAddresses.length !== 2) { + const memberInboxIds = await alixGroup.memberInboxIds() + if (memberInboxIds.length !== 2) { throw new Error('num group members should be 2') } - const lowercasedAddresses: string[] = memberAddresses.map((s) => - s.toLowerCase() - ) if ( !( - lowercasedAddresses.includes(alixClient.address.toLowerCase()) && - lowercasedAddresses.includes(boClient.address.toLowerCase()) + memberInboxIds.includes(alixClient.inboxId) && + memberInboxIds.includes(boClient.inboxId) ) ) { throw new Error('missing address') @@ -477,7 +434,7 @@ test('can add members to a group', async () => { } await boGroups[0].sync() - const boGroupMembers = await boGroups[0].memberAddresses() + const boGroupMembers = await boGroups[0].memberInboxIds() if (boGroupMembers.length !== 3) { throw new Error('num group members should be 3') } @@ -518,19 +475,16 @@ test('can remove members from a group', async () => { throw new Error('num groups should be 1') } - // alix can confirm memberAddresses + // alix can confirm memberInboxIds await alixGroup.sync() - const memberAddresses = await alixGroup.memberAddresses() - if (memberAddresses.length !== 3) { + const memberInboxIds = await alixGroup.memberInboxIds() + if (memberInboxIds.length !== 3) { throw new Error('num group members should be 3') } - const lowercasedAddresses: string[] = memberAddresses.map((s) => - s.toLowerCase() - ) if ( !( - lowercasedAddresses.includes(alixClient.address.toLowerCase()) && - lowercasedAddresses.includes(boClient.address.toLowerCase()) + memberInboxIds.includes(alixClient.inboxId) && + memberInboxIds.includes(boClient.inboxId) ) ) { throw new Error('missing address') @@ -565,7 +519,7 @@ test('can remove members from a group', async () => { await alixGroup.removeMembers([caroClient.address]) await alixGroup.sync() - const alixGroupMembers = await alixGroup.memberAddresses() + const alixGroupMembers = await alixGroup.memberInboxIds() if (alixGroupMembers.length !== 2) { throw new Error('num group members should be 2') } @@ -575,7 +529,7 @@ test('can remove members from a group', async () => { throw new Error('caros group should not be active') } - const caroGroupMembers = await caroGroups[0].memberAddresses() + const caroGroupMembers = await caroGroups[0].memberInboxIds() if (caroGroupMembers.length !== 2) { throw new Error('num group members should be 2') } @@ -583,6 +537,40 @@ test('can remove members from a group', async () => { return true }) +test('can remove and add members from a group by inbox id', async () => { + // Create three MLS enabled Clients + const [alixClient, boClient, caroClient] = await createClients(3) + + // alix creates a group + const alixGroup = await alixClient.conversations.newGroup([ + boClient.address, + caroClient.address, + ]) + + // alix can confirm memberInboxIds + await alixGroup.sync() + const memberInboxIds = await alixGroup.memberInboxIds() + if (memberInboxIds.length !== 3) { + throw new Error('num group members should be 3') + } + + await alixGroup.removeMembersByInboxId([caroClient.inboxId]) + await alixGroup.sync() + const alixGroupMembers = await alixGroup.memberInboxIds() + if (alixGroupMembers.length !== 2) { + throw new Error('num group members should be 2') + } + + await alixGroup.addMembersByInboxId([caroClient.inboxId]) + await alixGroup.sync() + const alixGroupMembers2 = await alixGroup.memberInboxIds() + if (alixGroupMembers2.length !== 3) { + throw new Error('num group members should be 3') + } + + return true +}) + test('can stream groups', async () => { const [alixClient, boClient, caroClient] = await createClients(3) @@ -889,12 +877,12 @@ test('can make a group with admin permissions', async () => { const group = await adminClient.conversations.newGroup( [anotherClient.address], - 'creator_admin' + 'admin_only' ) - if (group.permissionLevel !== 'creator_admin') { + if (group.permissionLevel !== 'admin_only') { throw Error( - `Group permission level should be creator_admin but was ${group.permissionLevel}` + `Group permission level should be admin_only but was ${group.permissionLevel}` ) } @@ -903,8 +891,10 @@ test('can make a group with admin permissions', async () => { throw Error(`adminClient should be the admin`) } - if (group.adminAddress.toLowerCase !== adminClient.address.toLowerCase) { - throw Error(`adminClient should be the admin but was ${group.adminAddress}`) + if (group.creatorInboxId !== adminClient.inboxId) { + throw Error( + `adminClient should be the admin but was ${group.creatorInboxId}` + ) } return true @@ -1333,7 +1323,7 @@ test('sync function behaves as expected', async () => { assert(boGroups.length === 1, 'num groups for bo is 1') // Num members will include the initial num of members even before sync - let numMembers = (await boGroups[0].memberAddresses()).length + let numMembers = (await boGroups[0].memberInboxIds()).length assert(numMembers === 2, 'num members should be 2') // Num messages for a group will be 0 until we sync the group @@ -1354,18 +1344,18 @@ test('sync function behaves as expected', async () => { await alixGroup.addMembers([caro.address]) - numMembers = (await boGroups[0].memberAddresses()).length + numMembers = (await boGroups[0].memberInboxIds()).length assert(numMembers === 2, 'num members should be 2') await bo.conversations.syncGroups() // Even though we synced the groups, we need to sync the group itself to see the new member - numMembers = (await boGroups[0].memberAddresses()).length + numMembers = (await boGroups[0].memberInboxIds()).length assert(numMembers === 2, 'num members should be 2') await boGroups[0].sync() - numMembers = (await boGroups[0].memberAddresses()).length + numMembers = (await boGroups[0].memberInboxIds()).length assert(numMembers === 3, 'num members should be 3') // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1378,7 +1368,7 @@ test('sync function behaves as expected', async () => { assert(boGroups.length === 2, 'num groups for bo is 2') // Even before syncing the group, syncGroups will return the initial number of members - numMembers = (await boGroups[1].memberAddresses()).length + numMembers = (await boGroups[1].memberInboxIds()).length assert(numMembers === 3, 'num members should be 3') return true diff --git a/ios/Wrappers/ConversationWrapper.swift b/ios/Wrappers/ConversationWrapper.swift index d61100db4..acddcc7b6 100644 --- a/ios/Wrappers/ConversationWrapper.swift +++ b/ios/Wrappers/ConversationWrapper.swift @@ -26,7 +26,7 @@ struct ConversationWrapper { "topic": conversation.topic, "createdAt": UInt64(conversation.createdAt.timeIntervalSince1970 * 1000), "context": context, - "peerAddress": conversation.peerAddress, + "peerAddress": try conversation.peerAddress, "version": "DIRECT", "conversationID": conversation.conversationID ?? "", "keyMaterial": conversation.keyMaterial?.base64EncodedString() ?? "", diff --git a/ios/Wrappers/DecodedMessageWrapper.swift b/ios/Wrappers/DecodedMessageWrapper.swift index 8bc453bdd..c3d1a136d 100644 --- a/ios/Wrappers/DecodedMessageWrapper.swift +++ b/ios/Wrappers/DecodedMessageWrapper.swift @@ -45,7 +45,7 @@ struct ContentJson { ReplyCodec(), RemoteAttachmentCodec(), ReadReceiptCodec(), - GroupMembershipChangedCodec(), + GroupUpdatedCodec(), ] static func initCodecs(client: Client) { @@ -162,19 +162,19 @@ struct ContentJson { ]] case ContentTypeReadReceipt.id where content is XMTP.ReadReceipt: return ["readReceipt": ""] - case ContentTypeGroupMembershipChanged.id where content is XMTP.GroupMembershipChanges: - let groupChange = content as! XMTP.GroupMembershipChanges - return ["groupChange": [ - "membersAdded": groupChange.membersAdded.map { member in + case ContentTypeGroupUpdated.id where content is XMTP.GroupUpdated: + let groupUpdated = content as! XMTP.GroupUpdated + return ["groupUpdated": [ + "membersAdded": groupUpdated.addedInboxes.map { member in [ - "address": member.accountAddress, - "initiatedByAddress": member.initiatedByAccountAddress + "inboxId": member.inboxID, + "initiatedByInboxId": groupUpdated.initiatedByInboxID ] }, - "membersRemoved": groupChange.membersRemoved.map { member in + "membersRemoved": groupUpdated.removedInboxes.map { member in [ - "address": member.accountAddress, - "initiatedByAddress": member.initiatedByAccountAddress + "inboxId": member.inboxID, + "initiatedByInboxId": groupUpdated.initiatedByInboxID ] }, ]] diff --git a/ios/Wrappers/GroupWrapper.swift b/ios/Wrappers/GroupWrapper.swift index f7f7d680b..f0dd86b0e 100644 --- a/ios/Wrappers/GroupWrapper.swift +++ b/ios/Wrappers/GroupWrapper.swift @@ -12,20 +12,22 @@ import XMTP struct GroupWrapper { static func encodeToObj(_ group: XMTP.Group, client: XMTP.Client) throws -> [String: Any] { let permissionString = switch try group.permissionLevel() { - case .everyoneIsAdmin: - "everyone_admin" - case .groupCreatorIsAdmin: - "creator_admin" + case .allMembers: + "all_members" + case .adminOnly: + "admin_only" } return [ "clientAddress": client.address, "id": group.id.toHex, "createdAt": UInt64(group.createdAt.timeIntervalSince1970 * 1000), - "peerAddresses": XMTP.Conversation.group(group).peerAddresses, + "peerInboxIds": try group.peerInboxIds, "version": "GROUP", "topic": group.topic, "permissionLevel": permissionString, - "adminAddress": try group.adminAddress() + "creatorInboxId": try group.creatorInboxId(), + "name": try group.groupName(), + "isActive": try group.isActive() ] } diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 498061ff0..02f1287fa 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -13,12 +13,12 @@ extension Conversation { } extension XMTP.Group { - static func cacheKeyForId(clientAddress: String, id: String) -> String { - return "\(clientAddress):\(id)" + static func cacheKeyForId(inboxId: String, id: String) -> String { + return "\(inboxId):\(id)" } - func cacheKey(_ clientAddress: String) -> String { - return XMTP.Group.cacheKeyForId(clientAddress: clientAddress, id: id.toHex) + func cacheKey(_ inboxId: String) -> String { + return XMTP.Group.cacheKeyForId(inboxId: inboxId, id: id.toHex) } } @@ -90,6 +90,14 @@ public class XMTPModule: Module { return "No Client." } } + + AsyncFunction("inboxId") { (clientAddress: String) -> String in + if let client = await clientsManager.getClient(key: clientAddress) { + return client.inboxID + } else { + return "No Client." + } + } AsyncFunction("deleteLocalDatabase") { (clientAddress: String) in guard let client = await clientsManager.getClient(key: clientAddress) else { @@ -97,11 +105,25 @@ public class XMTPModule: Module { } try client.deleteLocalDatabase() } + + AsyncFunction("dropLocalDatabaseConnection") { (clientAddress: String) in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + try client.dropLocalDatabaseConnection() + } + + AsyncFunction("reconnectLocalDatabase") { (clientAddress: String) in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + try await client.reconnectLocalDatabase() + } // // Auth functions // - AsyncFunction("auth") { (address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, enableAlphaMls: Bool?, dbEncryptionKey: [UInt8]?, dbPath: String?) in + AsyncFunction("auth") { (address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, enableAlphaMls: Bool?, dbEncryptionKey: [UInt8]?) in try requireNotProductionEnvForAlphaMLS(enableAlphaMls: enableAlphaMls, environment: environment) let signer = ReactNativeSigner(module: self, address: address) @@ -116,10 +138,11 @@ public class XMTPModule: Module { let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: enableAlphaMls == true, encryptionKey: encryptionKeyData, dbPath: dbPath) - try await clientsManager.updateClient(key: address, client: await XMTP.Client.create(account: signer, options: options)) + let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: enableAlphaMls == true, encryptionKey: encryptionKeyData) + let client = try await XMTP.Client.create(account: signer, options: options) + await clientsManager.updateClient(key: address, client: client) self.signer = nil - sendEvent("authed") + sendEvent("authed", ["inboxId": client.inboxID]) } Function("receiveSignature") { (requestID: String, signature: String) in @@ -127,7 +150,7 @@ public class XMTPModule: Module { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { (environment: String, appVersion: String?, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, enableAlphaMls: Bool?, dbEncryptionKey: [UInt8]?, dbPath: String?) -> String in + AsyncFunction("createRandom") { (environment: String, appVersion: String?, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, enableAlphaMls: Bool?, dbEncryptionKey: [UInt8]?) -> [String: String] in try requireNotProductionEnvForAlphaMLS(enableAlphaMls: enableAlphaMls, environment: environment) let privateKey = try PrivateKey.generate() @@ -141,15 +164,18 @@ public class XMTPModule: Module { let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: enableAlphaMls == true, encryptionKey: encryptionKeyData, dbPath: dbPath) + let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: enableAlphaMls == true, encryptionKey: encryptionKeyData) let client = try await Client.create(account: privateKey, options: options) await clientsManager.updateClient(key: client.address, client: client) - return client.address + return [ + "address": client.address, + "inboxId": client.inboxID + ] } // Create a client using its serialized key bundle. - AsyncFunction("createFromKeyBundle") { (keyBundle: String, environment: String, appVersion: String?, enableAlphaMls: Bool?, dbEncryptionKey: [UInt8]?, dbPath: String?) -> String in + AsyncFunction("createFromKeyBundle") { (keyBundle: String, environment: String, appVersion: String?, enableAlphaMls: Bool?, dbEncryptionKey: [UInt8]?) -> [String: String] in try requireNotProductionEnvForAlphaMLS(enableAlphaMls: enableAlphaMls, environment: environment) do { @@ -159,10 +185,13 @@ public class XMTPModule: Module { throw Error.invalidKeyBundle } let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let options = createClientConfig(env: environment, appVersion: appVersion, mlsAlpha: enableAlphaMls == true, encryptionKey: encryptionKeyData, dbPath: dbPath) + let options = createClientConfig(env: environment, appVersion: appVersion, mlsAlpha: enableAlphaMls == true, encryptionKey: encryptionKeyData) let client = try await Client.from(bundle: bundle, options: options) await clientsManager.updateClient(key: client.address, client: client) - return client.address + return [ + "address": client.address, + "inboxId": client.inboxID + ] } catch { print("ERRO! Failed to create client: \(error)") throw error @@ -642,10 +671,10 @@ public class XMTPModule: Module { } let permissionLevel: GroupPermissions = { switch permission { - case "creator_admin": - return .groupCreatorIsAdmin + case "admin_only": + return .adminOnly default: - return .everyoneIsAdmin + return .allMembers } }() do { @@ -657,7 +686,7 @@ public class XMTPModule: Module { } } - AsyncFunction("listMemberAddresses") { (clientAddress: String, groupId: String) -> [String] in + AsyncFunction("listMemberInboxIds") { (clientAddress: String, groupId: String) -> [String] in guard let client = await clientsManager.getClient(key: clientAddress) else { throw Error.noClient } @@ -665,7 +694,7 @@ public class XMTPModule: Module { guard let group = try await findGroup(clientAddress: clientAddress, id: groupId) else { throw Error.conversationNotFound("no group found for \(groupId)") } - return group.memberAddresses + return try group.members.map(\.inboxId) } AsyncFunction("syncGroups") { (clientAddress: String) in @@ -708,6 +737,30 @@ public class XMTPModule: Module { try await group.removeMembers(addresses: peerAddresses) } + + AsyncFunction("addGroupMembersByInboxId") { (clientAddress: String, id: String, inboxIds: [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.addMembersByInboxId(inboxIds: inboxIds) + } + + AsyncFunction("removeGroupMembersByInboxId") { (clientAddress: String, id: String, inboxIds: [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.removeMembersByInboxId(inboxIds: inboxIds) + } + AsyncFunction("groupName") { (clientAddress: String, id: String) -> String in guard let client = await clientsManager.getClient(key: clientAddress) else { @@ -744,12 +797,12 @@ public class XMTPModule: Module { return try group.isActive() } - AsyncFunction("addedByAddress") { (clientAddress: String, id: String) -> String in + AsyncFunction("addedByInboxId") { (clientAddress: String, id: String) -> String in guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { throw Error.conversationNotFound("no group found for \(id)") } - return try group.addedByAddress() + return try group.addedByInboxId() } AsyncFunction("isGroupAdmin") { (clientAddress: String, id: String) -> Bool in @@ -760,7 +813,7 @@ public class XMTPModule: Module { throw Error.conversationNotFound("no group found for \(id)") } - return try group.isAdmin() + return try group.isAdmin(inboxId: client.inboxID) } AsyncFunction("processGroupMessage") { (clientAddress: String, id: String, encryptedMessage: String) -> String in @@ -829,8 +882,8 @@ public class XMTPModule: Module { await subscriptionsManager.get(getMessagesKey(clientAddress: clientAddress))?.cancel() } - AsyncFunction("unsubscribeFromAllGroupMessages") { (clientAddress: String) in - await subscriptionsManager.get(getGroupMessagesKey(clientAddress: clientAddress))?.cancel() + AsyncFunction("unsubscribeFromAllGroupMessages") { (inboxId: String) in + await subscriptionsManager.get(getGroupMessagesKey(inboxId: inboxId))?.cancel() } @@ -842,8 +895,8 @@ public class XMTPModule: Module { try await unsubscribeFromGroupMessages(clientAddress: clientAddress, id: id) } - AsyncFunction("unsubscribeFromGroups") { (clientAddress: String) in - await subscriptionsManager.get(getGroupsKey(clientAddress: clientAddress))?.cancel() + AsyncFunction("unsubscribeFromGroups") { (inboxId: String) in + await subscriptionsManager.get(getGroupsKey(inboxId: inboxId))?.cancel() } AsyncFunction("registerPushToken") { (pushServer: String, token: String) in @@ -947,14 +1000,14 @@ public class XMTPModule: Module { guard let conversation = try await findConversation(clientAddress: clientAddress, topic: conversationTopic) else { throw Error.conversationNotFound(conversationTopic) } - return ConsentWrapper.consentStateToString(state: await conversation.consentState()) + return try ConsentWrapper.consentStateToString(state: await conversation.consentState()) } AsyncFunction("groupConsentState") { (clientAddress: String, groupId: String) -> String in guard let group = try await findGroup(clientAddress: clientAddress, id: groupId) else { throw Error.conversationNotFound("no group found for \(groupId)") } - return ConsentWrapper.consentStateToString(state: await XMTP.Conversation.group(group).consentState()) + return try ConsentWrapper.consentStateToString(state: await XMTP.Conversation.group(group).consentState()) } AsyncFunction("consentList") { (clientAddress: String) -> [String] in @@ -1005,7 +1058,7 @@ public class XMTPModule: Module { guard let groupDataId = Data(hex: groupId) else { throw Error.invalidString } - return try await client.contacts.isGroupAllowed(groupId: groupDataId) + return await client.contacts.isGroupAllowed(groupId: groupDataId) } AsyncFunction("isGroupDenied") { (clientAddress: String, groupId: String) -> Bool in @@ -1015,7 +1068,7 @@ public class XMTPModule: Module { guard let groupDataId = Data(hex: groupId) else { throw Error.invalidString } - return try await client.contacts.isGroupDenied(groupId: groupDataId) + return await client.contacts.isGroupDenied(groupId: groupDataId) } } @@ -1023,7 +1076,7 @@ public class XMTPModule: Module { // Helpers // - func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, mlsAlpha: Bool = false, encryptionKey: Data? = nil, dbPath: String? = nil) -> XMTP.ClientOptions { + func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, mlsAlpha: Bool = false, encryptionKey: Data? = nil) -> XMTP.ClientOptions { // Ensure that all codecs have been registered. switch env { case "local": @@ -1031,19 +1084,19 @@ public class XMTPModule: Module { env: XMTP.XMTPEnvironment.local, isSecure: false, appVersion: appVersion - ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: mlsAlpha, mlsEncryptionKey: encryptionKey, mlsDbPath: dbPath) + ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: mlsAlpha, mlsEncryptionKey: encryptionKey) case "production": return XMTP.ClientOptions(api: XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.production, isSecure: true, appVersion: appVersion - ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: false, mlsEncryptionKey: encryptionKey, mlsDbPath: dbPath) + ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: false, mlsEncryptionKey: encryptionKey) default: return XMTP.ClientOptions(api: XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.dev, isSecure: true, appVersion: appVersion - ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: mlsAlpha, mlsEncryptionKey: encryptionKey, mlsDbPath: dbPath) + ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: mlsAlpha, mlsEncryptionKey: encryptionKey) } } @@ -1068,7 +1121,7 @@ public class XMTPModule: Module { throw Error.noClient } - let cacheKey = XMTP.Group.cacheKeyForId(clientAddress: clientAddress, id: id) + let cacheKey = XMTP.Group.cacheKeyForId(inboxId: client.inboxID, id: id) if let group = await groupsManager.get(cacheKey) { return group } else if let group = try await client.conversations.groups().first(where: { $0.id.toHex == id }) { @@ -1131,8 +1184,8 @@ public class XMTPModule: Module { return } - await subscriptionsManager.get(getGroupMessagesKey(clientAddress: clientAddress))?.cancel() - await subscriptionsManager.set(getGroupMessagesKey(clientAddress: clientAddress), Task { + await subscriptionsManager.get(getGroupMessagesKey(inboxId: client.inboxID))?.cancel() + await subscriptionsManager.set(getGroupMessagesKey(inboxId: client.inboxID), Task { do { for try await message in await client.conversations.streamAllGroupDecryptedMessages() { do { @@ -1185,8 +1238,8 @@ public class XMTPModule: Module { guard let client = await clientsManager.getClient(key: clientAddress) else { return } - await subscriptionsManager.get(getGroupsKey(clientAddress: clientAddress))?.cancel() - await subscriptionsManager.set(getGroupsKey(clientAddress: clientAddress), Task { + await subscriptionsManager.get(getGroupsKey(inboxId: client.inboxID))?.cancel() + await subscriptionsManager.set(getGroupsKey(inboxId: client.inboxID), Task { do { for try await group in try await client.conversations.streamGroups() { try sendEvent("group", [ @@ -1196,7 +1249,7 @@ public class XMTPModule: Module { } } catch { print("Error in groups subscription: \(error)") - await subscriptionsManager.get(getGroupsKey(clientAddress: clientAddress))?.cancel() + await subscriptionsManager.get(getGroupsKey(inboxId: client.inboxID))?.cancel() } }) } @@ -1231,8 +1284,8 @@ public class XMTPModule: Module { throw Error.noClient } - await subscriptionsManager.get(group.cacheKey(clientAddress))?.cancel() - await subscriptionsManager.set(group.cacheKey(clientAddress), Task { + await subscriptionsManager.get(group.cacheKey(client.inboxID))?.cancel() + await subscriptionsManager.set(group.cacheKey(client.inboxID), Task { do { for try await message in group.streamDecryptedMessages() { do { @@ -1273,16 +1326,16 @@ public class XMTPModule: Module { return "messages:\(clientAddress)" } - func getGroupMessagesKey(clientAddress: String) -> String { - return "groupMessages:\(clientAddress)" + func getGroupMessagesKey(inboxId: String) -> String { + return "groupMessages:\(inboxId)" } func getConversationsKey(clientAddress: String) -> String { return "conversations:\(clientAddress)" } - func getGroupsKey(clientAddress: String) -> String { - return "groups:\(clientAddress)" + func getGroupsKey(inboxId: String) -> String { + return "groups:\(inboxId)" } func preEnableIdentityCallback() { diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 9f1df1ff9..ecfcde58d 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.10.11" + s.dependency "XMTP", "= 0.11.0" end diff --git a/src/index.ts b/src/index.ts index 580bde3b5..96ad52471 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ import { getAddress } from './utils/address' export * from './context' export * from './hooks' -export { GroupChangeCodec } from './lib/NativeCodecs/GroupChangeCodec' +export { GroupUpdatedCodec } from './lib/NativeCodecs/GroupUpdatedCodec' export { ReactionCodec } from './lib/NativeCodecs/ReactionCodec' export { ReadReceiptCodec } from './lib/NativeCodecs/ReadReceiptCodec' export { RemoteAttachmentCodec } from './lib/NativeCodecs/RemoteAttachmentCodec' @@ -40,10 +40,22 @@ export function address(): string { return XMTPModule.address() } +export function inboxId(): string { + return XMTPModule.inboxId() +} + export async function deleteLocalDatabase(address: string) { return XMTPModule.deleteLocalDatabase(address) } +export async function dropLocalDatabaseConnection(address: string) { + return XMTPModule.dropLocalDatabaseConnection(address) +} + +export async function reconnectLocalDatabase(address: string) { + return XMTPModule.reconnectLocalDatabase(address) +} + export async function auth( address: string, environment: 'local' | 'dev' | 'production', @@ -51,8 +63,7 @@ export async function auth( hasCreateIdentityCallback?: boolean | undefined, hasEnableIdentityCallback?: boolean | undefined, enableAlphaMls?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, - dbPath?: string | undefined + dbEncryptionKey?: Uint8Array | undefined ) { return await XMTPModule.auth( address, @@ -61,8 +72,7 @@ export async function auth( hasCreateIdentityCallback, hasEnableIdentityCallback, enableAlphaMls, - dbEncryptionKey ? Array.from(dbEncryptionKey) : undefined, - dbPath + dbEncryptionKey ? Array.from(dbEncryptionKey) : undefined ) } @@ -76,8 +86,7 @@ export async function createRandom( hasCreateIdentityCallback?: boolean | undefined, hasEnableIdentityCallback?: boolean | undefined, enableAlphaMls?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, - dbPath?: string | undefined + dbEncryptionKey?: Uint8Array | undefined ): Promise { return await XMTPModule.createRandom( environment, @@ -85,8 +94,7 @@ export async function createRandom( hasCreateIdentityCallback, hasEnableIdentityCallback, enableAlphaMls, - dbEncryptionKey ? Array.from(dbEncryptionKey) : undefined, - dbPath + dbEncryptionKey ? Array.from(dbEncryptionKey) : undefined ) } @@ -95,16 +103,14 @@ export async function createFromKeyBundle( environment: 'local' | 'dev' | 'production', appVersion?: string | undefined, enableAlphaMls?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, - dbPath?: string | undefined + dbEncryptionKey?: Uint8Array | undefined ): Promise { return await XMTPModule.createFromKeyBundle( keyBundle, environment, appVersion, enableAlphaMls, - dbEncryptionKey ? Array.from(dbEncryptionKey) : undefined, - dbPath + dbEncryptionKey ? Array.from(dbEncryptionKey) : undefined ) } @@ -113,7 +119,7 @@ export async function createGroup< >( client: Client, peerAddresses: string[], - permissionLevel: 'everyone_admin' | 'creator_admin' = 'everyone_admin' + permissionLevel: 'all_members' | 'admin_only' = 'all_members' ): Promise> { return new Group( client, @@ -135,10 +141,10 @@ export async function listGroups< }) } -export async function listMemberAddresses< +export async function listMemberInboxIds< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >(client: Client, id: string): Promise { - return XMTPModule.listMemberAddresses(client.address, id) + return XMTPModule.listMemberInboxIds(client.address, id) } export async function sendMessageToGroup( @@ -220,6 +226,22 @@ export async function removeGroupMembers( return XMTPModule.removeGroupMembers(clientAddress, id, addresses) } +export async function addGroupMembersByInboxId( + clientAddress: string, + id: string, + inboxIds: string[] +): Promise { + return XMTPModule.addGroupMembersByInboxId(clientAddress, id, inboxIds) +} + +export async function removeGroupMembersByInboxId( + clientAddress: string, + id: string, + inboxIds: string[] +): Promise { + return XMTPModule.removeGroupMembersByInboxId(clientAddress, id, inboxIds) +} + export function groupName( address: string, id: string @@ -577,16 +599,16 @@ export function unsubscribeFromConversations(clientAddress: string) { return XMTPModule.unsubscribeFromConversations(clientAddress) } -export function unsubscribeFromGroups(clientAddress: string) { - return XMTPModule.unsubscribeFromGroups(clientAddress) +export function unsubscribeFromGroups(inboxId: string) { + return XMTPModule.unsubscribeFromGroups(inboxId) } export function unsubscribeFromAllMessages(clientAddress: string) { return XMTPModule.unsubscribeFromAllMessages(clientAddress) } -export function unsubscribeFromAllGroupMessages(clientAddress: string) { - return XMTPModule.unsubscribeFromAllGroupMessages(clientAddress) +export function unsubscribeFromAllGroupMessages(inboxId: string) { + return XMTPModule.unsubscribeFromAllGroupMessages(inboxId) } export async function unsubscribeFromMessages( @@ -696,11 +718,11 @@ export async function isGroupActive( return XMTPModule.isGroupActive(clientAddress, id) } -export async function addedByAddress( +export async function addedByInboxId( clientAddress: string, id: string ): Promise { - return XMTPModule.addedByAddress(clientAddress, id) + return XMTPModule.addedByInboxId(clientAddress, id) } export async function isGroupAdmin( diff --git a/src/lib/Client.ts b/src/lib/Client.ts index d938e2b93..1143fd67e 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -19,16 +19,19 @@ import { DecodedMessage } from '../index' declare const Buffer -export type GetMessageContentTypeFromClient = - C extends Client ? T : never +export type GetMessageContentTypeFromClient = C extends Client + ? T + : never -export type ExtractDecodedType = - C extends XMTPModule.ContentCodec ? T : never +export type ExtractDecodedType = C extends XMTPModule.ContentCodec + ? T + : never export class Client< ContentTypes extends DefaultContentTypes = DefaultContentTypes, > { address: string + inboxId: string conversations: Conversations contacts: Contacts codecRegistry: { [key: string]: XMTPModule.ContentCodec } @@ -91,13 +94,13 @@ export class Client< this.authSubscription = XMTPModule.emitter.addListener( 'authed', - async () => { + async (message: { inboxId: string }) => { this.removeSubscription(enableSubscription) this.removeSubscription(createSubscription) this.removeSignSubscription() this.removeAuthSubscription() const address = await signer.getAddress() - resolve(new Client(address, opts?.codecs || [])) + resolve(new Client(address, message.inboxId, opts?.codecs || [])) } ) await XMTPModule.auth( @@ -107,8 +110,7 @@ export class Client< Boolean(createSubscription), Boolean(enableSubscription), Boolean(options.enableAlphaMls), - options.dbEncryptionKey, - options.dbPath + options.dbEncryptionKey ) })().catch((error) => { console.error('ERROR in create: ', error) @@ -142,19 +144,22 @@ export class Client< const options = defaultOptions(opts) const { enableSubscription, createSubscription } = this.setupSubscriptions(options) - const address = await XMTPModule.createRandom( + const addressInboxId = await XMTPModule.createRandom( options.env, options.appVersion, Boolean(createSubscription), Boolean(enableSubscription), Boolean(options.enableAlphaMls), - options.dbEncryptionKey, - options.dbPath + options.dbEncryptionKey ) this.removeSubscription(enableSubscription) this.removeSubscription(createSubscription) - return new Client(address, opts?.codecs || []) + return new Client( + addressInboxId['address'], + addressInboxId['inboxId'], + opts?.codecs || [] + ) } /** @@ -174,49 +179,19 @@ export class Client< opts?: Partial & { codecs?: ContentCodecs } ): Promise> { const options = defaultOptions(opts) - const address = await XMTPModule.createFromKeyBundle( + const addressInboxId = await XMTPModule.createFromKeyBundle( keyBundle, options.env, options.appVersion, Boolean(options.enableAlphaMls), - options.dbEncryptionKey, - options.dbPath + options.dbEncryptionKey ) - return new Client(address, opts?.codecs || []) - } - - /** - * Determines whether the current user can send messages to a specified peer over 1:1 conversations. - * - * This method checks if the specified peer has signed up for XMTP - * and ensures that the message is not addressed to the sender (no self-messaging). - * - * @param {string} peerAddress - The address of the peer to check for messaging eligibility. - * @returns {Promise} A Promise resolving to true if messaging is allowed, and false otherwise. - */ - async canMessage(peerAddress: string): Promise { - return await XMTPModule.canMessage(this.address, peerAddress) - } - - /** - * Deletes the local database. This cannot be undone and these stored messages will not be refetched from the network. - */ - async deleteLocalDatabase() { - return await XMTPModule.deleteLocalDatabase(this.address) - } - /** - * Determines whether the current user can send messages to the specified peers over groups. - * - * This method checks if the specified peers are using clients that support group messaging. - * - * @param {string[]} addresses - The addresses of the peers to check for messaging eligibility. - * @returns {Promise<{ [key: string]: boolean }>} A Promise resolving to a hash of addresses and booleans if they can message on the V3 network. - */ - async canGroupMessage( - addresses: string[] - ): Promise<{ [key: string]: boolean }> { - return await XMTPModule.canGroupMessage(this.address, addresses) + return new Client( + addressInboxId['address'], + addressInboxId['inboxId'], + opts?.codecs || [] + ) } /** @@ -294,9 +269,11 @@ export class Client< constructor( address: string, + inboxId: string, codecs: XMTPModule.ContentCodec[] = [] ) { this.address = address + this.inboxId = inboxId this.conversations = new Conversations(this) this.contacts = new Contacts(this) this.codecRegistry = {} @@ -338,6 +315,54 @@ export class Client< return XMTPModule.exportKeyBundle(this.address) } + /** + * Determines whether the current user can send messages to a specified peer over 1:1 conversations. + * + * This method checks if the specified peer has signed up for XMTP + * and ensures that the message is not addressed to the sender (no self-messaging). + * + * @param {string} peerAddress - The address of the peer to check for messaging eligibility. + * @returns {Promise} A Promise resolving to true if messaging is allowed, and false otherwise. + */ + async canMessage(peerAddress: string): Promise { + return await XMTPModule.canMessage(this.address, peerAddress) + } + + /** + * Deletes the local database. This cannot be undone and these stored messages will not be refetched from the network. + */ + async deleteLocalDatabase() { + return await XMTPModule.deleteLocalDatabase(this.address) + } + + /** + * Drop the local database connection. This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase() + */ + async dropLocalDatabaseConnection() { + return await XMTPModule.dropLocalDatabaseConnection(this.address) + } + + /** + * Reconnects the local database after being dropped. + */ + async reconnectLocalDatabase() { + return await XMTPModule.reconnectLocalDatabase(this.address) + } + + /** + * Determines whether the current user can send messages to the specified peers over groups. + * + * This method checks if the specified peers are using clients that support group messaging. + * + * @param {string[]} addresses - The addresses of the peers to check for messaging eligibility. + * @returns {Promise<{ [key: string]: boolean }>} A Promise resolving to a hash of addresses and booleans if they can message on the V3 network. + */ + async canGroupMessage( + addresses: string[] + ): Promise<{ [key: string]: boolean }> { + return await XMTPModule.canGroupMessage(this.address, addresses) + } + // TODO: support persisting conversations for quick lookup // async importConversation(exported: string): Promise { ... } // async exportConversation(topic: string): Promise { ... } @@ -445,10 +470,6 @@ export type ClientOptions = { * OPTIONAL specify the encryption key for the database. The encryption key must be exactly 32 bytes. */ dbEncryptionKey?: Uint8Array - /** - * OPTIONAL specify the XMTP managed database path - */ - dbPath?: string } export type KeyType = { @@ -466,7 +487,6 @@ export function defaultOptions(opts?: Partial): ClientOptions { env: 'dev', enableAlphaMls: false, dbEncryptionKey: undefined, - dbPath: undefined, } return { ..._defaultOptions, ...opts } as ClientOptions diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index f98765355..b86daa905 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -151,7 +151,7 @@ export default class Conversations< */ async newGroup( peerAddresses: string[], - permissionLevel: 'everyone_admin' | 'creator_admin' = 'everyone_admin' + permissionLevel: 'all_members' | 'admin_only' = 'all_members' ): Promise> { return await XMTPModule.createGroup( this.client, @@ -357,7 +357,7 @@ export default class Conversations< this.subscriptions[EventTypes.Group].remove() delete this.subscriptions[EventTypes.Group] } - XMTPModule.unsubscribeFromGroups(this.client.address) + XMTPModule.unsubscribeFromGroups(this.client.inboxId) } /** @@ -379,6 +379,6 @@ export default class Conversations< this.subscriptions[EventTypes.AllGroupMessage].remove() delete this.subscriptions[EventTypes.AllGroupMessage] } - XMTPModule.unsubscribeFromAllGroupMessages(this.client.address) + XMTPModule.unsubscribeFromAllGroupMessages(this.client.inboxId) } } diff --git a/src/lib/Group.ts b/src/lib/Group.ts index c2287ad31..7c4c4f08b 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -17,30 +17,36 @@ export class Group< client: XMTP.Client id: string createdAt: number - peerAddresses: string[] + peerInboxIds: string[] version = ConversationVersion.GROUP topic: string - adminAddress: string - permissionLevel: 'everyone_admin' | 'creator_admin' + creatorInboxId: string + permissionLevel: 'all_members' | 'admin_only' + name: string + isGroupActive: boolean constructor( client: XMTP.Client, params: { id: string createdAt: number - peerAddresses: string[] - adminAddress: string - permissionLevel: 'everyone_admin' | 'creator_admin' + peerInboxIds: string[] + creatorInboxId: string + permissionLevel: 'all_members' | 'admin_only' topic: string + name: string + isGroupActive: boolean } ) { this.client = client this.id = params.id this.createdAt = params.createdAt - this.peerAddresses = params.peerAddresses + this.peerInboxIds = params.peerInboxIds this.topic = params.topic - this.adminAddress = params.adminAddress + this.creatorInboxId = params.creatorInboxId this.permissionLevel = params.permissionLevel + this.name = params.name + this.isGroupActive = params.isGroupActive } get clientAddress(): string { @@ -52,8 +58,8 @@ export class Group< * To get the latest member addresses from the network, call sync() first. * @returns {Promise[]>} A Promise that resolves to an array of DecodedMessage objects. */ - async memberAddresses(): Promise { - return XMTP.listMemberAddresses(this.client, this.id) + async memberInboxIds(): Promise { + return XMTP.listMemberInboxIds(this.client, this.id) } /** @@ -179,6 +185,18 @@ export class Group< return XMTP.removeGroupMembers(this.client.address, this.id, addresses) } + async addMembersByInboxId(inboxIds: string[]): Promise { + return XMTP.addGroupMembersByInboxId(this.client.address, this.id, inboxIds) + } + + async removeMembersByInboxId(inboxIds: string[]): Promise { + return XMTP.removeGroupMembersByInboxId( + this.client.address, + this.id, + inboxIds + ) + } + // Returns the group name. // To get the latest group name from the network, call sync() first. async groupName(): Promise { @@ -197,8 +215,8 @@ export class Group< // Returns the address that added you to the group. // To get the latest added by address from the network, call sync() first. - async addedByAddress(): Promise { - return XMTP.addedByAddress(this.client.address, this.id) + async addedByInboxId(): Promise { + return XMTP.addedByInboxId(this.client.address, this.id) } // Returns whether you are an admin of the group. diff --git a/src/lib/NativeCodecs/GroupChangeCodec.ts b/src/lib/NativeCodecs/GroupUpdatedCodec.ts similarity index 57% rename from src/lib/NativeCodecs/GroupChangeCodec.ts rename to src/lib/NativeCodecs/GroupUpdatedCodec.ts index c5d5b446e..1970d2d1e 100644 --- a/src/lib/NativeCodecs/GroupChangeCodec.ts +++ b/src/lib/NativeCodecs/GroupUpdatedCodec.ts @@ -1,17 +1,17 @@ import { ContentTypeId, - GroupChangeContent, + GroupUpdatedContent, NativeContentCodec, NativeMessageContent, } from '../ContentCodec' -export class GroupChangeCodec - implements NativeContentCodec +export class GroupUpdatedCodec + implements NativeContentCodec { - contentKey: 'groupChange' = 'groupChange' + contentKey: 'groupUpdated' = 'groupUpdated' contentType: ContentTypeId = { authorityId: 'xmtp.org', - typeId: 'group_membership_change', + typeId: 'group_updated', versionMajor: 1, versionMinor: 0, } @@ -20,8 +20,8 @@ export class GroupChangeCodec return {} } - decode(nativeContent: NativeMessageContent): GroupChangeContent { - return nativeContent.groupChange! + decode(nativeContent: NativeMessageContent): GroupUpdatedContent { + return nativeContent.groupUpdated! } fallback(): string | undefined { diff --git a/src/lib/types/ContentCodec.ts b/src/lib/types/ContentCodec.ts index 00b6c93d4..a869931ed 100644 --- a/src/lib/types/ContentCodec.ts +++ b/src/lib/types/ContentCodec.ts @@ -53,14 +53,14 @@ export type RemoteAttachmentContent = RemoteAttachmentMetadata & { url: string } -export type GroupChangeEntry = { - address: string - initiatedByAddress: string +export type GroupUpdatedEntry = { + inboxId: string + initiatedByInboxId: string } -export type GroupChangeContent = { - membersAdded: GroupChangeEntry[] - membersRemoved: GroupChangeEntry[] +export type GroupUpdatedContent = { + membersAdded: GroupUpdatedEntry[] + membersRemoved: GroupUpdatedEntry[] } // This contains a message that has been prepared for sending. @@ -101,7 +101,7 @@ export type NativeMessageContent = { remoteAttachment?: RemoteAttachmentContent readReceipt?: ReadReceiptContent encoded?: string - groupChange?: GroupChangeContent + groupUpdated?: GroupUpdatedContent } export interface JSContentCodec {