mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
#53 - Refactored various components:
- updated the authentication coordinator to use async await - replaced the UserSession with a ClientProxy (similar to the RoomProxy) - renamed `completion` to `callback` on the coordinators and view models - renamed `viewModelResult` to `viewModelAction` - introduced back UserSession as a user specific service container
This commit is contained in:
committed by
Stefan Ceriu
parent
edf765b7cd
commit
1b4d3a650e
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
private let window: UIWindow
|
||||
@@ -18,8 +19,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
private let keychainController: KeychainControllerProtocol
|
||||
private let authenticationCoordinator: AuthenticationCoordinator!
|
||||
|
||||
private var userSession: UserSession!
|
||||
|
||||
private let memberDetailProviderManager: MemberDetailProviderManager
|
||||
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var errorIndicator: UserIndicator?
|
||||
@@ -70,15 +73,22 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
showLoginErrorToast()
|
||||
}
|
||||
|
||||
func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator) {
|
||||
func authenticationCoordinatorDidSetupClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) {
|
||||
guard let clientProxy = authenticationCoordinator.clientProxy else {
|
||||
fatalError("User session should be setup at this point")
|
||||
}
|
||||
|
||||
userSession = .init(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy, imageCache: ImageCache.default))
|
||||
|
||||
presentHomeScreen()
|
||||
}
|
||||
|
||||
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator) {
|
||||
func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) {
|
||||
if let presentedCoordinator = childCoordinators.first {
|
||||
remove(childCoordinator: presentedCoordinator)
|
||||
}
|
||||
|
||||
|
||||
mainNavigationController.setViewControllers([splashViewController], animated: false)
|
||||
authenticationCoordinator.start()
|
||||
}
|
||||
@@ -89,18 +99,17 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
hideLoadingIndicator()
|
||||
|
||||
guard let userSession = authenticationCoordinator.userSession else {
|
||||
guard let userSession = userSession else {
|
||||
fatalError("User session should be already setup at this point")
|
||||
}
|
||||
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
attributedStringBuilder: AttributedStringBuilder(),
|
||||
memberDetailProviderManager: memberDetailProviderManager)
|
||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.completion = { [weak self] result in
|
||||
switch result {
|
||||
coordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
case .logout:
|
||||
self?.authenticationCoordinator.logout()
|
||||
case .selectRoom(let roomIdentifier):
|
||||
@@ -113,11 +122,11 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
}
|
||||
|
||||
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
|
||||
guard let userSession = authenticationCoordinator.userSession else {
|
||||
guard let userSession = userSession else {
|
||||
fatalError("User session should be already setup at this point")
|
||||
}
|
||||
|
||||
guard let roomProxy = userSession.rooms.first(where: { $0.id == roomIdentifier }) else {
|
||||
guard let roomProxy = userSession.clientProxy.rooms.first(where: { $0.id == roomIdentifier }) else {
|
||||
MXLog.error("Invalid room identifier: \(roomIdentifier)")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,13 +18,12 @@ import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct HomeScreenCoordinatorParameters {
|
||||
let userSession: UserSession
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userSession: UserSessionProtocol
|
||||
let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
let memberDetailProviderManager: MemberDetailProviderManager
|
||||
}
|
||||
|
||||
enum HomeScreenCoordinatorResult {
|
||||
enum HomeScreenCoordinatorAction {
|
||||
case logout
|
||||
case selectRoom(roomIdentifier: String)
|
||||
}
|
||||
@@ -47,7 +46,7 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: ((HomeScreenCoordinatorResult) -> Void)?
|
||||
var callback: ((HomeScreenCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -59,22 +58,22 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
let view = HomeScreen(context: viewModel.context)
|
||||
hostingController = UIHostingController(rootView: view)
|
||||
|
||||
viewModel.completion = { [weak self] result in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
switch action {
|
||||
case .logout:
|
||||
self.completion?(.logout)
|
||||
self.callback?(.logout)
|
||||
case .selectRoom(let roomIdentifier):
|
||||
self.completion?(.selectRoom(roomIdentifier: roomIdentifier))
|
||||
self.callback?(.selectRoom(roomIdentifier: roomIdentifier))
|
||||
}
|
||||
}
|
||||
|
||||
parameters.userSession
|
||||
parameters.userSession.clientProxy
|
||||
.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
switch result {
|
||||
.sink { [weak self] action in
|
||||
switch action {
|
||||
case .updatedRoomsList:
|
||||
self?.updateRoomsList()
|
||||
}
|
||||
@@ -83,13 +82,13 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
updateRoomsList()
|
||||
|
||||
Task {
|
||||
if case let .success(userAvatarURL) = await parameters.userSession.loadUserAvatarURL() {
|
||||
if case let .success(avatar) = await parameters.mediaProvider.loadImageFromURL(userAvatarURL) {
|
||||
if case let .success(userAvatarURLString) = await parameters.userSession.clientProxy.loadUserAvatarURLString() {
|
||||
if case let .success(avatar) = await parameters.userSession.mediaProvider.loadImageFromURLString(userAvatarURLString) {
|
||||
self.viewModel.updateWithUserAvatar(avatar)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .success(userDisplayName) = await parameters.userSession.loadUserDisplayName() {
|
||||
if case let .success(userDisplayName) = await parameters.userSession.clientProxy.loadUserDisplayName() {
|
||||
self.viewModel.updateWithUserDisplayName(userDisplayName)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +106,7 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Private
|
||||
|
||||
func updateRoomsList() {
|
||||
self.roomSummaries = parameters.userSession.rooms.compactMap { roomProxy in
|
||||
roomSummaries = parameters.userSession.clientProxy.rooms.compactMap { roomProxy in
|
||||
guard !roomProxy.isSpace, !roomProxy.isTombstoned else {
|
||||
return nil
|
||||
}
|
||||
@@ -119,10 +118,10 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
let memberDetailProvider = parameters.memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy)
|
||||
|
||||
return RoomSummary(roomProxy: roomProxy,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
mediaProvider: parameters.userSession.mediaProvider,
|
||||
eventBriefFactory: EventBriefFactory(memberDetailProvider: memberDetailProvider))
|
||||
}
|
||||
|
||||
self.viewModel.updateWithRoomList(roomSummaries)
|
||||
viewModel.updateWithRoomSummaries(roomSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum HomeScreenViewModelResult {
|
||||
enum HomeScreenViewModelAction {
|
||||
case logout
|
||||
case selectRoom(roomIdentifier: String)
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
private var roomUpdateListeners = Set<AnyCancellable>()
|
||||
|
||||
private var roomList: [RoomSummaryProtocol]? {
|
||||
private var roomSummaries: [RoomSummaryProtocol]? {
|
||||
didSet {
|
||||
self.state.isLoadingRooms = (roomList?.count ?? 0 == 0)
|
||||
self.state.isLoadingRooms = (roomSummaries?.count ?? 0 == 0)
|
||||
}
|
||||
}
|
||||
|
||||
var completion: ((HomeScreenViewModelResult) -> Void)?
|
||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -46,24 +46,24 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
override func process(viewAction: HomeScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .logout:
|
||||
completion?(.logout)
|
||||
callback?(.logout)
|
||||
case .loadRoomData(let roomIdentifier):
|
||||
loadRoomDataForIdentifier(roomIdentifier)
|
||||
case .selectRoom(let roomIdentifier):
|
||||
completion?(.selectRoom(roomIdentifier: roomIdentifier))
|
||||
callback?(.selectRoom(roomIdentifier: roomIdentifier))
|
||||
}
|
||||
}
|
||||
|
||||
func updateWithRoomList(_ roomList: [RoomSummaryProtocol]) {
|
||||
self.roomList = roomList
|
||||
func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) {
|
||||
self.roomSummaries = roomSummaries
|
||||
|
||||
state.rooms = roomList.map { roomSummary in
|
||||
state.rooms = roomSummaries.map { roomSummary in
|
||||
buildOrUpdateRoomFromSummary(roomSummary)
|
||||
}
|
||||
|
||||
roomUpdateListeners.removeAll()
|
||||
|
||||
roomList.forEach({ roomSummary in
|
||||
roomSummaries.forEach({ roomSummary in
|
||||
roomSummary.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
@@ -94,7 +94,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
// MARK: - Private
|
||||
|
||||
private func loadRoomDataForIdentifier(_ roomIdentifier: String) {
|
||||
guard let roomSummary = roomList?.first(where: { $0.id == roomIdentifier }) else {
|
||||
guard let roomSummary = roomSummaries?.first(where: { $0.id == roomIdentifier }) else {
|
||||
MXLog.error("Invalid room identifier")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ import UIKit
|
||||
|
||||
@MainActor
|
||||
protocol HomeScreenViewModelProtocol {
|
||||
var completion: ((HomeScreenViewModelResult) -> Void)? { get set }
|
||||
var callback: ((HomeScreenViewModelAction) -> Void)? { get set }
|
||||
|
||||
var context: HomeScreenViewModelType.Context { get }
|
||||
|
||||
func updateWithUserAvatar(_ avatar: UIImage)
|
||||
func updateWithUserDisplayName(_ displayName: String)
|
||||
func updateWithRoomList(_ roomList: [RoomSummaryProtocol])
|
||||
func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol])
|
||||
}
|
||||
|
||||
@@ -176,11 +176,11 @@ struct HomeScreen_Previews: PreviewProvider {
|
||||
htmlBody: nil,
|
||||
date: .now)
|
||||
|
||||
let rooms = [MockRoomSummary(topic: "Topic", displayName: "Alpha"),
|
||||
MockRoomSummary(displayName: "Beta"),
|
||||
MockRoomSummary(displayName: "Omega", lastMessage: eventBrief)]
|
||||
let roomSummaries = [MockRoomSummary(displayName: "Alpha", topic: "Topic"),
|
||||
MockRoomSummary(displayName: "Beta"),
|
||||
MockRoomSummary(displayName: "Omega", lastMessage: eventBrief)]
|
||||
|
||||
viewModel.updateWithRoomList(rooms)
|
||||
viewModel.updateWithRoomSummaries(roomSummaries)
|
||||
|
||||
if let avatarImage = UIImage(systemName: "person.fill.questionmark") {
|
||||
viewModel.updateWithUserAvatar(avatarImage)
|
||||
|
||||
@@ -20,6 +20,10 @@ struct LoginScreenCoordinatorParameters {
|
||||
|
||||
}
|
||||
|
||||
enum LoginScreenCoordinatorAction {
|
||||
case login((username: String, password: String))
|
||||
}
|
||||
|
||||
final class LoginScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -34,7 +38,7 @@ final class LoginScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: ((LoginScreenViewModelResult) -> Void)?
|
||||
var callback: ((LoginScreenCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -47,10 +51,13 @@ final class LoginScreenCoordinator: Coordinator, Presentable {
|
||||
loginScreenHostingController = UIHostingController(rootView: view)
|
||||
loginScreenHostingController.isModalInPresentation = true
|
||||
|
||||
loginScreenViewModel.completion = { [weak self] result in
|
||||
loginScreenViewModel.callback = { [weak self] action in
|
||||
MXLog.debug("[LoginScreenCoordinator] LoginScreenViewModel did complete.")
|
||||
guard let self = self else { return }
|
||||
self.completion?(result)
|
||||
switch action {
|
||||
case .login(let credentials):
|
||||
self.callback?(.login(credentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum LoginScreenViewModelResult {
|
||||
enum LoginScreenViewModelAction {
|
||||
case login((username: String, password: String))
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var completion: ((LoginScreenViewModelResult) -> Void)?
|
||||
var callback: ((LoginScreenViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -40,7 +40,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
||||
override func process(viewAction: LoginScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .login:
|
||||
completion?(.login((username: context.username, password: context.password)))
|
||||
callback?(.login((username: context.username, password: context.password)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol LoginScreenViewModelProtocol {
|
||||
var completion: ((LoginScreenViewModelResult) -> Void)? { get set }
|
||||
var callback: ((LoginScreenViewModelAction) -> Void)? { get set }
|
||||
var context: LoginScreenViewModelType.Context { get }
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum RoomScreenViewModelResult {
|
||||
enum RoomScreenViewModelAction {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
|
||||
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
|
||||
func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
func authenticationCoordinatorDidSetupClientProxy(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
|
||||
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
|
||||
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
|
||||
didFailWithError error: AuthenticationCoordinatorError)
|
||||
@@ -32,7 +32,7 @@ class AuthenticationCoordinator: Coordinator {
|
||||
private let keychainController: KeychainControllerProtocol
|
||||
private let navigationRouter: NavigationRouter
|
||||
|
||||
private(set) var userSession: UserSession?
|
||||
private(set) var clientProxy: ClientProxyProtocol?
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
@@ -45,13 +45,13 @@ class AuthenticationCoordinator: Coordinator {
|
||||
|
||||
func start() {
|
||||
|
||||
let availableRestoreTokens = keychainController.restoreTokens()
|
||||
let availableAccessTokens = keychainController.accessTokens()
|
||||
|
||||
guard let usernameTokenTuple = availableRestoreTokens.first else {
|
||||
guard let usernameTokenTuple = availableAccessTokens.first else {
|
||||
startNewLoginFlow { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.delegate?.authenticationCoordinatorDidSetupUserSession(self)
|
||||
self.delegate?.authenticationCoordinatorDidSetupClientProxy(self)
|
||||
case .failure(let error):
|
||||
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
|
||||
MXLog.error("Failed logging in user with error: \(error)")
|
||||
@@ -60,33 +60,31 @@ class AuthenticationCoordinator: Coordinator {
|
||||
return
|
||||
}
|
||||
|
||||
restorePreviousLogin(usernameTokenTuple) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
Task {
|
||||
switch await restorePreviousLogin(usernameTokenTuple) {
|
||||
case .success:
|
||||
self.delegate?.authenticationCoordinatorDidSetupUserSession(self)
|
||||
self.delegate?.authenticationCoordinatorDidSetupClientProxy(self)
|
||||
case .failure(let error):
|
||||
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
|
||||
// On any restoration failure reset the token and restart
|
||||
self.keychainController.removeAllTokens()
|
||||
self.keychainController.removeAllAccessTokens()
|
||||
self.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logout() {
|
||||
keychainController.removeAllTokens()
|
||||
|
||||
if let userIdentifier = userSession?.userIdentifier {
|
||||
func logout() {
|
||||
keychainController.removeAllAccessTokens()
|
||||
|
||||
if let userIdentifier = clientProxy?.userIdentifier {
|
||||
deleteBaseDirectoryForUsername(userIdentifier)
|
||||
}
|
||||
|
||||
userSession = nil
|
||||
clientProxy = nil
|
||||
|
||||
delegate?.authenticationCoordinatorDidTearDownUserSession(self)
|
||||
delegate?.authenticationCoordinatorDidTearDownClientProxy(self)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -95,17 +93,15 @@ class AuthenticationCoordinator: Coordinator {
|
||||
let parameters = LoginScreenCoordinatorParameters()
|
||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.completion = { [weak self, weak coordinator] result in
|
||||
coordinator.callback = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .login(let result):
|
||||
self.login(username: result.username, password: result.password) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
Task {
|
||||
switch await self.login(username: result.username, password: result.password) {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
self.remove(childCoordinator: coordinator)
|
||||
@@ -113,6 +109,7 @@ class AuthenticationCoordinator: Coordinator {
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,71 +120,63 @@ class AuthenticationCoordinator: Coordinator {
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func login(username: String, password: String, completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||
private func login(username: String, password: String) async -> Result<Void, AuthenticationCoordinatorError> {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
|
||||
|
||||
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
do {
|
||||
self.setupUserSessionForClient(try loginNewClient(basePath: self.baseDirectoryPathForUsername(username),
|
||||
username: username,
|
||||
password: password))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
} catch {
|
||||
MXLog.error("Failed logging in with error: \(error)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.failedLoggingIn))
|
||||
}
|
||||
}
|
||||
let basePath = baseDirectoryPathForUsername(username)
|
||||
let loginTask = Task.detached {
|
||||
try loginNewClient(basePath: basePath,
|
||||
username: username,
|
||||
password: password)
|
||||
}
|
||||
|
||||
switch await loginTask.result {
|
||||
case .success(let client):
|
||||
return await setupProxyForClient(client)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed logging in with error: \(error)")
|
||||
return .failure(.failedLoggingIn)
|
||||
}
|
||||
}
|
||||
|
||||
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, token: String), completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, accessToken: String)) async -> Result<Void, AuthenticationCoordinatorError> {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login")
|
||||
|
||||
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
do {
|
||||
self.setupUserSessionForClient(try loginWithToken(basePath: self.baseDirectoryPathForUsername(usernameTokenTuple.username),
|
||||
restoreToken: usernameTokenTuple.token))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
} catch {
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.failedRestoringLogin))
|
||||
}
|
||||
}
|
||||
let basePath = baseDirectoryPathForUsername(usernameTokenTuple.username)
|
||||
let loginTask = Task.detached {
|
||||
try loginWithToken(basePath: basePath,
|
||||
restoreToken: usernameTokenTuple.accessToken)
|
||||
}
|
||||
|
||||
switch await loginTask.result {
|
||||
case .success(let client):
|
||||
return await setupProxyForClient(client)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
return .failure(.failedRestoringLogin)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUserSessionForClient(_ client: Client) {
|
||||
private func setupProxyForClient(_ client: Client) async -> Result<Void, AuthenticationCoordinatorError> {
|
||||
Benchmark.endTrackingForIdentifier("Login", message: "Finished login")
|
||||
|
||||
do {
|
||||
let restoreToken = try client.restoreToken()
|
||||
let accessToken = try client.restoreToken()
|
||||
let userId = try client.userId()
|
||||
|
||||
keychainController.setRestoreToken(restoreToken, forUsername: userId)
|
||||
keychainController.setAccessToken(accessToken, forUsername: userId)
|
||||
} catch {
|
||||
delegate?.authenticationCoordinator(self, didFailWithError: .failedSettingUpSession)
|
||||
MXLog.error("Failed setting up user session with error: \(error)")
|
||||
return .failure(.failedSettingUpSession)
|
||||
}
|
||||
|
||||
userSession = UserSession(client: client)
|
||||
clientProxy = ClientProxy(client: client)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
private func baseDirectoryPathForUsername(_ username: String) -> String {
|
||||
|
||||
@@ -9,26 +9,21 @@ import Foundation
|
||||
import KeychainAccess
|
||||
|
||||
class KeychainController: KeychainControllerProtocol {
|
||||
|
||||
struct Constants {
|
||||
static let restoreTokenGroupKey = "restoreTokens"
|
||||
}
|
||||
|
||||
private let keychain: Keychain
|
||||
|
||||
init(identifier: String) {
|
||||
keychain = Keychain(service: identifier)
|
||||
}
|
||||
|
||||
func setRestoreToken(_ token: String, forUsername username: String) {
|
||||
func setAccessToken(_ accessToken: String, forUsername username: String) {
|
||||
do {
|
||||
try keychain.set(token, key: username)
|
||||
try keychain.set(accessToken, key: username)
|
||||
} catch {
|
||||
MXLog.error("Failed storing user restore token with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func restoreTokenForUsername(_ username: String) -> String? {
|
||||
func accessTokenForUsername(_ username: String) -> String? {
|
||||
do {
|
||||
return try keychain.get(username)
|
||||
} catch {
|
||||
@@ -37,17 +32,17 @@ class KeychainController: KeychainControllerProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func restoreTokens() -> [(username: String, token: String)] {
|
||||
func accessTokens() -> [(username: String, accessToken: String)] {
|
||||
keychain.allKeys().compactMap { username in
|
||||
guard let token = restoreTokenForUsername(username) else {
|
||||
guard let accessToken = accessTokenForUsername(username) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (username, token)
|
||||
return (username, accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllTokens() {
|
||||
func removeAllAccessTokens() {
|
||||
do {
|
||||
try keychain.removeAll()
|
||||
} catch {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import Foundation
|
||||
|
||||
protocol KeychainControllerProtocol {
|
||||
func setRestoreToken(_ token: String, forUsername username: String)
|
||||
func restoreTokenForUsername(_ username: String) -> String?
|
||||
func restoreTokens() -> [(username: String, token: String)]
|
||||
func removeAllTokens()
|
||||
func setAccessToken(_ accessToken: String, forUsername username: String)
|
||||
func accessTokenForUsername(_ username: String) -> String?
|
||||
func accessTokens() -> [(username: String, accessToken: String)]
|
||||
func removeAllAccessTokens()
|
||||
}
|
||||
|
||||
+27
-34
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// UserSession.swift
|
||||
// ClientProxy.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 14.02.2022.
|
||||
@@ -9,53 +9,39 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
import Combine
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
enum UserSessionCallback {
|
||||
case updatedRoomsList
|
||||
}
|
||||
|
||||
enum UserSessionError: Error {
|
||||
case failedRetrievingAvatarURL
|
||||
case failedRetrievingDisplayName
|
||||
}
|
||||
|
||||
private class WeakUserSessionWrapper: ClientDelegate {
|
||||
private weak var userSession: UserSession?
|
||||
private class WeakClientProxyWrapper: ClientDelegate {
|
||||
private weak var clientProxy: ClientProxy?
|
||||
|
||||
init(userSession: UserSession) {
|
||||
self.userSession = userSession
|
||||
init(clientProxy: ClientProxy) {
|
||||
self.clientProxy = clientProxy
|
||||
}
|
||||
|
||||
@MainActor func didReceiveSyncUpdate() {
|
||||
userSession?.didReceiveSyncUpdate()
|
||||
func didReceiveSyncUpdate() {
|
||||
clientProxy?.didReceiveSyncUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class UserSession {
|
||||
class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
private let client: Client
|
||||
|
||||
private(set) var rooms: [RoomProxy] = [] {
|
||||
didSet {
|
||||
self.callbacks.send(.updatedRoomsList)
|
||||
callbacks.send(.updatedRoomsList)
|
||||
}
|
||||
}
|
||||
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
|
||||
deinit {
|
||||
client.setDelegate(delegate: nil)
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||
|
||||
init(client: Client) {
|
||||
self.client = client
|
||||
self.mediaProvider = MediaProvider(client: client, imageCache: ImageCache.default)
|
||||
|
||||
client.setDelegate(delegate: WeakUserSessionWrapper(userSession: self))
|
||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||
|
||||
Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
|
||||
client.startSync()
|
||||
@@ -72,8 +58,8 @@ class UserSession {
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, UserSessionError> {
|
||||
await Task.detached { () -> Result<String, UserSessionError> in
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||
await Task.detached { () -> Result<String, ClientProxyError> in
|
||||
do {
|
||||
let displayName = try self.client.displayName()
|
||||
return .success(displayName)
|
||||
@@ -85,8 +71,8 @@ class UserSession {
|
||||
.value
|
||||
}
|
||||
|
||||
func loadUserAvatarURL() async -> Result<String, UserSessionError> {
|
||||
await Task.detached { () -> Result<String, UserSessionError> in
|
||||
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
|
||||
await Task.detached { () -> Result<String, ClientProxyError> in
|
||||
do {
|
||||
let avatarURL = try self.client.avatarUrl()
|
||||
return .success(avatarURL)
|
||||
@@ -97,9 +83,18 @@ class UserSession {
|
||||
.value
|
||||
}
|
||||
|
||||
// MARK: ClientDelegate
|
||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
|
||||
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
||||
}
|
||||
|
||||
func didReceiveSyncUpdate() {
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
|
||||
let bytes = try client.getMediaContent(source: source)
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
fileprivate func didReceiveSyncUpdate() {
|
||||
Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update")
|
||||
|
||||
Task.detached {
|
||||
@@ -107,10 +102,8 @@ class UserSession {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private func updateRooms() async {
|
||||
var currentRooms = self.rooms
|
||||
var currentRooms = rooms
|
||||
Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms")
|
||||
let sdkRooms = client.rooms()
|
||||
Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) rooms")
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// ClientProxyProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 26/05/2022.
|
||||
// Copyright © 2022 element.io. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import Combine
|
||||
|
||||
enum ClientProxyCallback {
|
||||
case updatedRoomsList
|
||||
}
|
||||
|
||||
enum ClientProxyError: Error {
|
||||
case failedRetrievingAvatarURL
|
||||
case failedRetrievingDisplayName
|
||||
}
|
||||
|
||||
protocol ClientProxyProtocol {
|
||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||
|
||||
var userIdentifier: String { get }
|
||||
|
||||
var rooms: [RoomProxy] { get }
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError>
|
||||
|
||||
func loadUserAvatarURLString() async -> Result<String, ClientProxyError>
|
||||
|
||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data
|
||||
}
|
||||
@@ -7,16 +7,15 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MatrixRustSDK
|
||||
import Kingfisher
|
||||
|
||||
struct MediaProvider: MediaProviderProtocol {
|
||||
private let client: Client
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
private let processingQueue: DispatchQueue
|
||||
|
||||
init(client: Client, imageCache: Kingfisher.ImageCache) {
|
||||
self.client = client
|
||||
init(clientProxy: ClientProxyProtocol, imageCache: Kingfisher.ImageCache) {
|
||||
self.clientProxy = clientProxy
|
||||
self.imageCache = imageCache
|
||||
self.processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent)
|
||||
}
|
||||
@@ -29,18 +28,18 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: String?) -> UIImage? {
|
||||
guard let url = url else {
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
guard let urlString = urlString else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageFromSource(MediaSource(source: mediaSourceFromUrl(url: url)))
|
||||
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(MediaSource(source: mediaSourceFromUrl(url: url)))
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
|
||||
}
|
||||
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source) {
|
||||
return .success(image)
|
||||
@@ -59,9 +58,9 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
|
||||
return await Task.detached { () -> Result<UIImage, MediaProviderError> in
|
||||
do {
|
||||
let imageData = try client.getMediaContent(source: source.underlyingSource)
|
||||
let imageData = try clientProxy.loadMediaContentForSource(source.underlyingSource)
|
||||
|
||||
guard let image = UIImage(data: Data(bytes: imageData, count: imageData.count)) else {
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
MXLog.error("Invalid image data")
|
||||
return .failure(.invalidImageData)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ protocol MediaProviderProtocol {
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func imageFromURL(_ url: String?) -> UIImage?
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage?
|
||||
|
||||
func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError>
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError>
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ struct MediaSource: Equatable {
|
||||
let underlyingSource: MatrixRustSDK.MediaSource
|
||||
|
||||
init(source: MatrixRustSDK.MediaSource) {
|
||||
self.underlyingSource = source
|
||||
underlyingSource = source
|
||||
}
|
||||
|
||||
init(urlString: String) {
|
||||
self.underlyingSource = mediaSourceFromUrl(url: urlString)
|
||||
underlyingSource = MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
@@ -19,11 +19,11 @@ struct MockMediaProvider: MediaProviderProtocol {
|
||||
return .failure(.failedRetrievingImage)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: String?) -> UIImage? {
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
return .failure(.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@ class MemberDetailProvider: MemberDetailProviderProtocol {
|
||||
self.roomProxy = roomProxy
|
||||
}
|
||||
|
||||
func avatarURLForUserId(_ userId: String) -> String? {
|
||||
func avatarURLStringForUserId(_ userId: String) -> String? {
|
||||
memberAvatars[userId]
|
||||
}
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
|
||||
if let avatarURL = avatarURLForUserId(userId) {
|
||||
func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
|
||||
if let avatarURL = avatarURLStringForUserId(userId) {
|
||||
return .success(avatarURL)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ enum MemberDetailProviderError: Error {
|
||||
|
||||
@MainActor
|
||||
protocol MemberDetailProviderProtocol {
|
||||
func avatarURLForUserId(_ userId: String) -> String?
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
|
||||
func avatarURLStringForUserId(_ userId: String) -> String?
|
||||
func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String?
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
|
||||
|
||||
@@ -14,6 +14,8 @@ struct MockRoomSummary: RoomSummaryProtocol {
|
||||
|
||||
var name: String?
|
||||
|
||||
var displayName: String?
|
||||
|
||||
var topic: String?
|
||||
|
||||
var isDirect: Bool = false
|
||||
@@ -24,8 +26,6 @@ struct MockRoomSummary: RoomSummaryProtocol {
|
||||
|
||||
var isTombstoned: Bool = false
|
||||
|
||||
var displayName: String?
|
||||
|
||||
var lastMessage: EventBrief?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
@@ -124,11 +124,11 @@ class RoomSummary: RoomSummaryProtocol {
|
||||
}
|
||||
|
||||
private func loadAvatar() async {
|
||||
guard let avatarURL = roomProxy.avatarURL else {
|
||||
guard let avatarURLString = roomProxy.avatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURL(avatarURL) {
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString) {
|
||||
case .success(let avatar):
|
||||
self.avatar = avatar
|
||||
case .failure(let error):
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// UserSession.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 27/05/2022.
|
||||
// Copyright © 2022 element.io. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class UserSession: UserSessionProtocol {
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// UserSessionProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 27/05/2022.
|
||||
// Copyright © 2022 element.io. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol UserSessionProtocol {
|
||||
var clientProxy: ClientProxyProtocol { get }
|
||||
var mediaProvider: MediaProviderProtocol { get }
|
||||
}
|
||||
@@ -157,13 +157,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
switch await memberDetailProvider.loadAvatarURLForUserId(timelineItem.senderId) {
|
||||
case .success(let avatarURL):
|
||||
guard let avatarURL = avatarURL else {
|
||||
switch await memberDetailProvider.loadAvatarURLStringForUserId(timelineItem.senderId) {
|
||||
case .success(let avatarURLString):
|
||||
guard let avatarURLString = avatarURLString else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURL(avatarURL) {
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString) {
|
||||
case .success(let avatar):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? EventBasedTimelineItemProtocol else {
|
||||
|
||||
@@ -24,8 +24,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
func buildTimelineItemFor(message: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol {
|
||||
let displayName = memberDetailProvider.displayNameForUserId(message.sender)
|
||||
let avatarURL = memberDetailProvider.avatarURLForUserId(message.sender)
|
||||
let avatarImage = mediaProvider.imageFromURL(avatarURL)
|
||||
let avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender)
|
||||
let avatarImage = mediaProvider.imageFromURLString(avatarURL)
|
||||
|
||||
switch message {
|
||||
case let message as TextRoomMessage:
|
||||
|
||||
Reference in New Issue
Block a user