* Automatic discovery of the sliding sync proxy. * Handle homeserver detection from a MXID. * Update sliding sync info link, SDK and bump patch. Add a migration on the new version. The change in restoration token format requires the user to sign in again, but this will fail with the existing device ID.
419 lines
16 KiB
Swift
419 lines
16 KiB
Swift
//
|
|
// 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<AnyCancellable>()
|
|
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<ClientProxyCallback, Never>()
|
|
|
|
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<String, ClientProxyError> {
|
|
await Task.dispatch(on: clientQueue) {
|
|
do {
|
|
let displayName = try self.client.displayName()
|
|
return .success(displayName)
|
|
} catch {
|
|
return .failure(.failedRetrievingDisplayName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadUserAvatarURL() async -> Result<URL, ClientProxyError> {
|
|
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<Content>(type: String) async -> Result<Content?, ClientProxyError> where Content: Decodable {
|
|
await Task.dispatch(on: clientQueue) {
|
|
.failure(.failedRetrievingAccountData)
|
|
}
|
|
}
|
|
|
|
func setAccountData<Content: Encodable>(content: Content, type: String) async -> Result<Void, ClientProxyError> {
|
|
await Task.dispatch(on: clientQueue) {
|
|
.failure(.failedSettingAccountData)
|
|
}
|
|
}
|
|
|
|
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
|
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)
|
|
}
|
|
}
|