// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Combine import Foundation import MatrixRustSDK import UIKit private class WeakClientProxyWrapper: ClientDelegate, SlidingSyncObserver { private weak var clientProxy: ClientProxy? init(clientProxy: ClientProxy) { self.clientProxy = clientProxy } // MARK: - ClientDelegate func didReceiveSyncUpdate() { } func didReceiveAuthError(isSoftLogout: Bool) { MXLog.error("Received authentication error, softlogout=\(isSoftLogout)") clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout) } func didUpdateRestoreToken() { MXLog.info("Did update restoration token") clientProxy?.didUpdateRestoreToken() } // MARK: - SlidingSyncDelegate func didReceiveSyncUpdate(summary: UpdateSummary) { MXLog.info("Received sliding sync update") clientProxy?.didReceiveSlidingSyncUpdate(summary: summary) } } class ClientProxy: ClientProxyProtocol { private let client: ClientProtocol private let backgroundTaskService: BackgroundTaskServiceProtocol private var sessionVerificationControllerProxy: SessionVerificationControllerProxy? private let mediaLoader: MediaLoaderProtocol private let clientQueue: DispatchQueue private var slidingSyncObserverToken: StoppableSpawn? private var slidingSync: SlidingSync? var visibleRoomsSlidingSyncView: SlidingSyncView? var visibleRoomsSummaryProvider: RoomSummaryProviderProtocol? var allRoomsSlidingSyncView: SlidingSyncView? var allRoomsSummaryProvider: RoomSummaryProviderProtocol? private var cancellables = Set() private var visibleRoomsViewProxyStateObservationToken: AnyCancellable? deinit { // These need to be inlined instead of using stopSync() // as we can't call async methods safely from deinit client.setDelegate(delegate: nil) slidingSyncObserverToken?.cancel() slidingSync?.setObserver(observer: nil) } let callbacks = PassthroughSubject() init(client: ClientProtocol, backgroundTaskService: BackgroundTaskServiceProtocol) async { self.client = client self.backgroundTaskService = backgroundTaskService clientQueue = .init(label: "ClientProxyQueue", attributes: .concurrent) mediaLoader = MediaLoader(client: client, clientQueue: clientQueue) client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self)) configureSlidingSync() } var userID: String { do { return try client.userId() } catch { MXLog.error("Failed retrieving room info with error: \(error)") return "Unknown user identifier" } } var isSoftLogout: Bool { client.isSoftLogout() } var deviceId: String? { do { return try client.deviceId() } catch { MXLog.error("Failed retrieving device id with error: \(error)") return nil } } var homeserver: String { client.homeserver() } var restorationToken: RestorationToken? { do { return try RestorationToken(session: client.session()) } catch { MXLog.error("Failed retrieving restore token with error: \(error)") return nil } } func startSync() { MXLog.info("Starting sync") guard !client.isSoftLogout(), slidingSyncObserverToken == nil else { return } slidingSyncObserverToken = slidingSync?.sync() } func stopSync() { MXLog.info("Stopping sync") slidingSyncObserverToken?.cancel() slidingSyncObserverToken = nil } func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? { let (slidingSyncRoom, room) = await Task.dispatch(on: clientQueue) { self.roomTupleForIdentifier(identifier) } guard let slidingSyncRoom, let room else { return nil } return await RoomProxy(slidingSyncRoom: slidingSyncRoom, room: room, backgroundTaskService: backgroundTaskService) } func loadUserDisplayName() async -> Result { await Task.dispatch(on: clientQueue) { do { let displayName = try self.client.displayName() return .success(displayName) } catch { return .failure(.failedRetrievingDisplayName) } } } func loadUserAvatarURL() async -> Result { await Task.dispatch(on: clientQueue) { do { let urlString = try self.client.avatarUrl() guard let url = URL(string: urlString) else { MXLog.error("Invalid avatar URL string: \(String(describing: urlString))") return .failure(.failedRetrievingAvatarURL) } return .success(url) } catch { return .failure(.failedRetrievingAvatarURL) } } } func accountDataEvent(type: String) async -> Result where Content: Decodable { await Task.dispatch(on: clientQueue) { .failure(.failedRetrievingAccountData) } } func setAccountData(content: Content, type: String) async -> Result { await Task.dispatch(on: clientQueue) { .failure(.failedSettingAccountData) } } func sessionVerificationControllerProxy() async -> Result { await Task.dispatch(on: clientQueue) { do { let sessionVerificationController = try self.client.getSessionVerificationController() return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController)) } catch { return .failure(.failedRetrievingSessionVerificationController) } } } func logout() async { await Task.dispatch(on: clientQueue) { do { try self.client.logout() } catch { MXLog.error("Failed logging out with error: \(error)") } } } // swiftlint:disable:next function_parameter_count func setPusher(pushkey: String, kind: PusherKind?, appId: String, appDisplayName: String, deviceDisplayName: String, profileTag: String?, lang: String, url: URL?, format: PushFormat?, defaultPayload: [AnyHashable: Any]?) async throws { // let defaultPayloadString = jsonString(from: defaultPayload) // try await Task.dispatch(on: .global()) { // try self.client.setPusher(pushkey: pushkey, // kind: kind?.rustValue, // appId: appId, // appDisplayName: appDisplayName, // deviceDisplayName: deviceDisplayName, // profileTag: profileTag, // lang: lang, // url: url, // format: format?.rustValue, // defaultPayload: defaultPayloadString) // } } // MARK: Private private func restartSync() { MXLog.info("Restarting sync") stopSync() startSync() } private func configureSlidingSync() { guard slidingSync == nil else { fatalError("This shouldn't be called more than once") } do { let slidingSyncBuilder = client.slidingSync() // Build the visibleRoomsSlidingSyncView here so that it can take advantage of the SS builder cold cache // We will still register the allRoomsSlidingSyncView later, and than will have no cache let visibleRoomsView = try SlidingSyncViewBuilder() .timelineLimit(limit: UInt32(SlidingSyncConstants.initialTimelineLimit)) // Starts off with zero to quickly load rooms, then goes to 1 while scrolling to quickly load last messages and 20 when the scrolling stops to load room history .requiredState(requiredState: slidingSyncRequiredState) .filters(filters: slidingSyncFilters) .name(name: "CurrentlyVisibleRooms") .syncMode(mode: .selective) .addRange(from: 0, to: 20) .sendUpdatesForItems(enable: true) .build() let slidingSync = try slidingSyncBuilder .addView(v: visibleRoomsView) .withCommonExtensions() .coldCache(name: "ElementX") .build() slidingSync.setObserver(observer: WeakClientProxyWrapper(clientProxy: self)) self.slidingSync = slidingSync buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: slidingSync, visibleRoomsView: visibleRoomsView) buildAndConfigureAllRoomsSlidingSyncView(slidingSync: slidingSync) } catch { MXLog.error("Failed building sliding sync with error: \(error)") } } private func buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: SlidingSyncProtocol, visibleRoomsView: SlidingSyncView) { let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView) visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy, eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))) visibleRoomsSlidingSyncView = visibleRoomsView // Changes to the visibleRoomsSlidingSyncView range need to restart the connection to be applied visibleRoomsViewProxy.visibleRangeUpdatePublisher.sink { [weak self] in self?.restartSync() } .store(in: &cancellables) // The allRoomsSlidingSyncView will be registered as soon as the visibleRoomsSlidingSyncView receives its first update visibleRoomsViewProxyStateObservationToken = visibleRoomsViewProxy.diffPublisher.sink { [weak self] _ in MXLog.info("Visible rooms view received first update, configuring views post initial sync") self?.configureViewsPostInitialSync() self?.visibleRoomsViewProxyStateObservationToken = nil } } private func buildAndConfigureAllRoomsSlidingSyncView(slidingSync: SlidingSyncProtocol) { guard allRoomsSlidingSyncView == nil else { fatalError("This shouldn't be called more than once") } do { let allRoomsView = try SlidingSyncViewBuilder() .noTimelineLimit() .requiredState(requiredState: slidingSyncRequiredState) .filters(filters: slidingSyncFilters) .name(name: "AllRooms") .syncMode(mode: .growingFullSync) .batchSize(batchSize: 100) .sendUpdatesForItems(enable: true) .build() let allRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: allRoomsView) allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy, eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))) allRoomsSlidingSyncView = allRoomsView } catch { MXLog.error("Failed building the all rooms sliding sync view with error: \(error)") } } private lazy var slidingSyncRequiredState = [RequiredState(key: "m.room.avatar", value: ""), RequiredState(key: "m.room.encryption", value: "")] private lazy var slidingSyncFilters = SlidingSyncRequestListFilters(isDm: nil, spaces: [], isEncrypted: nil, isInvite: false, isTombstoned: false, roomTypes: [], notRoomTypes: ["m.space"], roomNameLike: nil, tags: [], notTags: []) private func configureViewsPostInitialSync() { if let visibleRoomsSlidingSyncView { MXLog.info("Setting visible rooms view timeline limit to \(SlidingSyncConstants.lastMessageTimelineLimit)") visibleRoomsSlidingSyncView.setTimelineLimit(value: UInt32(SlidingSyncConstants.lastMessageTimelineLimit)) } else { MXLog.error("Visible rooms sliding sync view unavailable") } if let allRoomsSlidingSyncView { MXLog.info("Registering all rooms view") _ = slidingSync?.addView(view: allRoomsSlidingSyncView) } else { MXLog.error("All rooms sliding sync view unavailable") } restartSync() } private func roomTupleForIdentifier(_ identifier: String) -> (SlidingSyncRoom?, Room?) { do { let slidingSyncRoom = try slidingSync?.getRoom(roomId: identifier) let fullRoom = slidingSyncRoom?.fullRoom() return (slidingSyncRoom, fullRoom) } catch { MXLog.error("Failed retrieving room with identifier: \(identifier)") return (nil, nil) } } fileprivate func didReceiveAuthError(isSoftLogout: Bool) { callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout)) } fileprivate func didUpdateRestoreToken() { callbacks.send(.updatedRestoreToken) } fileprivate func didReceiveSlidingSyncUpdate(summary: UpdateSummary) { callbacks.send(.receivedSyncUpdate) } /// Convenience method to get the json string of an Encodable private func jsonString(from dictionary: [AnyHashable: Any]?) -> String? { guard let dictionary, let data = try? JSONSerialization.data(withJSONObject: dictionary, options: [.fragmentsAllowed]) else { return nil } return String(data: data, encoding: .utf8) } } extension ClientProxy: MediaLoaderProtocol { func mediaSourceForURL(_ url: URL) async -> MediaSourceProxy { await mediaLoader.mediaSourceForURL(url) } func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data { try await mediaLoader.loadMediaContentForSource(source) } func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data { try await mediaLoader.loadMediaThumbnailForSource(source, width: width, height: height) } }