Skip to content

Commit

Permalink
Merge pull request #509 from xmtp/np/groups-lite-perf
Browse files Browse the repository at this point in the history
Performance enhancement to group lists
  • Loading branch information
nplasterer authored Oct 8, 2024
2 parents 08a2da8 + 07e8f93 commit 334af67
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 68 deletions.
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,89 @@
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) {
val lastMessage = group.decryptedMessages(limit = 1).firstOrNull()
if (lastMessage != null) {
put("lastMessage", DecodedMessageWrapper.encode(lastMessage))
}
}
}
}

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 {
if (groupParams.isEmpty()) return 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,
)
}
}
}

77 changes: 77 additions & 0 deletions example/src/tests/groupPerformanceTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,83 @@ async function beforeAll(
)
}

test('testing large group listings with ordering', async () => {
await beforeAll(1000, 10, 10)

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`)

await groups[5].send({ text: `Alix message` })
await groups[50].send({ text: `Alix message` })
await groups[150].send({ text: `Alix message` })
await groups[500].send({ text: `Alix message` })
await groups[700].send({ text: `Alix message` })
await groups[900].send({ text: `Alix message` })

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
})

test('testing large group listings', async () => {
await beforeAll(1000)

Expand Down
76 changes: 59 additions & 17 deletions example/src/tests/groupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,57 @@ test('can stream groups', async () => {
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].id}`
)

return true
})

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

Expand Down Expand Up @@ -1844,34 +1895,25 @@ test('can group consent', async () => {
)

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
1 change: 1 addition & 0 deletions example/src/tests/v3OnlyTests.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-extra-non-null-assertion */
import { Client } from 'xmtp-react-native-sdk'

Expand Down
Loading

0 comments on commit 334af67

Please sign in to comment.