Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance enhancement to group lists #509

Merged
merged 13 commits into from
Oct 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper
import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString
import expo.modules.xmtpreactnativesdk.wrappers.ContentJson
import expo.modules.xmtpreactnativesdk.wrappers.ConversationContainerWrapper
import expo.modules.xmtpreactnativesdk.wrappers.ConversationOrder
import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper
import expo.modules.xmtpreactnativesdk.wrappers.CreateGroupParamsWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.GroupParamsWrapper
import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper
import expo.modules.xmtpreactnativesdk.wrappers.InboxStateWrapper
import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper
Expand Down Expand Up @@ -624,14 +626,26 @@ class XMTPModule : Module() {
}
}

AsyncFunction("listGroups") Coroutine { inboxId: String ->
AsyncFunction("listGroups") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? ->
withContext(Dispatchers.IO) {
logV("listGroups")
val client = clients[inboxId] ?: throw XMTPException("No client")
val groupList = client.conversations.listGroups()
groupList.map { group ->
val params = GroupParamsWrapper.groupParamsFromJson(groupParams ?: "")
val order = getConversationSortOrder(sortOrder ?: "")
val sortedGroupList = if (order == ConversationOrder.LAST_MESSAGE) {
client.conversations.listGroups()
.sortedByDescending { group ->
group.decryptedMessages(limit = 1).firstOrNull()?.sentAt
}
.let { groups ->
if (limit != null && limit > 0) groups.take(limit) else groups
}
} else {
client.conversations.listGroups(limit = limit)
}
sortedGroupList.map { group ->
groups[group.cacheKey(inboxId)] = group
GroupWrapper.encode(client, group)
GroupWrapper.encode(client, group, params)
}
}
}
Expand Down Expand Up @@ -1718,6 +1732,13 @@ class XMTPModule : Module() {
}
}

private fun getConversationSortOrder(order: String): ConversationOrder {
return when (order) {
"lastMessage" -> ConversationOrder.LAST_MESSAGE
else -> ConversationOrder.CREATED_AT
}
}

private suspend fun findConversation(
inboxId: String,
topic: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,88 @@
package expo.modules.xmtpreactnativesdk.wrappers

import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString
import org.xmtp.android.library.Client
import org.xmtp.android.library.Group

enum class ConversationOrder {
LAST_MESSAGE, CREATED_AT
}

class GroupWrapper {

companion object {
suspend fun encodeToObj(client: Client, group: Group): Map<String, Any> {
return mapOf(
"clientAddress" to client.address,
"id" to group.id,
"createdAt" to group.createdAt.time,
"members" to group.members().map { MemberWrapper.encode(it) },
"version" to "GROUP",
"topic" to group.topic,
"creatorInboxId" to group.creatorInboxId(),
"isActive" to group.isActive(),
"addedByInboxId" to group.addedByInboxId(),
"name" to group.name,
"imageUrlSquare" to group.imageUrlSquare,
"description" to group.description,
"consentState" to consentStateToString(group.consentState())
// "pinnedFrameUrl" to group.pinnedFrameUrl
)
suspend fun encodeToObj(
client: Client,
group: Group,
groupParams: GroupParamsWrapper = GroupParamsWrapper(),
): Map<String, Any> {
return buildMap {
put("clientAddress", client.address)
put("id", group.id)
put("createdAt", group.createdAt.time)
put("version", "GROUP")
put("topic", group.topic)
if (groupParams.members) {
put("members", group.members().map { MemberWrapper.encode(it) })
}
if (groupParams.creatorInboxId) put("creatorInboxId", group.creatorInboxId())
if (groupParams.isActive) put("isActive", group.isActive())
if (groupParams.addedByInboxId) put("addedByInboxId", group.addedByInboxId())
if (groupParams.name) put("name", group.name)
if (groupParams.imageUrlSquare) put("imageUrlSquare", group.imageUrlSquare)
if (groupParams.description) put("description", group.description)
if (groupParams.consentState) {
put("consentState", consentStateToString(group.consentState()))
}
if (groupParams.lastMessage) {
put(
"lastMessage",
DecodedMessageWrapper.encode(group.decryptedMessages(limit = 1).first())
)
}
}
}

suspend fun encode(client: Client, group: Group): String {
suspend fun encode(
client: Client,
group: Group,
groupParams: GroupParamsWrapper = GroupParamsWrapper(),
): String {
val gson = GsonBuilder().create()
val obj = encodeToObj(client, group)
val obj = encodeToObj(client, group, groupParams)
return gson.toJson(obj)
}
}
}

class GroupParamsWrapper(
val members: Boolean = true,
val creatorInboxId: Boolean = true,
val isActive: Boolean = true,
val addedByInboxId: Boolean = true,
val name: Boolean = true,
val imageUrlSquare: Boolean = true,
val description: Boolean = true,
val consentState: Boolean = true,
val lastMessage: Boolean = false,
) {
companion object {
fun groupParamsFromJson(groupParams: String): GroupParamsWrapper {
val jsonOptions = JsonParser.parseString(groupParams).asJsonObject
return GroupParamsWrapper(
if (jsonOptions.has("members")) jsonOptions.get("members").asBoolean else true,
if (jsonOptions.has("creatorInboxId")) jsonOptions.get("creatorInboxId").asBoolean else true,
if (jsonOptions.has("isActive")) jsonOptions.get("isActive").asBoolean else true,
if (jsonOptions.has("addedByInboxId")) jsonOptions.get("addedByInboxId").asBoolean else true,
if (jsonOptions.has("name")) jsonOptions.get("name").asBoolean else true,
if (jsonOptions.has("imageUrlSquare")) jsonOptions.get("imageUrlSquare").asBoolean else true,
if (jsonOptions.has("description")) jsonOptions.get("description").asBoolean else true,
if (jsonOptions.has("consentState")) jsonOptions.get("consentState").asBoolean else true,
if (jsonOptions.has("lastMessage")) jsonOptions.get("lastMessage").asBoolean else false,
)
}
}
}

70 changes: 70 additions & 0 deletions example/src/tests/groupPerformanceTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,73 @@ test('testing sending message in large group', async () => {

return true
})

test('testing large group listings with ordering', async () => {
alexrisch marked this conversation as resolved.
Show resolved Hide resolved
await beforeAll(1000, 1, 20)

let start = Date.now()
let groups = await alixClient.conversations.listGroups()
let end = Date.now()
console.log(`Alix loaded ${groups.length} groups in ${end - start}ms`)

let start2 = Date.now()
let groups2 = await alixClient.conversations.listGroups(
{
members: false,
consentState: false,
description: false,
creatorInboxId: false,
addedByInboxId: false,
isActive: false,
lastMessage: true,
},
'lastMessage'
)
let end2 = Date.now()
console.log(`Alix loaded ${groups2.length} groups in ${end2 - start2}ms`)
assert(
end2 - start2 < end - start,
'listing 1000 groups without certain fields should take less time'
)

start = Date.now()
await alixClient.conversations.syncGroups()
end = Date.now()
console.log(`Alix synced ${groups.length} groups in ${end - start}ms`)
assert(
end - start < 100,
'syncing 1000 cached groups should take less than a .1 second'
)

start = Date.now()
await boClient.conversations.syncGroups()
end = Date.now()
console.log(`Bo synced ${groups.length} groups in ${end - start}ms`)

start = Date.now()
groups = await boClient.conversations.listGroups()
end = Date.now()
console.log(`Bo loaded ${groups.length} groups in ${end - start}ms`)

start2 = Date.now()
groups2 = await boClient.conversations.listGroups(
{
members: false,
consentState: false,
description: false,
creatorInboxId: false,
addedByInboxId: false,
isActive: false,
lastMessage: true,
},
'lastMessage'
)
end2 = Date.now()
console.log(`Bo loaded ${groups2.length} groups in ${end2 - start2}ms`)
assert(
end2 - start2 < end - start,
'listing 1000 groups without certain fields should take less time'
)

return true
})
80 changes: 63 additions & 17 deletions example/src/tests/groupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import { Platform } from 'expo-modules-core'
import RNFS from 'react-native-fs'
import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage'
import {
ConversationOrder,

Check warning on line 6 in example/src/tests/groupTests.ts

View workflow job for this annotation

GitHub Actions / lint

'ConversationOrder' is defined but never used
GroupOptions,

Check warning on line 7 in example/src/tests/groupTests.ts

View workflow job for this annotation

GitHub Actions / lint

'GroupOptions' is defined but never used
} from 'xmtp-react-native-sdk/lib/types/GroupOptions'

import {
Test,
Expand Down Expand Up @@ -1062,6 +1066,57 @@
return true
})

test('can list groups with params', async () => {
const [alixClient, boClient] = await createClients(2)

const boGroup1 = await boClient.conversations.newGroup([alixClient.address])
const boGroup2 = await boClient.conversations.newGroup([alixClient.address])

await boGroup1.send({ text: `first message` })
await boGroup1.send({ text: `second message` })
await boGroup1.send({ text: `third message` })
await boGroup2.send({ text: `first message` })

const boGroupsOrderCreated = await boClient.conversations.listGroups()
const boGroupsOrderLastMessage = await boClient.conversations.listGroups(
{ lastMessage: true },
'lastMessage'
)
const boGroupsLimit = await boClient.conversations.listGroups(
{},
undefined,
1
)

assert(
boGroupsOrderCreated.map((group: any) => group.id).toString() ===
[boGroup1.id, boGroup2.id].toString(),
`Group order should be group1 then group2 but was ${boGroupsOrderCreated.map((group: any) => group.id).toString()}`
)

assert(
boGroupsOrderLastMessage.map((group: any) => group.id).toString() ===
[boGroup2.id, boGroup1.id].toString(),
`Group order should be group2 then group1 but was ${boGroupsOrderLastMessage.map((group: any) => group.id).toString()}`
)

const messages = await boGroupsOrderLastMessage[0].messages()
assert(
messages[0].content() === 'first message',
`last message should be first message ${messages[0].content()}`
)
assert(
boGroupsLimit.length === 1,
`List length should be 1 but was ${boGroupsLimit.length}`
)
assert(
boGroupsLimit[0].id === boGroup1.id,
`Group should be ${boGroup1.id} but was ${boGroupsLimit[0].i}`
)

return true
})

test('can list groups', async () => {
const [alixClient, boClient] = await createClients(2)

Expand Down Expand Up @@ -1844,34 +1899,25 @@
)

isAllowed = await bo.contacts.isGroupAllowed(group.id)
assert(isAllowed === true, `bo group should be allowed but was ${isAllowed}`)
assert(
isAllowed === true,
`bo group should be allowed but was ${isAllowed}`
)
assert(
await group.state === 'allowed',
(await group.state) === 'allowed',
`the group should have a consent state of allowed but was ${await group.state}`
)

await bo.contacts.denyGroups([group.id])
let isDenied = await bo.contacts.isGroupDenied(group.id)
const isDenied = await bo.contacts.isGroupDenied(group.id)
assert(isDenied === true, `bo group should be denied but was ${isDenied}`)
assert(
isDenied === true,
`bo group should be denied but was ${isDenied}`
)
assert(
await group.consentState() === 'denied',
(await group.consentState()) === 'denied',
`the group should have a consent state of denied but was ${await group.consentState()}`
)

await group.updateConsent('allowed')
isAllowed = await bo.contacts.isGroupAllowed(group.id)
assert(isAllowed === true, `bo group should be allowed2 but was ${isAllowed}`)
assert(
isAllowed === true,
`bo group should be allowed2 but was ${isAllowed}`
)
assert(
await group.consentState() === 'allowed',
(await group.consentState()) === 'allowed',
`the group should have a consent state2 of allowed but was ${await group.consentState()}`
)

Expand Down
Loading
Loading