From b3a84df7fe1a8abbfc59a582b1103204ac1bf38c Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 3 Jun 2024 00:03:20 -0700 Subject: [PATCH 1/4] add ability to list group members in android --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 10 +++++++ .../wrappers/GroupWrapper.kt | 1 + .../wrappers/MemberWrapper.kt | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index c778655b9..597f608a4 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -67,6 +67,7 @@ import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import com.facebook.common.util.Hex +import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper import org.xmtp.android.library.messages.MessageDeliveryStatus import org.xmtp.android.library.messages.Topic import org.xmtp.android.library.push.Service @@ -805,6 +806,15 @@ class XMTPModule : Module() { } } + AsyncFunction("listGroupMember") Coroutine { clientAddress: String, groupId: String -> + withContext(Dispatchers.IO) { + logV("listGroupMember") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, groupId) + group?.members().map { MemberWrapper.encode(it) } + } + } + AsyncFunction("syncGroups") Coroutine { clientAddress: String -> withContext(Dispatchers.IO) { logV("syncGroups") 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 357ec1ac5..8240a444a 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -4,6 +4,7 @@ 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.codecs.Attachment import org.xmtp.android.library.toHex import uniffi.xmtpv3.GroupPermissions diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt new file mode 100644 index 000000000..dfd92f672 --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt @@ -0,0 +1,28 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import com.google.gson.GsonBuilder +import org.xmtp.android.library.libxmtp.Member +import org.xmtp.android.library.libxmtp.PermissionLevel + +class MemberWrapper { + companion object { + fun encodeToObj(member: Member): Map { + val permissionString = when (member.permissionLevel) { + PermissionLevel.MEMBER -> "member" + PermissionLevel.ADMIN -> "admin" + PermissionLevel.SUPER_ADMIN -> "super_admin" + } + return mapOf( + "inboxId" to member.inboxId, + "addresses" to member.addresses, + "permissionLevel" to permissionString + ) + } + + fun encode(member: Member): String { + val gson = GsonBuilder().create() + val obj = encodeToObj(member) + return gson.toJson(obj) + } + } +} \ No newline at end of file From db6b566e32f7a06c4052bd967648a7f788c3b44c Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 3 Jun 2024 00:11:59 -0700 Subject: [PATCH 2/4] fix: add the iOS side of group members --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 6 +-- ios/Wrappers/MemberWrapper.swift | 37 +++++++++++++++++++ ios/XMTPModule.swift | 14 +++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 ios/Wrappers/MemberWrapper.swift diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 597f608a4..4d133dbf0 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -806,12 +806,12 @@ class XMTPModule : Module() { } } - AsyncFunction("listGroupMember") Coroutine { clientAddress: String, groupId: String -> + AsyncFunction("listGroupMembers") Coroutine { clientAddress: String, groupId: String -> withContext(Dispatchers.IO) { - logV("listGroupMember") + logV("listGroupMembers") val client = clients[clientAddress] ?: throw XMTPException("No client") val group = findGroup(clientAddress, groupId) - group?.members().map { MemberWrapper.encode(it) } + group?.members()?.map { MemberWrapper.encode(it) } } } diff --git a/ios/Wrappers/MemberWrapper.swift b/ios/Wrappers/MemberWrapper.swift new file mode 100644 index 000000000..142dc55bd --- /dev/null +++ b/ios/Wrappers/MemberWrapper.swift @@ -0,0 +1,37 @@ +// +// MemberWrapper.swift +// XMTPReactNative +// +// Created by Naomi Plasterer on 6/3/24. +// + +import Foundation +import XMTP + +// Wrapper around XMTP.Group to allow passing these objects back into react native. +struct MemberWrapper { + static func encodeToObj(_ member: XMTP.Member) throws -> [String: Any] { + let permissionString = switch member.permissionLevel { + case .Member: + "member" + case .Admin: + "admin" + case .SuperAdmin: + "super_admin" + } + return [ + "inboxId": member.inboxId, + "addresses": member.addresses, + "permissionLevel": permissionString, + ] + } + + static func encode(_ member: XMTP.Member) throws -> String { + let obj = try encodeToObj(member) + let data = try JSONSerialization.data(withJSONObject: obj) + guard let result = String(data: data, encoding: .utf8) else { + throw WrapperError.encodeError("could not encode member") + } + return result + } +} diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 98ff9645c..210dae2e6 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -697,6 +697,20 @@ public class XMTPModule: Module { return try group.members.map(\.inboxId) } + AsyncFunction("listGroupMembers") { (clientAddress: String, groupId: String) -> [String] in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + + guard let group = try await findGroup(clientAddress: clientAddress, id: groupId) else { + throw Error.conversationNotFound("no group found for \(groupId)") + } + return try group.members.compactMap { member in + return try MemberWrapper.encode(member) + } + } + + AsyncFunction("syncGroups") { (clientAddress: String) in guard let client = await clientsManager.getClient(key: clientAddress) else { throw Error.noClient From 3e4aea4c0f8fb7e8e27079dd4cceb544310c4075 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 3 Jun 2024 00:24:04 -0700 Subject: [PATCH 3/4] expose it throught the methods --- src/index.ts | 13 +++++++++++++ src/lib/Group.ts | 5 +++++ src/lib/Member.ts | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/lib/Member.ts diff --git a/src/index.ts b/src/index.ts index db28d689f..765ccd0e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { } from './lib/ConversationContainer' import { DecodedMessage, MessageDeliveryStatus } from './lib/DecodedMessage' import { Group } from './lib/Group' +import { Member } from './lib/Member' import type { Query } from './lib/Query' import { ConversationSendPayload } from './lib/types' import { DefaultContentTypes } from './lib/types/DefaultContentType' @@ -147,6 +148,17 @@ export async function listMemberInboxIds< return XMTPModule.listMemberInboxIds(client.address, id) } +export async function listGroupMembers( + clientAddress: string, + id: string +): Promise { + const members = await XMTPModule.listGroupMembers(clientAddress, id) + + return members.map((json: string) => { + return Member.from(json) + }) +} + export async function sendMessageToGroup( clientAddress: string, groupId: string, @@ -893,3 +905,4 @@ export { Query } from './lib/Query' export { XMTPPush } from './lib/XMTPPush' export { ConsentListEntry, DecodedMessage, MessageDeliveryStatus } export { Group } from './lib/Group' +export { Member } from './lib/Member' diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 1870871af..69e600080 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -3,6 +3,7 @@ import { ConversationContainer, } from './ConversationContainer' import { DecodedMessage, MessageDeliveryStatus } from './DecodedMessage' +import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' @@ -295,4 +296,8 @@ export class Group< async isDenied(): Promise { return await XMTP.isGroupDenied(this.client.address, this.id) } + + async members(): Promise { + return await XMTP.listGroupMembers(this.client.address, this.id) + } } diff --git a/src/lib/Member.ts b/src/lib/Member.ts new file mode 100644 index 000000000..10aa2db1a --- /dev/null +++ b/src/lib/Member.ts @@ -0,0 +1,20 @@ +export class Member { + inboxId: string + addresses: string[] + permissionLevel: 'member' | 'admin' | 'super_admin' + + constructor( + inboxId: string, + addresses: string[], + permissionLevel: 'member' | 'admin' | 'super_admin' + ) { + this.inboxId = inboxId + this.addresses = addresses + this.permissionLevel = permissionLevel + } + + static from(json: string): Member { + const entry = JSON.parse(json) + return new Member(entry.inboxId, entry.addresses, entry.permissionLevel) + } +} From 050a7734ebfab78c8ed22a63df2e156533a2fef0 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 3 Jun 2024 00:40:08 -0700 Subject: [PATCH 4/4] write a test for it --- example/src/tests/groupTests.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 1620bab3d..34277e9c9 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -239,6 +239,26 @@ test('who added me to a group', async () => { return true }) +test('can get members of a group', async () => { + const [alixClient, boClient] = await createClients(2) + const group = await alixClient.conversations.newGroup([boClient.address]) + + const members = await group.members() + + assert(members.length === 2, `Should be 2 members but was ${members.length}`) + assert( + members[0].addresses[0].toLocaleLowerCase === + boClient.address.toLocaleLowerCase, + `Should be ${boClient.address} but was ${members[0].addresses[0]}` + ) + assert( + members[0].permissionLevel === "admin", + `Should be admin but was ${members[0].permissionLevel}` + ) + + return true +}) + test('can message in a group', async () => { // Create three MLS enabled Clients const [alixClient, boClient, caroClient] = await createClients(3)