#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:
Stefan Ceriu
2022-05-26 16:04:48 +03:00
committed by Stefan Ceriu
parent edf765b7cd
commit 1b4d3a650e
33 changed files with 312 additions and 214 deletions
@@ -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()
}
@@ -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: