#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
+33 -5
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 51; objectVersion = 52;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -27,6 +27,9 @@
13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; };
15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; }; 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; };
17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */; }; 17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */; };
189F120A283F8BC700AC0100 /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189F1209283F8BC700AC0100 /* ClientProxyProtocol.swift */; };
18D841AC284107E5000FD3FB /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D841AB284107E5000FD3FB /* UserSessionProtocol.swift */; };
18D841AE28410817000FD3FB /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D841AD28410817000FD3FB /* UserSession.swift */; };
1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; }; 1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; };
1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; };
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; };
@@ -157,7 +160,7 @@
D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; }; D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; };
D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; };
DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; }; DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; };
DD4ADDB73E0935B74D2D18D6 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E3FE65EE63CBA65592863C2 /* UserSession.swift */; }; DD4ADDB73E0935B74D2D18D6 /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E3FE65EE63CBA65592863C2 /* ClientProxy.swift */; };
DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */; }; DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */; };
DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; }; DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; };
DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; }; DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; };
@@ -222,6 +225,9 @@
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = "<group>"; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = "<group>"; };
184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = "<group>"; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = "<group>"; };
189F1209283F8BC700AC0100 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = "<group>"; };
18D841AB284107E5000FD3FB /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; };
18D841AD28410817000FD3FB /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorViewPresentable.swift; sourceTree = "<group>"; }; 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorViewPresentable.swift; sourceTree = "<group>"; };
1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; };
1A2082B5226B2A3A4D0798B6 /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; }; 1A2082B5226B2A3A4D0798B6 /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; };
@@ -359,7 +365,7 @@
8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailProviderManager.swift; sourceTree = "<group>"; }; 8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailProviderManager.swift; sourceTree = "<group>"; };
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
8E3FE65EE63CBA65592863C2 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; }; 8E3FE65EE63CBA65592863C2 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = "<group>"; };
90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; }; 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; };
92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = "<group>"; }; 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = "<group>"; };
938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -533,6 +539,8 @@
0787F81684E503024BD0C051 /* Services */ = { 0787F81684E503024BD0C051 /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
18D841AA284107DC000FD3FB /* Session */,
189F1208283F88F300AC0100 /* Client */,
AAFDD509929A0CCF8BCE51EB /* Authentication */, AAFDD509929A0CCF8BCE51EB /* Authentication */,
79E560F5113ED25D172E550C /* Media */, 79E560F5113ED25D172E550C /* Media */,
40E6246F03D1FE377BC5D963 /* Room */, 40E6246F03D1FE377BC5D963 /* Room */,
@@ -562,6 +570,24 @@
path = ViewModel; path = ViewModel;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
189F1208283F88F300AC0100 /* Client */ = {
isa = PBXGroup;
children = (
8E3FE65EE63CBA65592863C2 /* ClientProxy.swift */,
189F1209283F8BC700AC0100 /* ClientProxyProtocol.swift */,
);
path = Client;
sourceTree = "<group>";
};
18D841AA284107DC000FD3FB /* Session */ = {
isa = PBXGroup;
children = (
18D841AB284107E5000FD3FB /* UserSessionProtocol.swift */,
18D841AD28410817000FD3FB /* UserSession.swift */,
);
path = Session;
sourceTree = "<group>";
};
24FD174C31912A5FACFEAFB5 /* SupportingFiles */ = { 24FD174C31912A5FACFEAFB5 /* SupportingFiles */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -921,7 +947,6 @@
0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */, 0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */,
F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */, F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */,
956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */, 956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */,
8E3FE65EE63CBA65592863C2 /* UserSession.swift */,
); );
path = Authentication; path = Authentication;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1379,6 +1404,7 @@
C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */,
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */, EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */,
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */,
18D841AC284107E5000FD3FB /* UserSessionProtocol.swift in Sources */,
224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */, 224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */,
6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */, 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */,
68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */, 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */,
@@ -1400,6 +1426,7 @@
2D8A687149E46B8C8B989561 /* KeychainController.swift in Sources */, 2D8A687149E46B8C8B989561 /* KeychainController.swift in Sources */,
277D2531C70F207A2F9F5906 /* KeychainControllerProtocol.swift in Sources */, 277D2531C70F207A2F9F5906 /* KeychainControllerProtocol.swift in Sources */,
9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */, 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */,
18D841AE28410817000FD3FB /* UserSession.swift in Sources */,
D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */, D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */,
A941EAD7F407F2ED6DA54A31 /* LoginScreen.swift in Sources */, A941EAD7F407F2ED6DA54A31 /* LoginScreen.swift in Sources */,
306CC09DF101E7E9CDE79AA5 /* LoginScreenCoordinator.swift in Sources */, 306CC09DF101E7E9CDE79AA5 /* LoginScreenCoordinator.swift in Sources */,
@@ -1481,12 +1508,13 @@
8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */, 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */,
7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */, 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */,
0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */, 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */,
189F120A283F8BC700AC0100 /* ClientProxyProtocol.swift in Sources */,
7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */, 7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */,
10866439ABA58CCDB5D1459D /* UserIndicatorQueue.swift in Sources */, 10866439ABA58CCDB5D1459D /* UserIndicatorQueue.swift in Sources */,
15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */, 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */,
C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */, C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */,
80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */, 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */,
DD4ADDB73E0935B74D2D18D6 /* UserSession.swift in Sources */, DD4ADDB73E0935B74D2D18D6 /* ClientProxy.swift in Sources */,
01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */, 01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */,
77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */, 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */,
50391038BC50C8ED9A4D88A0 /* WeakDictionaryReference.swift in Sources */, 50391038BC50C8ED9A4D88A0 /* WeakDictionaryReference.swift in Sources */,
+17 -8
View File
@@ -6,6 +6,7 @@
// //
import UIKit import UIKit
import Kingfisher
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private let window: UIWindow private let window: UIWindow
@@ -18,6 +19,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private let keychainController: KeychainControllerProtocol private let keychainController: KeychainControllerProtocol
private let authenticationCoordinator: AuthenticationCoordinator! private let authenticationCoordinator: AuthenticationCoordinator!
private var userSession: UserSession!
private let memberDetailProviderManager: MemberDetailProviderManager private let memberDetailProviderManager: MemberDetailProviderManager
private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var indicatorPresenter: UserIndicatorTypePresenterProtocol
@@ -70,11 +73,18 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
showLoginErrorToast() 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() presentHomeScreen()
} }
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator) { func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) {
if let presentedCoordinator = childCoordinators.first { if let presentedCoordinator = childCoordinators.first {
remove(childCoordinator: presentedCoordinator) remove(childCoordinator: presentedCoordinator)
} }
@@ -89,18 +99,17 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
hideLoadingIndicator() hideLoadingIndicator()
guard let userSession = authenticationCoordinator.userSession else { guard let userSession = userSession else {
fatalError("User session should be already setup at this point") fatalError("User session should be already setup at this point")
} }
let parameters = HomeScreenCoordinatorParameters(userSession: userSession, let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
mediaProvider: userSession.mediaProvider,
attributedStringBuilder: AttributedStringBuilder(), attributedStringBuilder: AttributedStringBuilder(),
memberDetailProviderManager: memberDetailProviderManager) memberDetailProviderManager: memberDetailProviderManager)
let coordinator = HomeScreenCoordinator(parameters: parameters) let coordinator = HomeScreenCoordinator(parameters: parameters)
coordinator.completion = { [weak self] result in coordinator.callback = { [weak self] action in
switch result { switch action {
case .logout: case .logout:
self?.authenticationCoordinator.logout() self?.authenticationCoordinator.logout()
case .selectRoom(let roomIdentifier): case .selectRoom(let roomIdentifier):
@@ -113,11 +122,11 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
} }
private func presentRoomWithIdentifier(_ roomIdentifier: String) { 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") 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)") MXLog.error("Invalid room identifier: \(roomIdentifier)")
return return
} }
@@ -18,13 +18,12 @@ import SwiftUI
import Combine import Combine
struct HomeScreenCoordinatorParameters { struct HomeScreenCoordinatorParameters {
let userSession: UserSession let userSession: UserSessionProtocol
let mediaProvider: MediaProviderProtocol
let attributedStringBuilder: AttributedStringBuilderProtocol let attributedStringBuilder: AttributedStringBuilderProtocol
let memberDetailProviderManager: MemberDetailProviderManager let memberDetailProviderManager: MemberDetailProviderManager
} }
enum HomeScreenCoordinatorResult { enum HomeScreenCoordinatorAction {
case logout case logout
case selectRoom(roomIdentifier: String) case selectRoom(roomIdentifier: String)
} }
@@ -47,7 +46,7 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
// Must be used only internally // Must be used only internally
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
var completion: ((HomeScreenCoordinatorResult) -> Void)? var callback: ((HomeScreenCoordinatorAction) -> Void)?
// MARK: - Setup // MARK: - Setup
@@ -59,22 +58,22 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
let view = HomeScreen(context: viewModel.context) let view = HomeScreen(context: viewModel.context)
hostingController = UIHostingController(rootView: view) hostingController = UIHostingController(rootView: view)
viewModel.completion = { [weak self] result in viewModel.callback = { [weak self] action in
guard let self = self else { return } guard let self = self else { return }
switch result { switch action {
case .logout: case .logout:
self.completion?(.logout) self.callback?(.logout)
case .selectRoom(let roomIdentifier): case .selectRoom(let roomIdentifier):
self.completion?(.selectRoom(roomIdentifier: roomIdentifier)) self.callback?(.selectRoom(roomIdentifier: roomIdentifier))
} }
} }
parameters.userSession parameters.userSession.clientProxy
.callbacks .callbacks
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] result in .sink { [weak self] action in
switch result { switch action {
case .updatedRoomsList: case .updatedRoomsList:
self?.updateRoomsList() self?.updateRoomsList()
} }
@@ -83,13 +82,13 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
updateRoomsList() updateRoomsList()
Task { Task {
if case let .success(userAvatarURL) = await parameters.userSession.loadUserAvatarURL() { if case let .success(userAvatarURLString) = await parameters.userSession.clientProxy.loadUserAvatarURLString() {
if case let .success(avatar) = await parameters.mediaProvider.loadImageFromURL(userAvatarURL) { if case let .success(avatar) = await parameters.userSession.mediaProvider.loadImageFromURLString(userAvatarURLString) {
self.viewModel.updateWithUserAvatar(avatar) 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) self.viewModel.updateWithUserDisplayName(userDisplayName)
} }
} }
@@ -107,7 +106,7 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
// MARK: - Private // MARK: - Private
func updateRoomsList() { func updateRoomsList() {
self.roomSummaries = parameters.userSession.rooms.compactMap { roomProxy in roomSummaries = parameters.userSession.clientProxy.rooms.compactMap { roomProxy in
guard !roomProxy.isSpace, !roomProxy.isTombstoned else { guard !roomProxy.isSpace, !roomProxy.isTombstoned else {
return nil return nil
} }
@@ -119,10 +118,10 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
let memberDetailProvider = parameters.memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy) let memberDetailProvider = parameters.memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy)
return RoomSummary(roomProxy: roomProxy, return RoomSummary(roomProxy: roomProxy,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.userSession.mediaProvider,
eventBriefFactory: EventBriefFactory(memberDetailProvider: memberDetailProvider)) eventBriefFactory: EventBriefFactory(memberDetailProvider: memberDetailProvider))
} }
self.viewModel.updateWithRoomList(roomSummaries) viewModel.updateWithRoomSummaries(roomSummaries)
} }
} }
@@ -17,7 +17,7 @@
import Foundation import Foundation
import UIKit import UIKit
enum HomeScreenViewModelResult { enum HomeScreenViewModelAction {
case logout case logout
case selectRoom(roomIdentifier: String) case selectRoom(roomIdentifier: String)
} }
@@ -25,13 +25,13 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
private var roomUpdateListeners = Set<AnyCancellable>() private var roomUpdateListeners = Set<AnyCancellable>()
private var roomList: [RoomSummaryProtocol]? { private var roomSummaries: [RoomSummaryProtocol]? {
didSet { 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 // MARK: - Setup
@@ -46,24 +46,24 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
override func process(viewAction: HomeScreenViewAction) async { override func process(viewAction: HomeScreenViewAction) async {
switch viewAction { switch viewAction {
case .logout: case .logout:
completion?(.logout) callback?(.logout)
case .loadRoomData(let roomIdentifier): case .loadRoomData(let roomIdentifier):
loadRoomDataForIdentifier(roomIdentifier) loadRoomDataForIdentifier(roomIdentifier)
case .selectRoom(let roomIdentifier): case .selectRoom(let roomIdentifier):
completion?(.selectRoom(roomIdentifier: roomIdentifier)) callback?(.selectRoom(roomIdentifier: roomIdentifier))
} }
} }
func updateWithRoomList(_ roomList: [RoomSummaryProtocol]) { func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) {
self.roomList = roomList self.roomSummaries = roomSummaries
state.rooms = roomList.map { roomSummary in state.rooms = roomSummaries.map { roomSummary in
buildOrUpdateRoomFromSummary(roomSummary) buildOrUpdateRoomFromSummary(roomSummary)
} }
roomUpdateListeners.removeAll() roomUpdateListeners.removeAll()
roomList.forEach({ roomSummary in roomSummaries.forEach({ roomSummary in
roomSummary.callbacks roomSummary.callbacks
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] callback in .sink { [weak self] callback in
@@ -94,7 +94,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
// MARK: - Private // MARK: - Private
private func loadRoomDataForIdentifier(_ roomIdentifier: String) { 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") MXLog.error("Invalid room identifier")
return return
} }
@@ -19,11 +19,11 @@ import UIKit
@MainActor @MainActor
protocol HomeScreenViewModelProtocol { protocol HomeScreenViewModelProtocol {
var completion: ((HomeScreenViewModelResult) -> Void)? { get set } var callback: ((HomeScreenViewModelAction) -> Void)? { get set }
var context: HomeScreenViewModelType.Context { get } var context: HomeScreenViewModelType.Context { get }
func updateWithUserAvatar(_ avatar: UIImage) func updateWithUserAvatar(_ avatar: UIImage)
func updateWithUserDisplayName(_ displayName: String) func updateWithUserDisplayName(_ displayName: String)
func updateWithRoomList(_ roomList: [RoomSummaryProtocol]) func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol])
} }
@@ -176,11 +176,11 @@ struct HomeScreen_Previews: PreviewProvider {
htmlBody: nil, htmlBody: nil,
date: .now) date: .now)
let rooms = [MockRoomSummary(topic: "Topic", displayName: "Alpha"), let roomSummaries = [MockRoomSummary(displayName: "Alpha", topic: "Topic"),
MockRoomSummary(displayName: "Beta"), MockRoomSummary(displayName: "Beta"),
MockRoomSummary(displayName: "Omega", lastMessage: eventBrief)] MockRoomSummary(displayName: "Omega", lastMessage: eventBrief)]
viewModel.updateWithRoomList(rooms) viewModel.updateWithRoomSummaries(roomSummaries)
if let avatarImage = UIImage(systemName: "person.fill.questionmark") { if let avatarImage = UIImage(systemName: "person.fill.questionmark") {
viewModel.updateWithUserAvatar(avatarImage) viewModel.updateWithUserAvatar(avatarImage)
@@ -20,6 +20,10 @@ struct LoginScreenCoordinatorParameters {
} }
enum LoginScreenCoordinatorAction {
case login((username: String, password: String))
}
final class LoginScreenCoordinator: Coordinator, Presentable { final class LoginScreenCoordinator: Coordinator, Presentable {
// MARK: - Properties // MARK: - Properties
@@ -34,7 +38,7 @@ final class LoginScreenCoordinator: Coordinator, Presentable {
// Must be used only internally // Must be used only internally
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
var completion: ((LoginScreenViewModelResult) -> Void)? var callback: ((LoginScreenCoordinatorAction) -> Void)?
// MARK: - Setup // MARK: - Setup
@@ -47,10 +51,13 @@ final class LoginScreenCoordinator: Coordinator, Presentable {
loginScreenHostingController = UIHostingController(rootView: view) loginScreenHostingController = UIHostingController(rootView: view)
loginScreenHostingController.isModalInPresentation = true loginScreenHostingController.isModalInPresentation = true
loginScreenViewModel.completion = { [weak self] result in loginScreenViewModel.callback = { [weak self] action in
MXLog.debug("[LoginScreenCoordinator] LoginScreenViewModel did complete.") MXLog.debug("[LoginScreenCoordinator] LoginScreenViewModel did complete.")
guard let self = self else { return } 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 import Foundation
enum LoginScreenViewModelResult { enum LoginScreenViewModelAction {
case login((username: String, password: String)) case login((username: String, password: String))
} }
@@ -26,7 +26,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
// MARK: Public // MARK: Public
var completion: ((LoginScreenViewModelResult) -> Void)? var callback: ((LoginScreenViewModelAction) -> Void)?
// MARK: - Setup // MARK: - Setup
@@ -40,7 +40,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
override func process(viewAction: LoginScreenViewAction) async { override func process(viewAction: LoginScreenViewAction) async {
switch viewAction { switch viewAction {
case .login: 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 @MainActor
protocol LoginScreenViewModelProtocol { protocol LoginScreenViewModelProtocol {
var completion: ((LoginScreenViewModelResult) -> Void)? { get set } var callback: ((LoginScreenViewModelAction) -> Void)? { get set }
var context: LoginScreenViewModelType.Context { get } var context: LoginScreenViewModelType.Context { get }
} }
@@ -16,7 +16,7 @@
import Foundation import Foundation
enum RoomScreenViewModelResult { enum RoomScreenViewModelAction {
} }
@@ -19,9 +19,9 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator)
func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator) func authenticationCoordinatorDidSetupClientProxy(_ authenticationCoordinator: AuthenticationCoordinator)
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator) func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator)
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
didFailWithError error: AuthenticationCoordinatorError) didFailWithError error: AuthenticationCoordinatorError)
@@ -32,7 +32,7 @@ class AuthenticationCoordinator: Coordinator {
private let keychainController: KeychainControllerProtocol private let keychainController: KeychainControllerProtocol
private let navigationRouter: NavigationRouter private let navigationRouter: NavigationRouter
private(set) var userSession: UserSession? private(set) var clientProxy: ClientProxyProtocol?
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
weak var delegate: AuthenticationCoordinatorDelegate? weak var delegate: AuthenticationCoordinatorDelegate?
@@ -45,13 +45,13 @@ class AuthenticationCoordinator: Coordinator {
func start() { 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 startNewLoginFlow { result in
switch result { switch result {
case .success: case .success:
self.delegate?.authenticationCoordinatorDidSetupUserSession(self) self.delegate?.authenticationCoordinatorDidSetupClientProxy(self)
case .failure(let error): case .failure(let error):
self.delegate?.authenticationCoordinator(self, didFailWithError: error) self.delegate?.authenticationCoordinator(self, didFailWithError: error)
MXLog.error("Failed logging in user with error: \(error)") MXLog.error("Failed logging in user with error: \(error)")
@@ -60,33 +60,31 @@ class AuthenticationCoordinator: Coordinator {
return return
} }
restorePreviousLogin(usernameTokenTuple) { [weak self] result in Task {
guard let self = self else { return } switch await restorePreviousLogin(usernameTokenTuple) {
switch result {
case .success: case .success:
self.delegate?.authenticationCoordinatorDidSetupUserSession(self) self.delegate?.authenticationCoordinatorDidSetupClientProxy(self)
case .failure(let error): case .failure(let error):
self.delegate?.authenticationCoordinator(self, didFailWithError: error) self.delegate?.authenticationCoordinator(self, didFailWithError: error)
MXLog.error("Failed restoring login with error: \(error)") MXLog.error("Failed restoring login with error: \(error)")
// On any restoration failure reset the token and restart // On any restoration failure reset the token and restart
self.keychainController.removeAllTokens() self.keychainController.removeAllAccessTokens()
self.start() self.start()
} }
} }
} }
func logout() { func logout() {
keychainController.removeAllTokens() keychainController.removeAllAccessTokens()
if let userIdentifier = userSession?.userIdentifier { if let userIdentifier = clientProxy?.userIdentifier {
deleteBaseDirectoryForUsername(userIdentifier) deleteBaseDirectoryForUsername(userIdentifier)
} }
userSession = nil clientProxy = nil
delegate?.authenticationCoordinatorDidTearDownUserSession(self) delegate?.authenticationCoordinatorDidTearDownClientProxy(self)
} }
// MARK: - Private // MARK: - Private
@@ -95,17 +93,15 @@ class AuthenticationCoordinator: Coordinator {
let parameters = LoginScreenCoordinatorParameters() let parameters = LoginScreenCoordinatorParameters()
let coordinator = LoginScreenCoordinator(parameters: parameters) 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 { guard let self = self, let coordinator = coordinator else {
return return
} }
switch result { switch result {
case .login(let result): case .login(let result):
self.login(username: result.username, password: result.password) { [weak self] result in Task {
guard let self = self else { return } switch await self.login(username: result.username, password: result.password) {
switch result {
case .success: case .success:
completion(.success(())) completion(.success(()))
self.remove(childCoordinator: coordinator) self.remove(childCoordinator: coordinator)
@@ -113,6 +109,7 @@ class AuthenticationCoordinator: Coordinator {
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
} }
} }
} }
@@ -123,71 +120,63 @@ class AuthenticationCoordinator: Coordinator {
coordinator.start() 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") Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
delegate?.authenticationCoordinatorDidStartLoading(self) delegate?.authenticationCoordinatorDidStartLoading(self)
DispatchQueue.global(qos: .background).async { [weak self] in let basePath = baseDirectoryPathForUsername(username)
guard let self = self else { return } let loginTask = Task.detached {
try loginNewClient(basePath: basePath,
do {
self.setupUserSessionForClient(try loginNewClient(basePath: self.baseDirectoryPathForUsername(username),
username: username, username: username,
password: password)) password: password)
DispatchQueue.main.async {
completion(.success(()))
} }
} catch {
switch await loginTask.result {
case .success(let client):
return await setupProxyForClient(client)
case .failure(let error):
MXLog.error("Failed logging in with error: \(error)") MXLog.error("Failed logging in with error: \(error)")
return .failure(.failedLoggingIn)
DispatchQueue.main.async {
completion(.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") Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login")
delegate?.authenticationCoordinatorDidStartLoading(self) delegate?.authenticationCoordinatorDidStartLoading(self)
DispatchQueue.global(qos: .background).async { [weak self] in let basePath = baseDirectoryPathForUsername(usernameTokenTuple.username)
guard let self = self else { return } let loginTask = Task.detached {
try loginWithToken(basePath: basePath,
do { restoreToken: usernameTokenTuple.accessToken)
self.setupUserSessionForClient(try loginWithToken(basePath: self.baseDirectoryPathForUsername(usernameTokenTuple.username),
restoreToken: usernameTokenTuple.token))
DispatchQueue.main.async {
completion(.success(()))
} }
} catch {
switch await loginTask.result {
case .success(let client):
return await setupProxyForClient(client)
case .failure(let error):
MXLog.error("Failed restoring login with error: \(error)") MXLog.error("Failed restoring login with error: \(error)")
return .failure(.failedRestoringLogin)
DispatchQueue.main.async {
completion(.failure(.failedRestoringLogin))
}
}
} }
} }
private func setupUserSessionForClient(_ client: Client) { private func setupProxyForClient(_ client: Client) async -> Result<Void, AuthenticationCoordinatorError> {
Benchmark.endTrackingForIdentifier("Login", message: "Finished login") Benchmark.endTrackingForIdentifier("Login", message: "Finished login")
do { do {
let restoreToken = try client.restoreToken() let accessToken = try client.restoreToken()
let userId = try client.userId() let userId = try client.userId()
keychainController.setRestoreToken(restoreToken, forUsername: userId) keychainController.setAccessToken(accessToken, forUsername: userId)
} catch { } catch {
delegate?.authenticationCoordinator(self, didFailWithError: .failedSettingUpSession)
MXLog.error("Failed setting up user session with error: \(error)") 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 { private func baseDirectoryPathForUsername(_ username: String) -> String {
@@ -9,26 +9,21 @@ import Foundation
import KeychainAccess import KeychainAccess
class KeychainController: KeychainControllerProtocol { class KeychainController: KeychainControllerProtocol {
struct Constants {
static let restoreTokenGroupKey = "restoreTokens"
}
private let keychain: Keychain private let keychain: Keychain
init(identifier: String) { init(identifier: String) {
keychain = Keychain(service: identifier) keychain = Keychain(service: identifier)
} }
func setRestoreToken(_ token: String, forUsername username: String) { func setAccessToken(_ accessToken: String, forUsername username: String) {
do { do {
try keychain.set(token, key: username) try keychain.set(accessToken, key: username)
} catch { } catch {
MXLog.error("Failed storing user restore token with error: \(error)") MXLog.error("Failed storing user restore token with error: \(error)")
} }
} }
func restoreTokenForUsername(_ username: String) -> String? { func accessTokenForUsername(_ username: String) -> String? {
do { do {
return try keychain.get(username) return try keychain.get(username)
} catch { } 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 keychain.allKeys().compactMap { username in
guard let token = restoreTokenForUsername(username) else { guard let accessToken = accessTokenForUsername(username) else {
return nil return nil
} }
return (username, token) return (username, accessToken)
} }
} }
func removeAllTokens() { func removeAllAccessTokens() {
do { do {
try keychain.removeAll() try keychain.removeAll()
} catch { } catch {
@@ -8,8 +8,8 @@
import Foundation import Foundation
protocol KeychainControllerProtocol { protocol KeychainControllerProtocol {
func setRestoreToken(_ token: String, forUsername username: String) func setAccessToken(_ accessToken: String, forUsername username: String)
func restoreTokenForUsername(_ username: String) -> String? func accessTokenForUsername(_ username: String) -> String?
func restoreTokens() -> [(username: String, token: String)] func accessTokens() -> [(username: String, accessToken: String)]
func removeAllTokens() func removeAllAccessTokens()
} }
@@ -1,5 +1,5 @@
// //
// UserSession.swift // ClientProxy.swift
// ElementX // ElementX
// //
// Created by Stefan Ceriu on 14.02.2022. // Created by Stefan Ceriu on 14.02.2022.
@@ -9,53 +9,39 @@ import Foundation
import MatrixRustSDK import MatrixRustSDK
import Combine import Combine
import UIKit import UIKit
import Kingfisher
enum UserSessionCallback { private class WeakClientProxyWrapper: ClientDelegate {
case updatedRoomsList private weak var clientProxy: ClientProxy?
}
enum UserSessionError: Error { init(clientProxy: ClientProxy) {
case failedRetrievingAvatarURL self.clientProxy = clientProxy
case failedRetrievingDisplayName
}
private class WeakUserSessionWrapper: ClientDelegate {
private weak var userSession: UserSession?
init(userSession: UserSession) {
self.userSession = userSession
} }
@MainActor func didReceiveSyncUpdate() { func didReceiveSyncUpdate() {
userSession?.didReceiveSyncUpdate() clientProxy?.didReceiveSyncUpdate()
} }
} }
@MainActor class ClientProxy: ClientProxyProtocol {
class UserSession {
private let client: Client private let client: Client
private(set) var rooms: [RoomProxy] = [] { private(set) var rooms: [RoomProxy] = [] {
didSet { didSet {
self.callbacks.send(.updatedRoomsList) callbacks.send(.updatedRoomsList)
} }
} }
let mediaProvider: MediaProviderProtocol
deinit { deinit {
client.setDelegate(delegate: nil) client.setDelegate(delegate: nil)
} }
let callbacks = PassthroughSubject<UserSessionCallback, Never>() let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
init(client: Client) { init(client: Client) {
self.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.") Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
client.startSync() client.startSync()
@@ -72,8 +58,8 @@ class UserSession {
} }
} }
func loadUserDisplayName() async -> Result<String, UserSessionError> { func loadUserDisplayName() async -> Result<String, ClientProxyError> {
await Task.detached { () -> Result<String, UserSessionError> in await Task.detached { () -> Result<String, ClientProxyError> in
do { do {
let displayName = try self.client.displayName() let displayName = try self.client.displayName()
return .success(displayName) return .success(displayName)
@@ -85,8 +71,8 @@ class UserSession {
.value .value
} }
func loadUserAvatarURL() async -> Result<String, UserSessionError> { func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
await Task.detached { () -> Result<String, UserSessionError> in await Task.detached { () -> Result<String, ClientProxyError> in
do { do {
let avatarURL = try self.client.avatarUrl() let avatarURL = try self.client.avatarUrl()
return .success(avatarURL) return .success(avatarURL)
@@ -97,9 +83,18 @@ class UserSession {
.value .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") Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update")
Task.detached { Task.detached {
@@ -107,10 +102,8 @@ class UserSession {
} }
} }
// MARK: Private
private func updateRooms() async { private func updateRooms() async {
var currentRooms = self.rooms var currentRooms = rooms
Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms") Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms")
let sdkRooms = client.rooms() let sdkRooms = client.rooms()
Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) 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 UIKit
import MatrixRustSDK
import Kingfisher import Kingfisher
struct MediaProvider: MediaProviderProtocol { struct MediaProvider: MediaProviderProtocol {
private let client: Client private let clientProxy: ClientProxyProtocol
private let imageCache: Kingfisher.ImageCache private let imageCache: Kingfisher.ImageCache
private let processingQueue: DispatchQueue private let processingQueue: DispatchQueue
init(client: Client, imageCache: Kingfisher.ImageCache) { init(clientProxy: ClientProxyProtocol, imageCache: Kingfisher.ImageCache) {
self.client = client self.clientProxy = clientProxy
self.imageCache = imageCache self.imageCache = imageCache
self.processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent) self.processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent)
} }
@@ -29,16 +28,16 @@ struct MediaProvider: MediaProviderProtocol {
return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil) return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil)
} }
func imageFromURL(_ url: String?) -> UIImage? { func imageFromURLString(_ urlString: String?) -> UIImage? {
guard let url = url else { guard let urlString = urlString else {
return nil return nil
} }
return imageFromSource(MediaSource(source: mediaSourceFromUrl(url: url))) return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
} }
func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError> { func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
await loadImageFromSource(MediaSource(source: mediaSourceFromUrl(url: url))) await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
} }
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> { func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
@@ -59,9 +58,9 @@ struct MediaProvider: MediaProviderProtocol {
return await Task.detached { () -> Result<UIImage, MediaProviderError> in return await Task.detached { () -> Result<UIImage, MediaProviderError> in
do { 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") MXLog.error("Invalid image data")
return .failure(.invalidImageData) return .failure(.invalidImageData)
} }
@@ -20,7 +20,7 @@ protocol MediaProviderProtocol {
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> 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 let underlyingSource: MatrixRustSDK.MediaSource
init(source: MatrixRustSDK.MediaSource) { init(source: MatrixRustSDK.MediaSource) {
self.underlyingSource = source underlyingSource = source
} }
init(urlString: String) { init(urlString: String) {
self.underlyingSource = mediaSourceFromUrl(url: urlString) underlyingSource = MatrixRustSDK.mediaSourceFromUrl(url: urlString)
} }
// MARK: - Equatable // MARK: - Equatable
@@ -19,11 +19,11 @@ struct MockMediaProvider: MediaProviderProtocol {
return .failure(.failedRetrievingImage) return .failure(.failedRetrievingImage)
} }
func imageFromURL(_ url: String?) -> UIImage? { func imageFromURLString(_ urlString: String?) -> UIImage? {
return nil return nil
} }
func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError> { func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
return .failure(.failedRetrievingImage) return .failure(.failedRetrievingImage)
} }
} }
@@ -17,12 +17,12 @@ class MemberDetailProvider: MemberDetailProviderProtocol {
self.roomProxy = roomProxy self.roomProxy = roomProxy
} }
func avatarURLForUserId(_ userId: String) -> String? { func avatarURLStringForUserId(_ userId: String) -> String? {
memberAvatars[userId] memberAvatars[userId]
} }
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> { func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
if let avatarURL = avatarURLForUserId(userId) { if let avatarURL = avatarURLStringForUserId(userId) {
return .success(avatarURL) return .success(avatarURL)
} }
@@ -16,8 +16,8 @@ enum MemberDetailProviderError: Error {
@MainActor @MainActor
protocol MemberDetailProviderProtocol { protocol MemberDetailProviderProtocol {
func avatarURLForUserId(_ userId: String) -> String? func avatarURLStringForUserId(_ userId: String) -> String?
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
func displayNameForUserId(_ userId: String) -> String? func displayNameForUserId(_ userId: String) -> String?
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
@@ -14,6 +14,8 @@ struct MockRoomSummary: RoomSummaryProtocol {
var name: String? var name: String?
var displayName: String?
var topic: String? var topic: String?
var isDirect: Bool = false var isDirect: Bool = false
@@ -24,8 +26,6 @@ struct MockRoomSummary: RoomSummaryProtocol {
var isTombstoned: Bool = false var isTombstoned: Bool = false
var displayName: String?
var lastMessage: EventBrief? var lastMessage: EventBrief?
var avatar: UIImage? var avatar: UIImage?
@@ -124,11 +124,11 @@ class RoomSummary: RoomSummaryProtocol {
} }
private func loadAvatar() async { private func loadAvatar() async {
guard let avatarURL = roomProxy.avatarURL else { guard let avatarURLString = roomProxy.avatarURL else {
return return
} }
switch await mediaProvider.loadImageFromURL(avatarURL) { switch await mediaProvider.loadImageFromURLString(avatarURLString) {
case .success(let avatar): case .success(let avatar):
self.avatar = avatar self.avatar = avatar
case .failure(let error): 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 return
} }
switch await memberDetailProvider.loadAvatarURLForUserId(timelineItem.senderId) { switch await memberDetailProvider.loadAvatarURLStringForUserId(timelineItem.senderId) {
case .success(let avatarURL): case .success(let avatarURLString):
guard let avatarURL = avatarURL else { guard let avatarURLString = avatarURLString else {
return return
} }
switch await mediaProvider.loadImageFromURL(avatarURL) { switch await mediaProvider.loadImageFromURLString(avatarURLString) {
case .success(let avatar): case .success(let avatar):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }), guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = timelineItems[index] as? EventBasedTimelineItemProtocol else { var item = timelineItems[index] as? EventBasedTimelineItemProtocol else {
@@ -24,8 +24,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
func buildTimelineItemFor(message: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol { func buildTimelineItemFor(message: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol {
let displayName = memberDetailProvider.displayNameForUserId(message.sender) let displayName = memberDetailProvider.displayNameForUserId(message.sender)
let avatarURL = memberDetailProvider.avatarURLForUserId(message.sender) let avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender)
let avatarImage = mediaProvider.imageFromURL(avatarURL) let avatarImage = mediaProvider.imageFromURLString(avatarURL)
switch message { switch message {
case let message as TextRoomMessage: case let message as TextRoomMessage:
@@ -20,6 +20,11 @@ struct TemplateSimpleScreenCoordinatorParameters {
let promptType: TemplateSimpleScreenPromptType let promptType: TemplateSimpleScreenPromptType
} }
enum TemplateSimpleScreenCoordinatorAction {
case accept
case cancel
}
final class TemplateSimpleScreenCoordinator: Coordinator, Presentable { final class TemplateSimpleScreenCoordinator: Coordinator, Presentable {
// MARK: - Properties // MARK: - Properties
@@ -37,7 +42,7 @@ final class TemplateSimpleScreenCoordinator: Coordinator, Presentable {
// Must be used only internally // Must be used only internally
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? var callback: ((TemplateSimpleScreenCoordinatorAction) -> Void)?
// MARK: - Setup // MARK: - Setup
@@ -56,10 +61,15 @@ final class TemplateSimpleScreenCoordinator: Coordinator, Presentable {
func start() { func start() {
MXLog.debug("[TemplateSimpleScreenCoordinator] did start.") MXLog.debug("[TemplateSimpleScreenCoordinator] did start.")
templateSimpleScreenViewModel.completion = { [weak self] result in templateSimpleScreenViewModel.callback = { [weak self] action in
guard let self = self else { return } guard let self = self else { return }
MXLog.debug("[TemplateSimpleScreenCoordinator] TemplateSimpleScreenViewModel did complete with result: \(result).") MXLog.debug("[TemplateSimpleScreenCoordinator] TemplateSimpleScreenViewModel did complete with result: \(action).")
self.completion?(result) switch action {
case .accept:
self.callback?(.accept)
case .cancel:
self.callback?(.cancel)
}
} }
} }
@@ -47,7 +47,7 @@ extension TemplateSimpleScreenPromptType: Identifiable, CaseIterable {
// MARK: View model // MARK: View model
enum TemplateSimpleScreenViewModelResult { enum TemplateSimpleScreenViewModelAction {
case accept case accept
case cancel case cancel
} }
@@ -26,7 +26,7 @@ class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, Template
// MARK: Public // MARK: Public
var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? var callback: ((TemplateSimpleScreenViewModelAction) -> Void)?
// MARK: - Setup // MARK: - Setup
@@ -39,9 +39,9 @@ class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, Template
override func process(viewAction: TemplateSimpleScreenViewAction) async { override func process(viewAction: TemplateSimpleScreenViewAction) async {
switch viewAction { switch viewAction {
case .accept: case .accept:
completion?(.accept) callback?(.accept)
case .cancel: case .cancel:
completion?(.cancel) callback?(.cancel)
case .incrementCount: case .incrementCount:
state.count += 1 state.count += 1
case .decrementCount: case .decrementCount:
@@ -18,6 +18,6 @@ import Foundation
@MainActor @MainActor
protocol TemplateSimpleScreenViewModelProtocol { protocol TemplateSimpleScreenViewModelProtocol {
var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? { get set } var callback: ((TemplateSimpleScreenViewModelAction) -> Void)? { get set }
var context: TemplateSimpleScreenViewModelType.Context { get } var context: TemplateSimpleScreenViewModelType.Context { get }
} }