From 5851100f07a8f42938d91802e062d25705a527e3 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 2 Jul 2026 13:04:21 +0300 Subject: [PATCH] Expose a global search service from the client Wrap the SDK's SearchService in a SearchServiceProxy that publishes typed message results and paginates on demand, expose it on the ClientProxy, and enable the search index store on the session. Also opens up the timeline item factory's message content builder so the proxy can reuse it. --- ElementX/Sources/Other/SDKListener.swift | 8 + .../Sources/Services/Client/ClientProxy.swift | 8 + .../Services/Client/ClientProxyProtocol.swift | 2 + .../Services/Search/SearchServiceProxy.swift | 122 ++++++ .../Search/SearchServiceProxyProtocol.swift | 31 ++ .../RoomTimelineItemFactory.swift | 2 +- .../RoomTimelineItemFactoryProtocol.swift | 1 + .../UserSession/UserSessionStore.swift | 2 + .../Sources/Generated/SDKGeneratedMocks.swift | 395 +++++++++++------- 9 files changed, 418 insertions(+), 153 deletions(-) create mode 100644 ElementX/Sources/Services/Search/SearchServiceProxy.swift create mode 100644 ElementX/Sources/Services/Search/SearchServiceProxyProtocol.swift diff --git a/ElementX/Sources/Other/SDKListener.swift b/ElementX/Sources/Other/SDKListener.swift index 48f4644e3..d8f6f4165 100644 --- a/ElementX/Sources/Other/SDKListener.swift +++ b/ElementX/Sources/Other/SDKListener.swift @@ -293,3 +293,11 @@ nonisolated extension SDKListener: RoomDirectorySearchEntriesListener where T == onUpdateClosure(roomEntriesUpdate) } } + +// MARK: SearchServiceProxy + +nonisolated extension SDKListener: SearchServiceResultsListener where T == [SearchServiceResultsUpdate] { + func onUpdate(updates: [SearchServiceResultsUpdate]) { + onUpdateClosure(updates) + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 74d24e467..9218f132d 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -67,6 +67,7 @@ class ClientProxy: ClientProxyProtocol { private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol? let spaceService: SpaceServiceProxyProtocol + let searchService: SearchServiceProxyProtocol let capabilities: HomeserverCapabilitiesProxyProtocol @@ -236,6 +237,13 @@ class ClientProxy: ClientProxyProtocol { spaceService = await SpaceServiceProxy(spaceService: client.spaceService()) + let searchUserID = try client.userId() + searchService = SearchServiceProxy(searchService: client.searchService(), + timelineItemFactory: RoomTimelineItemFactory(userID: searchUserID, + attributedStringBuilder: AttributedStringBuilder(cacheKey: "search", + mentionBuilder: PlainMentionBuilder()), + stateEventStringBuilder: RoomStateEventStringBuilder(userID: searchUserID))) + capabilities = HomeserverCapabilitiesProxy(underlyingCapabilities: client.homeserverCapabilities()) let configuredAppService = try await ClientProxyServices(client: client, diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 85703b811..b1def4852 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -155,6 +155,8 @@ protocol ClientProxyProtocol: AnyObject { var spaceService: SpaceServiceProxyProtocol { get } + var searchService: SearchServiceProxyProtocol { get } + var capabilities: HomeserverCapabilitiesProxyProtocol { get } var isReportRoomSupported: Bool { get async } diff --git a/ElementX/Sources/Services/Search/SearchServiceProxy.swift b/ElementX/Sources/Services/Search/SearchServiceProxy.swift new file mode 100644 index 000000000..317fc8662 --- /dev/null +++ b/ElementX/Sources/Services/Search/SearchServiceProxy.swift @@ -0,0 +1,122 @@ +// Copyright 2025 Element Creations Ltd. +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. + +import Combine +import Foundation +import MatrixRustSDK + +class SearchServiceProxy: SearchServiceProxyProtocol { + private let searchService: SearchServiceProtocol + private let timelineItemFactory: RoomTimelineItemFactoryProtocol + + // periphery:ignore - required for instance retention in the rust codebase + private var resultsHandle: TaskHandle? + + private let resultsSubject = CurrentValueSubject<[SearchServiceResult], Never>([]) + var resultsPublisher: CurrentValuePublisher<[SearchServiceResult], Never> { + resultsSubject.asCurrentValuePublisher() + } + + init(searchService: SearchServiceProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) { + self.searchService = searchService + self.timelineItemFactory = timelineItemFactory + } + + func setQuery(_ query: String) async -> Result { + if resultsHandle == nil { + resultsHandle = await searchService.subscribeToResults(listener: SDKListener.onMainActor { [weak self] updates in + self?.handleResultUpdates(updates) + }) + } + + do { + try await searchService.setQuery(query: query) + return .success(()) + } catch { + MXLog.error("Failed to set search query: \(error)") + return .failure(.sdkError(error)) + } + } + + func paginate() async { + do { + try await searchService.paginate() + } catch { + MXLog.error("Failed to paginate search results: \(error)") + } + } + + // MARK: - Private + + private func handleResultUpdates(_ updates: [SearchServiceResultsUpdate]) { + var results = resultsSubject.value + + for update in updates { + switch update { + case .append(let values): + results.append(contentsOf: values.compactMap { self.makeResult($0) }) + case .clear: + results.removeAll() + case .pushFront(let value): + if let result = makeResult(value) { + results.insert(result, at: 0) + } + case .pushBack(let value): + if let result = makeResult(value) { + results.append(result) + } + case .popFront: + results.removeFirst() + case .popBack: + results.removeLast() + case .insert(let index, let value): + if let result = makeResult(value) { + results.insert(result, at: Int(index)) + } + case .set(let index, let value): + if let result = makeResult(value) { + results[Int(index)] = result + } + case .remove(let index): + results.remove(at: Int(index)) + case .truncate(let length): + results.removeSubrange(Int(length).. SearchServiceResult? { + switch searchResult { + case .message(let roomID, let result): + let sender = TimelineItemSender(senderID: result.sender, senderProfile: result.senderProfile) + + let content: TimelineEventContent + switch result.content { + case .msgLike(let msgLike): + switch msgLike.kind { + case .message(let msg): + content = .message(timelineItemFactory.buildMessageTimelineItemContent(messageType: msg.msgType, + senderID: result.sender, + senderDisplayName: sender.displayName)) + default: + content = .message(.text(.init(body: L10n.commonUnsupportedEvent))) + } + default: + content = .message(.text(.init(body: L10n.commonUnsupportedEvent))) + } + + return SearchServiceResult(roomID: roomID, + eventID: result.eventId, + sender: sender, + content: content, + timestamp: Date(timeIntervalSince1970: TimeInterval(result.timestamp) / 1000)) + } + } +} diff --git a/ElementX/Sources/Services/Search/SearchServiceProxyProtocol.swift b/ElementX/Sources/Services/Search/SearchServiceProxyProtocol.swift new file mode 100644 index 000000000..0db0fd11a --- /dev/null +++ b/ElementX/Sources/Services/Search/SearchServiceProxyProtocol.swift @@ -0,0 +1,31 @@ +// Copyright 2025 Element Creations Ltd. +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. + +import Combine +import Foundation +import MatrixRustSDK + +enum SearchServiceProxyError: Error { + case sdkError(Error) +} + +// sourcery: AutoMockable +protocol SearchServiceProxyProtocol { + var resultsPublisher: CurrentValuePublisher<[SearchServiceResult], Never> { get } + + func setQuery(_ query: String) async -> Result + + /// Loads the next page of results. No-ops if a page is already loading or the end has been reached. + func paginate() async +} + +struct SearchServiceResult { + let roomID: String + let eventID: String + let sender: TimelineItemSender + let content: TimelineEventContent + let timestamp: Date +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 91e7f0400..640d2976a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -895,7 +895,7 @@ nonisolated struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { // MARK: - Helpers - private func buildMessageTimelineItemContent(messageType: MessageType?, senderID: String, senderDisplayName: String?) -> EventBasedMessageTimelineItemContentType { + func buildMessageTimelineItemContent(messageType: MessageType?, senderID: String, senderDisplayName: String?) -> EventBasedMessageTimelineItemContentType { switch messageType { case .audio(let content): if content.voice != nil { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift index f13d56d9b..1e07e65e7 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift @@ -12,4 +12,5 @@ import MatrixRustSDK nonisolated protocol RoomTimelineItemFactoryProtocol: Sendable { func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol? func buildTimelineItemReply(_ details: MatrixRustSDK.InReplyToDetails) -> TimelineItemReply + func buildMessageTimelineItemContent(messageType: MessageType?, senderID: String, senderDisplayName: String?) -> EventBasedMessageTimelineItemContentType } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 2849a463d..33ba9cea8 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -140,6 +140,8 @@ class UserSessionStore: UserSessionStoreProtocol { .sqliteStore(config: .init(dataPath: credentials.restorationToken.sessionDirectories.dataPath, cachePath: credentials.restorationToken.sessionDirectories.cachePath) .passphrase(passphrase: credentials.restorationToken.passphrase)) + .withSearchIndexStore(path: credentials.restorationToken.sessionDirectories.dataPath, + password: credentials.restorationToken.passphrase) .username(username: credentials.userID) .homeserverUrl(url: homeserverURL) diff --git a/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift b/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift index 584f1cd2c..5753b9615 100644 --- a/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift +++ b/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift @@ -6,6 +6,21 @@ import Foundation +open class BackupSecretsSDKMock: MatrixRustSDK.BackupSecrets, @unchecked Sendable { + public init() { + super.init(noHandle: .init()) + } + + public required init(unsafeFromHandle handle: UInt64) { + fatalError("init(unsafeFromHandle:) has not been implemented") + } + + fileprivate var handle: UInt64 { + get { return underlyingHandle } + set(value) { underlyingHandle = value } + } + fileprivate var underlyingHandle: UInt64! +} open class CheckCodeSenderSDKMock: MatrixRustSDK.CheckCodeSender, @unchecked Sendable { public init() { super.init(noHandle: .init()) @@ -4173,50 +4188,32 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable { } } - //MARK: - searchMessages + //MARK: - searchService - open var searchMessagesQueryFilterThrowableError: Error? - private let searchMessagesQueryFilterCallsCountLock = NSLock() - private var searchMessagesQueryFilterUnderlyingCallsCount = 0 - open var searchMessagesQueryFilterCallsCount: Int { - get { searchMessagesQueryFilterCallsCountLock.withLock { searchMessagesQueryFilterUnderlyingCallsCount } } - set { searchMessagesQueryFilterCallsCountLock.withLock { searchMessagesQueryFilterUnderlyingCallsCount = newValue } } + private let searchServiceCallsCountLock = NSLock() + private var searchServiceUnderlyingCallsCount = 0 + open var searchServiceCallsCount: Int { + get { searchServiceCallsCountLock.withLock { searchServiceUnderlyingCallsCount } } + set { searchServiceCallsCountLock.withLock { searchServiceUnderlyingCallsCount = newValue } } } - open var searchMessagesQueryFilterCalled: Bool { - return searchMessagesQueryFilterCallsCount > 0 - } - private let searchMessagesQueryFilterReceivedArgumentsLock = NSLock() - private var searchMessagesQueryFilterUnderlyingReceivedArguments: (query: String, filter: SearchRoomFilter)? - open var searchMessagesQueryFilterReceivedArguments: (query: String, filter: SearchRoomFilter)? { - get { searchMessagesQueryFilterReceivedArgumentsLock.withLock { searchMessagesQueryFilterUnderlyingReceivedArguments } } - set { searchMessagesQueryFilterReceivedArgumentsLock.withLock { searchMessagesQueryFilterUnderlyingReceivedArguments = newValue } } - } - private let searchMessagesQueryFilterReceivedInvocationsLock = NSLock() - private var searchMessagesQueryFilterUnderlyingReceivedInvocations: [(query: String, filter: SearchRoomFilter)] = [] - open var searchMessagesQueryFilterReceivedInvocations: [(query: String, filter: SearchRoomFilter)] { - get { searchMessagesQueryFilterReceivedInvocationsLock.withLock { searchMessagesQueryFilterUnderlyingReceivedInvocations } } - set { searchMessagesQueryFilterReceivedInvocationsLock.withLock { searchMessagesQueryFilterUnderlyingReceivedInvocations = newValue } } + open var searchServiceCalled: Bool { + return searchServiceCallsCount > 0 } - private let searchMessagesQueryFilterReturnValueLock = NSLock() - open var searchMessagesQueryFilterUnderlyingReturnValue: GlobalSearchIterator! - open var searchMessagesQueryFilterReturnValue: GlobalSearchIterator! { - get { searchMessagesQueryFilterReturnValueLock.withLock { searchMessagesQueryFilterUnderlyingReturnValue } } - set { searchMessagesQueryFilterReturnValueLock.withLock { searchMessagesQueryFilterUnderlyingReturnValue = newValue } } + private let searchServiceReturnValueLock = NSLock() + open var searchServiceUnderlyingReturnValue: SearchService! + open var searchServiceReturnValue: SearchService! { + get { searchServiceReturnValueLock.withLock { searchServiceUnderlyingReturnValue } } + set { searchServiceReturnValueLock.withLock { searchServiceUnderlyingReturnValue = newValue } } } - open var searchMessagesQueryFilterClosure: ((String, SearchRoomFilter) async throws -> GlobalSearchIterator)? + open var searchServiceClosure: (() -> SearchService)? - open override func searchMessages(query: String, filter: SearchRoomFilter) async throws -> GlobalSearchIterator { - if let error = searchMessagesQueryFilterThrowableError { - throw error - } - searchMessagesQueryFilterCallsCountLock.withLock { searchMessagesQueryFilterUnderlyingCallsCount += 1 } - searchMessagesQueryFilterReceivedArguments = (query: query, filter: filter) - searchMessagesQueryFilterReceivedInvocationsLock.withLock { searchMessagesQueryFilterUnderlyingReceivedInvocations.append((query: query, filter: filter)) } - if let searchMessagesQueryFilterClosure = searchMessagesQueryFilterClosure { - return try await searchMessagesQueryFilterClosure(query, filter) + open override func searchService() -> SearchService { + searchServiceCallsCountLock.withLock { searchServiceUnderlyingCallsCount += 1 } + if let searchServiceClosure = searchServiceClosure { + return searchServiceClosure() } else { - return searchMessagesQueryFilterReturnValue + return searchServiceReturnValue } } } @@ -5421,6 +5418,21 @@ open class ContentScannerSDKMock: MatrixRustSDK.ContentScanner, @unchecked Senda } } } +open class CrossSigningSecretsSDKMock: MatrixRustSDK.CrossSigningSecrets, @unchecked Sendable { + public init() { + super.init(noHandle: .init()) + } + + public required init(unsafeFromHandle handle: UInt64) { + fatalError("init(unsafeFromHandle:) has not been implemented") + } + + fileprivate var handle: UInt64 { + get { return underlyingHandle } + set(value) { underlyingHandle = value } + } + fileprivate var underlyingHandle: UInt64! +} open class EncryptionSDKMock: MatrixRustSDK.Encryption, @unchecked Sendable { public init() { super.init(noHandle: .init()) @@ -6222,54 +6234,6 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption, @unchecked Sendable { await waitForE2eeInitializationTasksClosure?() } } -open class GlobalSearchIteratorSDKMock: MatrixRustSDK.GlobalSearchIterator, @unchecked Sendable { - public init() { - super.init(noHandle: .init()) - } - - public required init(unsafeFromHandle handle: UInt64) { - fatalError("init(unsafeFromHandle:) has not been implemented") - } - - fileprivate var handle: UInt64 { - get { return underlyingHandle } - set(value) { underlyingHandle = value } - } - fileprivate var underlyingHandle: UInt64! - - //MARK: - nextEvents - - open var nextEventsThrowableError: Error? - private let nextEventsCallsCountLock = NSLock() - private var nextEventsUnderlyingCallsCount = 0 - open var nextEventsCallsCount: Int { - get { nextEventsCallsCountLock.withLock { nextEventsUnderlyingCallsCount } } - set { nextEventsCallsCountLock.withLock { nextEventsUnderlyingCallsCount = newValue } } - } - open var nextEventsCalled: Bool { - return nextEventsCallsCount > 0 - } - - private let nextEventsReturnValueLock = NSLock() - open var nextEventsUnderlyingReturnValue: [GlobalSearchResult]? - open var nextEventsReturnValue: [GlobalSearchResult]? { - get { nextEventsReturnValueLock.withLock { nextEventsUnderlyingReturnValue } } - set { nextEventsReturnValueLock.withLock { nextEventsUnderlyingReturnValue = newValue } } - } - open var nextEventsClosure: (() async throws -> [GlobalSearchResult]?)? - - open override func nextEvents() async throws -> [GlobalSearchResult]? { - if let error = nextEventsThrowableError { - throw error - } - nextEventsCallsCountLock.withLock { nextEventsUnderlyingCallsCount += 1 } - if let nextEventsClosure = nextEventsClosure { - return try await nextEventsClosure() - } else { - return nextEventsReturnValue - } - } -} open class GrantLoginWithQrCodeHandlerSDKMock: MatrixRustSDK.GrantLoginWithQrCodeHandler, @unchecked Sendable { public init() { super.init(noHandle: .init()) @@ -8689,6 +8653,21 @@ open class OAuthAuthorizationDataSDKMock: MatrixRustSDK.OAuthAuthorizationData, } } } +open class PrivateStringSDKMock: MatrixRustSDK.PrivateString, @unchecked Sendable { + public init() { + super.init(noHandle: .init()) + } + + public required init(unsafeFromHandle handle: UInt64) { + fatalError("init(unsafeFromHandle:) has not been implemented") + } + + fileprivate var handle: UInt64 { + get { return underlyingHandle } + set(value) { underlyingHandle = value } + } + fileprivate var underlyingHandle: UInt64! +} open class QrCodeDataSDKMock: MatrixRustSDK.QrCodeData, @unchecked Sendable { public init() { super.init(noHandle: .init()) @@ -12307,49 +12286,6 @@ open class RoomSDKMock: MatrixRustSDK.Room, @unchecked Sendable { withdrawVerificationAndResendUserIdsSendHandleReceivedInvocationsLock.withLock { withdrawVerificationAndResendUserIdsSendHandleUnderlyingReceivedInvocations.append((userIds: userIds, sendHandle: sendHandle)) } try await withdrawVerificationAndResendUserIdsSendHandleClosure?(userIds, sendHandle) } - - //MARK: - searchMessages - - private let searchMessagesQueryCallsCountLock = NSLock() - private var searchMessagesQueryUnderlyingCallsCount = 0 - open var searchMessagesQueryCallsCount: Int { - get { searchMessagesQueryCallsCountLock.withLock { searchMessagesQueryUnderlyingCallsCount } } - set { searchMessagesQueryCallsCountLock.withLock { searchMessagesQueryUnderlyingCallsCount = newValue } } - } - open var searchMessagesQueryCalled: Bool { - return searchMessagesQueryCallsCount > 0 - } - private let searchMessagesQueryReceivedQueryLock = NSLock() - private var searchMessagesQueryUnderlyingReceivedQuery: String? - open var searchMessagesQueryReceivedQuery: String? { - get { searchMessagesQueryReceivedQueryLock.withLock { searchMessagesQueryUnderlyingReceivedQuery } } - set { searchMessagesQueryReceivedQueryLock.withLock { searchMessagesQueryUnderlyingReceivedQuery = newValue } } - } - private let searchMessagesQueryReceivedInvocationsLock = NSLock() - private var searchMessagesQueryUnderlyingReceivedInvocations: [String] = [] - open var searchMessagesQueryReceivedInvocations: [String] { - get { searchMessagesQueryReceivedInvocationsLock.withLock { searchMessagesQueryUnderlyingReceivedInvocations } } - set { searchMessagesQueryReceivedInvocationsLock.withLock { searchMessagesQueryUnderlyingReceivedInvocations = newValue } } - } - - private let searchMessagesQueryReturnValueLock = NSLock() - open var searchMessagesQueryUnderlyingReturnValue: RoomSearchIterator! - open var searchMessagesQueryReturnValue: RoomSearchIterator! { - get { searchMessagesQueryReturnValueLock.withLock { searchMessagesQueryUnderlyingReturnValue } } - set { searchMessagesQueryReturnValueLock.withLock { searchMessagesQueryUnderlyingReturnValue = newValue } } - } - open var searchMessagesQueryClosure: ((String) -> RoomSearchIterator)? - - open override func searchMessages(query: String) -> RoomSearchIterator { - searchMessagesQueryCallsCountLock.withLock { searchMessagesQueryUnderlyingCallsCount += 1 } - searchMessagesQueryReceivedQuery = query - searchMessagesQueryReceivedInvocationsLock.withLock { searchMessagesQueryUnderlyingReceivedInvocations.append(query) } - if let searchMessagesQueryClosure = searchMessagesQueryClosure { - return searchMessagesQueryClosure(query) - } else { - return searchMessagesQueryReturnValue - } - } } open class RoomDirectorySearchSDKMock: MatrixRustSDK.RoomDirectorySearch, @unchecked Sendable { public init() { @@ -14174,7 +14110,7 @@ open class RoomPreviewSDKMock: MatrixRustSDK.RoomPreview, @unchecked Sendable { } } } -open class RoomSearchIteratorSDKMock: MatrixRustSDK.RoomSearchIterator, @unchecked Sendable { +open class SearchServiceSDKMock: MatrixRustSDK.SearchService, @unchecked Sendable { public init() { super.init(noHandle: .init()) } @@ -14189,38 +14125,193 @@ open class RoomSearchIteratorSDKMock: MatrixRustSDK.RoomSearchIterator, @uncheck } fileprivate var underlyingHandle: UInt64! - //MARK: - nextEvents + //MARK: - paginate - open var nextEventsThrowableError: Error? - private let nextEventsCallsCountLock = NSLock() - private var nextEventsUnderlyingCallsCount = 0 - open var nextEventsCallsCount: Int { - get { nextEventsCallsCountLock.withLock { nextEventsUnderlyingCallsCount } } - set { nextEventsCallsCountLock.withLock { nextEventsUnderlyingCallsCount = newValue } } + open var paginateThrowableError: Error? + private let paginateCallsCountLock = NSLock() + private var paginateUnderlyingCallsCount = 0 + open var paginateCallsCount: Int { + get { paginateCallsCountLock.withLock { paginateUnderlyingCallsCount } } + set { paginateCallsCountLock.withLock { paginateUnderlyingCallsCount = newValue } } } - open var nextEventsCalled: Bool { - return nextEventsCallsCount > 0 + open var paginateCalled: Bool { + return paginateCallsCount > 0 } + open var paginateClosure: (() async throws -> Void)? - private let nextEventsReturnValueLock = NSLock() - open var nextEventsUnderlyingReturnValue: [RoomSearchResult]? - open var nextEventsReturnValue: [RoomSearchResult]? { - get { nextEventsReturnValueLock.withLock { nextEventsUnderlyingReturnValue } } - set { nextEventsReturnValueLock.withLock { nextEventsUnderlyingReturnValue = newValue } } - } - open var nextEventsClosure: (() async throws -> [RoomSearchResult]?)? - - open override func nextEvents() async throws -> [RoomSearchResult]? { - if let error = nextEventsThrowableError { + open override func paginate() async throws { + if let error = paginateThrowableError { throw error } - nextEventsCallsCountLock.withLock { nextEventsUnderlyingCallsCount += 1 } - if let nextEventsClosure = nextEventsClosure { - return try await nextEventsClosure() + paginateCallsCountLock.withLock { paginateUnderlyingCallsCount += 1 } + try await paginateClosure?() + } + + //MARK: - paginationState + + private let paginationStateCallsCountLock = NSLock() + private var paginationStateUnderlyingCallsCount = 0 + open var paginationStateCallsCount: Int { + get { paginationStateCallsCountLock.withLock { paginationStateUnderlyingCallsCount } } + set { paginationStateCallsCountLock.withLock { paginationStateUnderlyingCallsCount = newValue } } + } + open var paginationStateCalled: Bool { + return paginationStateCallsCount > 0 + } + + private let paginationStateReturnValueLock = NSLock() + open var paginationStateUnderlyingReturnValue: SearchServicePaginationState! + open var paginationStateReturnValue: SearchServicePaginationState! { + get { paginationStateReturnValueLock.withLock { paginationStateUnderlyingReturnValue } } + set { paginationStateReturnValueLock.withLock { paginationStateUnderlyingReturnValue = newValue } } + } + open var paginationStateClosure: (() -> SearchServicePaginationState)? + + open override func paginationState() -> SearchServicePaginationState { + paginationStateCallsCountLock.withLock { paginationStateUnderlyingCallsCount += 1 } + if let paginationStateClosure = paginationStateClosure { + return paginationStateClosure() } else { - return nextEventsReturnValue + return paginationStateReturnValue } } + + //MARK: - setQuery + + open var setQueryQueryThrowableError: Error? + private let setQueryQueryCallsCountLock = NSLock() + private var setQueryQueryUnderlyingCallsCount = 0 + open var setQueryQueryCallsCount: Int { + get { setQueryQueryCallsCountLock.withLock { setQueryQueryUnderlyingCallsCount } } + set { setQueryQueryCallsCountLock.withLock { setQueryQueryUnderlyingCallsCount = newValue } } + } + open var setQueryQueryCalled: Bool { + return setQueryQueryCallsCount > 0 + } + private let setQueryQueryReceivedQueryLock = NSLock() + private var setQueryQueryUnderlyingReceivedQuery: String? + open var setQueryQueryReceivedQuery: String? { + get { setQueryQueryReceivedQueryLock.withLock { setQueryQueryUnderlyingReceivedQuery } } + set { setQueryQueryReceivedQueryLock.withLock { setQueryQueryUnderlyingReceivedQuery = newValue } } + } + private let setQueryQueryReceivedInvocationsLock = NSLock() + private var setQueryQueryUnderlyingReceivedInvocations: [String] = [] + open var setQueryQueryReceivedInvocations: [String] { + get { setQueryQueryReceivedInvocationsLock.withLock { setQueryQueryUnderlyingReceivedInvocations } } + set { setQueryQueryReceivedInvocationsLock.withLock { setQueryQueryUnderlyingReceivedInvocations = newValue } } + } + open var setQueryQueryClosure: ((String) async throws -> Void)? + + open override func setQuery(query: String) async throws { + if let error = setQueryQueryThrowableError { + throw error + } + setQueryQueryCallsCountLock.withLock { setQueryQueryUnderlyingCallsCount += 1 } + setQueryQueryReceivedQuery = query + setQueryQueryReceivedInvocationsLock.withLock { setQueryQueryUnderlyingReceivedInvocations.append(query) } + try await setQueryQueryClosure?(query) + } + + //MARK: - subscribeToPaginationStateUpdates + + private let subscribeToPaginationStateUpdatesListenerCallsCountLock = NSLock() + private var subscribeToPaginationStateUpdatesListenerUnderlyingCallsCount = 0 + open var subscribeToPaginationStateUpdatesListenerCallsCount: Int { + get { subscribeToPaginationStateUpdatesListenerCallsCountLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingCallsCount } } + set { subscribeToPaginationStateUpdatesListenerCallsCountLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingCallsCount = newValue } } + } + open var subscribeToPaginationStateUpdatesListenerCalled: Bool { + return subscribeToPaginationStateUpdatesListenerCallsCount > 0 + } + private let subscribeToPaginationStateUpdatesListenerReceivedListenerLock = NSLock() + private var subscribeToPaginationStateUpdatesListenerUnderlyingReceivedListener: SearchServicePaginationStateListener? + open var subscribeToPaginationStateUpdatesListenerReceivedListener: SearchServicePaginationStateListener? { + get { subscribeToPaginationStateUpdatesListenerReceivedListenerLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingReceivedListener } } + set { subscribeToPaginationStateUpdatesListenerReceivedListenerLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingReceivedListener = newValue } } + } + private let subscribeToPaginationStateUpdatesListenerReceivedInvocationsLock = NSLock() + private var subscribeToPaginationStateUpdatesListenerUnderlyingReceivedInvocations: [SearchServicePaginationStateListener] = [] + open var subscribeToPaginationStateUpdatesListenerReceivedInvocations: [SearchServicePaginationStateListener] { + get { subscribeToPaginationStateUpdatesListenerReceivedInvocationsLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingReceivedInvocations } } + set { subscribeToPaginationStateUpdatesListenerReceivedInvocationsLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingReceivedInvocations = newValue } } + } + + private let subscribeToPaginationStateUpdatesListenerReturnValueLock = NSLock() + open var subscribeToPaginationStateUpdatesListenerUnderlyingReturnValue: TaskHandle! + open var subscribeToPaginationStateUpdatesListenerReturnValue: TaskHandle! { + get { subscribeToPaginationStateUpdatesListenerReturnValueLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingReturnValue } } + set { subscribeToPaginationStateUpdatesListenerReturnValueLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingReturnValue = newValue } } + } + open var subscribeToPaginationStateUpdatesListenerClosure: ((SearchServicePaginationStateListener) -> TaskHandle)? + + open override func subscribeToPaginationStateUpdates(listener: SearchServicePaginationStateListener) -> TaskHandle { + subscribeToPaginationStateUpdatesListenerCallsCountLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingCallsCount += 1 } + subscribeToPaginationStateUpdatesListenerReceivedListener = listener + subscribeToPaginationStateUpdatesListenerReceivedInvocationsLock.withLock { subscribeToPaginationStateUpdatesListenerUnderlyingReceivedInvocations.append(listener) } + if let subscribeToPaginationStateUpdatesListenerClosure = subscribeToPaginationStateUpdatesListenerClosure { + return subscribeToPaginationStateUpdatesListenerClosure(listener) + } else { + return subscribeToPaginationStateUpdatesListenerReturnValue + } + } + + //MARK: - subscribeToResults + + private let subscribeToResultsListenerCallsCountLock = NSLock() + private var subscribeToResultsListenerUnderlyingCallsCount = 0 + open var subscribeToResultsListenerCallsCount: Int { + get { subscribeToResultsListenerCallsCountLock.withLock { subscribeToResultsListenerUnderlyingCallsCount } } + set { subscribeToResultsListenerCallsCountLock.withLock { subscribeToResultsListenerUnderlyingCallsCount = newValue } } + } + open var subscribeToResultsListenerCalled: Bool { + return subscribeToResultsListenerCallsCount > 0 + } + private let subscribeToResultsListenerReceivedListenerLock = NSLock() + private var subscribeToResultsListenerUnderlyingReceivedListener: SearchServiceResultsListener? + open var subscribeToResultsListenerReceivedListener: SearchServiceResultsListener? { + get { subscribeToResultsListenerReceivedListenerLock.withLock { subscribeToResultsListenerUnderlyingReceivedListener } } + set { subscribeToResultsListenerReceivedListenerLock.withLock { subscribeToResultsListenerUnderlyingReceivedListener = newValue } } + } + private let subscribeToResultsListenerReceivedInvocationsLock = NSLock() + private var subscribeToResultsListenerUnderlyingReceivedInvocations: [SearchServiceResultsListener] = [] + open var subscribeToResultsListenerReceivedInvocations: [SearchServiceResultsListener] { + get { subscribeToResultsListenerReceivedInvocationsLock.withLock { subscribeToResultsListenerUnderlyingReceivedInvocations } } + set { subscribeToResultsListenerReceivedInvocationsLock.withLock { subscribeToResultsListenerUnderlyingReceivedInvocations = newValue } } + } + + private let subscribeToResultsListenerReturnValueLock = NSLock() + open var subscribeToResultsListenerUnderlyingReturnValue: TaskHandle! + open var subscribeToResultsListenerReturnValue: TaskHandle! { + get { subscribeToResultsListenerReturnValueLock.withLock { subscribeToResultsListenerUnderlyingReturnValue } } + set { subscribeToResultsListenerReturnValueLock.withLock { subscribeToResultsListenerUnderlyingReturnValue = newValue } } + } + open var subscribeToResultsListenerClosure: ((SearchServiceResultsListener) async -> TaskHandle)? + + open override func subscribeToResults(listener: SearchServiceResultsListener) async -> TaskHandle { + subscribeToResultsListenerCallsCountLock.withLock { subscribeToResultsListenerUnderlyingCallsCount += 1 } + subscribeToResultsListenerReceivedListener = listener + subscribeToResultsListenerReceivedInvocationsLock.withLock { subscribeToResultsListenerUnderlyingReceivedInvocations.append(listener) } + if let subscribeToResultsListenerClosure = subscribeToResultsListenerClosure { + return await subscribeToResultsListenerClosure(listener) + } else { + return subscribeToResultsListenerReturnValue + } + } +} +open class SecretsBundleSDKMock: MatrixRustSDK.SecretsBundle, @unchecked Sendable { + public init() { + super.init(noHandle: .init()) + } + + public required init(unsafeFromHandle handle: UInt64) { + fatalError("init(unsafeFromHandle:) has not been implemented") + } + + fileprivate var handle: UInt64 { + get { return underlyingHandle } + set(value) { underlyingHandle = value } + } + fileprivate var underlyingHandle: UInt64! } open class SecretsBundleWithUserIdSDKMock: MatrixRustSDK.SecretsBundleWithUserId, @unchecked Sendable { public init() {