Skip to content

Commit

Permalink
Conversation list performance (#456)
Browse files Browse the repository at this point in the history
* update to use the new more performant list and last message

* update the tests

* fix up the tests
  • Loading branch information
nplasterer authored Jan 7, 2025
1 parent 1062449 commit f0acecc
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 93 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
.package(url: "https://github.com/bufbuild/connect-swift", exact: "1.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", exact: "1.8.3"),
.package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "3.0.15")
.package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "3.0.18")
],
targets: [
.target(
Expand Down
9 changes: 9 additions & 0 deletions Sources/XMTPiOS/Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ public enum Conversation: Identifiable, Equatable, Hashable {
}
}

public func lastMessage() async throws -> DecodedMessage? {
switch self {
case let .group(group):
return try await group.lastMessage()
case let .dm(dm):
return try await dm.lastMessage()
}
}

public func isCreator() async throws -> Bool {
switch self {
case let .group(group):
Expand Down
75 changes: 15 additions & 60 deletions Sources/XMTPiOS/Conversations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ public enum ConversationError: Error, CustomStringConvertible, LocalizedError {
}
}

public enum ConversationOrder {
case createdAt, lastMessage
}

public enum ConversationType {
case all, groups, dms
}
Expand Down Expand Up @@ -88,12 +84,12 @@ public actor Conversations {

public func listGroups(
createdAfter: Date? = nil, createdBefore: Date? = nil,
limit: Int? = nil, order: ConversationOrder = .createdAt,
limit: Int? = nil,
consentState: ConsentState? = nil
) async throws -> [Group] {
) throws -> [Group] {
var options = FfiListConversationsOptions(
createdAfterNs: nil, createdBeforeNs: nil, limit: nil,
consentState: consentState?.toFFI)
consentState: consentState?.toFFI, includeDuplicateDms: false)
if let createdAfter {
options.createdAfterNs = Int64(createdAfter.millisecondsSinceEpoch)
}
Expand All @@ -104,25 +100,22 @@ public actor Conversations {
if let limit {
options.limit = Int64(limit)
}
let conversations = try await ffiConversations.listGroups(
let conversations = try ffiConversations.listGroups(
opts: options)

let sortedConversations = try await sortConversations(
conversations, order: order)

return sortedConversations.map {
return conversations.map {
$0.groupFromFFI(client: client)
}
}

public func listDms(
createdAfter: Date? = nil, createdBefore: Date? = nil,
limit: Int? = nil, order: ConversationOrder = .createdAt,
limit: Int? = nil,
consentState: ConsentState? = nil
) async throws -> [Dm] {
) throws -> [Dm] {
var options = FfiListConversationsOptions(
createdAfterNs: nil, createdBeforeNs: nil, limit: nil,
consentState: consentState?.toFFI)
consentState: consentState?.toFFI, includeDuplicateDms: false)
if let createdAfter {
options.createdAfterNs = Int64(createdAfter.millisecondsSinceEpoch)
}
Expand All @@ -133,25 +126,22 @@ public actor Conversations {
if let limit {
options.limit = Int64(limit)
}
let conversations = try await ffiConversations.listDms(
let conversations = try ffiConversations.listDms(
opts: options)

let sortedConversations = try await sortConversations(
conversations, order: order)

return sortedConversations.map {
return conversations.map {
$0.dmFromFFI(client: client)
}
}

public func list(
createdAfter: Date? = nil, createdBefore: Date? = nil,
limit: Int? = nil, order: ConversationOrder = .createdAt,
limit: Int? = nil,
consentState: ConsentState? = nil
) async throws -> [Conversation] {
var options = FfiListConversationsOptions(
createdAfterNs: nil, createdBeforeNs: nil, limit: nil,
consentState: consentState?.toFFI)
consentState: consentState?.toFFI, includeDuplicateDms: false)
if let createdAfter {
options.createdAfterNs = Int64(createdAfter.millisecondsSinceEpoch)
}
Expand All @@ -162,53 +152,18 @@ public actor Conversations {
if let limit {
options.limit = Int64(limit)
}
let ffiConversations = try await ffiConversations.list(
let ffiConversations = try ffiConversations.list(
opts: options)

let sortedConversations = try await sortConversations(
ffiConversations, order: order)

var conversations: [Conversation] = []
for sortedConversation in sortedConversations {
let conversation = try await sortedConversation.toConversation(
for conversation in ffiConversations {
let conversation = try await conversation.toConversation(
client: client)
conversations.append(conversation)
}

return conversations
}

private func sortConversations(
_ conversations: [FfiConversation],
order: ConversationOrder
) async throws -> [FfiConversation] {
switch order {
case .lastMessage:
var conversationWithTimestamp: [(FfiConversation, Int64?)] = []

for conversation in conversations {
let message = try await conversation.findMessages(
opts: FfiListMessagesOptions(
sentBeforeNs: nil,
sentAfterNs: nil,
limit: 1,
deliveryStatus: nil,
direction: .descending
)
).first
conversationWithTimestamp.append(
(conversation, message?.sentAtNs))
}

let sortedTuples = conversationWithTimestamp.sorted { (lhs, rhs) in
(lhs.1 ?? 0) > (rhs.1 ?? 0)
}
return sortedTuples.map { $0.0 }
case .createdAt:
return conversations
}
}

public func stream(type: ConversationType = .all) -> AsyncThrowingStream<
Conversation, Error
> {
Expand Down
16 changes: 14 additions & 2 deletions Sources/XMTPiOS/Dm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import LibXMTP

public struct Dm: Identifiable, Equatable, Hashable {
var ffiConversation: FfiConversation
var ffiLastMessage: FfiMessage? = nil
var client: Client
let streamHolder = StreamHolder()

Expand Down Expand Up @@ -200,6 +201,15 @@ public struct Dm: Identifiable, Equatable, Hashable {
}
}

public func lastMessage() async throws -> DecodedMessage? {
if let ffiMessage = ffiLastMessage {
return Message(client: self.client, ffiMessage: ffiMessage)
.decodeOrNull()
} else {
return try await messages(limit: 1).first
}
}

public func messages(
beforeNs: Int64? = nil,
afterNs: Int64? = nil,
Expand All @@ -212,7 +222,8 @@ public struct Dm: Identifiable, Equatable, Hashable {
sentAfterNs: nil,
limit: nil,
deliveryStatus: nil,
direction: nil
direction: nil,
contentTypes: nil
)

if let beforeNs {
Expand Down Expand Up @@ -253,7 +264,8 @@ public struct Dm: Identifiable, Equatable, Hashable {

options.direction = direction

return try await ffiConversation.findMessages(opts: options).compactMap {
return try await ffiConversation.findMessages(opts: options).compactMap
{
ffiMessage in
return Message(client: self.client, ffiMessage: ffiMessage)
.decodeOrNull()
Expand Down
26 changes: 25 additions & 1 deletion Sources/XMTPiOS/Extensions/Ffi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ extension FfiConversation {
}
}

extension FfiConversationListItem {
func groupFromFFI(client: Client) -> Group {
Group(
ffiGroup: self.conversation(), ffiLastMessage: self.lastMessage(),
client: client)
}

func dmFromFFI(client: Client) -> Dm {
Dm(
ffiConversation: self.conversation(),
ffiLastMessage: self.lastMessage(), client: client)
}

func toConversation(client: Client) async throws -> Conversation {
if try await conversation().conversationType() == .dm {
return Conversation.dm(self.dmFromFFI(client: client))
} else {
return Conversation.group(self.groupFromFFI(client: client))
}
}
}

extension FfiConversationMember {
var fromFFI: Member {
Member(ffiGroupMember: self)
Expand Down Expand Up @@ -75,6 +97,8 @@ extension ConsentRecord {

extension FfiConsent {
var fromFfi: ConsentRecord {
ConsentRecord(value: self.entity, entryType: self.entityType.fromFFI, consentType: self.state.fromFFI)
ConsentRecord(
value: self.entity, entryType: self.entityType.fromFFI,
consentType: self.state.fromFFI)
}
}
13 changes: 12 additions & 1 deletion Sources/XMTPiOS/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class StreamHolder {

public struct Group: Identifiable, Equatable, Hashable {
var ffiGroup: FfiConversation
var ffiLastMessage: FfiMessage? = nil
var client: Client
let streamHolder = StreamHolder()

Expand Down Expand Up @@ -398,6 +399,15 @@ public struct Group: Identifiable, Equatable, Hashable {
}
}

public func lastMessage() async throws -> DecodedMessage? {
if let ffiMessage = ffiLastMessage {
return Message(client: self.client, ffiMessage: ffiMessage)
.decodeOrNull()
} else {
return try await messages(limit: 1).first
}
}

public func messages(
beforeNs: Int64? = nil,
afterNs: Int64? = nil,
Expand All @@ -410,7 +420,8 @@ public struct Group: Identifiable, Equatable, Hashable {
sentAfterNs: nil,
limit: nil,
deliveryStatus: nil,
direction: nil
direction: nil,
contentTypes: nil
)

if let beforeNs {
Expand Down
9 changes: 1 addition & 8 deletions Tests/XMTPTests/ConversationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,10 @@ class ConversationTests: XCTestCase {

let conversations = try await fixtures.boClient.conversations
.list()
let conversationsOrdered = try await fixtures.boClient.conversations
.list(order: .lastMessage)

XCTAssertEqual(conversations.count, 3)
XCTAssertEqual(conversationsOrdered.count, 3)

XCTAssertEqual(
conversations.map { $0.id }, [dm.id, group1.id, group2.id])
XCTAssertEqual(
conversationsOrdered.map { $0.id },
[group2.id, dm.id, group1.id])
conversations.map { $0.id }, [group2.id, dm.id, group1.id])
}

func testCanStreamConversations() async throws {
Expand Down
10 changes: 1 addition & 9 deletions Tests/XMTPTests/DmTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,9 @@ class DmTests: XCTestCase {

let conversations = try await fixtures.boClient.conversations
.listDms()
let conversationsOrdered = try await fixtures.boClient.conversations
.listDms(order: .lastMessage)

XCTAssertEqual(conversations.count, 2)
XCTAssertEqual(conversationsOrdered.count, 2)

XCTAssertEqual(
try conversations.map { try $0.id }, [dm.id, dm2.id])
XCTAssertEqual(
try conversationsOrdered.map { try $0.id },
[dm2.id, dm.id])
try conversations.map { try $0.id }, [dm2.id, dm.id])
}

func testCanSendMessageToDm() async throws {
Expand Down
9 changes: 1 addition & 8 deletions Tests/XMTPTests/GroupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,17 +223,10 @@ class GroupTests: XCTestCase {

let conversations = try await fixtures.boClient.conversations
.listGroups()
let conversationsOrdered = try await fixtures.boClient.conversations
.listGroups(order: .lastMessage)

XCTAssertEqual(conversations.count, 2)
XCTAssertEqual(conversationsOrdered.count, 2)

XCTAssertEqual(
conversations.map { $0.id }, [group1.id, group2.id])
XCTAssertEqual(
conversationsOrdered.map { $0.id },
[group2.id, group1.id])
conversations.map { $0.id }, [group2.id, group1.id])
}

func testCanListGroupMembers() async throws {
Expand Down
2 changes: 1 addition & 1 deletion XMTP.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Pod::Spec.new do |spec|

spec.dependency 'CSecp256k1', '~> 0.2'
spec.dependency "Connect-Swift", "= 1.0.0"
spec.dependency 'LibXMTP', '= 3.0.15'
spec.dependency 'LibXMTP', '= 3.0.18'
spec.dependency 'CryptoSwift', '= 1.8.3'
spec.dependency 'SQLCipher', '= 4.5.7'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/xmtp/libxmtp-swift.git",
"state" : {
"revision" : "2fd0b26c9b3e20fcae7366176fbe41bc4528d0c7",
"version" : "3.0.15"
"revision" : "dc39e1b15013bbb8574d8ed7faa3ebebdd3b4863",
"version" : "3.0.18"
}
},
{
Expand Down

0 comments on commit f0acecc

Please sign in to comment.