From 50a0d16c7789ff0cfd8d08a911df9be1ce847c90 Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Thu, 10 Nov 2022 01:52:53 -0300 Subject: [PATCH] migrating to router --- Package.swift | 96 +- Sources/App/AppDelegate.swift | 6 +- Sources/App/DependencyRegistrator.swift | 270 ++--- .../Controllers/BackupConfigController.swift | 616 +++++----- .../Coordinator/BackupCoordinator.swift | 91 -- .../BackupFeature/Service/BackupService.swift | 7 +- .../ViewModels/BackupConfigViewModel.swift | 13 +- .../ViewModels/BackupSFTPViewModel.swift | 2 +- .../Controllers/GroupChatController.swift | 144 ++- .../Controllers/SheetController.swift | 93 +- .../Controllers/SingleChatController.swift | 200 ++-- .../Coordinator/ChatCoordinator.swift | 105 -- .../ViewModels/SingleChatViewModel.swift | 4 +- .../Controller/ChatListController.swift | 150 +-- .../ChatListSearchTableController.swift | 218 ++-- .../Controller/ChatListSheetController.swift | 79 +- .../Controller/ChatListTableController.swift | 372 +++--- .../Coordinator/ChatListCoordinator.swift | 102 -- .../ViewModel/ChatListViewModel.swift | 50 +- .../ChatListFeature/Views/ChatListCell.swift | 290 ++--- .../Views/ChatListContainerView.swift | 195 ++-- .../Views/ChatListEmptyView.swift | 84 +- .../Views/ChatListMenuView.swift | 128 +- .../Views/ChatListRecentContactCell.swift | 158 ++- .../Views/ChatListTopLeftNavView.swift | 126 +- .../Views/ChatListTopRightNavView.swift | 86 +- .../ChatListFeature/Views/ChatListView.swift | 130 +-- .../Views/ChatSearchEmptyView.swift | 98 +- .../Views/ChatSearchListContainerView.swift | 17 + .../Controllers/ContactController.swift | 253 ++-- .../Controllers/NicknameController.swift | 2 +- .../Coordinator/ContactCoordinator.swift | 69 -- .../ViewModels/ContactViewModel.swift | 6 +- .../Controllers/ContactListController.swift | 65 +- .../Controllers/CreateGroupController.swift | 333 +++--- .../Coordinator/ContactListCoordinator.swift | 128 -- .../ViewModels/CreateGroupViewModel.swift | 2 +- Sources/Countries/Country.swift | 45 - Sources/Countries/CountryListController.swift | 55 +- Sources/LaunchFeature/LaunchController.swift | 5 +- .../LaunchViewModel+Messenger.swift | 1 - .../Controllers/MenuController.swift | 157 ++- .../Coordinator/MenuCoordinator.swift | 73 -- .../OnboardingCodeController.swift | 29 +- .../OnboardingEmailController.swift | 64 +- .../OnboardingPhoneController.swift | 42 +- .../OnboardingStartController.swift | 1 - .../OnboardingUsernameController.swift | 24 +- .../OnboardingWelcomeController.swift | 100 +- .../ViewModels/OnboardingCodeViewModel.swift | 2 +- .../ViewModels/OnboardingEmailViewModel.swift | 2 +- .../ViewModels/OnboardingPhoneViewModel.swift | 2 +- .../RequestPermissionController.swift | 64 +- .../Controllers/ProfileCodeController.swift | 87 +- .../Controllers/ProfileController.swift | 81 +- .../Controllers/ProfileEmailController.swift | 24 +- .../Controllers/ProfilePhoneController.swift | 78 +- .../Coordinator/ProfileCoordinator.swift | 89 -- .../ViewModels/ProfileCodeViewModel.swift | 2 +- .../ViewModels/ProfileEmailViewModel.swift | 15 +- .../ViewModels/ProfilePhoneViewModel.swift | 2 +- .../ViewModels/ProfileViewModel.swift | 2 +- .../RequestsContainerController.swift | 12 +- .../RequestsFailedController.swift | 2 +- .../RequestsReceivedController.swift | 1026 +++++++++-------- .../Controllers/RequestsSentController.swift | 2 +- .../Coordinator/RequestsCoordinator.swift | 119 -- .../ViewModels/RequestsFailedViewModel.swift | 2 +- .../RequestsReceivedViewModel.swift | 6 +- .../ViewModels/RequestsSentViewModel.swift | 2 +- .../Controllers/RestoreController.swift | 247 ++-- .../Controllers/RestoreListController.swift | 45 +- .../RestoreSuccessController.swift | 11 +- .../Coordinator/RestoreCoordinator.swift | 116 -- .../ViewModels/RestoreSFTPViewModel.swift | 2 +- .../ViewModels/RestoreViewModel.swift | 4 +- .../Controllers/ScanContainerController.swift | 50 +- .../Controllers/ScanController.swift | 213 ++-- .../Coordinator/ScanCoordinator.swift | 87 -- .../ViewModels/ScanDisplayViewModel.swift | 1 + .../SearchContainerController.swift | 52 +- .../Controllers/SearchLeftController.swift | 850 +++++++------- .../Controllers/SearchRightController.swift | 135 ++- .../Coordinator/SearchCoordinator.swift | 83 -- .../ViewModels/SearchContainerViewModel.swift | 2 +- .../ViewModels/SearchLeftViewModel.swift | 12 +- .../ViewModels/SearchRightViewModel.swift | 2 +- .../Views/SearchLeftPlaceholderView.swift | 2 +- .../Controllers/AccountDeleteController.swift | 213 ++-- .../SettingsAdvancedController.swift | 187 +-- .../Controllers/SettingsController.swift | 175 +-- .../Coordinator/SettingsCoordinator.swift | 70 -- .../ViewModels/AccountDeleteViewModel.swift | 4 +- .../SettingsAdvancedViewModel.swift | 122 +- .../ViewModels/SettingsViewModel.swift | 7 +- .../Views/AccountDeleteView.swift | 210 ++-- .../Views/SettingsAdvancedView.swift | 111 +- .../Views/SettingsSwitcher.swift | 408 ++++--- .../SettingsFeature/Views/SettingsView.swift | 337 +++--- .../Controllers/RootViewController.swift | 2 +- Sources/Shared/Models/Country.swift | 44 + Sources/Shared/Models/HUDModel.swift | 4 + Sources/Shared/Models/MenuItem.swift | 11 + Sources/Shared/Models/PermissionType.swift | 5 + .../Resources/country_codes.json | 0 .../TermsConditionsController.swift | 26 +- Sources/TermsFeature/TermsCoordinator.swift | 25 - Sources/VersionChecking/VersionChecking.swift | 8 +- .../XXNavigation/Actions/PresentChat.swift | 12 - .../Actions/PresentChatList.swift | 9 - .../Actions/PresentCountryList.swift | 9 - .../XXNavigation/Actions/PresentDrawer.swift | 18 - .../Actions/PresentGroupChat.swift | 12 - .../Actions/PresentOnboardingCode.swift | 20 - .../Actions/PresentOnboardingEmail.swift | 9 - .../Actions/PresentOnboardingPhone.swift | 9 - .../Actions/PresentOnboardingStart.swift | 9 - .../Actions/PresentOnboardingUsername.swift | 9 - .../Actions/PresentOnboardingWelcome.swift | 9 - .../Actions/PresentRestoreList.swift | 9 - .../XXNavigation/Actions/PresentSearch.swift | 11 - .../Actions/PresentTermsAndConditions.swift | 14 - .../PresentCamera.swift} | 4 +- .../PresentChat.swift} | 13 + .../PresentChatList.swift} | 8 + .../PresentGroupChat.swift} | 13 + .../XXNavigation/Chat/PresentMemberList.swift | 15 + .../XXNavigation/Chat/PresentNewGroup.swift | 30 + .../XXNavigation/Chat/PresentWebsite.swift | 15 + .../XXNavigation/Contact/PresentContact.swift | 36 + .../Contact/PresentContactList.swift | 30 + .../Contact/PresentNickname.swift | 17 + .../XXNavigation/CustomActions/OpenLeft.swift | 310 +++++ Sources/XXNavigation/Export.swift | 1 + .../PresentCountryListNavigator.swift | 23 - .../Navigators/PresentSearchNavigator.swift | 22 - .../PresentOnboardingCode.swift} | 19 + .../PresentOnboardingEmail.swift} | 8 + .../PresentOnboardingPhone.swift} | 8 + .../PresentOnboardingStart.swift} | 8 + .../PresentOnboardingUsername.swift} | 8 + .../PresentOnboardingWelcome.swift} | 8 + .../PresentTermsAndConditions.swift} | 13 + .../XXNavigation/PresentActivitySheet.swift | 42 + Sources/XXNavigation/PresentCountryList.swift | 38 + ...werNavigator.swift => PresentDrawer.swift} | 16 + Sources/XXNavigation/PresentMenu.swift | 42 + .../PresentPermissionRequest.swift | 38 + .../XXNavigation/PresentPhotoLibrary.swift | 34 + Sources/XXNavigation/PresentScan.swift | 30 + Sources/XXNavigation/PresentSearch.swift | 43 + .../XXNavigation/Profile/PresentProfile.swift | 30 + .../Profile/PresentProfileCode.swift | 42 + .../Profile/PresentProfileEmail.swift | 30 + .../Profile/PresentProfilePhone.swift | 30 + .../RestoreAndBackup/PresentPassphrase.swift | 17 + .../PresentRequests.swift} | 8 + .../PresentRestoreList.swift} | 8 + .../RestoreAndBackup/PresentSFTP.swift | 14 + .../Settings/PresentSettings.swift | 30 + .../PresentSettingsAccountDelete.swift | 30 + .../Settings/PresentSettingsAdvanced.swift | 30 + .../Settings/PresentSettingsBackup.swift | 30 + .../Coordinator/ChatCoordinatorSpec.swift | 118 -- .../Coordinator/ChatListCoordinatorSpec.swift | 243 ---- .../Coordinator/ContactCoordinatorSpec.swift | 129 --- .../ContactListCoordinatorSpec.swift | 162 --- .../OnboardingCoordinatorSpec.swift | 244 ---- .../Coordinator/ProfileCoordinatorSpec.swift | 140 --- .../Coordinator/RequestsCoordinatorSpec.swift | 100 -- .../Coordinator/ScanCoordinatorSpec.swift | 87 -- .../Coordinator/SearchCoordinatorSpec.swift | 80 -- .../Coordinator/SettingsCoordinatorSpec.swift | 79 -- 173 files changed, 6100 insertions(+), 7520 deletions(-) delete mode 100644 Sources/BackupFeature/Coordinator/BackupCoordinator.swift delete mode 100644 Sources/ChatFeature/Coordinator/ChatCoordinator.swift delete mode 100644 Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift create mode 100644 Sources/ChatListFeature/Views/ChatSearchListContainerView.swift delete mode 100644 Sources/ContactFeature/Coordinator/ContactCoordinator.swift delete mode 100644 Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift delete mode 100644 Sources/Countries/Country.swift delete mode 100644 Sources/MenuFeature/Coordinator/MenuCoordinator.swift delete mode 100644 Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift delete mode 100644 Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift delete mode 100644 Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift delete mode 100644 Sources/ScanFeature/Coordinator/ScanCoordinator.swift delete mode 100644 Sources/SearchFeature/Coordinator/SearchCoordinator.swift delete mode 100644 Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift create mode 100644 Sources/Shared/Models/Country.swift create mode 100644 Sources/Shared/Models/MenuItem.swift create mode 100644 Sources/Shared/Models/PermissionType.swift rename Sources/{Countries => Shared}/Resources/country_codes.json (100%) delete mode 100644 Sources/TermsFeature/TermsCoordinator.swift delete mode 100644 Sources/XXNavigation/Actions/PresentChat.swift delete mode 100644 Sources/XXNavigation/Actions/PresentChatList.swift delete mode 100644 Sources/XXNavigation/Actions/PresentCountryList.swift delete mode 100644 Sources/XXNavigation/Actions/PresentDrawer.swift delete mode 100644 Sources/XXNavigation/Actions/PresentGroupChat.swift delete mode 100644 Sources/XXNavigation/Actions/PresentOnboardingCode.swift delete mode 100644 Sources/XXNavigation/Actions/PresentOnboardingEmail.swift delete mode 100644 Sources/XXNavigation/Actions/PresentOnboardingPhone.swift delete mode 100644 Sources/XXNavigation/Actions/PresentOnboardingStart.swift delete mode 100644 Sources/XXNavigation/Actions/PresentOnboardingUsername.swift delete mode 100644 Sources/XXNavigation/Actions/PresentOnboardingWelcome.swift delete mode 100644 Sources/XXNavigation/Actions/PresentRestoreList.swift delete mode 100644 Sources/XXNavigation/Actions/PresentSearch.swift delete mode 100644 Sources/XXNavigation/Actions/PresentTermsAndConditions.swift rename Sources/XXNavigation/{Actions/PresentRequests.swift => Chat/PresentCamera.swift} (52%) rename Sources/XXNavigation/{Navigators/PresentChatNavigator.swift => Chat/PresentChat.swift} (75%) rename Sources/XXNavigation/{Navigators/PresentChatListNavigator.swift => Chat/PresentChatList.swift} (81%) rename Sources/XXNavigation/{Navigators/PresentGroupChatNavigator.swift => Chat/PresentGroupChat.swift} (75%) create mode 100644 Sources/XXNavigation/Chat/PresentMemberList.swift create mode 100644 Sources/XXNavigation/Chat/PresentNewGroup.swift create mode 100644 Sources/XXNavigation/Chat/PresentWebsite.swift create mode 100644 Sources/XXNavigation/Contact/PresentContact.swift create mode 100644 Sources/XXNavigation/Contact/PresentContactList.swift create mode 100644 Sources/XXNavigation/Contact/PresentNickname.swift create mode 100644 Sources/XXNavigation/CustomActions/OpenLeft.swift create mode 100644 Sources/XXNavigation/Export.swift delete mode 100644 Sources/XXNavigation/Navigators/PresentCountryListNavigator.swift delete mode 100644 Sources/XXNavigation/Navigators/PresentSearchNavigator.swift rename Sources/XXNavigation/{Navigators/PresentOnboardingCodeNavigator.swift => Onboarding/PresentOnboardingCode.swift} (66%) rename Sources/XXNavigation/{Navigators/PresentOnboardingEmailNavigator.swift => Onboarding/PresentOnboardingEmail.swift} (81%) rename Sources/XXNavigation/{Navigators/PresentOnboardingPhoneNavigator.swift => Onboarding/PresentOnboardingPhone.swift} (81%) rename Sources/XXNavigation/{Navigators/PresentOnboardingStartNavigator.swift => Onboarding/PresentOnboardingStart.swift} (81%) rename Sources/XXNavigation/{Navigators/PresentOnboardingUsernameNavigator.swift => Onboarding/PresentOnboardingUsername.swift} (81%) rename Sources/XXNavigation/{Navigators/PresentOnboardingWelcomeNavigator.swift => Onboarding/PresentOnboardingWelcome.swift} (81%) rename Sources/XXNavigation/{Navigators/PresentTermsAndConditionsNavigator.swift => Onboarding/PresentTermsAndConditions.swift} (76%) create mode 100644 Sources/XXNavigation/PresentActivitySheet.swift create mode 100644 Sources/XXNavigation/PresentCountryList.swift rename Sources/XXNavigation/{Navigators/PresentDrawerNavigator.swift => PresentDrawer.swift} (73%) create mode 100644 Sources/XXNavigation/PresentMenu.swift create mode 100644 Sources/XXNavigation/PresentPermissionRequest.swift create mode 100644 Sources/XXNavigation/PresentPhotoLibrary.swift create mode 100644 Sources/XXNavigation/PresentScan.swift create mode 100644 Sources/XXNavigation/PresentSearch.swift create mode 100644 Sources/XXNavigation/Profile/PresentProfile.swift create mode 100644 Sources/XXNavigation/Profile/PresentProfileCode.swift create mode 100644 Sources/XXNavigation/Profile/PresentProfileEmail.swift create mode 100644 Sources/XXNavigation/Profile/PresentProfilePhone.swift create mode 100644 Sources/XXNavigation/RestoreAndBackup/PresentPassphrase.swift rename Sources/XXNavigation/{Navigators/PresentRequestsNavigator.swift => RestoreAndBackup/PresentRequests.swift} (82%) rename Sources/XXNavigation/{Navigators/PresentRestoreListNavigator.swift => RestoreAndBackup/PresentRestoreList.swift} (81%) create mode 100644 Sources/XXNavigation/RestoreAndBackup/PresentSFTP.swift create mode 100644 Sources/XXNavigation/Settings/PresentSettings.swift create mode 100644 Sources/XXNavigation/Settings/PresentSettingsAccountDelete.swift create mode 100644 Sources/XXNavigation/Settings/PresentSettingsAdvanced.swift create mode 100644 Sources/XXNavigation/Settings/PresentSettingsBackup.swift delete mode 100644 Tests/ChatFeatureTests/Coordinator/ChatCoordinatorSpec.swift delete mode 100644 Tests/ChatListFeatureTests/Coordinator/ChatListCoordinatorSpec.swift delete mode 100644 Tests/ContactFeatureTests/Coordinator/ContactCoordinatorSpec.swift delete mode 100644 Tests/ContactListFeatureTests/Coordinator/ContactListCoordinatorSpec.swift delete mode 100644 Tests/OnboardingFeatureTests/Coordinator/OnboardingCoordinatorSpec.swift delete mode 100644 Tests/ProfileFeatureTests/Coordinator/ProfileCoordinatorSpec.swift delete mode 100644 Tests/RequestsFeatureTests/Coordinator/RequestsCoordinatorSpec.swift delete mode 100644 Tests/ScanFeatureTests/Coordinator/ScanCoordinatorSpec.swift delete mode 100644 Tests/SearchFeatureTests/Coordinator/SearchCoordinatorSpec.swift delete mode 100644 Tests/SettingsFeatureTests/Coordinator/SettingsCoordinatorSpec.swift diff --git a/Package.swift b/Package.swift index 531a2a48..5bf030c0 100644 --- a/Package.swift +++ b/Package.swift @@ -137,6 +137,7 @@ let package = Package( .target(name: "ChatFeature"), .target(name: "MenuFeature"), .target(name: "PushFeature"), + .target(name: "XXNavigation"), .target(name: "TermsFeature"), .target(name: "CrashService"), .target(name: "BackupFeature"), @@ -152,8 +153,6 @@ let package = Package( .target(name: "ReportingFeature"), .target(name: "OnboardingFeature"), .target(name: "ContactListFeature"), - .target(name: "XXNavigation"), - .product(name: "Navigation", package: "Navigation"), ] ), .testTarget( @@ -193,12 +192,14 @@ let package = Package( name: "Permissions", dependencies: [ .target(name: "Shared"), + .target(name: "XXNavigation"), .target(name: "DependencyInjection"), ] ), .target( name: "XXNavigation", dependencies: [ + .target(name: "DrawerFeature"), .target(name: "DependencyInjection"), .product(name: "Navigation", package: "Navigation"), .product(name: "XXModels", package: "client-ios-db"), @@ -244,10 +245,8 @@ let package = Package( name: "Countries", dependencies: [ .target(name: "Shared"), + .target(name: "XXNavigation"), .target(name: "DependencyInjection"), - ], - resources: [ - .process("Resources"), ] ), .target( @@ -306,6 +305,7 @@ let package = Package( dependencies: [ .target(name: "Shared"), .target(name: "Presentation"), + .target(name: "XXNavigation"), .target(name: "DependencyInjection"), .product(name: "XXDatabase", package: "client-ios-db"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), @@ -326,14 +326,6 @@ let package = Package( .product(name: "ScrollViewController", package: "ScrollViewController"), ] ), - .testTarget( - name: "ContactFeatureTests", - dependencies: [ - .target(name: "ContactFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "ChatFeature", dependencies: [ @@ -343,23 +335,16 @@ let package = Package( .target(name: "Voxophone"), .target(name: "Permissions"), .target(name: "Presentation"), + .target(name: "XXNavigation"), .target(name: "DrawerFeature"), .target(name: "ChatInputFeature"), .target(name: "ReportingFeature"), .target(name: "DependencyInjection"), .product(name: "ChatLayout", package: "ChatLayout"), .product(name: "DifferenceKit", package: "DifferenceKit"), - .product(name: "ScrollViewController", package: "ScrollViewController"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), - ] - ), - .testTarget( - name: "ChatFeatureTests", - dependencies: [ - .target(name: "ChatFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), + .product(name: "ScrollViewController", package: "ScrollViewController"), ] ), .target( @@ -376,14 +361,6 @@ let package = Package( .product(name: "XXDatabase", package: "client-ios-db"), ] ), - .testTarget( - name: "SearchFeatureTests", - dependencies: [ - .target(name: "SearchFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "LaunchFeature", dependencies: [ @@ -409,6 +386,7 @@ let package = Package( .target(name: "Shared"), .target(name: "Defaults"), .target(name: "Presentation"), + .target(name: "XXNavigation"), ] ), .target( @@ -420,14 +398,6 @@ let package = Package( .product(name: "DifferenceKit", package: "DifferenceKit"), ] ), - .testTarget( - name: "RequestsFeatureTests", - dependencies: [ - .target(name: "RequestsFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "ProfileFeature", dependencies: [ @@ -448,14 +418,6 @@ let package = Package( .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), ] ), - .testTarget( - name: "ProfileFeatureTests", - dependencies: [ - .target(name: "ProfileFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "ChatListFeature", dependencies: [ @@ -463,6 +425,7 @@ let package = Package( .target(name: "Defaults"), .target(name: "MenuFeature"), .target(name: "ChatFeature"), + .target(name: "XXNavigation"), .target(name: "ProfileFeature"), .target(name: "SettingsFeature"), .target(name: "ContactListFeature"), @@ -470,14 +433,6 @@ let package = Package( .product(name: "DifferenceKit", package: "DifferenceKit"), ] ), - .testTarget( - name: "ChatListFeatureTests", - dependencies: [ - .target(name: "ChatListFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "OnboardingFeature", dependencies: [ @@ -496,20 +451,13 @@ let package = Package( .product(name: "ScrollViewController", package: "ScrollViewController"), ] ), - .testTarget( - name: "OnboardingFeatureTests", - dependencies: [ - .target(name: "OnboardingFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "MenuFeature", dependencies: [ .target(name: "Shared"), .target(name: "Defaults"), .target(name: "Presentation"), + .target(name: "XXNavigation"), .target(name: "DrawerFeature"), .target(name: "ReportingFeature"), .target(name: "DependencyInjection"), @@ -546,14 +494,6 @@ let package = Package( .product(name: "SnapKit", package: "SnapKit"), ] ), - .testTarget( - name: "ScanFeatureTests", - dependencies: [ - .target(name: "ScanFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "ContactListFeature", dependencies: [ @@ -564,14 +504,6 @@ let package = Package( .product(name: "DifferenceKit", package: "DifferenceKit"), ] ), - .testTarget( - name: "ContactListFeatureTests", - dependencies: [ - .target(name: "ContactListFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "SettingsFeature", dependencies: [ @@ -590,14 +522,6 @@ let package = Package( .product(name: "ScrollViewController", package: "ScrollViewController"), ] ), - .testTarget( - name: "SettingsFeatureTests", - dependencies: [ - .target(name: "SettingsFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "CollectionView", dependencies: [ diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index a10b8e67..25a3041a 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -1,17 +1,17 @@ import UIKit -import Navigation import BackgroundTasks import Shared -import XXModels -import XXLogger import Defaults import PushFeature import LaunchFeature import CrashReporting import DependencyInjection +import XXModels +import XXLogger import XXClient +import XXNavigation import XXMessengerClient import CloudFiles diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 1217667e..795e5d7d 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -43,7 +43,6 @@ import ContactListFeature import Shared import XXClient -import Navigation import XXNavigation import KeychainAccess import XXMessengerClient @@ -112,6 +111,7 @@ struct DependencyRegistrator { SetStackNavigator(), OpenUpNavigator(), + OpenLeftNavigator(), PresentOnboardingStartNavigator( screen: OnboardingStartController.init, @@ -168,13 +168,110 @@ struct DependencyRegistrator { PresentDrawerNavigator( screen: DrawerController.init(_:), navigationController: { navController } + ), + PresentContactListNavigator( + screen: ContactListController.init, + navigationController: { navController } + ), + PresentMenuNavigator( + screen: MenuController.init(_:), + navigationController: { navController } + ), + PresentScanNavigator( + screen: ScanContainerController.init, + navigationController: { navController } + ), + PresentNewGroupNavigator( + screen: CreateGroupController.init, + navigationController: { navController } + ), + PresentCountryListNavigator( + screen: CountryListController.init(_:), + navigationController: { navController } + ), + PresentProfileNavigator( + screen: ProfileController.init, + navigationController: { navController } + ), + PresentSettingsNavigator( + screen: SettingsController.init, + navigationController: { navController } + ), + PresentSettingsAdvancedNavigator( + screen: SettingsAdvancedController.init, + navigationController: { navController } + ), + PresentSettingsBackupNavigator( + screen: BackupController.init, + navigationController: { navController } + ), + PresentSettingsAccountDeleteNavigator( + screen: AccountDeleteController.init, + navigationController: { navController } + ), + PresentContactNavigator( + screen: ContactController.init(_:), + navigationController: { navController } + ), + PresentActivitySheetNavigator( + screen: { UIActivityViewController( + activityItems: $0, + applicationActivities: nil + )}, + navigationController: { navController } + ), + PresentProfileEmailNavigator( + screen: ProfileEmailController.init, + navigationController: { navController } + ), + PresentProfilePhoneNavigator( + screen: ProfilePhoneController.init, + navigationController: { navController } + ), + PresentPermissionRequestNavigator( + screen: RequestPermissionController.init, + navigationController: { navController } + ), + PresentPhotoLibraryNavigator( + screen: UIImagePickerController.init, + navigationController: { navController } + ), + PresentProfileCodeNavigator( + screen: ProfileCodeController.init(_:_:_:), + navigationController: { navController } ) - // searchFactory: SearchContainerController.init, - // restoreListFactory: RestoreListController.init, - // countriesFactory: CountryListController.init(_:), ) as Navigator) } + // container.register( + // ProfileCoordinator( + // imagePickerFactory: UIImagePickerController.init, + // permissionFactory: RequestPermissionController.init, + // countriesFactory: CountryListController.init(_:) + // //codeFactory: ProfileCodeController.init(_:_:) + // ) as ProfileCoordinating) + + // container.register( + // SearchCoordinator( + // contactsFactory: ContactListController.init, + // requestsFactory: RequestsContainerController.init, + // contactFactory: ContactController.init(_:), + // countriesFactory: CountryListController.init(_:) + // ) as SearchCoordinating) + + // container.register( + // ContactListCoordinator( + // scanFactory: ScanContainerController.init, + // searchFactory: SearchContainerController.init, + // newGroupFactory: CreateGroupController.init, + // requestsFactory: RequestsContainerController.init, + // contactFactory: ContactController.init(_:), + // singleChatFactory: SingleChatController.init(_:), + // groupChatFactory: GroupChatController.init(_:), + // sideMenuFactory: MenuController.init(_:_:), + // groupDrawerFactory: CreateDrawerController.init(_:_:) + // ) as ContactListCoordinating) + static private func registerCommonDependencies() { var environment: MessengerEnvironment = .live() environment.ndfEnvironment = .mainnet @@ -197,129 +294,6 @@ struct DependencyRegistrator { container.register(HUDController()) container.register(ToastController()) container.register(StatusBarStylist()) - - container.register( - TermsCoordinator.live( - usernameFactory: OnboardingUsernameController.init, - chatListFactory: ChatListController.init - ) - ) - - container.register( - BackupCoordinator( - sftpFactory: BackupSFTPController.init(_:), - passphraseFactory: BackupPassphraseController.init(_:_:) - ) as BackupCoordinating) - - container.register( - MenuCoordinator( - scanFactory: ScanContainerController.init, - chatsFactory: ChatListController.init, - profileFactory: ProfileController.init, - settingsFactory: SettingsController.init, - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init - ) as MenuCoordinating) - - container.register( - SearchCoordinator( - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - countriesFactory: CountryListController.init(_:) - ) as SearchCoordinating) - - container.register( - ProfileCoordinator( - emailFactory: ProfileEmailController.init, - phoneFactory: ProfilePhoneController.init, - imagePickerFactory: UIImagePickerController.init, - permissionFactory: RequestPermissionController.init, - sideMenuFactory: MenuController.init(_:_:), - countriesFactory: CountryListController.init(_:) - //codeFactory: ProfileCodeController.init(_:_:) - ) as ProfileCoordinating) - - container.register( - SettingsCoordinator( - backupFactory: BackupController.init, - advancedFactory: SettingsAdvancedController.init, - accountDeleteFactory: AccountDeleteController.init, - sideMenuFactory: MenuController.init(_:_:) - ) as SettingsCoordinating) - - container.register( - RestoreCoordinator( - successFactory: RestoreSuccessController.init, - chatListFactory: ChatListController.init, - restoreFactory: RestoreController.init(_:), - sftpFactory: RestoreSFTPController.init(_:), - passphraseFactory: RestorePassphraseController.init(_:_:) - ) as RestoreCoordinating) - - container.register( - ChatCoordinator( - retryFactory: RetrySheetController.init, - webFactory: WebScreen.init(_:), - previewFactory: QLPreviewController.init, - contactFactory: ContactController.init(_:), - imagePickerFactory: UIImagePickerController.init, - permissionFactory: RequestPermissionController.init - ) as ChatCoordinating) - - container.register( - ContactCoordinator( - requestsFactory: RequestsContainerController.init, - singleChatFactory: SingleChatController.init(_:), - imagePickerFactory: UIImagePickerController.init, - nicknameFactory: NicknameController.init(_:_:) - ) as ContactCoordinating) - - container.register( - RequestsCoordinator( - searchFactory: SearchContainerController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:), - nicknameFactory: NicknameController.init(_:_:) - ) as RequestsCoordinating) - - container.register( - ContactListCoordinator( - scanFactory: ScanContainerController.init, - searchFactory: SearchContainerController.init, - newGroupFactory: CreateGroupController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:), - groupDrawerFactory: CreateDrawerController.init(_:_:) - ) as ContactListCoordinating) - - container.register( - ScanCoordinator( - emailFactory: ProfileEmailController.init, - phoneFactory: ProfilePhoneController.init, - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - sideMenuFactory: MenuController.init(_:_:) - ) as ScanCoordinating) - - - container.register( - ChatListCoordinator( - scanFactory: ScanContainerController.init, - searchFactory: SearchContainerController.init, - newGroupFactory: CreateGroupController.init, - contactsFactory: ContactListController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:) - ) as ChatListCoordinating) } } @@ -339,27 +313,27 @@ extension PasswordStorage { private enum AlternativeUDConstants { static let address = "46.101.98.49:18001" static let cert = """ - -----BEGIN CERTIFICATE----- - MIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV - BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx - GzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp - cDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT - MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV - BAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw - DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh - Dwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs - WYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE - tJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA - m3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9 - bJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA - AaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA - neUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf - U/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2 - qvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4 - cyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R - tgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5 - 6m52PyzMNV+2N21IPppKwA== - -----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx +GzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp +cDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV +BAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh +Dwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs +WYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE +tJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA +m3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9 +bJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA +AaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA +neUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf +U/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2 +qvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4 +cyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R +tgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5 +6m52PyzMNV+2N21IPppKwA== +-----END CERTIFICATE----- """ static let contact = """ <xxc(2)7mbKFLE201WzH4SGxAOpHjjehwztIV+KGifi5L/PYPcDkAZiB9kZo+Dl3Vc7dD2SdZCFMOJVgwqGzfYRDkjc8RGEllBqNxq2sRRX09iQVef0kJQUgJCHNCOcvm6Ki0JJwvjLceyFh36iwK8oLbhLgqEZY86UScdACTyBCzBIab3ob5mBthYc3mheV88yq5PGF2DQ+dEvueUm+QhOSfwzppAJA/rpW9Wq9xzYcQzaqc3ztAGYfm2BBAHS7HVmkCbvZ/K07Xrl4EBPGHJYq12tWAN/C3mcbbBYUOQXyEzbSl/mO7sL3ORr0B4FMuqCi8EdlD6RO52pVhY+Cg6roRH1t5Ng1JxPt8Mv1yyjbifPhZ5fLKwxBz8UiFORfk0/jnhwgm25LRHqtNRRUlYXLvhv0HhqyYTUt17WNtCLATSVbqLrFGdy2EGadn8mP+kQNHp93f27d/uHgBNNe7LpuYCJMdWpoG6bOqmHEftxt0/MIQA8fTtTm3jJzv+7/QjZJDvQIv0SNdp8HFogpuwde+GuS4BcY7v5xz+ArGWcRR63ct2z83MqQEn9ODr1/gAAAgA7szRpDDQIdFUQo9mkWg8xBA==xxc> diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift index 9fd0b273..79cbd438 100644 --- a/Sources/BackupFeature/Controllers/BackupConfigController.swift +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -2,333 +2,333 @@ import UIKit import Shared import Combine import CloudFiles +import XXNavigation import DrawerFeature import DependencyInjection final class BackupConfigController: UIViewController { - @Dependency private var coordinator: BackupCoordinating - - private lazy var screenView = BackupConfigView() - - private let viewModel: BackupConfigViewModel - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() + @Dependency var navigator: Navigator + + private lazy var screenView = BackupConfigView() + + private let viewModel: BackupConfigViewModel + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + private var wifiOnly = false + private var manualBackups = false + private var serviceName: String = "" + + override func loadView() { + view = screenView + } + + init(_ viewModel: BackupConfigViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + setupBindings() + } + + private func setupBindings() { + viewModel.actionState() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in screenView.actionView.setState($0) } + .store(in: &cancellables) + + viewModel.connectedServices() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in decorate(connectedServices: $0) } + .store(in: &cancellables) + + viewModel.enabledService() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in decorate(enabledService: $0) } + .store(in: &cancellables) + + viewModel.automatic() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.frequencyDetailView.subtitleLabel.text = $0 ? "Automatic" : "Manual" + manualBackups = !$0 + }.store(in: &cancellables) + + viewModel.wifiOnly() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.infrastructureDetailView.subtitleLabel.text = $0 ? "Wi-Fi Only" : "Wi-Fi and Cellular" + wifiOnly = $0 + }.store(in: &cancellables) + + viewModel.lastBackup() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let backup = $0 else { + screenView.latestBackupDetailView.subtitleLabel.text = "Never" + return + } - private var wifiOnly = false - private var manualBackups = false - private var serviceName: String = "" + screenView.latestBackupDetailView.subtitleLabel.text = backup.lastModified.backupStyle() + }.store(in: &cancellables) + + screenView.actionView.backupNowButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapBackupNow() } + .store(in: &cancellables) + + screenView.frequencyDetailView + .publisher(for: .touchUpInside) + .sink { [unowned self] in presentFrequencyDrawer(manual: manualBackups) } + .store(in: &cancellables) + + screenView.infrastructureDetailView + .publisher(for: .touchUpInside) + .sink { [unowned self] in presentInfrastructureDrawer(wifiOnly: wifiOnly) } + .store(in: &cancellables) + + screenView.googleDriveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.drive, self) } + .store(in: &cancellables) + + screenView.googleDriveButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .drive, screenView.googleDriveButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.dropboxButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .dropbox, screenView.dropboxButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.sftpButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .sftp, screenView.sftpButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.iCloudButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .icloud, screenView.iCloudButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.dropboxButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.dropbox, self) } + .store(in: &cancellables) + + screenView.sftpButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.sftp, self) } + .store(in: &cancellables) + + screenView.iCloudButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.icloud, self) } + .store(in: &cancellables) + } + + private func decorate(enabledService: CloudService?) { + var button: BackupSwitcherButton? + + switch enabledService { + case .none: + break + case .icloud: + serviceName = Localized.Backup.iCloud + button = screenView.iCloudButton + case .dropbox: + serviceName = Localized.Backup.dropbox + button = screenView.dropboxButton + case .drive: + serviceName = Localized.Backup.googleDrive + button = screenView.googleDriveButton + case .sftp: + serviceName = Localized.Backup.sftp + button = screenView.sftpButton + } - override func loadView() { - view = screenView + screenView.enabledSubtitleLabel.text + = Localized.Backup.Config.disclaimer(serviceName) + screenView.frequencyDetailView.titleLabel.text + = Localized.Backup.Config.frequency(serviceName).uppercased() + + guard let button = button else { + screenView.sftpButton.isHidden = false + screenView.iCloudButton.isHidden = false + screenView.dropboxButton.isHidden = false + screenView.googleDriveButton.isHidden = false + + screenView.sftpButton.switcherView.isOn = false + screenView.iCloudButton.switcherView.isOn = false + screenView.dropboxButton.switcherView.isOn = false + screenView.googleDriveButton.switcherView.isOn = false + + screenView.frequencyDetailView.isHidden = true + screenView.enabledSubtitleView.isHidden = true + screenView.latestBackupDetailView.isHidden = true + screenView.infrastructureDetailView.isHidden = true + return } - init(_ viewModel: BackupConfigViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + screenView.frequencyDetailView.isHidden = false + screenView.enabledSubtitleView.isHidden = false + screenView.latestBackupDetailView.isHidden = false + screenView.infrastructureDetailView.isHidden = false + + [screenView.iCloudButton, + screenView.dropboxButton, + screenView.googleDriveButton, + screenView.sftpButton].forEach { + $0.isHidden = $0 != button + $0.switcherView.isOn = $0 == button } + } - required init?(coder: NSCoder) { nil } + private func decorate(connectedServices: Set<CloudService>) { + if connectedServices.contains(.icloud) { + screenView.iCloudButton.showSwitcher(enabled: false) + } else { + screenView.iCloudButton.showChevron() + } - override func viewDidLoad() { - super.viewDidLoad() - setupBindings() + if connectedServices.contains(.dropbox) { + screenView.dropboxButton.showSwitcher(enabled: false) + } else { + screenView.dropboxButton.showChevron() } - private func setupBindings() { - viewModel.actionState() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.actionView.setState($0) } - .store(in: &cancellables) - - viewModel.connectedServices() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in decorate(connectedServices: $0) } - .store(in: &cancellables) - - viewModel.enabledService() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in decorate(enabledService: $0) } - .store(in: &cancellables) - - viewModel.automatic() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.frequencyDetailView.subtitleLabel.text = $0 ? "Automatic" : "Manual" - manualBackups = !$0 - }.store(in: &cancellables) - - viewModel.wifiOnly() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.infrastructureDetailView.subtitleLabel.text = $0 ? "Wi-Fi Only" : "Wi-Fi and Cellular" - wifiOnly = $0 - }.store(in: &cancellables) - - viewModel.lastBackup() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard let backup = $0 else { - screenView.latestBackupDetailView.subtitleLabel.text = "Never" - return - } - - screenView.latestBackupDetailView.subtitleLabel.text = backup.lastModified.backupStyle() - }.store(in: &cancellables) - - screenView.actionView.backupNowButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapBackupNow() } - .store(in: &cancellables) - - screenView.frequencyDetailView - .publisher(for: .touchUpInside) - .sink { [unowned self] in presentFrequencyDrawer(manual: manualBackups) } - .store(in: &cancellables) - - screenView.infrastructureDetailView - .publisher(for: .touchUpInside) - .sink { [unowned self] in presentInfrastructureDrawer(wifiOnly: wifiOnly) } - .store(in: &cancellables) - - screenView.googleDriveButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.drive, self) } - .store(in: &cancellables) - - screenView.googleDriveButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .drive, screenView.googleDriveButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.dropboxButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .dropbox, screenView.dropboxButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.sftpButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .sftp, screenView.sftpButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.iCloudButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .icloud, screenView.iCloudButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.dropboxButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.dropbox, self) } - .store(in: &cancellables) - - screenView.sftpButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.sftp, self) } - .store(in: &cancellables) - - screenView.iCloudButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.icloud, self) } - .store(in: &cancellables) + if connectedServices.contains(.drive) { + screenView.googleDriveButton.showSwitcher(enabled: false) + } else { + screenView.googleDriveButton.showChevron() } - private func decorate(enabledService: CloudService?) { - var button: BackupSwitcherButton? - - switch enabledService { - case .none: - break - case .icloud: - serviceName = Localized.Backup.iCloud - button = screenView.iCloudButton - case .dropbox: - serviceName = Localized.Backup.dropbox - button = screenView.dropboxButton - case .drive: - serviceName = Localized.Backup.googleDrive - button = screenView.googleDriveButton - case .sftp: - serviceName = Localized.Backup.sftp - button = screenView.sftpButton + if connectedServices.contains(.sftp) { + screenView.sftpButton.showSwitcher(enabled: false) + } else { + screenView.sftpButton.showChevron() + } + } + + private func presentInfrastructureDrawer(wifiOnly: Bool) { + let cancelButton = DrawerCapsuleButton(model: .init( + title: Localized.ChatList.Dashboard.cancel, + style: .seeThrough + )) + let wifiOnlyButton = DrawerRadio( + title: "Wi-Fi Only", + isSelected: wifiOnly + ) + let wifiAndCellularButton = DrawerRadio( + title: "Wi-Fi and Cellular", + isSelected: !wifiOnly, + spacingAfter: 40 + ) + + wifiOnlyButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseWifiOnly(true) } - - screenView.enabledSubtitleLabel.text - = Localized.Backup.Config.disclaimer(serviceName) - screenView.frequencyDetailView.titleLabel.text - = Localized.Backup.Config.frequency(serviceName).uppercased() - - guard let button = button else { - screenView.sftpButton.isHidden = false - screenView.iCloudButton.isHidden = false - screenView.dropboxButton.isHidden = false - screenView.googleDriveButton.isHidden = false - - screenView.sftpButton.switcherView.isOn = false - screenView.iCloudButton.switcherView.isOn = false - screenView.dropboxButton.switcherView.isOn = false - screenView.googleDriveButton.switcherView.isOn = false - - screenView.frequencyDetailView.isHidden = true - screenView.enabledSubtitleView.isHidden = true - screenView.latestBackupDetailView.isHidden = true - screenView.infrastructureDetailView.isHidden = true - return + }.store(in: &drawerCancellables) + + wifiAndCellularButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseWifiOnly(false) } - - screenView.frequencyDetailView.isHidden = false - screenView.enabledSubtitleView.isHidden = false - screenView.latestBackupDetailView.isHidden = false - screenView.infrastructureDetailView.isHidden = false - - [screenView.iCloudButton, - screenView.dropboxButton, - screenView.googleDriveButton, - screenView.sftpButton].forEach { - $0.isHidden = $0 != button - $0.switcherView.isOn = $0 == button + }.store(in: &drawerCancellables) + + cancelButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() } - } - - private func decorate(connectedServices: Set<CloudService>) { - if connectedServices.contains(.icloud) { - screenView.iCloudButton.showSwitcher(enabled: false) - } else { - screenView.iCloudButton.showChevron() + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.extraBold.font(size: 28.0), + text: Localized.Backup.Config.infrastructure, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 30 + ), + wifiOnlyButton, + wifiAndCellularButton, + cancelButton + ])) + } + + private func presentFrequencyDrawer(manual: Bool) { + let cancelButton = DrawerCapsuleButton(model: .init( + title: Localized.ChatList.Dashboard.cancel, + style: .seeThrough + )) + let manualButton = DrawerRadio( + title: "Manual", + isSelected: manual + ) + let automaticButton = DrawerRadio( + title: "Automatic", + isSelected: !manual, + spacingAfter: 40 + ) + manualButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseAutomatic(false) } - - if connectedServices.contains(.dropbox) { - screenView.dropboxButton.showSwitcher(enabled: false) - } else { - screenView.dropboxButton.showChevron() + }.store(in: &drawerCancellables) + + automaticButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseAutomatic(true) } - - if connectedServices.contains(.drive) { - screenView.googleDriveButton.showSwitcher(enabled: false) - } else { - screenView.googleDriveButton.showChevron() + }.store(in: &drawerCancellables) + + cancelButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() } - - if connectedServices.contains(.sftp) { - screenView.sftpButton.showSwitcher(enabled: false) - } else { - screenView.sftpButton.showChevron() - } - } - - private func presentInfrastructureDrawer(wifiOnly: Bool) { - let cancelButton = DrawerCapsuleButton(model: .init( - title: Localized.ChatList.Dashboard.cancel, - style: .seeThrough - )) - - let wifiOnlyButton = DrawerRadio( - title: "Wi-Fi Only", - isSelected: wifiOnly - ) - - let wifiAndCellularButton = DrawerRadio( - title: "Wi-Fi and Cellular", - isSelected: !wifiOnly, - spacingAfter: 40 - ) - - let drawer = DrawerController([ - DrawerText( - font: Fonts.Mulish.extraBold.font(size: 28.0), - text: Localized.Backup.Config.infrastructure, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 30 - ), - wifiOnlyButton, - wifiAndCellularButton, - cancelButton - ]) - - wifiOnlyButton.action - .sink { [unowned self] in - viewModel.didChooseWifiOnly(true) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - wifiAndCellularButton.action - .sink { [unowned self] in - viewModel.didChooseWifiOnly(false) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - cancelButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } - - private func presentFrequencyDrawer(manual: Bool) { - let cancelButton = DrawerCapsuleButton(model: .init( - title: Localized.ChatList.Dashboard.cancel, - style: .seeThrough - )) - - let manualButton = DrawerRadio( - title: "Manual", - isSelected: manual - ) - - let automaticButton = DrawerRadio( - title: "Automatic", - isSelected: !manual, - spacingAfter: 40 - ) - - let drawer = DrawerController([ - DrawerText( - font: Fonts.Mulish.extraBold.font(size: 28.0), - text: Localized.Backup.Config.frequency(serviceName), - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 30 - ), - manualButton, - automaticButton, - cancelButton - ]) - - manualButton.action - .sink { [unowned self] in - viewModel.didChooseAutomatic(false) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - automaticButton.action - .sink { [unowned self] in - viewModel.didChooseAutomatic(true) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - cancelButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.extraBold.font(size: 28.0), + text: Localized.Backup.Config.frequency(serviceName), + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 30 + ), + manualButton, + automaticButton, + cancelButton + ])) + } } diff --git a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift deleted file mode 100644 index 9ef0fdfb..00000000 --- a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift +++ /dev/null @@ -1,91 +0,0 @@ -import UIKit -import Shared -import Presentation -import ScrollViewController - -public typealias SFTPDetailsClosure = (String, String, String) -> Void - -public protocol BackupCoordinating { - func toDrawer( - _: UIViewController, - from: UIViewController - ) - - func toSFTP( - from: UIViewController, - detailsClosure: @escaping SFTPDetailsClosure - ) - - func toPassphrase( - from: UIViewController, - cancelClosure: @escaping EmptyClosure, - passphraseClosure: @escaping StringClosure - ) -} - -public struct BackupCoordinator: BackupCoordinating { - var pushPresenter: Presenting = PushPresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - - var sftpFactory: (@escaping SFTPDetailsClosure) -> UIViewController - - var passphraseFactory: ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController - - public init( - sftpFactory: @escaping ( - @escaping SFTPDetailsClosure - ) -> UIViewController, - passphraseFactory: @escaping ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController - ) { - self.sftpFactory = sftpFactory - self.passphraseFactory = passphraseFactory - } -} - -public extension BackupCoordinator { - func toSFTP( - from parent: UIViewController, - detailsClosure: @escaping SFTPDetailsClosure - ) { - let screen = sftpFactory(detailsClosure) - pushPresenter.present(screen, from: parent) - } - - func toDrawer( - _ screen: UIViewController, - from parent: UIViewController - ) { - let target = ScrollViewController.embedding(screen) - fullscreenPresenter.present(target, from: parent) - } - - func toPassphrase( - from parent: UIViewController, - cancelClosure: @escaping EmptyClosure, - passphraseClosure: @escaping StringClosure - ) { - let screen = passphraseFactory(cancelClosure, passphraseClosure) - let target = ScrollViewController.embedding(screen) - fullscreenPresenter.present(target, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift index 4c888b88..18e057f0 100644 --- a/Sources/BackupFeature/Service/BackupService.swift +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -28,7 +28,7 @@ public final class BackupService { public var settingsPublisher: AnyPublisher<CloudSettings, Never> { settings.handleEvents(receiveSubscription: { [weak self] _ in - guard let self = self else { return } + guard let self else { return } self.connectedServicesSubject.send(CloudFilesManager.all.linkedServices()) self.fetchBackupOnAllProviders() }).eraseToAnyPublisher() @@ -117,10 +117,7 @@ public final class BackupService { func initializeBackup(passphrase: String) { do { try messenger.startBackup( - password: passphrase, - params: .init( - username: username! - ) + password: passphrase ) } catch { print(">>> Exception when calling `messenger.startBackup`: \(error.localizedDescription)") diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index 38adaf92..edaf65b5 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -8,6 +8,7 @@ import Foundation import DependencyInjection import CloudFiles +import XXNavigation enum BackupActionState { case backupFinished @@ -33,9 +34,9 @@ struct BackupConfigViewModel { extension BackupConfigViewModel { static func live() -> Self { class Context { + @Dependency var navigator: Navigator @Dependency var service: BackupService @Dependency var hudController: HUDController - @Dependency var coordinator: BackupCoordinating } let context = Context() @@ -56,22 +57,22 @@ extension BackupConfigViewModel { context.service.stopBackups() return } - context.coordinator.toPassphrase(from: controller, cancelClosure: { + context.navigator.perform(PresentPassphrase(onCancel: { context.service.toggle(service: service, enabling: false) - }, passphraseClosure: { passphrase in + }, onPassphrase: { passphrase in context.hudController.show(.init( content: "Initializing and securing your backup file will take few seconds, please keep the app open." )) context.service.toggle(service: service, enabling: enabling) context.service.initializeBackup(passphrase: passphrase) context.hudController.dismiss() - }) + })) }, didTapService: { service, controller in if service == .sftp { - context.coordinator.toSFTP(from: controller) { host, username, password in + context.navigator.perform(PresentSFTP { host, username, password in context.service.setupSFTP(host: host, username: username, password: password) - } + }) return } diff --git a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift index 109bc6ef..14931edd 100644 --- a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift @@ -54,7 +54,7 @@ final class BackupSFTPViewModel { let anyController = UIViewController() DispatchQueue.global().async { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try CloudFilesManager.sftp( host: host, diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift index e101c035..0634e2cc 100644 --- a/Sources/ChatFeature/Controllers/GroupChatController.swift +++ b/Sources/ChatFeature/Controllers/GroupChatController.swift @@ -4,6 +4,7 @@ import Combine import XXModels import Voxophone import ChatLayout +import XXNavigation import DrawerFeature import DifferenceKit import ReportingFeature @@ -19,13 +20,12 @@ typealias OutgoingFailedGroupReplyCell = CollectionCell<FlexibleSpace, ReplyStac public final class GroupChatController: UIViewController { @Dependency var database: Database + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ChatCoordinating @Dependency var reportingStatus: ReportingStatus @Dependency var makeReportDrawer: MakeReportDrawer @Dependency var makeAppScreenshot: MakeAppScreenshot - private let members: MembersController private var collectionView: UICollectionView! private lazy var header = GroupHeaderView() private let inputComponent: ChatInputView @@ -34,6 +34,7 @@ public final class GroupChatController: UIViewController { private let viewModel: GroupChatViewModel private let layoutDelegate = LayoutDelegate() private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() private let chatLayout = CollectionViewChatLayout() private var sections = [ArraySection<ChatSection, Message>]() private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>() @@ -44,7 +45,6 @@ public final class GroupChatController: UIViewController { public init(_ info: GroupInfo) { let viewModel = GroupChatViewModel(info) self.viewModel = viewModel - self.members = .init(with: info.members) self.inputComponent = ChatInputView(store: .init( initialState: .init(canAddAttachments: false), @@ -148,7 +148,7 @@ public final class GroupChatController: UIViewController { private func setupInputController() { inputComponent.setMaxHeight { [weak self] in - guard let self = self else { return 150 } + guard let self else { return 150 } let maxHeight = self.collectionView.frame.height - self.collectionView.adjustedContentInset.top @@ -167,24 +167,104 @@ public final class GroupChatController: UIViewController { } private func setupBindings() { - viewModel.routesPublisher + viewModel + .routesPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in switch $0 { case .waitingRound: - coordinator.toDrawer(makeWaitingRoundDrawer(), from: self) + let button = DrawerCapsuleButton(model: .init( + title: Localized.Chat.RoundDrawer.action, + style: .brandColored + )) + + button + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.RoundDrawer.title, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), button + ])) + case .webview(let urlString): - coordinator.toWebview(with: urlString, from: self) + navigator.perform(PresentWebsite(url: URL(string: urlString)!)) } }.store(in: &cancellables) - viewModel.reportPopupPublisher + viewModel + .reportPopupPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] contact in - presentReportDrawer(contact) + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Chat.Report.cancel, for: .normal) + + let reportButton = CapsuleButton() + reportButton.setStyle(.red) + reportButton.setTitle(Localized.Chat.Report.action, for: .normal) + + reportButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + let screenshot = try! self.makeAppScreenshot() + self.viewModel.report(contact: contact, screenshot: screenshot) { + self.collectionView.reloadData() + } + } + }.store(in: &drawerCancellables) + + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Chat.Report.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.Report.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + axis: .vertical, + spacing: 20.0, + views: [reportButton, cancelButton] + ) + ])) }.store(in: &cancellables) - viewModel.messages + viewModel + .messages .receive(on: DispatchQueue.main) .sink { [unowned self] sections in func process() { @@ -234,45 +314,7 @@ public final class GroupChatController: UIViewController { } @objc private func didTapDots() { - coordinator.toMembersList(members, from: self) - } - - private func presentReportDrawer(_ contact: Contact) { - var config = MakeReportDrawer.Config() - config.onReport = { [weak self] in - guard let self = self else { return } - let screenshot = try! self.makeAppScreenshot() - self.viewModel.report(contact: contact, screenshot: screenshot) { - self.collectionView.reloadData() - } - } - let drawer = makeReportDrawer(config) - coordinator.toDrawer(drawer, from: self) - } - - private func makeWaitingRoundDrawer() -> UIViewController { - let text = DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Chat.RoundDrawer.title, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ) - - let button = DrawerCapsuleButton(model: .init( - title: Localized.Chat.RoundDrawer.action, - style: .brandColored - )) - - let drawer = DrawerController([text, button]) - - button.action - .receive(on: DispatchQueue.main) - .sink { [weak drawer] in - drawer?.dismiss(animated: true) - }.store(in: &drawer.cancellables) - - return drawer + navigator.perform(PresentMemberList(members: viewModel.info.members)) } func scrollToBottom(completion: (() -> Void)? = nil) { @@ -290,7 +332,7 @@ public final class GroupChatController: UIViewController { if abs(delta) > chatLayout.visibleBounds.height { animator = ManualAnimator() animator?.animate(duration: TimeInterval(0.25), curve: .easeInOut) { [weak self] percentage in - guard let self = self else { return } + guard let self else { return } self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: initialOffset + (delta * percentage)) if percentage == 1.0 { @@ -598,7 +640,7 @@ extension GroupChatController: UICollectionViewDelegate { previewProvider: nil ) { [weak self] suggestedActions in - guard let self = self else { return nil } + guard let self else { return nil } let item = self.sections[indexPath.section].elements[indexPath.item] diff --git a/Sources/ChatFeature/Controllers/SheetController.swift b/Sources/ChatFeature/Controllers/SheetController.swift index b0f9ed29..12dd2e5a 100644 --- a/Sources/ChatFeature/Controllers/SheetController.swift +++ b/Sources/ChatFeature/Controllers/SheetController.swift @@ -2,50 +2,51 @@ import UIKit import Combine final class SheetController: UIViewController { - enum Action { - case clear - case details - case report - } - - private lazy var screenView = SheetView() - - var actionPublisher: AnyPublisher<Action, Never> { - actionRelay.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let actionRelay = PassthroughSubject<Action, Never>() - - public override func loadView() { - view = screenView - } - - public override func viewDidLoad() { - super.viewDidLoad() - - screenView.clearButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { [weak actionRelay] in - actionRelay?.send(.clear) - } - }.store(in: &cancellables) - - screenView.detailsButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { [weak actionRelay] in - actionRelay?.send(.details) - } - }.store(in: &cancellables) - - screenView.reportButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { [weak actionRelay] in - actionRelay?.send(.report) - } - }.store(in: &cancellables) - } + enum Action { + case clear + case details + case report + } + + private lazy var screenView = SheetView() + + var actionPublisher: AnyPublisher<Action, Never> { + actionRelay.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let actionRelay = PassthroughSubject<Action, Never>() + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + screenView + .clearButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak actionRelay] in + actionRelay?.send(.clear) + } + }.store(in: &cancellables) + + screenView.detailsButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak actionRelay] in + actionRelay?.send(.details) + } + }.store(in: &cancellables) + + screenView.reportButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak actionRelay] in + actionRelay?.send(.report) + } + }.store(in: &cancellables) + } } diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 9f2f7f82..5c3a7b27 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -6,6 +6,7 @@ import QuickLook import XXModels import Voxophone import ChatLayout +import XXNavigation import DrawerFeature import DifferenceKit import ChatInputFeature @@ -23,9 +24,9 @@ extension Message: Differentiable { public final class SingleChatController: UIViewController { @Dependency var logger: XXLogger + @Dependency var navigator: Navigator @Dependency var voxophone: Voxophone @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ChatCoordinating @Dependency var reportingStatus: ReportingStatus @Dependency var makeReportDrawer: MakeReportDrawer @Dependency var makeAppScreenshot: MakeAppScreenshot @@ -45,6 +46,7 @@ public final class SingleChatController: UIViewController { private let viewModel: SingleChatViewModel private let layoutDelegate = LayoutDelegate() private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() private let chatLayout = CollectionViewChatLayout() private var sections = [ArraySection<ChatSection, Message>]() private var currentInterfaceActions: SetActor<Set<InterfaceActions>, ReactionTypes> = SetActor() @@ -134,8 +136,6 @@ public final class SingleChatController: UIViewController { screenView.bringSubviewToFront(screenView.snackBar) } - // MARK: Private - private func setupCollectionView() { chatLayout.configure(layoutDelegate) collectionView = .init(on: screenView, with: chatLayout) @@ -192,7 +192,7 @@ public final class SingleChatController: UIViewController { private func setupInputController() { inputComponent.setMaxHeight { [weak self] in - guard let self = self else { return 150 } + guard let self else { return 150 } let maxHeight = self.collectionView.frame.height - self.collectionView.adjustedContentInset.top @@ -215,19 +215,43 @@ public final class SingleChatController: UIViewController { .sink { [unowned self] in switch $0 { case .library: - coordinator.toLibrary(from: self) + navigator.perform(PresentPhotoLibrary()) case .camera: - coordinator.toCamera(from: self) + navigator.perform(PresentCamera()) case .cameraPermission: - coordinator.toPermission(type: .camera, from: self) + navigator.perform(PresentPermissionRequest(type: .camera)) case .microphonePermission: - coordinator.toPermission(type: .microphone, from: self) + navigator.perform(PresentPermissionRequest(type: .microphone)) case .libraryPermission: - coordinator.toPermission(type: .library, from: self) + navigator.perform(PresentPermissionRequest(type: .library)) case .webview(let urlString): - coordinator.toWebview(with: urlString, from: self) + navigator.perform(PresentWebsite(url: URL(string: urlString)!)) case .waitingRound: - coordinator.toDrawer(makeWaitingRoundDrawer(), from: self) + let button = DrawerCapsuleButton(model: .init( + title: Localized.Chat.RoundDrawer.action, + style: .brandColored + )) + + button + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.RoundDrawer.title, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + button + ])) case .none: break } @@ -237,14 +261,15 @@ public final class SingleChatController: UIViewController { } private func setupBindings() { - sheet.actionPublisher + sheet + .actionPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in switch $0 { case .clear: presentDeleteAllDrawer() case .details: - coordinator.toContact(viewModel.contact, from: self) + navigator.perform(PresentContact(contact: viewModel.contact)) case .report: presentReportDrawer() } @@ -261,19 +286,23 @@ public final class SingleChatController: UIViewController { } }.store(in: &cancellables) - viewModel.reportPopupPublisher + viewModel + .reportPopupPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in presentReportDrawer() }.store(in: &cancellables) - viewModel.isOnline + viewModel + .isOnline .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.displayNetworkIssue(!$0) } - .store(in: &cancellables) + .sink { [weak screenView] in + screenView?.displayNetworkIssue(!$0) + }.store(in: &cancellables) - viewModel.messages + viewModel + .messages .receive(on: DispatchQueue.main) .sink { [unowned self] sections in func process() { @@ -337,7 +366,7 @@ public final class SingleChatController: UIViewController { if abs(delta) > chatLayout.visibleBounds.height { animator = ManualAnimator() animator?.animate(duration: TimeInterval(0.25), curve: .easeInOut) { [weak self] percentage in - guard let self = self else { return } + guard let self else { return } self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: initialOffset + (delta * percentage)) if percentage == 1.0 { @@ -359,42 +388,62 @@ public final class SingleChatController: UIViewController { } } - private func makeWaitingRoundDrawer() -> UIViewController { - let text = DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Chat.RoundDrawer.title, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ) - - let button = DrawerCapsuleButton(model: .init( - title: Localized.Chat.RoundDrawer.action, - style: .brandColored - )) + private func presentReportDrawer() { + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Chat.Report.cancel, for: .normal) - let drawer = DrawerController([text, button]) + let reportButton = CapsuleButton() + reportButton.setStyle(.red) + reportButton.setTitle(Localized.Chat.Report.action, for: .normal) - button.action + reportButton + .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned drawer] in drawer.dismiss(animated: true) } - .store(in: &drawer.cancellables) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + let screenshot = try! self.makeAppScreenshot() + self.viewModel.report(screenshot: screenshot) { success in + guard success else { return } + self.navigationController?.popViewController(animated: true) + } + } + }.store(in: &drawerCancellables) - return drawer - } + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - private func presentReportDrawer() { - var config = MakeReportDrawer.Config() - config.onReport = { [weak self] in - guard let self = self else { return } - let screenshot = try! self.makeAppScreenshot() - self.viewModel.report(screenshot: screenshot) { success in - guard success else { return } - self.navigationController?.popViewController(animated: true) - } - } - let drawer = makeReportDrawer(config) - coordinator.toDrawer(drawer, from: self) + navigator.perform(PresentDrawer(items: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Chat.Report.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.Report.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + axis: .vertical, + spacing: 20.0, + views: [reportButton, cancelButton] + ) + ])) } private func presentDeleteAllDrawer() { @@ -406,7 +455,28 @@ public final class SingleChatController: UIViewController { cancelButton.setStyle(.seeThrough) cancelButton.setTitle(Localized.Chat.Clear.cancel, for: .normal) - let drawer = DrawerController([ + clearButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestDeleteAll() + } + }.store(in: &drawerCancellables) + + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerImage( image: Asset.drawerNegative.image ), @@ -426,23 +496,7 @@ public final class SingleChatController: UIViewController { spacing: 20.0, views: [clearButton, cancelButton] ) - ]) - - clearButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned drawer, weak self] in - drawer.dismiss(animated: true) { - self?.viewModel.didRequestDeleteAll() - } - } - .store(in: &drawer.cancellables) - - cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned drawer] in drawer.dismiss(animated: true) } - .store(in: &drawer.cancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } private func previewItemAt(_ indexPath: IndexPath) { @@ -453,17 +507,15 @@ public final class SingleChatController: UIViewController { let ft = viewModel.getFileTransferWith(id: ftid) fileURL = FileManager.url(for: "\(ft.name).\(ft.type)") - coordinator.toPreview(from: self) + //coordinator.toPreview(from: self) } - // MARK: Selectors - @objc private func didTapDots() { - coordinator.toMenuSheet(sheet, from: self) + //coordinator.toMenuSheet(sheet, from: self) } @objc private func didTapInfo() { - coordinator.toContact(viewModel.contact, from: self) + navigator.perform(PresentContact(contact: viewModel.contact)) } } @@ -638,7 +690,7 @@ extension SingleChatController: UICollectionViewDelegate { previewProvider: nil ) { [weak self] _ in - guard let self = self else { return nil } + guard let self else { return nil } let item = self.sections[indexPath.section].elements[indexPath.item] var children = [ diff --git a/Sources/ChatFeature/Coordinator/ChatCoordinator.swift b/Sources/ChatFeature/Coordinator/ChatCoordinator.swift deleted file mode 100644 index 5c9045dd..00000000 --- a/Sources/ChatFeature/Coordinator/ChatCoordinator.swift +++ /dev/null @@ -1,105 +0,0 @@ -import UIKit -import Shared -import XXModels -import QuickLook -import Permissions -import Presentation - -public protocol ChatCoordinating { - func toCamera(from: UIViewController) - func toLibrary(from: UIViewController) - func toPreview(from: UIViewController) - func toRetrySheet(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toWebview(with: String, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toMenuSheet(_: UIViewController, from: UIViewController) - func toPermission(type: PermissionType, from: UIViewController) - func toMembersList(_: UIViewController, from: UIViewController) -} - -public struct ChatCoordinator: ChatCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var retryFactory: () -> UIViewController - var webFactory: (String) -> UIViewController - var previewFactory: () -> QLPreviewController - var contactFactory: (Contact) -> UIViewController - var imagePickerFactory: () -> UIImagePickerController - var permissionFactory: () -> RequestPermissionController - - public init( - retryFactory: @escaping () -> UIViewController, - webFactory: @escaping (String) -> UIViewController, - previewFactory: @escaping () -> QLPreviewController, - contactFactory: @escaping (Contact) -> UIViewController, - imagePickerFactory: @escaping () -> UIImagePickerController, - permissionFactory: @escaping () -> RequestPermissionController - ) { - self.webFactory = webFactory - self.retryFactory = retryFactory - self.previewFactory = previewFactory - self.contactFactory = contactFactory - self.permissionFactory = permissionFactory - self.imagePickerFactory = imagePickerFactory - } -} - -public extension ChatCoordinator { - func toPreview(from parent: UIViewController) { - let screen = previewFactory() - screen.delegate = (parent as? QLPreviewControllerDelegate) - screen.dataSource = (parent as? QLPreviewControllerDataSource) - pushPresenter.present(screen, from: parent) - } - - func toLibrary(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = false - modalPresenter.present(screen, from: parent) - } - - func toCamera(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.sourceType = .camera - screen.allowsEditing = false - modalPresenter.present(screen, from: parent) - } - - func toRetrySheet(from parent: UIViewController) { - let screen = retryFactory() - bottomPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toWebview(with urlString: String, from parent: UIViewController) { - let screen = webFactory(urlString) - modalPresenter.present(screen, from: parent) - } - - func toPermission(type: PermissionType, from parent: UIViewController) { - let screen = permissionFactory() - screen.setup(type: type) - pushPresenter.present(screen, from: parent) - } - - func toMembersList(_ screen: UIViewController, from parent: UIViewController) { - bottomPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toMenuSheet(_ screen: UIViewController, from parent: UIViewController) { - bottomPresenter.present(screen, from: parent) - } -} diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 4b95f84b..5aa7c08a 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -120,7 +120,7 @@ final class SingleChatViewModel: NSObject { .store(in: &cancellables) healthCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in - guard let self = self else { return } + guard let self else { return } self.networkMonitor.update($0) })) } @@ -362,7 +362,7 @@ final class SingleChatViewModel: NSObject { ) DispatchQueue.global().async { [weak self] in - guard let self = self else { return } + guard let self else { return } do { message = try self.database.saveMessage(message) diff --git a/Sources/ChatListFeature/Controller/ChatListController.swift b/Sources/ChatListFeature/Controller/ChatListController.swift index 34c6fac7..35768fd4 100644 --- a/Sources/ChatListFeature/Controller/ChatListController.swift +++ b/Sources/ChatListFeature/Controller/ChatListController.swift @@ -3,54 +3,55 @@ import Shared import Combine import XXModels import MenuFeature +import XXNavigation import DependencyInjection public final class ChatListController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ChatListCoordinating - + private lazy var screenView = ChatListView() private lazy var topLeftView = ChatListTopLeftNavView() private lazy var topRightView = ChatListTopRightNavView() private lazy var tableController = ChatListTableController(viewModel) private lazy var searchTableController = ChatSearchTableController(viewModel) private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>! - + private let viewModel = ChatListViewModel() private var cancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>() - + private var isEditingSearch = false { didSet { screenView.listContainerView .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents) } } - + private var shouldBeShowingRecents = false { didSet { screenView.listContainerView .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents) } } - + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) navigationItem.backButtonTitle = "" } - + required init?(coder: NSCoder) { nil } - + public override func loadView() { view = screenView } - + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) barStylist.styleSubject.send(.darkContent) navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) } - + public override func viewDidLoad() { super.viewDidLoad() setupChatList() @@ -58,64 +59,66 @@ public final class ChatListController: UIViewController { setupNavigationBar() setupRecentContacts() } - + private func setupNavigationBar() { navigationItem.leftBarButtonItem = UIBarButtonItem(customView: topLeftView) navigationItem.rightBarButtonItem = UIBarButtonItem(customView: topRightView) - - topRightView.actionPublisher + + topRightView + .actionPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in switch $0 { case .didTapSearch: - coordinator.toSearch(from: self) + navigator.perform(PresentSearch(replacing: false)) case .didTapNewGroup: - coordinator.toNewGroup(from: self) + navigator.perform(PresentNewGroup()) } }.store(in: &cancellables) - - viewModel.badgeCountPublisher + + viewModel + .badgeCountPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in topLeftView.updateBadge($0) } - .store(in: &cancellables) - - topLeftView.actionPublisher + .sink { [unowned self] in + topLeftView.updateBadge($0) + }.store(in: &cancellables) + + topLeftView + .actionPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSideMenu(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentMenu(currentItem: .chats)) + }.store(in: &cancellables) } - + private func setupChatList() { addChild(tableController) addChild(searchTableController) - screenView.listContainerView.addSubview(tableController.view) screenView.searchListContainerView.addSubview(searchTableController.view) - + tableController.view.snp.makeConstraints { $0.top.equalTo(screenView.listContainerView.collectionContainerView.snp.bottom) $0.left.equalToSuperview() $0.right.equalToSuperview() $0.bottom.equalToSuperview() } - searchTableController.view.snp.makeConstraints { $0.top.equalToSuperview() $0.left.equalToSuperview() $0.right.equalToSuperview() $0.bottom.equalToSuperview() } - tableController.didMove(toParent: self) searchTableController.didMove(toParent: self) } - + private func setupRecentContacts() { screenView .listContainerView .collectionView .register(ChatListRecentContactCell.self) - + collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( collectionView: screenView.listContainerView.collectionView ) { collectionView, indexPath, contact in @@ -124,26 +127,30 @@ public final class ChatListController: UIViewController { cell.setup(title: title, image: contact.photo) return cell } - + screenView.listContainerView.collectionView.delegate = self screenView.listContainerView.collectionView.dataSource = collectionDataSource - - viewModel.recentsPublisher + + viewModel + .recentsPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in collectionDataSource.apply($0) shouldBeShowingRecents = $0.numberOfItems > 0 }.store(in: &cancellables) } - + private func setupBindings() { - screenView.searchView + screenView + .searchView .rightPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toScan(from: self) } - .store(in: &cancellables) - - screenView.searchView + .sink { [unowned self] in + navigator.perform(PresentScan()) + }.store(in: &cancellables) + + screenView + .searchView .textPublisher .removeDuplicates() .receive(on: DispatchQueue.main) @@ -151,7 +158,7 @@ public final class ChatListController: UIViewController { viewModel.updateSearch(query: query) screenView.searchListContainerView.emptyView.updateSearched(content: query) }.store(in: &cancellables) - + Publishers.CombineLatest( viewModel.searchPublisher, screenView.searchView.textPublisher.removeDuplicates() @@ -164,30 +171,29 @@ public final class ChatListController: UIViewController { screenView.bringSubviewToFront(screenView.listContainerView) return } - screenView.listContainerView.isHidden = true screenView.searchListContainerView.isHidden = false - guard items.numberOfItems > 0 else { screenView.searchListContainerView.emptyView.isHidden = false screenView.bringSubviewToFront(screenView.searchListContainerView) screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) return } - screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) screenView.searchListContainerView.emptyView.isHidden = true - } - .store(in: &cancellables) - - screenView.searchView + }.store(in: &cancellables) + + screenView + .searchView .isEditingPublisher .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [unowned self] in isEditingSearch = $0 } - .store(in: &cancellables) - - viewModel.chatsPublisher + .sink { [unowned self] in + isEditingSearch = $0 + }.store(in: &cancellables) + + viewModel + .chatsPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in guard $0.isEmpty == false else { @@ -195,30 +201,36 @@ public final class ChatListController: UIViewController { screenView.listContainerView.emptyView.isHidden = false return } - screenView.listContainerView.bringSubviewToFront(tableController.view) screenView.listContainerView.emptyView.isHidden = true - } - .store(in: &cancellables) - - screenView.searchListContainerView - .emptyView.searchButton + }.store(in: &cancellables) + + screenView + .searchListContainerView + .emptyView + .searchButton .publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toSearch(from: self) } - .store(in: &cancellables) - - screenView.listContainerView - .emptyView.contactsButton + .sink { [unowned self] in + navigator.perform(PresentSearch(replacing: false)) + }.store(in: &cancellables) + + screenView + .listContainerView + .emptyView + .contactsButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toContacts(from: self) } - .store(in: &cancellables) - - viewModel.isOnline + .sink { [unowned self] in + navigator.perform(PresentContactList()) + }.store(in: &cancellables) + + viewModel + .isOnline .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [weak screenView] connected in screenView?.showConnectingBanner(!connected) } - .store(in: &cancellables) + .sink { [weak screenView] connected in + screenView?.showConnectingBanner(!connected) + }.store(in: &cancellables) } } @@ -228,7 +240,7 @@ extension ChatListController: UICollectionViewDelegate { didSelectItemAt indexPath: IndexPath ) { if let contact = collectionDataSource.itemIdentifier(for: indexPath) { - coordinator.toSingleChat(with: contact, from: self) + navigator.perform(PresentChat(contact: contact)) } } } diff --git a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift index c1651f26..3ea3a782 100644 --- a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift @@ -1,128 +1,126 @@ import UIKit import Shared import Combine +import XXNavigation import DependencyInjection class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch snapshot().sectionIdentifiers[section] { - case .chats: - return "CHATS" - case .connections: - return "CONNECTIONS" - } + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch snapshot().sectionIdentifiers[section] { + case .chats: + return "CHATS" + case .connections: + return "CONNECTIONS" } + } } final class ChatSearchTableController: UITableViewController { - @Dependency private var coordinator: ChatListCoordinating - - private let viewModel: ChatListViewModel - private let cellHeight: CGFloat = 83.0 - private var cancellables = Set<AnyCancellable>() - private var tableDataSource: ChatSearchListTableViewDiffableDataSource? - - init(_ viewModel: ChatListViewModel) { - self.viewModel = viewModel - super.init(style: .grouped) - - tableDataSource = ChatSearchListTableViewDiffableDataSource( - tableView: tableView - ) { table, indexPath, item in - let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) - switch item { - case .chat(let info): - switch info { - case .group(let group): - cell.setupGroup( - name: group.name, - date: group.createdAt, - preview: nil, - unreadCount: 0 - ) - - case .groupChat(let groupChatInfo): - cell.setupGroup( - name: groupChatInfo.group.name, - date: groupChatInfo.lastMessage.date, - preview: groupChatInfo.lastMessage.text, - unreadCount: groupChatInfo.unreadCount - ) - - case .contactChat(let contactChatInfo): - cell.setupContact( - name: (contactChatInfo.contact.nickname ?? contactChatInfo.contact.username) ?? "", - image: contactChatInfo.contact.photo, - date: contactChatInfo.lastMessage.date, - unreadCount: contactChatInfo.unreadCount, - preview: contactChatInfo.lastMessage.text - ) - } - - case .connection(let contact): - cell.setupContact( - name: (contact.nickname ?? contact.username) ?? "", - image: contact.photo, - date: nil, - unreadCount: 0, - preview: contact.username ?? "" - ) - } - - return cell + @Dependency var navigator: Navigator + + private let viewModel: ChatListViewModel + private let cellHeight: CGFloat = 83.0 + private var cancellables = Set<AnyCancellable>() + private var tableDataSource: ChatSearchListTableViewDiffableDataSource? + + init(_ viewModel: ChatListViewModel) { + self.viewModel = viewModel + super.init(style: .grouped) + + tableDataSource = ChatSearchListTableViewDiffableDataSource( + tableView: tableView + ) { table, indexPath, item in + let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) + switch item { + case .chat(let info): + switch info { + case .group(let group): + cell.setupGroup( + name: group.name, + date: group.createdAt, + preview: nil, + unreadCount: 0 + ) + + case .groupChat(let groupChatInfo): + cell.setupGroup( + name: groupChatInfo.group.name, + date: groupChatInfo.lastMessage.date, + preview: groupChatInfo.lastMessage.text, + unreadCount: groupChatInfo.unreadCount + ) + + case .contactChat(let contactChatInfo): + cell.setupContact( + name: (contactChatInfo.contact.nickname ?? contactChatInfo.contact.username) ?? "", + image: contactChatInfo.contact.photo, + date: contactChatInfo.lastMessage.date, + unreadCount: contactChatInfo.unreadCount, + preview: contactChatInfo.lastMessage.text + ) } + + case .connection(let contact): + cell.setupContact( + name: (contact.nickname ?? contact.username) ?? "", + image: contact.photo, + date: nil, + unreadCount: 0, + preview: contact.username ?? "" + ) + } + + return cell } - - required init?(coder: NSCoder) { nil } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.separatorStyle = .none - tableView.tableFooterView = UIView() - tableView.sectionIndexColor = .blue - tableView.register(ChatListCell.self) - tableView.dataSource = tableDataSource - view.backgroundColor = Asset.neutralWhite.color - - viewModel.searchPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } - .store(in: &cancellables) - } + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + tableView.sectionIndexColor = .blue + tableView.register(ChatListCell.self) + tableView.dataSource = tableDataSource + view.backgroundColor = Asset.neutralWhite.color + + viewModel.searchPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } + .store(in: &cancellables) + } } extension ChatSearchTableController { - override func tableView( - _ tableView: UITableView, - heightForRowAt: IndexPath - ) -> CGFloat { - return cellHeight - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let item = tableDataSource?.itemIdentifier(for: indexPath) { - switch item { - case .chat(let chatInfo): - switch chatInfo { - case .group(let group): - if let groupInfo = viewModel.groupInfo(from: group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .groupChat(let info): - if let groupInfo = viewModel.groupInfo(from: info.group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .contactChat(let info): - guard info.contact.authStatus == .friend else { return } - coordinator.toSingleChat(with: info.contact, from: self) - } - - case .connection(let contact): - coordinator.toContact(contact, from: self) - } + override func tableView( + _ tableView: UITableView, + heightForRowAt: IndexPath + ) -> CGFloat { + return cellHeight + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let item = tableDataSource?.itemIdentifier(for: indexPath) { + switch item { + case .chat(let chatInfo): + switch chatInfo { + case .group(let group): + if let groupInfo = viewModel.groupInfo(from: group) { + navigator.perform(PresentGroupChat(model: groupInfo)) + } + case .groupChat(let info): + if let groupInfo = viewModel.groupInfo(from: info.group) { + navigator.perform(PresentGroupChat(model: groupInfo)) + } + case .contactChat(let info): + guard info.contact.authStatus == .friend else { return } + navigator.perform(PresentChat(contact: info.contact)) } + case .connection(let contact): + navigator.perform(PresentContact(contact: contact)) + } } + } } diff --git a/Sources/ChatListFeature/Controller/ChatListSheetController.swift b/Sources/ChatListFeature/Controller/ChatListSheetController.swift index 79ad33eb..6b0d2d1d 100644 --- a/Sources/ChatListFeature/Controller/ChatListSheetController.swift +++ b/Sources/ChatListFeature/Controller/ChatListSheetController.swift @@ -2,41 +2,46 @@ import UIKit import Combine public final class ChatListSheetController: UIViewController { - public enum Action { - case delete - case deleteAll - } - - private lazy var screenView = ChatListMenuView() - - var didChooseAction: (Action) -> Void - private var cancellables = Set<AnyCancellable>() - - public init(_ didChooseAction: @escaping ChatListSheetClosure) { - self.didChooseAction = didChooseAction - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupBindings() - } - - private func setupBindings() { - screenView.deleteButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in dismiss(animated: true) { [weak self] in self?.didChooseAction(.delete) }} - .store(in: &cancellables) - - screenView.deleteAllButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in dismiss(animated: true) { [weak self] in self?.didChooseAction(.deleteAll) }} - .store(in: &cancellables) - } + private lazy var screenView = ChatListMenuView() + + private let didTapDelete: () -> Void + private let didTapDeleteAll: () -> Void + private var cancellables = Set<AnyCancellable>() + + public init( + _ didTapDelete: @escaping () -> Void, + _ didTapDeleteAll: @escaping () -> Void + ) { + self.didTapDelete = didTapDelete + self.didTapDeleteAll = didTapDeleteAll + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + screenView + .deleteButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak self] in + self?.didTapDelete() + } + }.store(in: &cancellables) + + screenView + .deleteAllButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak self] in + self?.didTapDeleteAll() + } + }.store(in: &cancellables) + } } diff --git a/Sources/ChatListFeature/Controller/ChatListTableController.swift b/Sources/ChatListFeature/Controller/ChatListTableController.swift index ed8bd0e9..744b95cc 100644 --- a/Sources/ChatListFeature/Controller/ChatListTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListTableController.swift @@ -2,206 +2,206 @@ import UIKit import Shared import Combine import XXModels +import XXNavigation import DifferenceKit import DrawerFeature import DependencyInjection extension ChatInfo: Differentiable { - public var differenceIdentifier: ChatInfo.ID { id } + public var differenceIdentifier: ChatInfo.ID { id } } final class ChatListTableController: UITableViewController { - @Dependency private var coordinator: ChatListCoordinating - - private var rows = [ChatInfo]() - private let viewModel: ChatListViewModel - private let cellHeight: CGFloat = 83.0 - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - init(_ viewModel: ChatListViewModel) { - self.viewModel = viewModel - super.init(style: .grouped) - } - - required init?(coder: NSCoder) { nil } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - tableView.alwaysBounceVertical = true - tableView.register(ChatListCell.self) - tableView.tableFooterView = UIView() - - viewModel - .chatsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard !self.rows.isEmpty else { - self.rows = $0 - tableView.reloadData() - return - } - - self.tableView.reload( - using: StagedChangeset(source: self.rows, target: $0), - deleteSectionsAnimation: .automatic, - insertSectionsAnimation: .automatic, - reloadSectionsAnimation: .none, - deleteRowsAnimation: .automatic, - insertRowsAnimation: .automatic, - reloadRowsAnimation: .none - ) { [unowned self] in - self.rows = $0 - } - }.store(in: &cancellables) - } + @Dependency var navigator: Navigator + + private var rows = [ChatInfo]() + private let viewModel: ChatListViewModel + private let cellHeight: CGFloat = 83.0 + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + init(_ viewModel: ChatListViewModel) { + self.viewModel = viewModel + super.init(style: .grouped) + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.alwaysBounceVertical = true + tableView.register(ChatListCell.self) + tableView.tableFooterView = UIView() + + viewModel + .chatsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard !self.rows.isEmpty else { + self.rows = $0 + tableView.reloadData() + return + } + + self.tableView.reload( + using: StagedChangeset(source: self.rows, target: $0), + deleteSectionsAnimation: .automatic, + insertSectionsAnimation: .automatic, + reloadSectionsAnimation: .none, + deleteRowsAnimation: .automatic, + insertRowsAnimation: .automatic, + reloadRowsAnimation: .none + ) { [unowned self] in + self.rows = $0 + } + }.store(in: &cancellables) + } } extension ChatListTableController { - override func tableView( - _ tableView: UITableView, - numberOfRowsInSection: Int - ) -> Int { - return rows.count + override func tableView( + _ tableView: UITableView, + numberOfRowsInSection: Int + ) -> Int { + return rows.count + } + + override func tableView( + _ tableView: UITableView, + heightForRowAt: IndexPath + ) -> CGFloat { + return cellHeight + } + + override func tableView( + _ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { + + let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in + guard let self else { return } + self.didRequestDeletionOf(self.rows[indexPath.row]) + complete(true) } - - override func tableView( - _ tableView: UITableView, - heightForRowAt: IndexPath - ) -> CGFloat { - return cellHeight + + delete.image = Asset.chatListDeleteSwipe.image + delete.backgroundColor = Asset.accentDanger.color + return UISwipeActionsConfiguration(actions: [delete]) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch rows[indexPath.row] { + case .group(let group): + if let groupInfo = viewModel.groupInfo(from: group) { + navigator.perform(PresentGroupChat(model: groupInfo)) + } + case .groupChat(let info): + if let groupInfo = viewModel.groupInfo(from: info.group) { + navigator.perform(PresentGroupChat(model: groupInfo)) + } + case .contactChat(let info): + guard info.contact.authStatus == .friend else { return } + navigator.perform(PresentChat(contact: info.contact)) } - - override func tableView( - _ tableView: UITableView, - trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath - ) -> UISwipeActionsConfiguration? { - - let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in - guard let self = self else { return } - self.didRequestDeletionOf(self.rows[indexPath.row]) - complete(true) - } - - delete.image = Asset.chatListDeleteSwipe.image - delete.backgroundColor = Asset.accentDanger.color - return UISwipeActionsConfiguration(actions: [delete]) + } + + override func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) + + switch rows[indexPath.row] { + case .group(let group): + cell.setupGroup( + name: group.name, + date: group.createdAt, + preview: nil, + unreadCount: 0 + ) + + case .groupChat(let info): + cell.setupGroup( + name: info.group.name, + date: info.lastMessage.date, + preview: info.lastMessage.text, + unreadCount: info.unreadCount + ) + + case .contactChat(let info): + cell.setupContact( + name: (info.contact.nickname ?? info.contact.username) ?? "", + image: info.contact.photo, + date: info.lastMessage.date, + unreadCount: info.unreadCount, + preview: info.lastMessage.text + ) } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch rows[indexPath.row] { - case .group(let group): - if let groupInfo = viewModel.groupInfo(from: group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .groupChat(let info): - if let groupInfo = viewModel.groupInfo(from: info.group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .contactChat(let info): - guard info.contact.authStatus == .friend else { return } - coordinator.toSingleChat(with: info.contact, from: self) - } - } - - override func tableView( - _ tableView: UITableView, - cellForRowAt indexPath: IndexPath - ) -> UITableViewCell { - - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) - - switch rows[indexPath.row] { - case .group(let group): - cell.setupGroup( - name: group.name, - date: group.createdAt, - preview: nil, - unreadCount: 0 - ) - - case .groupChat(let info): - cell.setupGroup( - name: info.group.name, - date: info.lastMessage.date, - preview: info.lastMessage.text, - unreadCount: info.unreadCount - ) - - case .contactChat(let info): - cell.setupContact( - name: (info.contact.nickname ?? info.contact.username) ?? "", - image: info.contact.photo, - date: info.lastMessage.date, - unreadCount: info.unreadCount, - preview: info.lastMessage.text - ) - } - - return cell + + return cell + } + + private func didRequestDeletionOf(_ item: ChatInfo) { + let title: String + let subtitle: String + let actionTitle: String + let actionClosure: () -> Void + + switch item { + case .group(let group): + title = Localized.ChatList.DeleteGroup.title + subtitle = Localized.ChatList.DeleteGroup.subtitle + actionTitle = Localized.ChatList.DeleteGroup.action + actionClosure = { [weak viewModel] in viewModel?.leave(group) } + + case .contactChat(let info): + title = Localized.ChatList.Delete.title + subtitle = Localized.ChatList.Delete.subtitle + actionTitle = Localized.ChatList.Delete.delete + actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } + + case .groupChat(let info): + title = Localized.ChatList.DeleteGroup.title + subtitle = Localized.ChatList.DeleteGroup.subtitle + actionTitle = Localized.ChatList.DeleteGroup.action + actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } } - - private func didRequestDeletionOf(_ item: ChatInfo) { - let title: String - let subtitle: String - let actionTitle: String - let actionClosure: () -> Void - - switch item { - case .group(let group): - title = Localized.ChatList.DeleteGroup.title - subtitle = Localized.ChatList.DeleteGroup.subtitle - actionTitle = Localized.ChatList.DeleteGroup.action - actionClosure = { [weak viewModel] in viewModel?.leave(group) } - - case .contactChat(let info): - title = Localized.ChatList.Delete.title - subtitle = Localized.ChatList.Delete.subtitle - actionTitle = Localized.ChatList.Delete.delete - actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } - - case .groupChat(let info): - title = Localized.ChatList.DeleteGroup.title - subtitle = Localized.ChatList.DeleteGroup.subtitle - actionTitle = Localized.ChatList.DeleteGroup.action - actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } + + let actionButton = DrawerCapsuleButton( + model: .init(title: actionTitle, style: .red) + ) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + actionClosure() } - - let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) - - let drawer = DrawerController([ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 39 - ), - actionButton - ]) - - actionButton.action.receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - actionClosure() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 39 + ), + actionButton + ])) + } } diff --git a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift deleted file mode 100644 index 57f94bb3..00000000 --- a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift +++ /dev/null @@ -1,102 +0,0 @@ -import UIKit -import Shared -import XXModels -import MenuFeature -import ChatFeature -import Presentation - -public typealias ChatListSheetClosure = (ChatListSheetController.Action) -> Void - -public protocol ChatListCoordinating { - func toScan(from: UIViewController) - func toSearch(from: UIViewController) - func toContacts(from: UIViewController) - func toNewGroup(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toGroupChat(with: GroupInfo, from: UIViewController) -} - -public struct ChatListCoordinator: ChatListCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var scanFactory: () -> UIViewController - var searchFactory: (String?) -> UIViewController - var newGroupFactory: () -> UIViewController - var contactsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupInfo) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - - public init( - scanFactory: @escaping () -> UIViewController, - searchFactory: @escaping (String?) -> UIViewController, - newGroupFactory: @escaping () -> UIViewController, - contactsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupInfo) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController - ) { - self.scanFactory = scanFactory - self.searchFactory = searchFactory - self.contactFactory = contactFactory - self.newGroupFactory = newGroupFactory - self.contactsFactory = contactsFactory - self.sideMenuFactory = sideMenuFactory - self.groupChatFactory = groupChatFactory - self.singleChatFactory = singleChatFactory - } -} - -public extension ChatListCoordinator { - func toSearch(from parent: UIViewController) { - let screen = searchFactory(nil) - pushPresenter.present(screen, from: parent) - } - - func toScan(from parent: UIViewController) { - let screen = scanFactory() - pushPresenter.present(screen, from: parent) - } - - func toContacts(from parent: UIViewController) { - let screen = contactsFactory() - pushPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toSingleChat(with contact: Contact, from parent: UIViewController) { - let screen = singleChatFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toGroupChat(with group: GroupInfo, from parent: UIViewController) { - let screen = groupChatFactory(group) - pushPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.chats, parent) - sidePresenter.present(screen, from: parent) - } - - func toNewGroup(from parent: UIViewController) { - let screen = newGroupFactory() - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } -} diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index 3792e0c1..f9214da7 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -29,20 +29,20 @@ final class ChatListViewModel { @Dependency var groupManager: GroupChat @Dependency var hudController: HUDController @Dependency var reportingStatus: ReportingStatus - + // TO REFACTOR: var isOnline: AnyPublisher<Bool, Never> { Just(.init(true)).eraseToAnyPublisher() } - + var myId: Data { try! messenger.e2e.get()!.getContact().getId() } - + var chatsPublisher: AnyPublisher<[ChatInfo], Never> { chatsSubject.eraseToAnyPublisher() } - + var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { let query = Contact.Query( authStatus: [.friend], @@ -50,7 +50,7 @@ final class ChatListViewModel { isBlocked: reportingStatus.isEnabled() ? false : nil, isBanned: reportingStatus.isEnabled() ? false : nil ) - + return database.fetchContactsPublisher(query) .replaceError(with: []) .map { @@ -61,13 +61,13 @@ final class ChatListViewModel { return snapshot }.eraseToAnyPublisher() } - + var searchPublisher: AnyPublisher<SearchSnapshot, Never> { let contactsQuery = Contact.Query( isBlocked: reportingStatus.isEnabled() ? false : nil, isBanned: reportingStatus.isEnabled() ? false : nil ) - + return Publishers.CombineLatest3( database.fetchContactsPublisher(contactsQuery) .replaceError(with: []) @@ -84,42 +84,42 @@ final class ChatListViewModel { let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false return username || nickname }.map(SearchItem.connection) - + let chatItems = chats.filter { switch $0 { case .group(let group): return group.name.lowercased().contains(query.lowercased()) - + case .groupChat(let info): let name = info.group.name.lowercased().contains(query.lowercased()) let last = info.lastMessage.text.lowercased().contains(query.lowercased()) return name || last - + case .contactChat(let info): let username = info.contact.username?.lowercased().contains(query.lowercased()) ?? false let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false let lastMessage = info.lastMessage.text.lowercased().contains(query.lowercased()) return username || nickname || lastMessage - + } }.map(SearchItem.chat) - + var snapshot = SearchSnapshot() - + if connectionItems.count > 0 { snapshot.appendSections([.connections]) snapshot.appendItems(connectionItems, toSection: .connections) } - + if chatItems.count > 0 { snapshot.appendSections([.chats]) snapshot.appendItems(chatItems, toSection: .chats) } - + return snapshot }.eraseToAnyPublisher() } - + var badgeCountPublisher: AnyPublisher<Int, Never> { let groupQuery = Group.Query(authStatus: [.pending]) let contactsQuery = Contact.Query( @@ -133,7 +133,7 @@ final class ChatListViewModel { isBlocked: reportingStatus.isEnabled() ? false : nil, isBanned: reportingStatus.isEnabled() ? false : nil ) - + return Publishers.CombineLatest( database.fetchContactsPublisher(contactsQuery).replaceError(with: []), database.fetchGroupsPublisher(groupQuery).replaceError(with: []) @@ -141,11 +141,11 @@ final class ChatListViewModel { .map { $0.0.count + $0.1.count } .eraseToAnyPublisher() } - + private var cancellables = Set<AnyCancellable>() private let searchSubject = CurrentValueSubject<String, Never>("") private let chatsSubject = CurrentValueSubject<[ChatInfo], Never>([]) - + init() { database.fetchChatInfosPublisher( ChatInfo.Query( @@ -168,14 +168,14 @@ final class ChatListViewModel { .sink { [unowned self] in chatsSubject.send($0) } .store(in: &cancellables) } - + func updateSearch(query: String) { searchSubject.send(query) } - + func leave(_ group: Group) { hudController.show() - + do { try groupManager.leaveGroup(groupId: group.id) try database.deleteMessages(.init(chat: .group(group.id))) @@ -185,17 +185,17 @@ final class ChatListViewModel { hudController.show(.init(error: error)) } } - + func clear(_ contact: XXModels.Contact) { _ = try? database.deleteMessages(.init(chat: .direct(myId, contact.id))) } - + func groupInfo(from group: Group) -> GroupInfo? { let query = GroupInfo.Query(groupId: group.id) guard let info = try? database.fetchGroupInfos(query).first else { return nil } - + return info } } diff --git a/Sources/ChatListFeature/Views/ChatListCell.swift b/Sources/ChatListFeature/Views/ChatListCell.swift index bb455395..e097c7fc 100644 --- a/Sources/ChatListFeature/Views/ChatListCell.swift +++ b/Sources/ChatListFeature/Views/ChatListCell.swift @@ -2,156 +2,156 @@ import UIKit import Shared final class ChatListCell: UITableViewCell { - private let titleLabel = UILabel() - private let unreadView = UIView() - private let unreadCountLabel = UILabel() - private let previewLabel = UILabel() - private let dateLabel = UILabel() - private let avatarView = AvatarView() - private var lastDate: Date? { - didSet { updateTimeAgoLabel() } + let titleLabel = UILabel() + let unreadView = UIView() + let unreadCountLabel = UILabel() + let previewLabel = UILabel() + let dateLabel = UILabel() + let avatarView = AvatarView() + var lastDate: Date? { + didSet { updateTimeAgoLabel() } + } + + private var timer: Timer? + + deinit { timer?.invalidate() } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + previewLabel.numberOfLines = 2 + dateLabel.textAlignment = .right + + unreadView.layer.cornerRadius = 8 + avatarView.layer.cornerRadius = 21 + avatarView.layer.masksToBounds = true + + dateLabel.textAlignment = .right + selectedBackgroundView = UIView() + unreadView.backgroundColor = .clear + backgroundColor = Asset.neutralWhite.color + dateLabel.textColor = Asset.neutralWeak.color + titleLabel.textColor = Asset.neutralActive.color + unreadCountLabel.textColor = Asset.neutralWhite.color + + dateLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + unreadCountLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + + timer = Timer.scheduledTimer(withTimeInterval: 59, repeats: true) { [weak self] _ in + self?.updateTimeAgoLabel() } - - private var timer: Timer? - - deinit { timer?.invalidate() } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - previewLabel.numberOfLines = 2 - dateLabel.textAlignment = .right - - unreadView.layer.cornerRadius = 8 - avatarView.layer.cornerRadius = 21 - avatarView.layer.masksToBounds = true - - dateLabel.textAlignment = .right - selectedBackgroundView = UIView() - unreadView.backgroundColor = .clear - backgroundColor = Asset.neutralWhite.color - dateLabel.textColor = Asset.neutralWeak.color - titleLabel.textColor = Asset.neutralActive.color - unreadCountLabel.textColor = Asset.neutralWhite.color - - dateLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) - unreadCountLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - - timer = Timer.scheduledTimer(withTimeInterval: 59, repeats: true) { [weak self] _ in - self?.updateTimeAgoLabel() - } - - dateLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - contentView.addSubview(titleLabel) - contentView.addSubview(unreadView) - contentView.addSubview(avatarView) - contentView.addSubview(previewLabel) - contentView.addSubview(dateLabel) - unreadView.addSubview(unreadCountLabel) - - avatarView.snp.makeConstraints { - $0.top.equalToSuperview().offset(14) - $0.left.equalToSuperview().offset(24) - $0.width.height.equalTo(48) - } - - unreadCountLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.left.equalTo(avatarView.snp.right).offset(16) - $0.right.lessThanOrEqualTo(dateLabel.snp.left).offset(-10) - } - - dateLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel) - $0.right.equalToSuperview().offset(-25) - } - - previewLabel.snp.makeConstraints { - $0.left.equalTo(titleLabel) - $0.top.equalTo(titleLabel.snp.bottom).offset(2) - $0.right.lessThanOrEqualTo(unreadView.snp.left).offset(-3) - $0.bottom.lessThanOrEqualToSuperview().offset(-10) - } - - unreadView.snp.makeConstraints { - $0.right.equalTo(dateLabel) - $0.centerY.equalTo(previewLabel) - $0.width.height.equalTo(20) - } + + dateLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + contentView.addSubview(titleLabel) + contentView.addSubview(unreadView) + contentView.addSubview(avatarView) + contentView.addSubview(previewLabel) + contentView.addSubview(dateLabel) + unreadView.addSubview(unreadCountLabel) + + avatarView.snp.makeConstraints { + $0.top.equalToSuperview().offset(14) + $0.left.equalToSuperview().offset(24) + $0.width.height.equalTo(48) } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - lastDate = nil - titleLabel.text = nil - unreadCountLabel.text = nil - previewLabel.attributedText = nil - avatarView.prepareForReuse() + + unreadCountLabel.snp.makeConstraints { + $0.center.equalToSuperview() } - - private func updateTimeAgoLabel() { - if let date = lastDate { - dateLabel.text = date.asRelativeFromNow() - } + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.left.equalTo(avatarView.snp.right).offset(16) + $0.right.lessThanOrEqualTo(dateLabel.snp.left).offset(-10) } - - func setupContact( - name: String, - image: Data?, - date: Date?, - unreadCount: Int, - preview: String - ) { - titleLabel.text = name - setPreview(string: preview) - avatarView.setupProfile(title: name, image: image, size: .large) - unreadCountLabel.text = "\(unreadCount)" - unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear - - if let date = date { - lastDate = date - } else { - dateLabel.text = nil - } + + dateLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel) + $0.right.equalToSuperview().offset(-25) } - - func setupGroup( - name: String, - date: Date, - preview: String?, - unreadCount: Int - ) { - lastDate = date - titleLabel.text = name - setPreview(string: preview) - avatarView.setupGroup(size: .large) - unreadCountLabel.text = "\(unreadCount)" - unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear + + previewLabel.snp.makeConstraints { + $0.left.equalTo(titleLabel) + $0.top.equalTo(titleLabel.snp.bottom).offset(2) + $0.right.lessThanOrEqualTo(unreadView.snp.left).offset(-3) + $0.bottom.lessThanOrEqualToSuperview().offset(-10) } - - private func setPreview(string: String?) { - guard let preview = string else { - previewLabel.attributedText = nil - return - } - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.1 - - previewLabel.attributedText = NSAttributedString( - string: preview, - attributes: [ - .paragraphStyle: paragraphStyle, - .font: Fonts.Mulish.regular.font(size: 14.0), - .foregroundColor: Asset.neutralSecondaryAlternative.color - ]) + + unreadView.snp.makeConstraints { + $0.right.equalTo(dateLabel) + $0.centerY.equalTo(previewLabel) + $0.width.height.equalTo(20) + } + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + lastDate = nil + titleLabel.text = nil + unreadCountLabel.text = nil + previewLabel.attributedText = nil + avatarView.prepareForReuse() + } + + private func updateTimeAgoLabel() { + if let date = lastDate { + dateLabel.text = date.asRelativeFromNow() + } + } + + func setupContact( + name: String, + image: Data?, + date: Date?, + unreadCount: Int, + preview: String + ) { + titleLabel.text = name + setPreview(string: preview) + avatarView.setupProfile(title: name, image: image, size: .large) + unreadCountLabel.text = "\(unreadCount)" + unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear + + if let date = date { + lastDate = date + } else { + dateLabel.text = nil + } + } + + func setupGroup( + name: String, + date: Date, + preview: String?, + unreadCount: Int + ) { + lastDate = date + titleLabel.text = name + setPreview(string: preview) + avatarView.setupGroup(size: .large) + unreadCountLabel.text = "\(unreadCount)" + unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear + } + + private func setPreview(string: String?) { + guard let preview = string else { + previewLabel.attributedText = nil + return } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.1 + + previewLabel.attributedText = NSAttributedString( + string: preview, + attributes: [ + .paragraphStyle: paragraphStyle, + .font: Fonts.Mulish.regular.font(size: 14.0), + .foregroundColor: Asset.neutralSecondaryAlternative.color + ]) + } } diff --git a/Sources/ChatListFeature/Views/ChatListContainerView.swift b/Sources/ChatListFeature/Views/ChatListContainerView.swift index 1be2c99c..b34fc50b 100644 --- a/Sources/ChatListFeature/Views/ChatListContainerView.swift +++ b/Sources/ChatListFeature/Views/ChatListContainerView.swift @@ -1,114 +1,95 @@ import UIKit import Shared -final class ChatSearchListContainerView: UIView{ - let emptyView = ChatSearchEmptyView() - - init() { - super.init(frame: .zero) - - addSubview(emptyView) - - emptyView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} - final class ChatListContainerView: UIView { - let separatorView = UIView() - let emptyView = ChatListEmptyView() - let collectionContainerView = UIView() - lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - - private let layout: UICollectionViewFlowLayout = { - let layout = UICollectionViewFlowLayout() - layout.minimumLineSpacing = 35 - layout.itemSize = CGSize(width: 56, height: 80) - layout.scrollDirection = .horizontal - return layout - }() - - init() { - super.init(frame: .zero) - - collectionView.showsHorizontalScrollIndicator = false - separatorView.backgroundColor = Asset.neutralLine.color - collectionView.backgroundColor = Asset.neutralWhite.color - collectionView.contentInset = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30) - - addSubview(emptyView) - addSubview(collectionContainerView) - collectionContainerView.addSubview(collectionView) - collectionContainerView.addSubview(separatorView) - - collectionContainerView.snp.makeConstraints { - $0.bottom.equalTo(snp.top) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.height.equalTo(110) - } - - collectionView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - } - - separatorView.snp.makeConstraints { - $0.top.equalTo(collectionView.snp.bottom).offset(20) - $0.height.equalTo(1) - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.equalToSuperview() - } - - emptyView.snp.makeConstraints { - $0.top.equalTo(collectionContainerView.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + let separatorView = UIView() + let emptyView = ChatListEmptyView() + let collectionContainerView = UIView() + lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + + private let layout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 35 + layout.itemSize = CGSize(width: 56, height: 80) + layout.scrollDirection = .horizontal + return layout + }() + + init() { + super.init(frame: .zero) + + collectionView.showsHorizontalScrollIndicator = false + separatorView.backgroundColor = Asset.neutralLine.color + collectionView.backgroundColor = Asset.neutralWhite.color + collectionView.contentInset = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30) + + addSubview(emptyView) + addSubview(collectionContainerView) + collectionContainerView.addSubview(collectionView) + collectionContainerView.addSubview(separatorView) + + collectionContainerView.snp.makeConstraints { + $0.bottom.equalTo(snp.top) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.height.equalTo(110) } - - required init?(coder: NSCoder) { nil } - - func showRecentsCollection(_ show: Bool) { - if show == true && collectionContainerView.alpha != 0.0 || - show == false && collectionContainerView.alpha == 0.0 { - return - } - - if show == true { - collectionContainerView.alpha = 0.0 - collectionContainerView.snp.updateConstraints { - $0.bottom.equalTo(snp.top).offset(collectionContainerView.bounds.height + 20) - } - - UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseInOut) { - self.collectionContainerView.alpha = 1.0 - } - - UIView.animate(withDuration: 0.3, delay: 0.15, options: .curveEaseInOut) { - self.setNeedsLayout() - self.layoutIfNeeded() - } - } else { - collectionContainerView.alpha = 1.0 - collectionContainerView.snp.updateConstraints { - $0.bottom.equalTo(snp.top) - } - - UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut) { - self.collectionContainerView.alpha = 0.0 - } - - UIView.animate(withDuration: 0.2, delay: 0.15, options: .curveEaseInOut) { - self.setNeedsLayout() - self.layoutIfNeeded() - } - } + collectionView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + } + separatorView.snp.makeConstraints { + $0.top.equalTo(collectionView.snp.bottom).offset(20) + $0.height.equalTo(1) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalToSuperview() + } + emptyView.snp.makeConstraints { + $0.top.equalTo(collectionContainerView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func showRecentsCollection(_ show: Bool) { + if show == true && collectionContainerView.alpha != 0.0 || + show == false && collectionContainerView.alpha == 0.0 { + return + } + + if show == true { + collectionContainerView.alpha = 0.0 + collectionContainerView.snp.updateConstraints { + $0.bottom.equalTo(snp.top).offset(collectionContainerView.bounds.height + 20) + } + + UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseInOut) { + self.collectionContainerView.alpha = 1.0 + } + + UIView.animate(withDuration: 0.3, delay: 0.15, options: .curveEaseInOut) { + self.setNeedsLayout() + self.layoutIfNeeded() + } + } else { + collectionContainerView.alpha = 1.0 + collectionContainerView.snp.updateConstraints { + $0.bottom.equalTo(snp.top) + } + + UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut) { + self.collectionContainerView.alpha = 0.0 + } + + UIView.animate(withDuration: 0.2, delay: 0.15, options: .curveEaseInOut) { + self.setNeedsLayout() + self.layoutIfNeeded() + } } + } } diff --git a/Sources/ChatListFeature/Views/ChatListEmptyView.swift b/Sources/ChatListFeature/Views/ChatListEmptyView.swift index 374e1849..449c7145 100644 --- a/Sources/ChatListFeature/Views/ChatListEmptyView.swift +++ b/Sources/ChatListFeature/Views/ChatListEmptyView.swift @@ -2,47 +2,47 @@ import UIKit import Shared final class ChatListEmptyView: UIView { - private let titleLabel = UILabel() - private let stackView = UIStackView() - private(set) var contactsButton = CapsuleButton() - - init() { - super.init(frame: .zero) - - backgroundColor = Asset.neutralWhite.color - let paragraph = NSMutableParagraphStyle() - paragraph.lineHeightMultiple = 1.2 - paragraph.alignment = .center - - titleLabel.numberOfLines = 0 - titleLabel.attributedText = NSAttributedString( - string: Localized.ChatList.emptyTitle, - attributes: [ - .paragraphStyle: paragraph, - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.bold.font(size: 24.0) - ] - ) - - contactsButton.setStyle(.brandColored) - contactsButton.setTitle(Localized.ChatList.action, for: .normal) - - stackView.spacing = 24 - stackView.axis = .vertical - stackView.alignment = .center - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(contactsButton) - - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let stackView = UIStackView() + let contactsButton = CapsuleButton() + + init() { + super.init(frame: .zero) + + backgroundColor = Asset.neutralWhite.color + let paragraph = NSMutableParagraphStyle() + paragraph.lineHeightMultiple = 1.2 + paragraph.alignment = .center + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = NSAttributedString( + string: Localized.ChatList.emptyTitle, + attributes: [ + .paragraphStyle: paragraph, + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.bold.font(size: 24.0) + ] + ) + + contactsButton.setStyle(.brandColored) + contactsButton.setTitle(Localized.ChatList.action, for: .normal) + + stackView.spacing = 24 + stackView.axis = .vertical + stackView.alignment = .center + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(contactsButton) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.lessThanOrEqualToSuperview() } - - required init?(coder: NSCoder) { nil } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/ChatListFeature/Views/ChatListMenuView.swift b/Sources/ChatListFeature/Views/ChatListMenuView.swift index 132b3840..4c700f31 100644 --- a/Sources/ChatListFeature/Views/ChatListMenuView.swift +++ b/Sources/ChatListFeature/Views/ChatListMenuView.swift @@ -3,67 +3,71 @@ import Shared import Combine final class ChatListMenuView: UIToolbar { - enum Action { - case delete - case deleteAll - } - - let stackView = UIStackView() - let deleteButton = UIButton() - let deleteAllButton = UIButton() - - @Published var isDeleteEnabled = false - - var publisher: AnyPublisher<Action, Never> { - actionRelay.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let actionRelay = PassthroughSubject<Action, Never>() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - private func setup() { - clipsToBounds = true - layer.cornerRadius = 15 - barTintColor = Asset.neutralSecondary.color - - deleteButton.setImage(Asset.chatListMenuDelete.image, for: .normal) - deleteAllButton.setTitleColor(Asset.accentDanger.color, for: .normal) - deleteAllButton.setTitle(Localized.ChatList.Menu.deleteAll, for: .normal) - deleteAllButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) - - stackView.spacing = 35 - stackView.addArrangedSubview(deleteButton) - stackView.addArrangedSubview(deleteAllButton) - stackView.distribution = .fillEqually - addSubview(stackView) - - translatesAutoresizingMaskIntoConstraints = false - - $isDeleteEnabled - .assign(to: \.isEnabled, on: deleteButton) - .store(in: &cancellables) - - deleteButton.publisher(for: .touchUpInside) - .sink { [weak actionRelay] in actionRelay?.send(.delete) } - .store(in: &cancellables) - - deleteAllButton.publisher(for: .touchUpInside) - .sink { [weak actionRelay] in actionRelay?.send(.deleteAll) } - .store(in: &cancellables) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-10) - make.height.equalTo(83) - } + enum Action { + case delete + case deleteAll + } + + let stackView = UIStackView() + let deleteButton = UIButton() + let deleteAllButton = UIButton() + + @Published var isDeleteEnabled = false + + var publisher: AnyPublisher<Action, Never> { + actionRelay.eraseToAnyPublisher() + } + + var cancellables = Set<AnyCancellable>() + let actionRelay = PassthroughSubject<Action, Never>() + + init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + private func setup() { + clipsToBounds = true + layer.cornerRadius = 15 + barTintColor = Asset.neutralSecondary.color + + deleteButton.setImage(Asset.chatListMenuDelete.image, for: .normal) + deleteAllButton.setTitleColor(Asset.accentDanger.color, for: .normal) + deleteAllButton.setTitle(Localized.ChatList.Menu.deleteAll, for: .normal) + deleteAllButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) + + stackView.spacing = 35 + stackView.addArrangedSubview(deleteButton) + stackView.addArrangedSubview(deleteAllButton) + stackView.distribution = .fillEqually + addSubview(stackView) + + translatesAutoresizingMaskIntoConstraints = false + + $isDeleteEnabled + .assign(to: \.isEnabled, on: deleteButton) + .store(in: &cancellables) + + deleteButton + .publisher(for: .touchUpInside) + .sink { [weak actionRelay] in + actionRelay?.send(.delete) + }.store(in: &cancellables) + + deleteAllButton + .publisher(for: .touchUpInside) + .sink { [weak actionRelay] in + actionRelay?.send(.deleteAll) + }.store(in: &cancellables) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalToSuperview().offset(-10) + $0.height.equalTo(83) } + } } diff --git a/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift b/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift index 4adbdeef..6f452d4e 100644 --- a/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift +++ b/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift @@ -2,88 +2,84 @@ import UIKit import Shared final class ChatListRecentContactCell: UICollectionViewCell { - private let titleLabel = UILabel() - private let containerView = UIView() - private let avatarView = AvatarView() - - override init(frame: CGRect) { - super.init(frame: frame) - - contentView.backgroundColor = .white - - let newLabel = UILabel() - newLabel.text = "NEW" - newLabel.textColor = Asset.neutralWhite.color - newLabel.font = Fonts.Mulish.bold.font(size: 8.0) - - let newContainerView = UIView() - newContainerView.layer.cornerRadius = 6.0 - newContainerView.layer.masksToBounds = true - newContainerView.backgroundColor = Asset.accentSafe.color - - titleLabel.numberOfLines = 2 - titleLabel.textAlignment = .center - titleLabel.lineBreakMode = .byWordWrapping - titleLabel.textColor = Asset.neutralDark.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - contentView.addSubview(titleLabel) - contentView.addSubview(containerView) - - containerView.addSubview(avatarView) - containerView.addSubview(newContainerView) - - newContainerView.addSubview(newLabel) - - containerView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - } - - newContainerView.snp.makeConstraints { - $0.top.equalTo(containerView.snp.top) - $0.right.equalTo(containerView.snp.right) - } - - newLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(3) - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(3) - } - - avatarView.snp.makeConstraints { - $0.width.equalTo(48) - $0.height.equalTo(48) - $0.top.equalToSuperview().offset(4) - $0.left.equalToSuperview().offset(4) - $0.right.equalToSuperview().offset(-4) - $0.bottom.equalToSuperview().offset(-4) - } - - titleLabel.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.top.equalTo(containerView.snp.bottom).offset(5) - $0.left.greaterThanOrEqualToSuperview() - $0.right.lessThanOrEqualToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let containerView = UIView() + let avatarView = AvatarView() + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.backgroundColor = .white + + let newLabel = UILabel() + newLabel.text = "NEW" + newLabel.textColor = Asset.neutralWhite.color + newLabel.font = Fonts.Mulish.bold.font(size: 8.0) + + let newContainerView = UIView() + newContainerView.layer.cornerRadius = 6.0 + newContainerView.layer.masksToBounds = true + newContainerView.backgroundColor = Asset.accentSafe.color + + titleLabel.numberOfLines = 2 + titleLabel.textAlignment = .center + titleLabel.lineBreakMode = .byWordWrapping + titleLabel.textColor = Asset.neutralDark.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + contentView.addSubview(titleLabel) + contentView.addSubview(containerView) + + containerView.addSubview(avatarView) + containerView.addSubview(newContainerView) + + newContainerView.addSubview(newLabel) + + containerView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - titleLabel.text = nil - avatarView.prepareForReuse() + newContainerView.snp.makeConstraints { + $0.top.equalTo(containerView.snp.top) + $0.right.equalTo(containerView.snp.right) } - - func setup(title: String, image: Data?) { - titleLabel.text = title - avatarView.setupProfile( - title: title, - image: image, - size: .large - ) + newLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(3) + $0.center.equalToSuperview() + $0.left.equalToSuperview().offset(3) + } + avatarView.snp.makeConstraints { + $0.width.equalTo(48) + $0.height.equalTo(48) + $0.top.equalToSuperview().offset(4) + $0.left.equalToSuperview().offset(4) + $0.right.equalToSuperview().offset(-4) + $0.bottom.equalToSuperview().offset(-4) + } + titleLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.top.equalTo(containerView.snp.bottom).offset(5) + $0.left.greaterThanOrEqualToSuperview() + $0.right.lessThanOrEqualToSuperview() + $0.bottom.lessThanOrEqualToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + avatarView.prepareForReuse() + } + + func setup(title: String, image: Data?) { + titleLabel.text = title + avatarView.setupProfile( + title: title, + image: image, + size: .large + ) + } } diff --git a/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift b/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift index 6cc8a78d..7ed956f4 100644 --- a/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift +++ b/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift @@ -3,70 +3,72 @@ import Shared import Combine final class ChatListTopLeftNavView: UIView { - private let titleLabel = UILabel() - private let badgeLabel = UILabel() - private let menuButton = UIButton() - private let stackView = UIStackView() - private let badgeContainerView = UIView() - - var actionPublisher: AnyPublisher<Void, Never> { - actionSubject.eraseToAnyPublisher() + let titleLabel = UILabel() + let badgeLabel = UILabel() + let menuButton = UIButton() + let stackView = UIStackView() + let badgeContainerView = UIView() + + var actionPublisher: AnyPublisher<Void, Never> { + actionSubject.eraseToAnyPublisher() + } + + private let actionSubject = PassthroughSubject<Void, Never>() + + init() { + super.init(frame: .zero) + + titleLabel.text = Localized.ChatList.title + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) + + menuButton.tintColor = Asset.neutralDark.color + menuButton.setImage(Asset.chatListMenu.image, for: .normal) + menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) + + badgeLabel.textColor = Asset.neutralWhite.color + badgeLabel.font = Fonts.Mulish.bold.font(size: 12.0) + + badgeContainerView.layer.cornerRadius = 5 + badgeContainerView.layer.masksToBounds = true + badgeContainerView.backgroundColor = Asset.brandPrimary.color + + badgeContainerView.addSubview(badgeLabel) + menuButton.addSubview(badgeContainerView) + stackView.addArrangedSubview(menuButton) + stackView.addArrangedSubview(titleLabel) + addSubview(stackView) + + badgeLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(3) + $0.center.equalToSuperview() + $0.left.equalToSuperview().offset(3) } - - private let actionSubject = PassthroughSubject<Void, Never>() - - init() { - super.init(frame: .zero) - - titleLabel.text = Localized.ChatList.title - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) - - menuButton.tintColor = Asset.neutralDark.color - menuButton.setImage(Asset.chatListMenu.image, for: .normal) - menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) - - badgeLabel.textColor = Asset.neutralWhite.color - badgeLabel.font = Fonts.Mulish.bold.font(size: 12.0) - - badgeContainerView.layer.cornerRadius = 5 - badgeContainerView.layer.masksToBounds = true - badgeContainerView.backgroundColor = Asset.brandPrimary.color - - badgeContainerView.addSubview(badgeLabel) - menuButton.addSubview(badgeContainerView) - stackView.addArrangedSubview(menuButton) - stackView.addArrangedSubview(titleLabel) - addSubview(stackView) - - badgeLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(3) - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(3) - } - - badgeContainerView.snp.makeConstraints { - $0.centerY.equalTo(menuButton.snp.top) - $0.centerX.equalTo(menuButton.snp.right).multipliedBy(0.8) - } - - menuButton.snp.makeConstraints { $0.width.equalTo(50) } - stackView.snp.makeConstraints { $0.edges.equalToSuperview() } + badgeContainerView.snp.makeConstraints { + $0.centerY.equalTo(menuButton.snp.top) + $0.centerX.equalTo(menuButton.snp.right).multipliedBy(0.8) } - - required init?(coder: NSCoder) { nil } - - @objc private func didTapMenu() { - actionSubject.send() + menuButton.snp.makeConstraints { + $0.width.equalTo(50) } - - func updateBadge(_ count: Int) { - guard count > 0 else { - badgeContainerView.isHidden = true - return - } - - badgeLabel.text = "\(count)" - badgeContainerView.isHidden = false + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + @objc private func didTapMenu() { + actionSubject.send() + } + + func updateBadge(_ count: Int) { + guard count > 0 else { + badgeContainerView.isHidden = true + return } + + badgeLabel.text = "\(count)" + badgeContainerView.isHidden = false + } } diff --git a/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift b/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift index 817893e1..182187b9 100644 --- a/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift +++ b/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift @@ -3,47 +3,53 @@ import Shared import Combine final class ChatListTopRightNavView: UIView { - enum Action { - case didTapSearch - case didTapNewGroup + enum Action { + case didTapSearch + case didTapNewGroup + } + + var actionPublisher: AnyPublisher<Action, Never> { + actionSubject.eraseToAnyPublisher() + } + + let stackView = UIStackView() + let searchButton = UIButton() + let newGroupButton = UIButton() + let actionSubject = PassthroughSubject<Action, Never>() + + init() { + super.init(frame: .zero) + + searchButton.tintColor = Asset.neutralDark.color + newGroupButton.tintColor = Asset.neutralDark.color + searchButton.setImage(Asset.chatListUd.image, for: .normal) + newGroupButton.setImage(Asset.chatListNewGroup.image, for: .normal) + searchButton.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside) + newGroupButton.addTarget(self, action: #selector(didTapNewGroup), for: .touchUpInside) + + stackView.spacing = 10 + stackView.addArrangedSubview(newGroupButton) + stackView.addArrangedSubview(searchButton) + addSubview(stackView) + + searchButton.snp.makeConstraints { + $0.width.equalTo(40) } - - var actionPublisher: AnyPublisher<Action, Never> { - actionSubject.eraseToAnyPublisher() + newGroupButton.snp.makeConstraints { + $0.width.equalTo(40) } - - private let stackView = UIStackView() - private let searchButton = UIButton() - private let newGroupButton = UIButton() - private let actionSubject = PassthroughSubject<Action, Never>() - - init() { - super.init(frame: .zero) - - searchButton.tintColor = Asset.neutralDark.color - newGroupButton.tintColor = Asset.neutralDark.color - searchButton.setImage(Asset.chatListUd.image, for: .normal) - newGroupButton.setImage(Asset.chatListNewGroup.image, for: .normal) - searchButton.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside) - newGroupButton.addTarget(self, action: #selector(didTapNewGroup), for: .touchUpInside) - - stackView.spacing = 10 - stackView.addArrangedSubview(newGroupButton) - stackView.addArrangedSubview(searchButton) - addSubview(stackView) - - searchButton.snp.makeConstraints { $0.width.equalTo(40) } - newGroupButton.snp.makeConstraints { $0.width.equalTo(40) } - stackView.snp.makeConstraints { $0.edges.equalToSuperview() } - } - - required init?(coder: NSCoder) { nil } - - @objc private func didTapSearch() { - actionSubject.send(.didTapSearch) - } - - @objc private func didTapNewGroup() { - actionSubject.send(.didTapNewGroup) + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + @objc private func didTapSearch() { + actionSubject.send(.didTapSearch) + } + + @objc private func didTapNewGroup() { + actionSubject.send(.didTapNewGroup) + } } diff --git a/Sources/ChatListFeature/Views/ChatListView.swift b/Sources/ChatListFeature/Views/ChatListView.swift index 03a40798..4bfe400f 100644 --- a/Sources/ChatListFeature/Views/ChatListView.swift +++ b/Sources/ChatListFeature/Views/ChatListView.swift @@ -2,79 +2,71 @@ import UIKit import Shared final class ChatListView: UIView { - let snackBar = SnackBar() - let containerView = UIView() - let searchView = SearchComponent() - let listContainerView = ChatListContainerView() - let searchListContainerView = ChatSearchListContainerView() + let snackBar = SnackBar() + let containerView = UIView() + let searchView = SearchComponent() + let listContainerView = ChatListContainerView() + let searchListContainerView = ChatSearchListContainerView() + + init() { + super.init(frame: .zero) - init() { - super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + listContainerView.backgroundColor = Asset.neutralWhite.color + searchListContainerView.backgroundColor = Asset.neutralWhite.color + searchView.update(placeholder: Localized.ChatList.Search.title) - backgroundColor = Asset.neutralWhite.color - listContainerView.backgroundColor = Asset.neutralWhite.color - searchListContainerView.backgroundColor = Asset.neutralWhite.color - searchView.update(placeholder: Localized.ChatList.Search.title) + addSubview(snackBar) + addSubview(searchView) + addSubview(containerView) + containerView.addSubview(searchListContainerView) + containerView.addSubview(listContainerView) - addSubview(snackBar) - addSubview(searchView) - addSubview(containerView) - containerView.addSubview(searchListContainerView) - containerView.addSubview(listContainerView) - - setupConstraints() + snackBar.snp.makeConstraints { + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalTo(snp.top) } - - required init?(coder: NSCoder) { nil } - - func showConnectingBanner(_ show: Bool) { - if show == true { - snackBar.alpha = 0.0 - snackBar.snp.updateConstraints { - $0.bottom - .equalTo(snp.top) - .offset(snackBar.bounds.height) - } - } else { - snackBar.alpha = 1.0 - snackBar.snp.updateConstraints { - $0.bottom.equalTo(snp.top) - } - } - - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { - self.setNeedsLayout() - self.layoutIfNeeded() - self.snackBar.alpha = show ? 1.0 : 0.0 - } + searchView.snp.makeConstraints { + $0.top.equalTo(snackBar.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) } - - private func setupConstraints() { - snackBar.snp.makeConstraints { - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalTo(snp.top) - } - - searchView.snp.makeConstraints { - $0.top.equalTo(snackBar.snp.bottom).offset(20) - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - } - - containerView.snp.makeConstraints { - $0.top.equalTo(searchView.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - - listContainerView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - searchListContainerView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + containerView.snp.makeConstraints { + $0.top.equalTo(searchView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + listContainerView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + searchListContainerView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func showConnectingBanner(_ show: Bool) { + if show == true { + snackBar.alpha = 0.0 + snackBar.snp.updateConstraints { + $0.bottom + .equalTo(snp.top) + .offset(snackBar.bounds.height) + } + } else { + snackBar.alpha = 1.0 + snackBar.snp.updateConstraints { + $0.bottom.equalTo(snp.top) + } + } + + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { + self.setNeedsLayout() + self.layoutIfNeeded() + self.snackBar.alpha = show ? 1.0 : 0.0 } + } } diff --git a/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift b/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift index 2fe1471c..ec551938 100644 --- a/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift +++ b/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift @@ -2,56 +2,56 @@ import UIKit import Shared final class ChatSearchEmptyView: UIView { - private let titleLabel = UILabel() - private let stackView = UIStackView() - private let descriptionLabel = UILabel() - private(set) var searchButton = CapsuleButton() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - titleLabel.textColor = Asset.brandPrimary.color - titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) - - let paragraph = NSMutableParagraphStyle() - paragraph.lineHeightMultiple = 1.2 - - descriptionLabel.numberOfLines = 0 - descriptionLabel.attributedText = NSAttributedString( - string: "was not found in your connections or in a chat. Click below to search for them as a new connection.", - attributes: [ - .paragraphStyle: paragraph, - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.regular.font(size: 16.0) - ] - ) - - searchButton.setStyle(.brandColored) - searchButton.setTitle("Search for a connection", for: .normal) - - stackView.axis = .vertical - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(descriptionLabel) - stackView.addArrangedSubview(searchButton) - - stackView.setCustomSpacing(10, after: titleLabel) - stackView.setCustomSpacing(30, after: descriptionLabel) - - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.centerY.equalToSuperview().multipliedBy(0.5) - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let stackView = UIStackView() + let descriptionLabel = UILabel() + let searchButton = CapsuleButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.brandPrimary.color + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.lineHeightMultiple = 1.2 + + descriptionLabel.numberOfLines = 0 + descriptionLabel.attributedText = NSAttributedString( + string: "was not found in your connections or in a chat. Click below to search for them as a new connection.", + attributes: [ + .paragraphStyle: paragraph, + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.regular.font(size: 16.0) + ] + ) + + searchButton.setStyle(.brandColored) + searchButton.setTitle("Search for a connection", for: .normal) + + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(descriptionLabel) + stackView.addArrangedSubview(searchButton) + + stackView.setCustomSpacing(10, after: titleLabel) + stackView.setCustomSpacing(30, after: descriptionLabel) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.centerY.equalToSuperview().multipliedBy(0.5) + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.lessThanOrEqualToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func updateSearched(content: String) { - titleLabel.text = content - } + func updateSearched(content: String) { + titleLabel.text = content + } } diff --git a/Sources/ChatListFeature/Views/ChatSearchListContainerView.swift b/Sources/ChatListFeature/Views/ChatSearchListContainerView.swift new file mode 100644 index 00000000..75c25dc6 --- /dev/null +++ b/Sources/ChatListFeature/Views/ChatSearchListContainerView.swift @@ -0,0 +1,17 @@ +import UIKit + +final class ChatSearchListContainerView: UIView { + let emptyView = ChatSearchEmptyView() + + init() { + super.init(frame: .zero) + + addSubview(emptyView) + + emptyView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift index 87a3f073..11e82eaa 100644 --- a/Sources/ContactFeature/Controllers/ContactController.swift +++ b/Sources/ContactFeature/Controllers/ContactController.swift @@ -2,13 +2,14 @@ import UIKit import Shared import Combine import XXModels +import XXNavigation import DrawerFeature import DependencyInjection import ScrollViewController public final class ContactController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ContactCoordinating private lazy var screenView = ContactView() private lazy var scrollViewController = ScrollViewController() @@ -47,11 +48,14 @@ public final class ContactController: UIViewController { setupBindings() screenView.didTapSend = { [weak self] in - guard let self = self else { return } - self.coordinator.toSingleChat(with: self.viewModel.contact, from: self) + guard let self else { return } + self.navigator.perform(PresentChat( + contact: self.viewModel.contact + )) } screenView.didTapInfo = { [weak self] in - self?.presentInfo( + guard let self else { return } + self.presentInfo( title: Localized.Contact.SendMessage.Info.title, subtitle: Localized.Contact.SendMessage.Info.subtitle, urlString: "https://links.xx.network/cmix" @@ -72,40 +76,54 @@ public final class ContactController: UIViewController { } private func setupBindings() { - screenView.cardComponent.avatarView.editButton + screenView + .cardComponent + .avatarView + .editButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toPhotos(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentPhotoLibrary()) + }.store(in: &cancellables) - viewModel.statePublisher + viewModel + .statePublisher .map(\.photo) .compactMap { $0 } .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.cardComponent.image = $0 } - .store(in: &cancellables) + .sink { [unowned self] in + screenView.cardComponent.image = $0 + }.store(in: &cancellables) - viewModel.statePublisher + viewModel + .statePublisher .map(\.title) .compactMap { $0 } .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.cardComponent.nameLabel.text = $0 } - .store(in: &cancellables) + .sink { [unowned self] in + screenView.cardComponent.nameLabel.text = $0 + }.store(in: &cancellables) - viewModel.popPublisher + viewModel + .popPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in navigationController?.popViewController(animated: true) } - .store(in: &cancellables) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) - viewModel.popToRootPublisher + viewModel + .popToRootPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in navigationController?.popToRootViewController(animated: true) } - .store(in: &cancellables) + .sink { [unowned self] in + navigationController?.popToRootViewController(animated: true) + }.store(in: &cancellables) - viewModel.successPublisher + viewModel + .successPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.updateToSuccess() } - .store(in: &cancellables) + .sink { [unowned self] in + screenView.updateToSuccess() + }.store(in: &cancellables) setupScannedBindings() setupReceivedBindings() @@ -115,17 +133,24 @@ public final class ContactController: UIViewController { } private func setupSuccessBindings() { - screenView.successView.keepAdding + screenView + .successView + .keepAdding .publisher(for: .touchUpInside) - .sink { [unowned self] in navigationController?.popViewController(animated: true) } - .store(in: &cancellables) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) - screenView.successView.sentRequests + screenView + .successView + .sentRequests .publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toRequests(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentRequests()) + }.store(in: &cancellables) - viewModel.statePublisher + viewModel + .statePublisher .map(\.username) .removeDuplicates() .combineLatest( @@ -137,49 +162,52 @@ public final class ContactController: UIViewController { Localized.Contact.email: $0.1, Localized.Contact.phone: $0.2].forEach { pair in guard let value = pair.value else { return } - let attributeView = AttributeComponent() attributeView.set( title: pair.key, value: value ) - screenView.successView.stack.addArrangedSubview(attributeView) } }.store(in: &cancellables) } private func setupScannedBindings() { - screenView.scannedView.add + screenView + .scannedView.add .publisher(for: .touchUpInside) .sink { [unowned self] in - coordinator.toNickname( - from: self, - prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", - viewModel.didTapRequest(with:) - ) + let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" + navigator.perform(PresentNickname(prefilled: nickname) { [weak self] in + guard let self else { return } + self.viewModel.didTapRequest(with: $0) + }) }.store(in: &cancellables) } private func setupReceivedBindings() { - screenView.receivedView.accept + screenView + .receivedView.accept .publisher(for: .touchUpInside) .sink { [unowned self] in - coordinator.toNickname( - from: self, - prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", - viewModel.didTapAccept(_:) - ) + let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" + navigator.perform(PresentNickname(prefilled: nickname) { [weak self] in + guard let self else { return } + self.viewModel.didTapAccept($0) + }) }.store(in: &cancellables) - screenView.receivedView.reject + screenView + .receivedView.reject .publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapReject() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didTapReject() + }.store(in: &cancellables) } private func setupInProgressBindings() { - viewModel.statePublisher + viewModel + .statePublisher .map(\.username) .removeDuplicates() .combineLatest( @@ -202,10 +230,12 @@ public final class ContactController: UIViewController { } }.store(in: &cancellables) - screenView.inProgressView.feedback + screenView + .inProgressView.feedback .button.publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapResend() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didTapResend() + }.store(in: &cancellables) } private func setupConfirmedBindings() { @@ -225,15 +255,16 @@ public final class ContactController: UIViewController { nicknameAttribute.set(title: Localized.Contact.nickname, value: $0.0, style: .requiredEditable) screenView.confirmedView.stackView.insertArrangedSubview(nicknameAttribute, at: 0) - nicknameAttribute.actionButton.publisher(for: .touchUpInside) + nicknameAttribute + .actionButton + .publisher(for: .touchUpInside) .sink { [unowned self] in - coordinator.toNickname( - from: self, - prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", - viewModel.didUpdateNickname(_:) - ) - } - .store(in: &cancellables) + let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" + navigator.perform(PresentNickname(prefilled: nickname, completion: { [weak self] in + guard let self else { return } + self.viewModel.didUpdateNickname($0) + })) + }.store(in: &cancellables) let usernameAttribute = AttributeComponent() usernameAttribute.set(title: Localized.Contact.username, value: $0.1) @@ -262,10 +293,13 @@ public final class ContactController: UIViewController { .store(in: &cancellables) }.store(in: &cancellables) - screenView.confirmedView.clearButton + screenView + .confirmedView + .clearButton .publisher(for: .touchUpInside) - .sink { [unowned self] in presentClearDrawer() } - .store(in: &cancellables) + .sink { [unowned self] in + presentClearDrawer() + }.store(in: &cancellables) } private func presentClearDrawer() { @@ -277,7 +311,28 @@ public final class ContactController: UIViewController { cancelButton.setStyle(.seeThrough) cancelButton.setTitle(Localized.Contact.Clear.cancel, for: .normal) - let drawer = DrawerController([ + clearButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didTapClear() + } + }.store(in: &drawerCancellables) + + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerImage( image: Asset.drawerNegative.image ), @@ -297,27 +352,7 @@ public final class ContactController: UIViewController { spacing: 20.0, views: [clearButton, cancelButton] ) - ]) - - clearButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - self.viewModel.didTapClear() - } - }.store(in: &drawerCancellables) - - cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } } @@ -360,7 +395,17 @@ extension ContactController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController([ + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -377,18 +422,7 @@ extension ContactController { actionButton, FlexibleSpace() ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } private func presentDeleteInfo() { @@ -397,7 +431,18 @@ extension ContactController { style: .red )) - let drawer = DrawerController([ + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didTapDelete() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.Contact.Delete.Drawer.title, @@ -411,18 +456,6 @@ extension ContactController { customAttributes: [.font: Fonts.Mulish.bold.font(size: 16.0)] ), actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - self.viewModel.didTapDelete() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } } diff --git a/Sources/ContactFeature/Controllers/NicknameController.swift b/Sources/ContactFeature/Controllers/NicknameController.swift index 2492083e..509f2cba 100644 --- a/Sources/ContactFeature/Controllers/NicknameController.swift +++ b/Sources/ContactFeature/Controllers/NicknameController.swift @@ -46,7 +46,7 @@ public final class NicknameController: UIViewController { private func setupKeyboard() { keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in - guard let self = self else { return } + guard let self else { return } let inset = self.view.frame.height - self.view.convert(keyboard.frame, from: nil).minY diff --git a/Sources/ContactFeature/Coordinator/ContactCoordinator.swift b/Sources/ContactFeature/Coordinator/ContactCoordinator.swift deleted file mode 100644 index 2b366da6..00000000 --- a/Sources/ContactFeature/Coordinator/ContactCoordinator.swift +++ /dev/null @@ -1,69 +0,0 @@ -import UIKit -import Shared -import XXModels -import ChatFeature -import Presentation - -public protocol ContactCoordinating: AnyObject { - func toPhotos(from: UIViewController) - func toRequests(from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toNickname(from: UIViewController, prefilled: String, _: @escaping StringClosure) -} - -public final class ContactCoordinator: ContactCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter(mode: .replaceBackwards(SingleChatController.self)) - - var requestsFactory: () -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var imagePickerFactory: () -> UIImagePickerController - var nicknameFactory: (String, @escaping StringClosure) -> UIViewController - - public init( - requestsFactory: @escaping () -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - imagePickerFactory: @escaping () -> UIImagePickerController, - nicknameFactory: @escaping (String, @escaping StringClosure) -> UIViewController - ) { - self.requestsFactory = requestsFactory - self.singleChatFactory = singleChatFactory - self.imagePickerFactory = imagePickerFactory - self.nicknameFactory = nicknameFactory - } -} - -public extension ContactCoordinator { - func toPhotos(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = true - modalPresenter.present(screen, from: parent) - } - - func toNickname( - from parent: UIViewController, - prefilled: String, - _ completion: @escaping StringClosure - ) { - let screen = nicknameFactory(prefilled, completion) - bottomPresenter.present(screen, from: parent) - } - - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toSingleChat(with contact: Contact, from parent: UIViewController) { - let screen = singleChatFactory(contact) - replacePresenter.present(screen, from: parent) - } -} diff --git a/Sources/ContactFeature/ViewModels/ContactViewModel.swift b/Sources/ContactFeature/ViewModels/ContactViewModel.swift index 3876c0b9..43e861d7 100644 --- a/Sources/ContactFeature/ViewModels/ContactViewModel.swift +++ b/Sources/ContactFeature/ViewModels/ContactViewModel.swift @@ -105,7 +105,7 @@ final class ContactViewModel { contact.authStatus = .requesting backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.database.saveContact(self.contact) @@ -149,7 +149,7 @@ final class ContactViewModel { contact.authStatus = .requesting backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.database.saveContact(self.contact) @@ -193,7 +193,7 @@ final class ContactViewModel { contact.authStatus = .confirming backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.database.saveContact(self.contact) diff --git a/Sources/ContactListFeature/Controllers/ContactListController.swift b/Sources/ContactListFeature/Controllers/ContactListController.swift index 0b276e5f..a21f9ae9 100644 --- a/Sources/ContactListFeature/Controllers/ContactListController.swift +++ b/Sources/ContactListFeature/Controllers/ContactListController.swift @@ -1,11 +1,12 @@ import UIKit import Shared import Combine +import XXNavigation import DependencyInjection public final class ContactListController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ContactListCoordinating private lazy var screenView = ContactListView() private lazy var tableController = ContactListTableController(viewModel) @@ -65,55 +66,65 @@ public final class ContactListController: UIViewController { navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightStack) - search.snp.makeConstraints { $0.width.equalTo(40) } + search.snp.makeConstraints { + $0.width.equalTo(40) + } } private func setupTableView() { addChild(tableController) screenView.addSubview(tableController.view) - - tableController.view.snp.makeConstraints { make in - make.top.equalTo(screenView.topStackView.snp.bottom) - make.left.bottom.right.equalToSuperview() + tableController.view.snp.makeConstraints { + $0.top.equalTo(screenView.topStackView.snp.bottom) + $0.left.bottom.right.equalToSuperview() } - tableController.didMove(toParent: self) } private func setupBindings() { - tableController.didTap + tableController + .didTap .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSingleChat(with: $0, from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentChat(contact: $0)) + }.store(in: &cancellables) - screenView.requestsButton + screenView + .requestsButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toRequests(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentRequests()) + }.store(in: &cancellables) - screenView.newGroupButton + screenView + .newGroupButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toNewGroup(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentNewGroup()) + }.store(in: &cancellables) - screenView.searchButton + screenView + .searchButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSearch(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentSearch(replacing: false)) + }.store(in: &cancellables) - viewModel.requestCount + viewModel + .requestCount .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.requestsButton.updateNotification($0) } - .store(in: &cancellables) + .sink { [weak screenView] in + screenView?.requestsButton.updateNotification($0) + }.store(in: &cancellables) - viewModel.contacts + viewModel + .contacts .receive(on: DispatchQueue.main) .sink { [unowned self] in screenView.stackView.isHidden = !$0.isEmpty - if $0.isEmpty { screenView.bringSubviewToFront(screenView.stackView) } @@ -121,14 +132,14 @@ public final class ContactListController: UIViewController { } @objc private func didTapSearch() { - coordinator.toSearch(from: self) + navigator.perform(PresentSearch(replacing: false)) } @objc private func didTapScan() { - coordinator.toScan(from: self) + navigator.perform(PresentScan()) } @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) + navigator.perform(PresentMenu(currentItem: .contacts)) } } diff --git a/Sources/ContactListFeature/Controllers/CreateGroupController.swift b/Sources/ContactListFeature/Controllers/CreateGroupController.swift index 8f2b2c93..d772b630 100644 --- a/Sources/ContactListFeature/Controllers/CreateGroupController.swift +++ b/Sources/ContactListFeature/Controllers/CreateGroupController.swift @@ -2,185 +2,192 @@ import UIKit import Shared import Combine import XXModels +import XXNavigation import DependencyInjection public final class CreateGroupController: UIViewController { - @Dependency private var coordinator: ContactListCoordinating - - private lazy var titleLabel = UILabel() - private lazy var createButton = UIButton() - private lazy var screenView = CreateGroupView() - - private var selectedElements = [Contact]() { - didSet { screenView.tableView.reloadData() } - } - private let viewModel = CreateGroupViewModel() - private var cancellables = Set<AnyCancellable>() - private var tableDataSource: UITableViewDiffableDataSource<SectionId, Contact>! - private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>! - - private var count = 0 { - didSet { - createButton.isEnabled = count >= 2 && count <= 10 - - let text = Localized.CreateGroup.title("\(count)") - let attString = NSMutableAttributedString(string: text) - attString.addAttribute(.font, value: Fonts.Mulish.semiBold.font(size: 18.0) as Any) - attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) - attString.addAttributes(attributes: [ - .foregroundColor: Asset.neutralDisabled.color, - .font: Fonts.Mulish.regular.font(size: 14.0) as Any - ], betweenCharacters: "#") - - titleLabel.attributedText = attString - titleLabel.sizeToFit() - } - } - - public override func loadView() { - view = screenView + @Dependency var navigator: Navigator + + private lazy var titleLabel = UILabel() + private lazy var createButton = UIButton() + private lazy var screenView = CreateGroupView() + + private var selectedElements = [Contact]() { + didSet { screenView.tableView.reloadData() } + } + private let viewModel = CreateGroupViewModel() + private var cancellables = Set<AnyCancellable>() + private var tableDataSource: UITableViewDiffableDataSource<SectionId, Contact>! + private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>! + + private var count = 0 { + didSet { + createButton.isEnabled = count >= 2 && count <= 10 + + let text = Localized.CreateGroup.title("\(count)") + let attString = NSMutableAttributedString(string: text) + attString.addAttribute(.font, value: Fonts.Mulish.semiBold.font(size: 18.0) as Any) + attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) + attString.addAttributes(attributes: [ + .foregroundColor: Asset.neutralDisabled.color, + .font: Fonts.Mulish.regular.font(size: 14.0) as Any + ], betweenCharacters: "#") + + titleLabel.attributedText = attString + titleLabel.sizeToFit() } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupTableAndCollection() - setupBindings() - - count = 0 + } + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupTableAndCollection() + setupBindings() + + count = 0 + } + + private func setupNavigationBar() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: titleLabel) + navigationItem.leftItemsSupplementBackButton = true + + createButton.setTitle(Localized.CreateGroup.create, for: .normal) + createButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + createButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 16.0) + createButton.setTitleColor(Asset.neutralDisabled.color, for: .disabled) + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: createButton) + } + + private func setupTableAndCollection() { + screenView.tableView.rowHeight = 64.0 + screenView.tableView.register(AvatarCell.self) + screenView.collectionView.register(CreateGroupCollectionCell.self) + + collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( + collectionView: screenView.collectionView + ) { [weak viewModel] collectionView, indexPath, contact in + let cell: CreateGroupCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + let title = (contact.nickname ?? contact.username) ?? "" + cell.setup(title: title, image: contact.photo) + cell.didTapRemove = { viewModel?.didSelect(contact: contact) } + + return cell } - private func setupNavigationBar() { - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: titleLabel) - navigationItem.leftItemsSupplementBackButton = true - - createButton.setTitle(Localized.CreateGroup.create, for: .normal) - createButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - createButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 16.0) - createButton.setTitleColor(Asset.neutralDisabled.color, for: .disabled) - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: createButton) - } - - private func setupTableAndCollection() { - screenView.tableView.rowHeight = 64.0 - screenView.tableView.register(AvatarCell.self) - screenView.collectionView.register(CreateGroupCollectionCell.self) - - collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( - collectionView: screenView.collectionView - ) { [weak viewModel] collectionView, indexPath, contact in - let cell: CreateGroupCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - let title = (contact.nickname ?? contact.username) ?? "" - cell.setup(title: title, image: contact.photo) - cell.didTapRemove = { viewModel?.didSelect(contact: contact) } - - return cell - } - - tableDataSource = DiffEditableDataSource<SectionId, Contact>( - tableView: screenView.tableView - ) { [weak self] tableView, indexPath, contact in - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) - let title = (contact.nickname ?? contact.username) ?? "" - - cell.setup(title: title, image: contact.photo) + tableDataSource = DiffEditableDataSource<SectionId, Contact>( + tableView: screenView.tableView + ) { [weak self] tableView, indexPath, contact in + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) + let title = (contact.nickname ?? contact.username) ?? "" - if let selectedElements = self?.selectedElements, selectedElements.contains(contact) { - tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) - } else { - tableView.deselectRow(at: indexPath, animated: true) - } + cell.setup(title: title, image: contact.photo) - return cell - } + if let selectedElements = self?.selectedElements, selectedElements.contains(contact) { + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) + } else { + tableView.deselectRow(at: indexPath, animated: true) + } - screenView.tableView.delegate = self - screenView.tableView.dataSource = tableDataSource - screenView.collectionView.dataSource = collectionDataSource + return cell } - private func setupBindings() { - let selected = viewModel.selected.share() - - selected - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.collectionView.isHidden = $0.count < 1 - - count = $0.count - selectedElements = $0 - }.store(in: &cancellables) - - selected.map { selectedContacts in - var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() - let sections = [SectionId()] - snapshot.appendSections(sections) - sections.forEach { section in snapshot.appendItems(selectedContacts, toSection: section) } - return snapshot - } - .receive(on: DispatchQueue.main) - .sink { [unowned self] in collectionDataSource.apply($0) } - .store(in: &cancellables) - - viewModel.contacts - .map { contacts in - var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() - let sections = [SectionId()] - snapshot.appendSections(sections) - sections.forEach { section in snapshot.appendItems(contacts, toSection: section) } - return snapshot - } - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - tableDataSource.apply($0, animatingDifferences: tableDataSource.snapshot().numberOfItems > 0) - }.store(in: &cancellables) - - screenView.searchComponent.textPublisher - .removeDuplicates() - .sink { [unowned self] in viewModel.filter($0) } - .store(in: &cancellables) - - viewModel.info - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toGroupChat(with: $0, from: self) } - .store(in: &cancellables) - - createButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toGroupDrawer( - with: count, - from: self, { (name, welcome) in - self.viewModel.create( - name: name, - welcome: welcome, - members: self.selectedElements - ) - } - ) - }.store(in: &cancellables) + screenView.tableView.delegate = self + screenView.tableView.dataSource = tableDataSource + screenView.collectionView.dataSource = collectionDataSource + } + + private func setupBindings() { + let selected = viewModel.selected.share() + + selected + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.collectionView.isHidden = $0.count < 1 + + count = $0.count + selectedElements = $0 + }.store(in: &cancellables) + + selected.map { selectedContacts in + var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() + let sections = [SectionId()] + snapshot.appendSections(sections) + sections.forEach { section in snapshot.appendItems(selectedContacts, toSection: section) } + return snapshot } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in collectionDataSource.apply($0) } + .store(in: &cancellables) + + viewModel + .contacts + .map { contacts in + var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() + let sections = [SectionId()] + snapshot.appendSections(sections) + sections.forEach { section in snapshot.appendItems(contacts, toSection: section) } + return snapshot + } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + tableDataSource.apply($0, animatingDifferences: tableDataSource.snapshot().numberOfItems > 0) + }.store(in: &cancellables) + + screenView + .searchComponent + .textPublisher + .removeDuplicates() + .sink { [unowned self] in + viewModel.filter($0) + }.store(in: &cancellables) + + viewModel + .info + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentGroupChat(model: $0)) + }.store(in: &cancellables) + + createButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in +// coordinator.toGroupDrawer( +// with: count, +// from: self, { (name, welcome) in +// self.viewModel.create( +// name: name, +// welcome: welcome, +// members: self.selectedElements +// ) +// } +// ) + }.store(in: &cancellables) + } } extension CreateGroupController: UITableViewDelegate { - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let contact = tableDataSource.itemIdentifier(for: indexPath) { - viewModel.didSelect(contact: contact) - } + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let contact = tableDataSource.itemIdentifier(for: indexPath) { + viewModel.didSelect(contact: contact) } + } - public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { - if let contact = tableDataSource.itemIdentifier(for: indexPath) { - viewModel.didSelect(contact: contact) - } + public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + if let contact = tableDataSource.itemIdentifier(for: indexPath) { + viewModel.didSelect(contact: contact) } + } } diff --git a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift b/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift deleted file mode 100644 index 650f031d..00000000 --- a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift +++ /dev/null @@ -1,128 +0,0 @@ -import UIKit -import Shared -import XXModels -import MenuFeature -import ChatFeature -import Presentation -import ContactFeature -import ScrollViewController - -public protocol ContactListCoordinating { - func toScan(from: UIViewController) - func toSearch(from: UIViewController) - func toRequests(from: UIViewController) - func toNewGroup(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toGroupChat(with: GroupInfo, from: UIViewController) - func toGroupDrawer(with: Int, from: UIViewController, _: @escaping (String, String?) -> Void) -} - -public struct ContactListCoordinator: ContactListCoordinating { - var pushPresenter: Presenting = PushPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - var replacePresenter: Presenting = ReplacePresenter(mode: .replaceLast) - - var scanFactory: () -> UIViewController - var searchFactory: (String?) -> UIViewController - var newGroupFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupInfo) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var groupDrawerFactory: (Int, @escaping (String, String?) -> Void) -> UIViewController - - public init( - scanFactory: @escaping () -> UIViewController, - searchFactory: @escaping (String?) -> UIViewController, - newGroupFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupInfo) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, - groupDrawerFactory: @escaping (Int, @escaping (String, String?) -> Void) -> UIViewController - ) { - self.scanFactory = scanFactory - self.searchFactory = searchFactory - self.contactFactory = contactFactory - self.newGroupFactory = newGroupFactory - self.requestsFactory = requestsFactory - self.sideMenuFactory = sideMenuFactory - self.groupChatFactory = groupChatFactory - self.singleChatFactory = singleChatFactory - self.groupDrawerFactory = groupDrawerFactory - } -} - -public extension ContactListCoordinator { - func toGroupDrawer( - with count: Int, - from parent: UIViewController, - _ completion: @escaping (String, String?) -> Void - ) { - let screen = ScrollViewController.embedding(groupDrawerFactory(count, completion)) - fullscreenPresenter.present(screen, from: parent) - } - - func toSingleChat( - with contact: Contact, - from parent: UIViewController - ) { - let screen = singleChatFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toScan(from parent: UIViewController) { - let screen = scanFactory() - pushPresenter.present(screen, from: parent) - } - - func toSearch(from parent: UIViewController) { - let screen = searchFactory(nil) - pushPresenter.present(screen, from: parent) - } - - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - pushPresenter.present(screen, from: parent) - } - - func toNewGroup(from parent: UIViewController) { - let screen = newGroupFactory() - pushPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toGroupChat(with info: GroupInfo, from parent: UIViewController) { - let screen = groupChatFactory(info) - replacePresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.contacts, parent) - sidePresenter.present(screen, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift index 724aa19f..d24fd2e9 100644 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift @@ -87,7 +87,7 @@ final class CreateGroupViewModel { hudController.show() backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { let report = try self.groupManager.makeGroup( diff --git a/Sources/Countries/Country.swift b/Sources/Countries/Country.swift deleted file mode 100644 index acb0bae1..00000000 --- a/Sources/Countries/Country.swift +++ /dev/null @@ -1,45 +0,0 @@ -import os -import Foundation - -public struct Country { - public var name: String - public var code: String - public var flag: String - public var regex: String - public var prefix: String - public var example: String - public var prefixWithFlag: String { "\(flag) \(prefix)" } - - public static func fromMyPhone() -> Self { - let all = all() - - guard let country = all.filter({ $0.code == Locale.current.regionCode }).first else { - return all.filter { $0.code == "US" }.first! - } - - return country - } - - public static func all() -> [Self] { - guard let url = Bundle.module.url(forResource: "country_codes", withExtension: "json"), - let data = try? Data(contentsOf: url), - let countries = try? JSONDecoder().decode([Country].self, from: data) else { - fatalError("Can't handle country codes json") - } - - return countries - } - - public static func findFrom(_ number: String) -> Self { - all().first { country in - let start = number.index(number.startIndex, offsetBy: number.count - 2) - let end = number.index(start, offsetBy: number.count - (number.count - 2)) - - return country.code == String(number[start ..< end]) - }! - } -} - -extension Country: Hashable {} -extension Country: Equatable {} -extension Country: Decodable {} diff --git a/Sources/Countries/CountryListController.swift b/Sources/Countries/CountryListController.swift index 5956946f..4a526743 100644 --- a/Sources/Countries/CountryListController.swift +++ b/Sources/Countries/CountryListController.swift @@ -1,21 +1,22 @@ -import os import UIKit import Shared import Combine +import XXNavigation import DependencyInjection -public final class CountryListController: UIViewController { +public final class CountryListController: UIViewController, UITableViewDelegate { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist private lazy var screenView = CountryListView() - private var didChoose: ((Country) -> Void)! + private let completion: (Country) -> Void private let viewModel = CountryListViewModel() private var cancellables = Set<AnyCancellable>() private var dataSource: UITableViewDiffableDataSource<SectionId, Country>! - public init(_ didChoose: @escaping (Country) -> Void) { - self.didChoose = didChoose + public init(_ completion: @escaping (Country) -> Void) { + self.completion = completion super.init(nibName: nil, bundle: nil) } @@ -25,7 +26,6 @@ public final class CountryListController: UIViewController { super.viewWillAppear(animated) navigationItem.backButtonTitle = "" barStylist.styleSubject.send(.darkContent) - navigationController?.navigationBar.customize( backgroundColor: Asset.neutralWhite.color, shadowColor: Asset.neutralDisabled.color @@ -38,28 +38,16 @@ public final class CountryListController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - screenView.tableView.register(CountryListCell.self) - setupNavigationBar() - setupBindings() - - viewModel.fetchCountryList() - } - - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.Countries.title - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) + screenView + .tableView + .register(CountryListCell.self) - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupBindings() { - viewModel.countries + viewModel + .countries .receive(on: DispatchQueue.main) - .sink { [unowned self] in dataSource.apply($0, animatingDifferences: false) } - .store(in: &cancellables) + .sink { [unowned self] in + dataSource.apply($0, animatingDifferences: false) + }.store(in: &cancellables) dataSource = UITableViewDiffableDataSource<SectionId, Country>( tableView: screenView.tableView @@ -71,22 +59,23 @@ public final class CountryListController: UIViewController { return cell } - screenView.searchComponent + screenView + .searchComponent .textPublisher .removeDuplicates() - .sink { [unowned self] in viewModel.didSearchFor($0) } - .store(in: &cancellables) + .sink { [unowned self] in + viewModel.didSearchFor($0) + }.store(in: &cancellables) screenView.tableView.delegate = self screenView.tableView.dataSource = dataSource + viewModel.fetchCountryList() } public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let country = dataSource.itemIdentifier(for: indexPath) { - didChoose(country) - navigationController?.popViewController(animated: true) + completion(country) + navigator.perform(DismissModal(from: self)) } } } - -extension CountryListController: UITableViewDelegate {} diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index 2eb224ca..8207138c 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -1,7 +1,6 @@ import UIKit import Shared import Combine -import Navigation import PushFeature import XXNavigation import DrawerFeature @@ -10,8 +9,9 @@ import DependencyInjection public final class LaunchController: UIViewController { @Dependency var navigator: Navigator - private let viewModel = LaunchViewModel() private lazy var screenView = LaunchView() + + private let viewModel = LaunchViewModel() public var pendingPushRoute: PushRouter.Route? private var cancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>() @@ -123,6 +123,7 @@ public final class LaunchController: UIViewController { .receive(on: DispatchQueue.main) .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() self.viewModel.didRefuseUpdating() } }.store(in: &drawerCancellables) diff --git a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift index 2971fe40..364be5cf 100644 --- a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift +++ b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift @@ -440,6 +440,5 @@ extension LaunchViewModel { try? messenger.resumeBackup() } // TODO: Biometric auth - } } diff --git a/Sources/MenuFeature/Controllers/MenuController.swift b/Sources/MenuFeature/Controllers/MenuController.swift index dbd58724..b14abe7b 100644 --- a/Sources/MenuFeature/Controllers/MenuController.swift +++ b/Sources/MenuFeature/Controllers/MenuController.swift @@ -1,39 +1,23 @@ import UIKit import Shared import Combine +import XXNavigation import DrawerFeature import DependencyInjection -public enum MenuItem { - case join - case scan - case chats - case share - case profile - case contacts - case requests - case settings - case dashboard -} - public final class MenuController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: MenuCoordinating private lazy var screenView = MenuView() - private let previousItem: MenuItem + private let currentItem: MenuItem private let viewModel = MenuViewModel() - private let previousController: UIViewController private var cancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>() - public init( - _ previousItem: MenuItem, - _ previousController: UIViewController - ) { - self.previousItem = previousItem - self.previousController = previousController + public init(_ currentItem: MenuItem) { + self.currentItem = currentItem super.init(nibName: nil, bundle: nil) } @@ -45,13 +29,11 @@ public final class MenuController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - screenView.headerView.set( username: viewModel.username, image: viewModel.avatar ) - - screenView.select(item: previousItem) + screenView.select(item: currentItem) screenView.xxdkVersionLabel.text = "XXDK \(viewModel.xxdk)" screenView.buildLabel.text = Localized.Menu.build(viewModel.build) screenView.versionLabel.text = Localized.Menu.version(viewModel.version) @@ -69,72 +51,81 @@ public final class MenuController: UIViewController { } private func setupBindings() { - screenView.headerView.scanButton + screenView + .headerView + .scanButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .scan else { return } - self.coordinator.toFlow(.scan, from: self.previousController) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .scan else { return } + self.navigator.perform(PresentScan()) } }.store(in: &cancellables) - screenView.headerView.nameButton + screenView + .headerView + .nameButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .profile else { return } - self.coordinator.toFlow(.profile, from: self.previousController) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .profile else { return } + self.navigator.perform(PresentProfile()) } }.store(in: &cancellables) - screenView.scanButton + screenView + .scanButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .scan else { return } - self.coordinator.toFlow(.scan, from: self.previousController) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .scan else { return } + self.navigator.perform(PresentScan()) } }.store(in: &cancellables) - screenView.chatsButton + screenView + .chatsButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .chats else { return } - self.coordinator.toFlow(.chats, from: self.previousController) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .chats else { return } + self.navigator.perform(PresentChatList()) } }.store(in: &cancellables) - screenView.contactsButton + screenView + .contactsButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .contacts else { return } - self.coordinator.toFlow(.contacts, from: self.previousController) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .contacts else { return } + self.navigator.perform(PresentContactList()) } }.store(in: &cancellables) - screenView.settingsButton + screenView + .settingsButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .settings else { return } - self.coordinator.toFlow(.settings, from: self.previousController) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .settings else { return } + self.navigator.perform(PresentSettings()) } }.store(in: &cancellables) - screenView.dashboardButton + screenView + .dashboardButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .dashboard else { return } + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .dashboard else { return } self.presentDrawer( title: Localized.ChatList.Dashboard.title, subtitle: Localized.ChatList.Dashboard.subtitle, @@ -145,22 +136,24 @@ public final class MenuController: UIViewController { } }.store(in: &cancellables) - screenView.requestsButton + screenView + .requestsButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .requests else { return } - self.coordinator.toFlow(.requests, from: self.previousController) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .requests else { return } + self.navigator.perform(PresentRequests()) } }.store(in: &cancellables) - screenView.joinButton + screenView + .joinButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .join else { return } + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .join else { return } self.presentDrawer( title: Localized.ChatList.Join.title, subtitle: Localized.ChatList.Join.subtitle, @@ -171,23 +164,25 @@ public final class MenuController: UIViewController { } }.store(in: &cancellables) - screenView.shareButton + screenView + .shareButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .share else { return } - self.coordinator.toActivityController( - with: [Localized.Menu.shareContent(self.viewModel.referralDeeplink)], - from: self.previousController - ) + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .share else { return } + self.navigator.perform(PresentActivitySheet(items: [ + Localized.Menu.shareContent(self.viewModel.referralDeeplink) + ])) } }.store(in: &cancellables) - viewModel.requestCount + viewModel + .requestCount .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.requestsButton.updateNotification($0) } - .store(in: &cancellables) + .sink { [weak screenView] in + screenView?.requestsButton.updateNotification($0) + }.store(in: &cancellables) } private func presentDrawer( @@ -201,7 +196,18 @@ public final class MenuController: UIViewController { style: .red )) - let drawer = DrawerController([ + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + action() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -218,17 +224,6 @@ public final class MenuController: UIViewController { spacingAfter: 39 ), actionButton - ]) - - actionButton.action.receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - action() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: previousController) + ])) } } diff --git a/Sources/MenuFeature/Coordinator/MenuCoordinator.swift b/Sources/MenuFeature/Coordinator/MenuCoordinator.swift deleted file mode 100644 index 3e7d20cc..00000000 --- a/Sources/MenuFeature/Coordinator/MenuCoordinator.swift +++ /dev/null @@ -1,73 +0,0 @@ -import UIKit -import Presentation - -public protocol MenuCoordinating { - func toFlow(_ item: MenuItem, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toActivityController(with: [Any], from: UIViewController) -} - -public struct MenuCoordinator: MenuCoordinating { - var modalPresenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() - - var scanFactory: () -> UIViewController - var chatsFactory: () -> UIViewController - var profileFactory: () -> UIViewController - var settingsFactory: () -> UIViewController - var contactsFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var activityControllerFactory: ([Any]) -> UIViewController - = { UIActivityViewController(activityItems: $0, applicationActivities: nil) } - - public init( - scanFactory: @escaping () -> UIViewController, - chatsFactory: @escaping () -> UIViewController, - profileFactory: @escaping () -> UIViewController, - settingsFactory: @escaping () -> UIViewController, - contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController - ) { - self.scanFactory = scanFactory - self.chatsFactory = chatsFactory - self.profileFactory = profileFactory - self.settingsFactory = settingsFactory - self.contactsFactory = contactsFactory - self.requestsFactory = requestsFactory - } -} - -public extension MenuCoordinator { - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toFlow(_ item: MenuItem, from parent: UIViewController) { - let controller: UIViewController - - switch item { - case .scan: - controller = scanFactory() - case .chats: - controller = chatsFactory() - case .profile: - controller = profileFactory() - case .contacts: - controller = contactsFactory() - case .requests: - controller = requestsFactory() - case .settings: - controller = settingsFactory() - default: - fatalError() - } - - replacePresenter.present(controller, from: parent) - } - - func toActivityController(with items: [Any], from parent: UIViewController) { - let screen = activityControllerFactory(items) - modalPresenter.present(screen, from: parent) - } -} diff --git a/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift index 82044ac6..4fd0f373 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift @@ -1,7 +1,6 @@ import UIKit import Shared import Combine -import Navigation import XXNavigation import DrawerFeature import DependencyInjection @@ -142,9 +141,20 @@ public final class OnboardingCodeController: UIViewController { private func presentInfo(title: String, subtitle: String) { let actionButton = CapsuleButton() - actionButton.set(style: .seeThrough, title: Localized.Settings.InfoDrawer.action) + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - let drawer = DrawerController([ + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -164,17 +174,6 @@ public final class OnboardingCodeController: UIViewController { actionButton, FlexibleSpace() ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - -// navigator.perform(PresentDrawer()) + ])) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift index 3844b24c..0c52a7a1 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift @@ -1,7 +1,6 @@ import UIKit import Shared import Combine -import Navigation import XXNavigation import DrawerFeature import DependencyInjection @@ -20,13 +19,13 @@ public final class OnboardingEmailController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + navigationItem.backButtonTitle = " " barStylist.styleSubject.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } public override func viewDidLoad() { super.viewDidLoad() - navigationItem.backButtonTitle = " " setupScrollView() setupBindings() screenView.didTapInfo = { [weak self] in @@ -114,43 +113,32 @@ public final class OnboardingEmailController: UIViewController { style: .seeThrough, title: Localized.Settings.InfoDrawer.action ) - -// navigator.perform(PresentDrawer([ -// DrawerText( -// font: Fonts.Mulish.bold.font(size: 26.0), -// text: title, -// color: Asset.neutralActive.color, -// alignment: .left, -// spacingAfter: 19 -// ), -// DrawerLinkText( -// text: subtitle, -// urlString: urlString, -// spacingAfter: 37 -// ), -// DrawerStack(views: [ -// actionButton, -// FlexibleSpace() -// ]) -// ])) - - actionButton.publisher(for: .touchUpInside) + actionButton + .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { - // drawer.dismiss(animated: true) { [weak self] in - // guard let self = self else { return } - // self.drawerCancellables.removeAll() - // } + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ])) } } - - // coordinator.toEmailConfirmation(with: $0, from: self) { controller in - // let successModel = OnboardingSuccessModel( - // title: Localized.Onboarding.Success.Email.title, - // subtitle: nil, - // nextController: self.coordinator.toPhone(from:) - // ) - // - // self.coordinator.toSuccess(with: successModel, from: controller) - // } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift index 1e647737..62c8d846 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift @@ -1,7 +1,6 @@ import UIKit import Shared import Combine -import Navigation import XXNavigation import DrawerFeature import DependencyInjection @@ -20,13 +19,13 @@ public final class OnboardingPhoneController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" barStylist.styleSubject.send(.darkContent) navigationController?.navigationBar.customize(translucent: true) } public override func viewDidLoad() { super.viewDidLoad() - navigationItem.backButtonTitle = " " setupScrollView() setupBindings() screenView.didTapInfo = { [weak self] in @@ -93,7 +92,11 @@ public final class OnboardingPhoneController: UIViewController { .codePublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentCountryList()) + navigator.perform(PresentCountryList(completion: { [weak self] in + guard let self else { return } + self.navigator.perform(DismissModal(from: self)) + self.viewModel.didChooseCountry($0) + })) }.store(in: &cancellables) viewModel @@ -132,8 +135,16 @@ public final class OnboardingPhoneController: UIViewController { style: .seeThrough, title: Localized.Settings.InfoDrawer.action ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - let drawer = DrawerController([ + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -150,27 +161,6 @@ public final class OnboardingPhoneController: UIViewController { actionButton, FlexibleSpace() ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - -// navigator.perform(PresentDrawer()) + ])) } } - -// coordinator.toPhoneConfirmation(with: $0, from: self) { controller in -// let successModel = OnboardingSuccessModel( -// title: Localized.Onboarding.Success.Phone.title, -// subtitle: nil, -// nextController: self.coordinator.toChats(from:) -// ) -// -// self.coordinator.toSuccess(with: successModel, from: controller) -// } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift index ac5df16e..c6307fc9 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift @@ -1,6 +1,5 @@ import UIKit import Combine -import Navigation import XXNavigation import DependencyInjection diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index 7edb740b..ef80c659 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -1,7 +1,6 @@ import UIKit import Shared import Combine -import Navigation import XXNavigation import DrawerFeature import DependencyInjection @@ -115,8 +114,16 @@ public final class OnboardingUsernameController: UIViewController { style: .seeThrough, title: Localized.Settings.InfoDrawer.action ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - let drawer = DrawerController([ + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -133,17 +140,6 @@ public final class OnboardingUsernameController: UIViewController { actionButton, FlexibleSpace() ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - -// navigator.perform(PresentDrawer()) + ])) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift index 69033a19..0aaa842a 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift @@ -2,7 +2,6 @@ import UIKit import Shared import Combine import Defaults -import Navigation import XXNavigation import DrawerFeature import DependencyInjection @@ -29,12 +28,26 @@ public final class OnboardingWelcomeController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - setupBindings() + screenView.setupTitle( + Localized.Onboarding.Welcome.title(username) + ) + screenView + .continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentOnboardingEmail()) + }.store(in: &cancellables) - screenView.setupTitle(Localized.Onboarding.Welcome.title(username)) + screenView + .skipButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentChatList()) + }.store(in: &cancellables) screenView.didTapInfo = { [weak self] in - self?.presentInfo( + guard let self else { return } + self.presentInfo( title: Localized.Onboarding.Welcome.Info.title, subtitle: Localized.Onboarding.Welcome.Info.subtitle, urlString: "https://links.xx.network/ud" @@ -42,57 +55,42 @@ public final class OnboardingWelcomeController: UIViewController { } } - private func setupBindings() { - screenView.continueButton.publisher(for: .touchUpInside) - .sink { [unowned self] in - navigator.perform(PresentOnboardingEmail()) - }.store(in: &cancellables) - - screenView.skipButton.publisher(for: .touchUpInside) - .sink { [unowned self] in - navigator.perform(PresentChatList()) - }.store(in: &cancellables) - } - private func presentInfo( title: String, subtitle: String, urlString: String = "" ) { -// let actionButton = CapsuleButton() -// actionButton.set( -// style: .seeThrough, -// title: Localized.Settings.InfoDrawer.action -// ) -// -// let drawer = DrawerController([ -// DrawerText( -// font: Fonts.Mulish.bold.font(size: 26.0), -// text: title, -// color: Asset.neutralActive.color, -// alignment: .left, -// spacingAfter: 19 -// ), -// DrawerLinkText( -// text: subtitle, -// urlString: urlString, -// spacingAfter: 37 -// ), -// DrawerStack(views: [ -// actionButton, -// FlexibleSpace() -// ]) -// ]) -// -// actionButton.publisher(for: .touchUpInside) -// .receive(on: DispatchQueue.main) -// .sink { -// drawer.dismiss(animated: true) { [weak self] in -// guard let self = self else { return } -// self.drawerCancellables.removeAll() -// } -// }.store(in: &drawerCancellables) -// -// navigator.perform(PresentDrawer()) + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ])) } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift index 47fde15f..98826365 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift @@ -52,7 +52,7 @@ final class OnboardingCodeViewModel { guard stateSubject.value.resendDebouncer == 0 else { return } stateSubject.value.resendDebouncer = 60 timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in - guard let self = self, self.stateSubject.value.resendDebouncer > 0 else { + guard let self, self.stateSubject.value.resendDebouncer > 0 else { $0.invalidate() return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift index ad07d4a7..29c0f4b2 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift @@ -36,7 +36,7 @@ final class OnboardingEmailViewModel { func didTapNext() { hudController.show() scheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( .init(type: .email, value: self.stateSubject.value.input) diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift index 435d12a9..4f66f0bb 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift @@ -44,7 +44,7 @@ final class OnboardingPhoneViewModel { func didTapNext() { hudController.show() scheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } let content = "\(self.stateSubject.value.input)\(self.stateSubject.value.country.code)" do { let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( diff --git a/Sources/Permissions/RequestPermissionController.swift b/Sources/Permissions/RequestPermissionController.swift index d424df15..cde2e784 100644 --- a/Sources/Permissions/RequestPermissionController.swift +++ b/Sources/Permissions/RequestPermissionController.swift @@ -1,43 +1,42 @@ import UIKit import Shared import Combine +import XXNavigation import DependencyInjection -public enum PermissionType { - case camera - case library - case microphone -} - public final class RequestPermissionController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist @Dependency var permissions: PermissionHandling private lazy var screenView = RequestPermissionView() - private var type: PermissionType! + private let permissionType: PermissionType private var cancellables = Set<AnyCancellable>() public override func loadView() { view = screenView } + public init(_ permissionType: PermissionType) { + self.permissionType = permissionType + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.backButtonTitle = "" barStylist.styleSubject.send(.darkContent) - navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) } public override func viewDidLoad() { super.viewDidLoad() - setupBindings() - } - public func setup(type: PermissionType) { - self.type = type - - switch type { + switch permissionType { case .camera: screenView.setup( title: Localized.Chat.Actions.Permission.Camera.title, @@ -57,43 +56,44 @@ public final class RequestPermissionController: UIViewController { image: Asset.permissionMicrophone.image ) } - } - private func setupBindings() { - screenView.notNowButton + screenView + .notNowButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.navigationController?.popViewController(animated: true) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) }.store(in: &cancellables) - screenView.continueButton + screenView + .continueButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - switch type { + switch permissionType { case .camera: permissions.requestCamera { [weak self] _ in - DispatchQueue.main.async { - self?.navigationController?.popViewController(animated: true) - } + guard let self else { return } + self.shouldDismissModal() } case .library: permissions.requestPhotos { [weak self] _ in - DispatchQueue.main.async { - self?.navigationController?.popViewController(animated: true) - } + guard let self else { return } + self.shouldDismissModal() } case .microphone: permissions.requestMicrophone { [weak self] _ in - DispatchQueue.main.async { - self?.navigationController?.popViewController(animated: true) - } + guard let self else { return } + self.shouldDismissModal() } - case .none: - break } }.store(in: &cancellables) } + private func shouldDismissModal() { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.navigator.perform(DismissModal(from: self)) + } + } } diff --git a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift index 60036d37..dedb64d4 100644 --- a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift @@ -1,7 +1,6 @@ import UIKit import Shared import Combine -import Navigation import XXNavigation import DependencyInjection import ScrollViewController @@ -18,13 +17,6 @@ public final class ProfileCodeController: UIViewController { private let viewModel: ProfileCodeViewModel private var cancellables = Set<AnyCancellable>() - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - public init( _ isEmail: Bool, _ content: String, @@ -42,38 +34,50 @@ public final class ProfileCodeController: UIViewController { required init?(coder: NSCoder) { nil } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + public override func viewDidLoad() { super.viewDidLoad() setupScrollView() setupBindings() -// var content: String! -// -// if confirmation.isEmail { -// content = confirmation.content -// } else { -// let country = Country.findFrom(confirmation.content) -// content = "\(country.prefix)\(confirmation.content.dropLast(2))" -// } -// -// screenView.set(content, isEmail: confirmation.isEmail) + if isEmail { + screenView.set(content, isEmail: true) + } else { + let country = Country.findFrom(content) + screenView.set( + "\(country.prefix)\(content.dropLast(2))", + isEmail: false + ) + } } private func setupScrollView() { addChild(scrollViewController) view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() + } scrollViewController.didMove(toParent: self) scrollViewController.contentView = screenView scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color } private func setupBindings() { - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) - viewModel.statePublisher + viewModel + .statePublisher .map(\.status) .removeDuplicates() .receive(on: DispatchQueue.main) @@ -86,29 +90,46 @@ public final class ProfileCodeController: UIViewController { } }.store(in: &cancellables) - screenView.saveButton + screenView + .saveButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) - viewModel.statePublisher + viewModel + .statePublisher .map(\.resendDebouncer) .receive(on: DispatchQueue.main) .sink { [unowned self] in screenView.resendButton.isEnabled = $0 == 0 - if $0 == 0 { - screenView.resendButton.setTitle(Localized.Profile.Code.resend(""), for: .normal) + screenView.resendButton.setTitle( + Localized.Profile.Code.resend(""), for: .normal + ) } else { - screenView.resendButton.setTitle(Localized.Profile.Code.resend("(\($0))"), for: .disabled) + screenView.resendButton.setTitle( + Localized.Profile.Code.resend("(\($0))"), for: .disabled + ) } }.store(in: &cancellables) - screenView.resendButton + screenView + .resendButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapResend() } - .store(in: &cancellables) + .sink { [unowned self] in + viewModel.didTapResend() + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.didConfirm) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let navigationController, $0 == true else { return } + navigator.perform(PopToRoot(on: navigationController)) + }.store(in: &cancellables) } } diff --git a/Sources/ProfileFeature/Controllers/ProfileController.swift b/Sources/ProfileFeature/Controllers/ProfileController.swift index 58a74cd0..49ac0ea4 100644 --- a/Sources/ProfileFeature/Controllers/ProfileController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileController.swift @@ -1,12 +1,13 @@ import UIKit import Shared import Combine +import XXNavigation import DrawerFeature import DependencyInjection public final class ProfileController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ProfileCoordinating private lazy var screenView = ProfileView() @@ -64,7 +65,7 @@ public final class ProfileController: UIViewController { self.viewModel.didTapDelete(isEmail: true) } } else { - coordinator.toEmail(from: self) + navigator.perform(PresentProfileEmail()) } }.store(in: &cancellables) @@ -86,17 +87,22 @@ public final class ProfileController: UIViewController { self.viewModel.didTapDelete(isEmail: false) } } else { - coordinator.toPhone(from: self) + navigator.perform(PresentProfilePhone()) } }.store(in: &cancellables) - screenView.cardComponent.avatarView.editButton + screenView + .cardComponent + .avatarView + .editButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didRequestLibraryAccess() } - .store(in: &cancellables) + .sink { [unowned self] in + viewModel.didRequestLibraryAccess() + }.store(in: &cancellables) - viewModel.navigation + viewModel + .navigation .receive(on: DispatchQueue.main) .removeDuplicates() .sink { [unowned self] in @@ -106,38 +112,43 @@ public final class ProfileController: UIViewController { title: Localized.Profile.Photo.title, subtitle: Localized.Profile.Photo.subtitle, actionTitle: Localized.Profile.Photo.continue) { - self.coordinator.toPhotos(from: self) - } + self.navigator.perform(PresentPhotoLibrary()) + } case .libraryPermission: - coordinator.toPermission(type: .library, from: self) + self.navigator.perform(PresentPermissionRequest(type: .library)) case .none: break } - viewModel.didNavigateSomewhere() }.store(in: &cancellables) - viewModel.state + viewModel + .state .map(\.email) .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.emailView.set(value: $0) } - .store(in: &cancellables) + .sink { [unowned self] in + screenView.emailView.set(value: $0) + }.store(in: &cancellables) - viewModel.state + viewModel + .state .map(\.phone) .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.phoneView.set(value: $0) } - .store(in: &cancellables) + .sink { [unowned self] in + screenView.phoneView.set(value: $0) + }.store(in: &cancellables) - viewModel.state + viewModel + .state .map(\.photo) .compactMap { $0 } .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.cardComponent.image = $0 } - .store(in: &cancellables) + .sink { [unowned self] in + screenView.cardComponent.image = $0 + }.store(in: &cancellables) } private func presentDrawer( @@ -151,7 +162,18 @@ public final class ProfileController: UIViewController { style: .red )) - let drawer = DrawerController([ + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + action() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -168,24 +190,11 @@ public final class ProfileController: UIViewController { spacingAfter: 37 ), actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - - action() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) + navigator.perform(PresentMenu(currentItem: .profile)) } } diff --git a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift index 46077336..3bfa34e1 100644 --- a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift @@ -1,12 +1,13 @@ import UIKit import Shared import Combine +import XXNavigation import DependencyInjection import ScrollViewController public final class ProfileEmailController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ProfileCoordinating private lazy var screenView = ProfileEmailView() private lazy var scrollViewController = ScrollViewController() @@ -18,8 +19,7 @@ public final class ProfileEmailController: UIViewController { super.viewWillAppear(animated) navigationItem.backButtonTitle = "" barStylist.styleSubject.send(.darkContent) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) + navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) } public override func viewDidLoad() { @@ -56,19 +56,17 @@ public final class ProfileEmailController: UIViewController { viewModel .statePublisher - .map(\.confirmationId) .receive(on: DispatchQueue.main) - .compactMap { $0 } .sink { [unowned self] in + guard let id = $0.confirmationId else { return } viewModel.clearUp() -// coordinator.toCode(with: $0, from: self) { _, _ in -// if let viewControllers = self.navigationController?.viewControllers { -// self.navigationController?.popToViewController( -// viewControllers[viewControllers.count - 3], -// animated: true -// ) -// } -// } + navigator.perform( + PresentProfileCode( + isEmail: true, + content: $0.input, + confirmationId: id + ) + ) }.store(in: &cancellables) viewModel diff --git a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift index 1708fc9e..60e1aef4 100644 --- a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift +++ b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift @@ -1,12 +1,13 @@ import UIKit import Shared import Combine +import XXNavigation import DependencyInjection import ScrollViewController public final class ProfilePhoneController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ProfileCoordinating private lazy var screenView = ProfilePhoneView() private lazy var scrollViewController = ScrollViewController() @@ -31,62 +32,79 @@ public final class ProfilePhoneController: UIViewController { private func setupScrollView() { addChild(scrollViewController) view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() + } scrollViewController.didMove(toParent: self) scrollViewController.contentView = screenView scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color } private func setupBindings() { - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) - screenView.inputField.returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) - screenView.inputField.codePublisher + screenView + .inputField + .codePublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in - coordinator.toCountries(from: self) { self.viewModel.didChooseCountry($0) } + navigator.perform(PresentCountryList(completion: { [weak self] in + guard let self else { return } + self.viewModel.didChooseCountry($0) + })) }.store(in: &cancellables) - viewModel.statePublisher - .map(\.confirmationId) + viewModel + .statePublisher .receive(on: DispatchQueue.main) - .compactMap { $0 } .sink { [unowned self] in + guard let id = $0.confirmationId, let content = $0.content else { return } viewModel.clearUp() -// coordinator.toCode(with: $0, from: self) { _, _ in -// if let viewControllers = self.navigationController?.viewControllers { -// self.navigationController?.popToViewController( -// viewControllers[viewControllers.count - 3], -// animated: true -// ) -// } -// } + navigator.perform( + PresentProfileCode( + isEmail: false, + content: content, + confirmationId: id + ) + ) }.store(in: &cancellables) - viewModel.statePublisher + viewModel + .statePublisher .map(\.country) .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [unowned self] in screenView.inputField.set(prefix: $0.prefixWithFlag) screenView.inputField.update(placeholder: $0.example) - } - .store(in: &cancellables) + }.store(in: &cancellables) - viewModel.statePublisher + viewModel + .statePublisher .map(\.status) .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) - screenView.saveButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) + screenView + .saveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) } } diff --git a/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift b/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift deleted file mode 100644 index 59b569ef..00000000 --- a/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift +++ /dev/null @@ -1,89 +0,0 @@ -import UIKit -import Shared -import Countries -import Permissions -import MenuFeature -import Presentation - -public protocol ProfileCoordinating { - func toEmail(from: UIViewController) - func toPhone(from: UIViewController) - func toPhotos(from: UIViewController) - func toSideMenu(from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toPermission(type: PermissionType, from: UIViewController) - - func toCountries( - from: UIViewController, - _: @escaping (Country) -> Void - ) -} - -public struct ProfileCoordinator: ProfileCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var emailFactory: () -> UIViewController - var phoneFactory: () -> UIViewController - var imagePickerFactory: () -> UIImagePickerController - var permissionFactory: () -> RequestPermissionController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - - public init( - emailFactory: @escaping () -> UIViewController, - phoneFactory: @escaping () -> UIViewController, - imagePickerFactory: @escaping () -> UIImagePickerController, - permissionFactory: @escaping () -> RequestPermissionController, // âš ï¸ - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, - countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController - ) { - self.emailFactory = emailFactory - self.phoneFactory = phoneFactory - self.sideMenuFactory = sideMenuFactory - self.countriesFactory = countriesFactory - self.permissionFactory = permissionFactory - self.imagePickerFactory = imagePickerFactory - } -} - -public extension ProfileCoordinator { - func toEmail(from parent: UIViewController) { - let screen = emailFactory() - pushPresenter.present(screen, from: parent) - } - - func toPhone(from parent: UIViewController) { - let screen = phoneFactory() - pushPresenter.present(screen, from: parent) - } - - func toPermission(type: PermissionType, from parent: UIViewController) { - let screen = permissionFactory() - screen.setup(type: type) - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { - let screen = countriesFactory(onChoose) - pushPresenter.present(screen, from: parent) - } - - func toPhotos(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = true - modalPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.profile, parent) - sidePresenter.present(screen, from: parent) - } -} diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift index 173c8e19..28027074 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift @@ -52,7 +52,7 @@ final class ProfileCodeViewModel { guard stateSubject.value.resendDebouncer == 0 else { return } stateSubject.value.resendDebouncer = 60 timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in - guard let self = self, self.stateSubject.value.resendDebouncer > 0 else { + guard let self, self.stateSubject.value.resendDebouncer > 0 else { $0.invalidate() return } diff --git a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift index 6e0f17fe..d2fd3b5a 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift @@ -10,7 +10,6 @@ import DependencyInjection final class ProfileEmailViewModel { struct ViewState: Equatable { var input: String = "" - var content: String? var confirmationId: String? var status: InputField.ValidationStatus = .unknown(nil) } @@ -25,29 +24,29 @@ final class ProfileEmailViewModel { private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) private var scheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + func clearUp() { + stateSubject.value.confirmationId = nil + } + func didInput(_ string: String) { stateSubject.value.input = string validate() } - func clearUp() { - stateSubject.value.confirmationId = nil - } - func didTapNext() { hudController.show() scheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( .init(type: .email, value: self.stateSubject.value.input) ) self.hudController.dismiss() self.stateSubject.value.confirmationId = confirmationId - self.stateSubject.value.content = self.stateSubject.value.input } catch { + self.hudController.dismiss() let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudController.show(.init(content: xxError)) + self.stateSubject.value.status = .invalid(xxError) } } } diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift index 8adc37cc..c341c598 100644 --- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift @@ -44,7 +44,7 @@ final class ProfilePhoneViewModel { func didTapNext() { hudController.show() scheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } let content = "\(self.stateSubject.value.input)\(self.stateSubject.value.country.code)" do { let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index 996af851..72ef8593 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -86,7 +86,7 @@ final class ProfileViewModel { hudController.show() backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.messenger.ud.get()!.removeFact( diff --git a/Sources/RequestsFeature/Controllers/RequestsContainerController.swift b/Sources/RequestsFeature/Controllers/RequestsContainerController.swift index 7a878ab3..464890cd 100644 --- a/Sources/RequestsFeature/Controllers/RequestsContainerController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsContainerController.swift @@ -1,12 +1,13 @@ import UIKit import Shared import Combine +import XXNavigation import ContactFeature import DependencyInjection public final class RequestsContainerController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: RequestsCoordinating private lazy var screenView = RequestsContainerView() private var cancellables = Set<AnyCancellable>() @@ -42,7 +43,7 @@ public final class RequestsContainerController: UIViewController { if let stack = navigationController?.viewControllers, stack.count > 1 { if stack[stack.count - 2].isKind(of: ContactController.self) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } + guard let self else { return } let point = CGPoint(x: self.screenView.frame.width, y: 0.0) self.screenView.scrollView.setContentOffset(point, animated: true) @@ -75,8 +76,9 @@ public final class RequestsContainerController: UIViewController { .sentController .connectionsPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSearch(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentSearch(replacing: false)) + }.store(in: &cancellables) screenView .segmentedControl @@ -106,7 +108,7 @@ public final class RequestsContainerController: UIViewController { } @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) + navigator.perform(PresentMenu(currentItem: .requests)) } } diff --git a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift index bb62744b..94d4a382 100644 --- a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift @@ -23,7 +23,7 @@ final class RequestsFailedController: UIViewController { let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) cell.setupFor(requestFailed: request) cell.didTapStateButton = { [weak self] in - guard let self = self else { return } + guard let self else { return } self.viewModel.didTapStateButtonFor(request: request) } return cell diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift index 279f29b6..3ebd5792 100644 --- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift @@ -3,561 +3,591 @@ import Shared import Combine import XXModels import Countries +import XXNavigation import DrawerFeature import DependencyInjection final class RequestsReceivedController: UIViewController { - @Dependency var toaster: ToastController - @Dependency var coordinator: RequestsCoordinating - - private lazy var screenView = RequestsReceivedView() - private var cancellables = Set<AnyCancellable>() - private let viewModel = RequestsReceivedViewModel() - private var drawerCancellables = Set<AnyCancellable>() - private var dataSource: UICollectionViewDiffableDataSource<Section, RequestReceived>? - - override func loadView() { - view = screenView + @Dependency var navigator: Navigator + @Dependency var toaster: ToastController + + private lazy var screenView = RequestsReceivedView() + private var cancellables = Set<AnyCancellable>() + private let viewModel = RequestsReceivedViewModel() + private var drawerCancellables = Set<AnyCancellable>() + private var dataSource: UICollectionViewDiffableDataSource<Section, RequestReceived>? + + override func loadView() { + view = screenView + } + + override func viewDidLoad() { + super.viewDidLoad() + + screenView.collectionView.delegate = self + screenView.collectionView.register(RequestCell.self) + screenView.collectionView.register(RequestReceivedEmptyCell.self) + screenView.collectionView.registerSectionHeader(RequestsBlankSectionHeader.self) + screenView.collectionView.registerSectionHeader(RequestsHiddenSectionHeader.self) + + dataSource = UICollectionViewDiffableDataSource<Section, RequestReceived>( + collectionView: screenView.collectionView + ) { collectionView, indexPath, requestReceived in + guard let request = requestReceived.request else { + let cell: RequestReceivedEmptyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + return cell + } + + let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + cell.setupFor(requestReceived: requestReceived, isHidden: indexPath.section == 1) + cell.didTapStateButton = { [weak self] in + guard let self else { return } + self.viewModel.didTapStateButtonFor(request: request) + } + + return cell } - override func viewDidLoad() { - super.viewDidLoad() - - screenView.collectionView.delegate = self - screenView.collectionView.register(RequestCell.self) - screenView.collectionView.register(RequestReceivedEmptyCell.self) - screenView.collectionView.registerSectionHeader(RequestsBlankSectionHeader.self) - screenView.collectionView.registerSectionHeader(RequestsHiddenSectionHeader.self) - - dataSource = UICollectionViewDiffableDataSource<Section, RequestReceived>( - collectionView: screenView.collectionView - ) { collectionView, indexPath, requestReceived in - guard let request = requestReceived.request else { - let cell: RequestReceivedEmptyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - return cell - } - - let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - cell.setupFor(requestReceived: requestReceived, isHidden: indexPath.section == 1) - cell.didTapStateButton = { [weak self] in - guard let self = self else { return } - self.viewModel.didTapStateButtonFor(request: request) - } - - return cell - } + dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in + let reuseIdentifier: String - dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in - let reuseIdentifier: String + if indexPath.section == Section.appearing.rawValue { + reuseIdentifier = String(describing: RequestsBlankSectionHeader.self) + } else { + reuseIdentifier = String(describing: RequestsHiddenSectionHeader.self) + } - if indexPath.section == Section.appearing.rawValue { - reuseIdentifier = String(describing: RequestsBlankSectionHeader.self) - } else { - reuseIdentifier = String(describing: RequestsHiddenSectionHeader.self) - } + let cell = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: reuseIdentifier, + for: indexPath + ) - let cell = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: reuseIdentifier, - for: indexPath - ) + if let cell = cell as? RequestsHiddenSectionHeader, let self = self { + cell.switcherView.setOn(self.viewModel.isShowingHiddenRequests, animated: true) - if let cell = cell as? RequestsHiddenSectionHeader, let self = self { - cell.switcherView.setOn(self.viewModel.isShowingHiddenRequests, animated: true) + cell.switcherView + .publisher(for: .valueChanged) + .sink { self.viewModel.didToggleHiddenRequestsSwitcher() } + .store(in: &cell.cancellables) + } - cell.switcherView - .publisher(for: .valueChanged) - .sink { self.viewModel.didToggleHiddenRequestsSwitcher() } - .store(in: &cell.cancellables) - } - - return cell - } - - viewModel.verifyingPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentVerifyingDrawer() } - .store(in: &cancellables) - - viewModel.itemsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in dataSource?.apply($0, animatingDifferences: true) } - .store(in: &cancellables) - - viewModel.contactConfirmationPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentSingleRequestSuccessDrawer(forContact: $0) } - .store(in: &cancellables) - - viewModel.groupConfirmationPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentGroupRequestSuccessDrawer(forGroup: $0) } - .store(in: &cancellables) + return cell } + + viewModel + .verifyingPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentVerifyingDrawer() + }.store(in: &cancellables) + + viewModel + .itemsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dataSource?.apply($0, animatingDifferences: true) + }.store(in: &cancellables) + + viewModel + .contactConfirmationPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentSingleRequestSuccessDrawer(forContact: $0) + }.store(in: &cancellables) + + viewModel + .groupConfirmationPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentGroupRequestSuccessDrawer(forGroup: $0) + }.store(in: &cancellables) + } } extension RequestsReceivedController: UICollectionViewDelegate { - func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let request = dataSource?.itemIdentifier(for: indexPath)?.request else { return } - - switch request { - case .group(let group): - guard group.authStatus == .pending || group.authStatus == .hidden else { return } - presentGroupRequestDrawer(forGroup: group) - case .contact(let contact): - guard contact.authStatus == .verified || contact.authStatus == .hidden else { return } - presentSingleRequestDrawer(forContact: contact) - } + func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let request = dataSource?.itemIdentifier(for: indexPath)?.request else { return } + + switch request { + case .group(let group): + guard group.authStatus == .pending || group.authStatus == .hidden else { return } + presentGroupRequestDrawer(forGroup: group) + case .contact(let contact): + guard contact.authStatus == .verified || contact.authStatus == .hidden else { return } + presentSingleRequestDrawer(forContact: contact) } + } } // MARK: - Group Request Success Drawer extension RequestsReceivedController { - func presentGroupRequestSuccessDrawer(forGroup group: Group) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Group.Success.title, - color: Asset.accentSuccess.color, - spacingAfter: 20, - leftImage: Asset.requestAccepted.image - ) - - let drawerNickname = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: group.name, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Requests.Drawer.Group.Success.subtitle, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerNickname, - drawerSubtitle - ]) - - let drawerSendButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.Success.send, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerLaterButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.Success.later, - style: .simplestColoredBrand - ) - ) - - items.append(contentsOf: [ - drawerSendButton, - drawerLaterButton - ]) - - let drawer = DrawerController(items) - - drawerSendButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - let chatInfo = self.viewModel.groupChatWith(group: group) - self.coordinator.toGroupChat(with: chatInfo, from: self) - } - }.store(in: &drawerCancellables) - - drawerLaterButton.action - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + func presentGroupRequestSuccessDrawer(forGroup group: Group) { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Group.Success.title, + color: Asset.accentSuccess.color, + spacingAfter: 20, + leftImage: Asset.requestAccepted.image + ) + + let drawerNickname = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: group.name, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Requests.Drawer.Group.Success.subtitle, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerNickname, + drawerSubtitle + ]) + + let drawerSendButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.Success.send, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerLaterButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.Success.later, + style: .simplestColoredBrand + ) + ) + + items.append(contentsOf: [ + drawerSendButton, + drawerLaterButton + ]) + + drawerSendButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.navigator.perform(PresentGroupChat( + model: self.viewModel.groupChatWith(group: group) + )) + } + }.store(in: &drawerCancellables) + + drawerLaterButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: items)) + } } // MARK: - Single Request Success Drawer extension RequestsReceivedController { - func presentSingleRequestSuccessDrawer(forContact contact: Contact) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.Success.title, - color: Asset.accentSuccess.color, - spacingAfter: 20, - leftImage: Asset.requestAccepted.image - ) - - let drawerNickname = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: (contact.nickname ?? contact.username) ?? "", - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Requests.Drawer.Single.Success.subtitle, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerNickname, - drawerSubtitle - ]) - - let drawerSendButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.Success.send, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerLaterButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.Success.later, - style: .simplestColoredBrand - ) - ) - - items.append(contentsOf: [ - drawerSendButton, - drawerLaterButton - ]) - - let drawer = DrawerController(items) - - drawerSendButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.coordinator.toSingleChat(with: contact, from: self) - } - }.store(in: &drawerCancellables) - - drawerLaterButton.action - .receive(on: DispatchQueue.main) - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + func presentSingleRequestSuccessDrawer(forContact contact: Contact) { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.Success.title, + color: Asset.accentSuccess.color, + spacingAfter: 20, + leftImage: Asset.requestAccepted.image + ) + + let drawerNickname = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: (contact.nickname ?? contact.username) ?? "", + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Requests.Drawer.Single.Success.subtitle, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerNickname, + drawerSubtitle + ]) + + let drawerSendButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.Success.send, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerLaterButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.Success.later, + style: .simplestColoredBrand + ) + ) + + items.append(contentsOf: [ + drawerSendButton, + drawerLaterButton + ]) + + drawerSendButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.navigator.perform(PresentChat(contact: contact)) + } + }.store(in: &drawerCancellables) + + drawerLaterButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: items)) + } } // MARK: - Group Request Drawer extension RequestsReceivedController { - func presentGroupRequestDrawer(forGroup group: Group) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Group.title, - spacingAfter: 20 - ) - - let drawerGroupName = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: group.name, - color: Asset.neutralDark.color, - spacingAfter: 25 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerGroupName - ]) - - let drawerLoading = DrawerLoadingRetry() - drawerLoading.startSpinning() + func presentGroupRequestDrawer(forGroup group: Group) { + drawerCancellables.removeAll() - items.append(drawerLoading) + var items: [DrawerItem] = [] - let drawerTable = DrawerTable(spacingAfter: 23) + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Group.title, + spacingAfter: 20 + ) - drawerLoading.retryPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawerLoading.startSpinning() + let drawerGroupName = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: group.name, + color: Asset.neutralDark.color, + spacingAfter: 25 + ) - viewModel.fetchMembers(group) { [weak self] in - guard let _ = self else { return } + items.append(contentsOf: [ + drawerTitle, + drawerGroupName + ]) - switch $0 { - case .success(let models): - DispatchQueue.main.async { - drawerTable.update(models: models) - drawerLoading.stopSpinning(withRetry: false) - } - case .failure: - drawerLoading.stopSpinning(withRetry: true) - } - } - }.store(in: &drawerCancellables) + let drawerLoading = DrawerLoadingRetry() + drawerLoading.startSpinning() - viewModel.fetchMembers(group) { [weak self] in - guard let _ = self else { return } - - switch $0 { - case .success(let models): - DispatchQueue.main.async { - drawerTable.update(models: models) - drawerLoading.stopSpinning(withRetry: false) - } - case .failure: - drawerLoading.stopSpinning(withRetry: true) - } - } - - items.append(drawerTable) + items.append(drawerLoading) - let drawerAcceptButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.accept, - style: .brandColored - ), spacingAfter: 5 - ) + let drawerTable = DrawerTable(spacingAfter: 23) - let drawerHideButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.hide, - style: .simplestColoredBrand - ), spacingAfter: 5 - ) - - items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) + drawerLoading.retryPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + drawerLoading.startSpinning() - let drawer = DrawerController(items) + viewModel.fetchMembers(group) { [weak self] in + guard let _ = self else { return } - drawerAcceptButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didRequestAccept(group: group) - } + switch $0 { + case .success(let models): + DispatchQueue.main.async { + drawerTable.update(models: models) + drawerLoading.stopSpinning(withRetry: false) } - .store(in: &drawerCancellables) - - drawerHideButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didRequestHide(group: group) - } - } - .store(in: &drawerCancellables) - - coordinator.toDrawerBottom(drawer, from: self) - } -} + case .failure: + drawerLoading.stopSpinning(withRetry: true) + } + } + }.store(in: &drawerCancellables) -// MARK: - Single Request Drawer + viewModel.fetchMembers(group) { [weak self] in + guard let _ = self else { return } -extension RequestsReceivedController { - func presentSingleRequestDrawer(forContact contact: Contact) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.title, - spacingAfter: 20 - ) - - let drawerUsername = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: contact.username ?? "", - color: Asset.neutralDark.color, - spacingAfter: 25 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerUsername - ]) - - let drawerEmailTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.email, - color: Asset.neutralWeak.color, - spacingAfter: 5 - ) - - if let email = contact.email { - let drawerEmailContent = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: email, - spacingAfter: 25 - ) - - items.append(contentsOf: [ - drawerEmailTitle, - drawerEmailContent - ]) + switch $0 { + case .success(let models): + DispatchQueue.main.async { + drawerTable.update(models: models) + drawerLoading.stopSpinning(withRetry: false) } + case .failure: + drawerLoading.stopSpinning(withRetry: true) + } + } - let drawerPhoneTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.phone, - color: Asset.neutralWeak.color, - spacingAfter: 5 - ) - - if let phone = contact.phone { - let drawerPhoneContent = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", - spacingAfter: 30 - ) - - items.append(contentsOf: [ - drawerPhoneTitle, - drawerPhoneContent - ]) + items.append(drawerTable) + + let drawerAcceptButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.accept, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerHideButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.hide, + style: .simplestColoredBrand + ), spacingAfter: 5 + ) + + items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) + + drawerAcceptButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestAccept(group: group) } + }.store(in: &drawerCancellables) + + drawerHideButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestHide(group: group) + } + }.store(in: &drawerCancellables) - let drawerNicknameTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 16.0), - text: Localized.Requests.Drawer.Single.nickname, - color: Asset.neutralDark.color, - spacingAfter: 21 - ) - - items.append(drawerNicknameTitle) - - let drawerNicknameInput = DrawerInput( - placeholder: contact.username ?? "", - validator: .init( - wrongIcon: .image(Asset.sharedError.image), - correctIcon: .image(Asset.sharedSuccess.image), - shouldAcceptPlaceholder: true - ), - spacingAfter: 29 - ) - - items.append(drawerNicknameInput) - - let drawerAcceptButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.accept, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerHideButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.hide, - style: .simplestColoredBrand - ), spacingAfter: 5 - ) - - items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) - - let drawer = DrawerController(items) - - var nickname: String? - var allowsSave = true - - drawerNicknameInput.validationPublisher - .receive(on: DispatchQueue.main) - .sink { allowsSave = $0 } - .store(in: &drawerCancellables) - - drawerNicknameInput.inputPublisher - .receive(on: DispatchQueue.main) - .sink { - guard !$0.isEmpty else { - nickname = contact.username - return - } - - nickname = $0 - } - .store(in: &drawerCancellables) - - drawerAcceptButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard allowsSave else { return } - - drawer.dismiss(animated: true) { - self.viewModel.didRequestAccept(contact: contact, nickname: nickname) - } - } - .store(in: &drawerCancellables) - - drawerHideButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didRequestHide(contact: contact) - } - } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + navigator.perform(PresentDrawer(items: items)) + } } -// MARK: - Verifying Drawer +// MARK: - Single Request Drawer extension RequestsReceivedController { - func presentVerifyingDrawer() { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: Localized.Requests.Received.Verifying.title, - spacingAfter: 20 - ) + func presentSingleRequestDrawer(forContact contact: Contact) { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.title, + spacingAfter: 20 + ) + + let drawerUsername = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: contact.username ?? "", + color: Asset.neutralDark.color, + spacingAfter: 25 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerUsername + ]) + + let drawerEmailTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.email, + color: Asset.neutralWeak.color, + spacingAfter: 5 + ) + + if let email = contact.email { + let drawerEmailContent = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: email, + spacingAfter: 25 + ) + + items.append(contentsOf: [ + drawerEmailTitle, + drawerEmailContent + ]) + } - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Requests.Received.Verifying.subtitle, - spacingAfter: 40 - ) + let drawerPhoneTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.phone, + color: Asset.neutralWeak.color, + spacingAfter: 5 + ) + + if let phone = contact.phone { + let drawerPhoneContent = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", + spacingAfter: 30 + ) + + items.append(contentsOf: [ + drawerPhoneTitle, + drawerPhoneContent + ]) + } - items.append(contentsOf: [ - drawerTitle, - drawerSubtitle - ]) + let drawerNicknameTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 16.0), + text: Localized.Requests.Drawer.Single.nickname, + color: Asset.neutralDark.color, + spacingAfter: 21 + ) + + items.append(drawerNicknameTitle) + + let drawerNicknameInput = DrawerInput( + placeholder: contact.username ?? "", + validator: .init( + wrongIcon: .image(Asset.sharedError.image), + correctIcon: .image(Asset.sharedSuccess.image), + shouldAcceptPlaceholder: true + ), + spacingAfter: 29 + ) + + items.append(drawerNicknameInput) + + let drawerAcceptButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.accept, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerHideButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.hide, + style: .simplestColoredBrand + ), spacingAfter: 5 + ) + + items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) + + var nickname: String? + var allowsSave = true + + drawerNicknameInput + .validationPublisher + .receive(on: DispatchQueue.main) + .sink { allowsSave = $0 } + .store(in: &drawerCancellables) + + drawerNicknameInput + .inputPublisher + .receive(on: DispatchQueue.main) + .sink { + guard !$0.isEmpty else { + nickname = contact.username + return + } - let drawerDoneButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Received.Verifying.action, - style: .brandColored - ), spacingAfter: 5 - ) + nickname = $0 + }.store(in: &drawerCancellables) + + drawerAcceptButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard allowsSave else { return } + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestAccept(contact: contact, nickname: nickname) + } + }.store(in: &drawerCancellables) + + drawerHideButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestHide(contact: contact) + } + }.store(in: &drawerCancellables) - items.append(drawerDoneButton) + navigator.perform(PresentDrawer(items: items)) + } +} - let drawer = DrawerController(items) +// MARK: - Verifying Drawer - drawerDoneButton.action - .receive(on: DispatchQueue.main) - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) +extension RequestsReceivedController { + func presentVerifyingDrawer() { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: Localized.Requests.Received.Verifying.title, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Requests.Received.Verifying.subtitle, + spacingAfter: 40 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerSubtitle + ]) + + let drawerDoneButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Received.Verifying.action, + style: .brandColored + ), spacingAfter: 5 + ) + + items.append(drawerDoneButton) + + drawerDoneButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - coordinator.toDrawer(drawer, from: self) - } + navigator.perform(PresentDrawer(items: items)) + } } diff --git a/Sources/RequestsFeature/Controllers/RequestsSentController.swift b/Sources/RequestsFeature/Controllers/RequestsSentController.swift index 7edec938..ff9e0384 100644 --- a/Sources/RequestsFeature/Controllers/RequestsSentController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsSentController.swift @@ -29,7 +29,7 @@ final class RequestsSentController: UIViewController { let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) cell.setupFor(requestSent: requestSent) cell.didTapStateButton = { [weak self] in - guard let self = self else { return } + guard let self else { return } self.viewModel.didTapStateButtonFor(request: requestSent) } return cell diff --git a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift b/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift deleted file mode 100644 index 9e8297c0..00000000 --- a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift +++ /dev/null @@ -1,119 +0,0 @@ -import UIKit -import Shared -import XXModels -import MenuFeature -import Presentation -import ContactFeature -import ScrollViewController - -public protocol RequestsCoordinating { - func toSearch(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toGroupChat(with: GroupInfo, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toDrawerBottom(_: UIViewController, from: UIViewController) - func toNickname(from: UIViewController, prefilled: String, _: @escaping StringClosure) -} - -public struct RequestsCoordinator: RequestsCoordinating { - var pushPresenter: Presenting = PushPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - - var searchFactory: (String?) -> UIViewController - var contactFactory: (Contact) -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupInfo) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var nicknameFactory: (String, @escaping StringClosure) -> UIViewController - - public init( - searchFactory: @escaping (String?) -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupInfo) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, - nicknameFactory: @escaping (String, @escaping StringClosure) -> UIViewController - ) { - self.searchFactory = searchFactory - self.contactFactory = contactFactory - self.nicknameFactory = nicknameFactory - self.sideMenuFactory = sideMenuFactory - self.groupChatFactory = groupChatFactory - self.singleChatFactory = singleChatFactory - } -} - -public extension RequestsCoordinator { - func toSingleChat( - with contact: Contact, - from parent: UIViewController - ) { - let screen = singleChatFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toGroupChat( - with info: GroupInfo, - from parent: UIViewController - ) { - let screen = groupChatFactory(info) - pushPresenter.present(screen, from: parent) - } - - func toDrawer( - _ drawer: UIViewController, - from parent: UIViewController - ) { - let target = ScrollViewController.embedding(drawer) - fullscreenPresenter.present(target, from: parent) - } - - func toDrawerBottom( - _ drawer: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(drawer, from: parent) - } - - func toSearch(from parent: UIViewController) { - let screen = searchFactory(nil) - pushPresenter.present(screen, from: parent) - } - - func toNickname( - from parent: UIViewController, - prefilled: String, - _ completion: @escaping StringClosure - ) { - let screen = nicknameFactory(prefilled, completion) - bottomPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.requests, parent) - sidePresenter.present(screen, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift index 351d3b37..1f54896a 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift @@ -47,7 +47,7 @@ final class RequestsFailedViewModel { hudController.show() backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { if request.status == .failedToRequest { diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index 055b7236..64ec6595 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -147,7 +147,7 @@ final class RequestsReceivedViewModel { if request.status == .failedToVerify { backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { contact.authStatus = .verificationInProgress @@ -188,7 +188,7 @@ final class RequestsReceivedViewModel { hudController.show() backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.groupManager.joinGroup(serializedGroupData: group.serialized) @@ -257,7 +257,7 @@ final class RequestsReceivedViewModel { contact.nickname = nickname ?? contact.username backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.database.saveContact(contact) diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index 4936e676..718b673f 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -69,7 +69,7 @@ final class RequestsSentViewModel { hudController.show() backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { var includedFacts: [Fact] = [] diff --git a/Sources/RestoreFeature/Controllers/RestoreController.swift b/Sources/RestoreFeature/Controllers/RestoreController.swift index f88ead1d..d1a57aa2 100644 --- a/Sources/RestoreFeature/Controllers/RestoreController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreController.swift @@ -1,134 +1,135 @@ import UIKit import Shared import Combine +import XXNavigation import DrawerFeature import DependencyInjection public final class RestoreController: UIViewController { - @Dependency private var coordinator: RestoreCoordinating - - private lazy var screenView = RestoreView() - - private let viewModel: RestoreViewModel - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public init(_ details: RestorationDetails) { - viewModel = .init(details: details) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - presentWarning() - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupBindings() - } - - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.AccountRestore.header - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupBindings() { - viewModel.stepPublisher - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [unowned self] in - screenView.updateFor(step: $0) - - if $0 == .wrongPass { - coordinator.toPassphrase( - from: self, - cancelClosure: { self.dismiss(animated: true) }, - passphraseClosure: { pwd in - self.viewModel.retryWith(passphrase: pwd) - } - ) - - return - } - - if $0 == .done { - coordinator.toSuccess(from: self) - } - }.store(in: &cancellables) - - screenView.backButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in didTapBack() } - .store(in: &cancellables) - - screenView.cancelButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in didTapBack() } - .store(in: &cancellables) - - screenView.restoreButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - coordinator.toPassphrase( - from: self, - cancelClosure: { self.dismiss(animated: true) }, - passphraseClosure: { pwd in - self.viewModel.didTapRestore(passphrase: pwd) - } - ) - }.store(in: &cancellables) - } - - @objc private func didTapBack() { - navigationController?.popViewController(animated: true) - } + @Dependency var navigator: Navigator + + private lazy var screenView = RestoreView() + + private let viewModel: RestoreViewModel + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public init(_ details: RestorationDetails) { + viewModel = .init(details: details) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + presentWarning() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + } + + private func setupNavigationBar() { + let title = UILabel() + title.text = Localized.AccountRestore.header + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) + navigationItem.leftItemsSupplementBackButton = true + } + + private func setupBindings() { + viewModel + .stepPublisher + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + screenView.updateFor(step: $0) + if $0 == .wrongPass { + navigator.perform(PresentPassphrase(onCancel: { + navigator.perform(DismissModal(from: self)) + }, onPassphrase: { [weak self] passphrase in + guard let self else { return } + self.viewModel.retryWith(passphrase: passphrase) + })) + return + } + if $0 == .done { +// coordinator.toSuccess(from: self) + } + }.store(in: &cancellables) + + screenView + .backButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + didTapBack() + }.store(in: &cancellables) + + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + didTapBack() + }.store(in: &cancellables) + + screenView + .restoreButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentPassphrase(onCancel: { + navigator.perform(DismissModal(from: self)) + }, onPassphrase: { [weak self] passphrase in + guard let self else { return } + self.viewModel.didTapRestore(passphrase: passphrase) + })) + }.store(in: &cancellables) + } + + @objc private func didTapBack() { + navigationController?.popViewController(animated: true) + } } extension RestoreController { - private func presentWarning() { - let actionButton = DrawerCapsuleButton(model: .init( - title: Localized.AccountRestore.Warning.action, - style: .brandColored - )) - - let drawer = DrawerController([ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: Localized.AccountRestore.Warning.title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - text: Localized.AccountRestore.Warning.subtitle, - spacingAfter: 37 - ), - actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + private func presentWarning() { + let actionButton = DrawerCapsuleButton(model: .init( + title: Localized.AccountRestore.Warning.action, + style: .brandColored + )) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.AccountRestore.Warning.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + text: Localized.AccountRestore.Warning.subtitle, + spacingAfter: 37 + ), + actionButton + ])) + } } diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift index 6e85c99f..84ebee37 100644 --- a/Sources/RestoreFeature/Controllers/RestoreListController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -1,11 +1,12 @@ import UIKit import Shared import Combine +import XXNavigation import DrawerFeature import DependencyInjection public final class RestoreListController: UIViewController { - @Dependency var coordinator: RestoreCoordinating + @Dependency var navigator: Navigator private lazy var screenView = RestoreListView() @@ -27,23 +28,20 @@ public final class RestoreListController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - viewModel.sftpPublisher + viewModel + .sftpPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] _ in - coordinator.toSFTP(from: self) { [weak self] host, username, password in + navigator.perform(PresentSFTP { [weak self] host, username, password in guard let self else { return } - self.viewModel.setupSFTP( - host: host, - username: username, - password: password - ) - } + self.viewModel.setupSFTP(host: host, username: username, password: password) + }) }.store(in: &cancellables) viewModel.detailsPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toRestore(with: $0, from: self) + .sink { [unowned self] _ in +// coordinator.toRestore(with: $0, from: self) }.store(in: &cancellables) screenView.cancelButton @@ -97,7 +95,17 @@ extension RestoreListController { style: .brandColored )) - let drawer = DrawerController([ + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.AccountRestore.Warning.title, @@ -108,17 +116,6 @@ extension RestoreListController { spacingAfter: 37 ), actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } } diff --git a/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift index 13c31363..83c70aa5 100644 --- a/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift @@ -1,11 +1,12 @@ import UIKit import Shared import Combine +import XXNavigation import DependencyInjection public final class RestoreSuccessController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: RestoreCoordinating private lazy var screenView = RestoreSuccessView() private var cancellables = Set<AnyCancellable>() @@ -44,9 +45,11 @@ public final class RestoreSuccessController: UIViewController { } private func setupBindings() { - screenView.nextButton + screenView + .nextButton .publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toChats(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentChatList()) + }.store(in: &cancellables) } } diff --git a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift deleted file mode 100644 index ee3e429c..00000000 --- a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift +++ /dev/null @@ -1,116 +0,0 @@ -import UIKit -import Shared -import Presentation -import ScrollViewController - -public typealias SFTPDetailsClosure = (String, String, String) -> Void - -public protocol RestoreCoordinating { - func toChats(from: UIViewController) - func toSuccess(from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toRestore(with: RestorationDetails, from: UIViewController) - - func toSFTP( - from: UIViewController, - detailsClosure: @escaping SFTPDetailsClosure - ) - - func toPassphrase( - from: UIViewController, - cancelClosure: @escaping EmptyClosure, - passphraseClosure: @escaping StringClosure - ) -} - -public struct RestoreCoordinator: RestoreCoordinating { - var pushPresenter: Presenting = PushPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - - var successFactory: () -> UIViewController - var chatListFactory: () -> UIViewController - var restoreFactory: (RestorationDetails) -> UIViewController - var sftpFactory: (@escaping SFTPDetailsClosure) -> UIViewController - - var passphraseFactory: ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController - - public init( - successFactory: @escaping () -> UIViewController, - chatListFactory: @escaping () -> UIViewController, - restoreFactory: @escaping (RestorationDetails) -> UIViewController, - sftpFactory: @escaping ( - @escaping SFTPDetailsClosure - ) -> UIViewController, - passphraseFactory: @escaping ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController - ) { - self.sftpFactory = sftpFactory - self.successFactory = successFactory - self.restoreFactory = restoreFactory - self.chatListFactory = chatListFactory - self.passphraseFactory = passphraseFactory - } -} - -public extension RestoreCoordinator { - func toSFTP( - from parent: UIViewController, - detailsClosure: @escaping SFTPDetailsClosure - ) { - let screen = sftpFactory(detailsClosure) - pushPresenter.present(screen, from: parent) - } - - func toRestore( - with details: RestorationDetails, - from parent: UIViewController - ) { - let screen = restoreFactory(details) - pushPresenter.present(screen, from: parent) - } - - func toChats(from parent: UIViewController) { - let screen = chatListFactory() - replacePresenter.present(screen, from: parent) - } - - func toSuccess(from parent: UIViewController) { - let screen = successFactory() - replacePresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toPassphrase( - from parent: UIViewController, - cancelClosure: @escaping EmptyClosure, - passphraseClosure: @escaping StringClosure - ) { - let screen = passphraseFactory(cancelClosure, passphraseClosure) - let target = ScrollViewController.embedding(screen) - fullscreenPresenter.present(target, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift index bb81db9e..d7ffd3e5 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift @@ -52,7 +52,7 @@ final class RestoreSFTPViewModel { let anyController = UIViewController() DispatchQueue.global().async { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try CloudFilesManager.sftp( host: host, diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift index 14fe7a35..c3d2cbe2 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -97,7 +97,7 @@ final class RestoreViewModel { stepSubject.send(.parsingData) DispatchQueue.global().async { [weak self] in - guard let self = self else { return } + guard let self else { return } do { print(">>> Calling messenger destroy") @@ -109,8 +109,8 @@ final class RestoreViewModel { backupPassphrase: self.passphrase ) - self.username = result.restoredParams.username let facts = try self.messenger.ud.tryGet().getFacts() + self.username = facts.get(.username)!.value self.email = facts.get(.email)?.value self.phone = facts.get(.phone)?.value diff --git a/Sources/ScanFeature/Controllers/ScanContainerController.swift b/Sources/ScanFeature/Controllers/ScanContainerController.swift index a85c06a7..ab6dc5f9 100644 --- a/Sources/ScanFeature/Controllers/ScanContainerController.swift +++ b/Sources/ScanFeature/Controllers/ScanContainerController.swift @@ -1,12 +1,13 @@ import UIKit import Shared import Combine +import XXNavigation import DrawerFeature import DependencyInjection public final class ScanContainerController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: ScanCoordinating private lazy var screenView = ScanContainerView() @@ -30,7 +31,6 @@ public final class ScanContainerController: UIViewController { $0.right.equalToSuperview() $0.bottom.equalTo(screenView) } - pageController.delegate = self pageController.dataSource = self pageController.didMove(toParent: self) @@ -50,20 +50,19 @@ public final class ScanContainerController: UIViewController { setupBindings() displayController.didTapInfo = { [weak self] in - self?.presentInfo( + guard let self else { return } + self.presentInfo( title: Localized.Scan.Info.title, subtitle: Localized.Scan.Info.subtitle ) } - displayController.didTapAddEmail = { [weak self] in - guard let self = self else { return } - self.coordinator.toEmail(from: self) + guard let self else { return } + self.navigator.perform(PresentProfileEmail()) } - displayController.didTapAddPhone = { [weak self] in - guard let self = self else { return } - self.coordinator.toPhone(from: self) + guard let self else { return } + self.navigator.perform(PresentProfilePhone()) } } @@ -87,7 +86,8 @@ public final class ScanContainerController: UIViewController { } private func setupBindings() { - screenView.leftButton + screenView + .leftButton .publisher(for: .touchUpInside) .sink { [unowned self] _ in screenView.leftButton.set(selected: true) @@ -95,7 +95,8 @@ public final class ScanContainerController: UIViewController { pageController.setViewControllers([scanController], direction: .reverse, animated: true, completion: nil) }.store(in: &cancellables) - screenView.rightButton + screenView + .rightButton .publisher(for: .touchUpInside) .sink { [unowned self] _ in screenView.leftButton.set(selected: false) @@ -105,7 +106,7 @@ public final class ScanContainerController: UIViewController { } @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) + navigator.perform(PresentMenu(currentItem: .scan)) } private func presentInfo(title: String, subtitle: String) { @@ -115,7 +116,17 @@ public final class ScanContainerController: UIViewController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController([ + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -135,18 +146,7 @@ public final class ScanContainerController: UIViewController { actionButton, FlexibleSpace() ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } } diff --git a/Sources/ScanFeature/Controllers/ScanController.swift b/Sources/ScanFeature/Controllers/ScanController.swift index 8f05a944..8482be4d 100644 --- a/Sources/ScanFeature/Controllers/ScanController.swift +++ b/Sources/ScanFeature/Controllers/ScanController.swift @@ -2,115 +2,122 @@ import UIKit import Shared import Combine import Permissions +import XXNavigation import CombineSchedulers import DependencyInjection final class ScanController: UIViewController { - @Dependency private var coordinator: ScanCoordinating - @Dependency private var permissions: PermissionHandling - - private lazy var screenView = ScanView() - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - private var status: ScanStatus? - private let camera: CameraType - private let viewModel = ScanViewModel() - private var cancellables = Set<AnyCancellable>() - - init(camera: CameraType = Camera()) { - #if DEBUG - self.camera = MockCamera() - #else - self.camera = camera - #endif - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - override func loadView() { - view = screenView - } - - override func viewDidLoad() { - super.viewDidLoad() - screenView.layer.insertSublayer(camera.previewLayer, at: 0) - setupBindings() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - camera.previewLayer.frame = screenView.bounds + @Dependency var navigator: Navigator + @Dependency var permissions: PermissionHandling + + private lazy var screenView = ScanView() + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + private var status: ScanStatus? + private let camera: CameraType + private let viewModel = ScanViewModel() + private var cancellables = Set<AnyCancellable>() + + init(camera: CameraType = Camera()) { +#if DEBUG + self.camera = MockCamera() +#else + self.camera = camera +#endif + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func loadView() { + view = screenView + } + + override func viewDidLoad() { + super.viewDidLoad() + screenView.layer.insertSublayer(camera.previewLayer, at: 0) + setupBindings() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + camera.previewLayer.frame = screenView.bounds + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.resetScanner() + startCamera() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + self.camera.stop() } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - viewModel.resetScanner() - startCamera() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - self.camera.stop() + } + + private func startCamera() { + permissions.requestCamera { [weak self] granted in + guard let self else { return } + + if granted { + self.backgroundScheduler.schedule { + self.camera.start() } - } - - private func startCamera() { - permissions.requestCamera { [weak self] granted in - guard let self = self else { return } - - if granted { - self.backgroundScheduler.schedule { - self.camera.start() - } - } else { - DispatchQueue.main.async { - self.status = .failed(.cameraPermission) - self.screenView.update(with: .failed(.cameraPermission)) - } - } + } else { + DispatchQueue.main.async { + self.status = .failed(.cameraPermission) + self.screenView.update(with: .failed(.cameraPermission)) } + } } - - private func setupBindings() { - viewModel.contactPublisher - .receive(on: DispatchQueue.main) - .delay(for: 1, scheduler: DispatchQueue.main) - .sink { [unowned self] in coordinator.toContact($0, from: self) } - .store(in: &cancellables) - - viewModel.statePublisher - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - status = $0 - screenView.update(with: $0) - }.store(in: &cancellables) - - screenView.actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch status { - case .failed(.cameraPermission): - guard let url = URL(string: UIApplication.openSettingsURLString) else { return } - UIApplication.shared.open(url, options: [:]) - case .failed(.requestOpened): - coordinator.toRequests(from: self) - case .failed(.alreadyFriends): - coordinator.toContacts(from: self) - default: - break - } - }.store(in: &cancellables) - - camera - .dataPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] data in viewModel.didScanData(data) } - .store(in: &cancellables) - } + } + + private func setupBindings() { + viewModel + .contactPublisher + .receive(on: DispatchQueue.main) + .delay(for: 1, scheduler: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentContact(contact: $0)) + }.store(in: &cancellables) + + viewModel + .statePublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + status = $0 + screenView.update(with: $0) + }.store(in: &cancellables) + + screenView + .actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch status { + case .failed(.cameraPermission): + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url, options: [:]) + case .failed(.requestOpened): + navigator.perform(PresentRequests()) + case .failed(.alreadyFriends): + navigator.perform(PresentContactList()) + default: + break + } + }.store(in: &cancellables) + + camera + .dataPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didScanData($0) + }.store(in: &cancellables) + } } diff --git a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift b/Sources/ScanFeature/Coordinator/ScanCoordinator.swift deleted file mode 100644 index cfdc386a..00000000 --- a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift +++ /dev/null @@ -1,87 +0,0 @@ -import UIKit -import XXModels -import MenuFeature -import Presentation -import ContactFeature - -public protocol ScanCoordinating { - func toEmail(from: UIViewController) - func toPhone(from: UIViewController) - func toContacts(from: UIViewController) - func toRequests(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) -} - -public struct ScanCoordinator: ScanCoordinating { - var pushPresenter: Presenting = PushPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter(mode: .replaceLast) - - var emailFactory: () -> UIViewController - var phoneFactory: () -> UIViewController - var contactsFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - - public init( - emailFactory: @escaping () -> UIViewController, - phoneFactory: @escaping () -> UIViewController, - contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController - ) { - self.emailFactory = emailFactory - self.phoneFactory = phoneFactory - self.contactFactory = contactFactory - self.contactsFactory = contactsFactory - self.requestsFactory = requestsFactory - self.sideMenuFactory = sideMenuFactory - } -} - -public extension ScanCoordinator { - func toContact( - _ contact: Contact, - from parent: UIViewController - ) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toDrawer( - _ drawer: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(drawer, from: parent) - } - - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - replacePresenter.present(screen, from: parent) - } - - func toContacts(from parent: UIViewController) { - let screen = contactsFactory() - replacePresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.scan, parent) - sidePresenter.present(screen, from: parent) - } - - func toEmail(from parent: UIViewController) { - let screen = emailFactory() - pushPresenter.present(screen, from: parent) - } - - func toPhone(from parent: UIViewController) { - let screen = phoneFactory() - pushPresenter.present(screen, from: parent) - } -} diff --git a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift index 00be78ca..d67716bc 100644 --- a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift +++ b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift @@ -1,4 +1,5 @@ import UIKit +import Shared import Combine import Defaults import Countries diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index a78f5993..6e81b0df 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -2,12 +2,13 @@ import UIKit import Shared import Combine import XXModels +import XXNavigation import DrawerFeature import DependencyInjection public final class SearchContainerController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: SearchCoordinating private lazy var screenView = SearchContainerView() @@ -130,14 +131,34 @@ extension SearchContainerController { style: .brandColored, title: Localized.ChatList.Traffic.positive ) - let dismissButton = CapsuleButton() dismissButton.set( style: .seeThrough, title: Localized.ChatList.Traffic.negative ) - let drawer = DrawerController([ + enableButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didEnableCoverTraffic() + } + }.store(in: &drawerCancellables) + + dismissButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.ChatList.Traffic.title, @@ -159,29 +180,6 @@ extension SearchContainerController { distribution: .fillEqually, views: [enableButton, dismissButton] ) - ]) - - enableButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - self.viewModel.didEnableCoverTraffic() - } - }.store(in: &drawerCancellables) - - dismissButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } } diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index bc6b1fbd..83701522 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -4,445 +4,469 @@ import Combine import XXModels import Defaults import Countries +import XXNavigation import DrawerFeature import DependencyInjection final class SearchLeftController: UIViewController { - @Dependency var coordinator: SearchCoordinating - - @KeyObject(.email, defaultValue: nil) var email: String? - @KeyObject(.phone, defaultValue: nil) var phone: String? - @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool - @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool - - private lazy var screenView = SearchLeftView() - - let viewModel: SearchLeftViewModel - private var dataSource: SearchDiffableDataSource! - private var drawerCancellables = Set<AnyCancellable>() - private let adrpURLString = "https://links.xx.network/adrp" - - private var cancellables = Set<AnyCancellable>() - private var hudCancellables = Set<AnyCancellable>() - - init(_ invitation: String? = nil) { - self.viewModel = .init(invitation) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } + @Dependency var navigator: Navigator + @KeyObject(.email, defaultValue: nil) var email: String? + @KeyObject(.phone, defaultValue: nil) var phone: String? + @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool + + private lazy var screenView = SearchLeftView() + + private(set) var viewModel: SearchLeftViewModel + private var dataSource: SearchDiffableDataSource! + private var drawerCancellables = Set<AnyCancellable>() + private let adrpURLString = "https://links.xx.network/adrp" + + private var cancellables = Set<AnyCancellable>() + private var hudCancellables = Set<AnyCancellable>() + + init(_ invitation: String? = nil) { + self.viewModel = .init(invitation) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func loadView() { + view = screenView + } + + override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + setupBindings() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.viewDidAppear() + } + + func endEditing() { + screenView.inputField.endEditing(true) + } + + private func setupTableView() { + screenView.tableView.separatorStyle = .none + screenView.tableView.tableFooterView = UIView() + screenView.tableView.register(AvatarCell.self) + screenView.tableView.dataSource = dataSource + screenView.tableView.delegate = self + + dataSource = SearchDiffableDataSource( + tableView: screenView.tableView + ) { tableView, indexPath, item in + let contact: Contact + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) + + let h1Text: String + var h2Text: String? + + switch item { + case .stranger(let stranger): + contact = stranger + h1Text = stranger.username ?? "" + + if stranger.authStatus == .requested { + h2Text = "Request pending" + } else if stranger.authStatus == .requestFailed { + h2Text = "Request failed" + } - override func loadView() { - view = screenView - } + case .connection(let connection): + contact = connection + h1Text = (connection.nickname ?? contact.username) ?? "" - override func viewDidLoad() { - super.viewDidLoad() - setupTableView() - setupBindings() + if connection.nickname != nil { + h2Text = contact.username ?? "" + } + } + + cell.setup( + title: h1Text, + image: contact.photo, + firstSubtitle: h2Text, + secondSubtitle: contact.email, + thirdSubtitle: contact.phone, + showSeparator: false, + sent: contact.authStatus == .requested + ) + + cell.didTapStateButton = { [weak self] in + guard let self else { return } + self.viewModel.didTapResend(contact: contact) + cell.updateToResent() + } + + return cell } + } + + private func setupBindings() { + // viewModel.hudPublisher + // .removeDuplicates() + // .receive(on: DispatchQueue.main) + // .sink { [unowned self] in + // hud.update(with: $0) + // + // if case .onAction = $0, let hudBtn = hud.actionButton { + // hudBtn.publisher(for: .touchUpInside) + // .receive(on: DispatchQueue.main) + // .sink { [unowned self] in viewModel.didTapCancelSearch() } + // .store(in: &self.hudCancellables) + // } else { + // hudCancellables.forEach { $0.cancel() } + // hudCancellables.removeAll() + // } + // } + // .store(in: &cancellables) + + viewModel + .statePublisher + .map(\.item) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.updateUIForItem(item: $0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.country) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.countryButton.setFlag($0.flag, prefix: $0.prefix) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.input) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.inputField.update(content: $0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .compactMap(\.snapshot) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.placeholderView.isHidden = true + screenView.emptyView.isHidden = $0.numberOfItems != 0 + dataSource.apply($0, animatingDifferences: false) + }.store(in: &cancellables) + + screenView + .placeholderView + .infoPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentSearchDisclaimer() + }.store(in: &cancellables) + + screenView + .countryButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentCountryList(completion: { [weak self] in + guard let self else { return } + self.viewModel.didPick(country: $0) + })) + }.store(in: &cancellables) + + screenView + .inputField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didEnterInput($0) + }.store(in: &cancellables) + + screenView + .inputField + .returnPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in + viewModel.didStartSearching() + }.store(in: &cancellables) + + screenView + .inputField + .isEditingPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] isEditing in + UIView.animate(withDuration: 0.25) { + self.screenView.placeholderView.titleLabel.alpha = isEditing ? 0.1 : 1.0 + self.screenView.placeholderView.subtitleWithInfo.alpha = isEditing ? 0.1 : 1.0 + } + }.store(in: &cancellables) + + viewModel + .successPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentSucessDrawerFor(contact: $0) + }.store(in: &cancellables) + } + + private func presentSearchDisclaimer() { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Ud.Placeholder.Drawer.action + ) + + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.Ud.Placeholder.Drawer.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: Localized.Ud.Placeholder.Drawer.subtitle, + urlString: adrpURLString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ])) + } + + private func presentSucessDrawerFor(contact: Contact) { + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: Localized.Ud.NicknameDrawer.title, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Ud.NicknameDrawer.subtitle, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerSubtitle + ]) + + let drawerNicknameInput = DrawerInput( + placeholder: contact.username!, + validator: .init( + wrongIcon: .image(Asset.sharedError.image), + correctIcon: .image(Asset.sharedSuccess.image), + shouldAcceptPlaceholder: true + ), + spacingAfter: 29 + ) + + items.append(drawerNicknameInput) + + let drawerSaveButton = DrawerCapsuleButton( + model: .init( + title: Localized.Ud.NicknameDrawer.save, + style: .brandColored + ), spacingAfter: 5 + ) + + items.append(drawerSaveButton) + + let drawer = DrawerController(items) + var nickname: String? + var allowsSave = true + + drawerNicknameInput + .validationPublisher + .receive(on: DispatchQueue.main) + .sink { allowsSave = $0 } + .store(in: &drawerCancellables) + + drawerNicknameInput + .inputPublisher + .receive(on: DispatchQueue.main) + .sink { + guard !$0.isEmpty else { + nickname = contact.username + return + } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - viewModel.viewDidAppear() - } + nickname = $0 + } + .store(in: &drawerCancellables) - func endEditing() { - screenView.inputField.endEditing(true) - } + drawerSaveButton.action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard allowsSave else { return } - private func setupTableView() { - screenView.tableView.separatorStyle = .none - screenView.tableView.tableFooterView = UIView() - screenView.tableView.register(AvatarCell.self) - screenView.tableView.dataSource = dataSource - screenView.tableView.delegate = self - - dataSource = SearchDiffableDataSource( - tableView: screenView.tableView - ) { tableView, indexPath, item in - let contact: Contact - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) - - let h1Text: String - var h2Text: String? - - switch item { - case .stranger(let stranger): - contact = stranger - h1Text = stranger.username ?? "" - - if stranger.authStatus == .requested { - h2Text = "Request pending" - } else if stranger.authStatus == .requestFailed { - h2Text = "Request failed" - } - - case .connection(let connection): - contact = connection - h1Text = (connection.nickname ?? contact.username) ?? "" - - if connection.nickname != nil { - h2Text = contact.username ?? "" - } - } - - cell.setup( - title: h1Text, - image: contact.photo, - firstSubtitle: h2Text, - secondSubtitle: contact.email, - thirdSubtitle: contact.phone, - showSeparator: false, - sent: contact.authStatus == .requested - ) - - cell.didTapStateButton = { [weak self] in - guard let self = self else { return } - self.viewModel.didTapResend(contact: contact) - cell.updateToResent() - } - - return cell + drawer.dismiss(animated: true) { + self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact) } + } + .store(in: &drawerCancellables) + + //coordinator.toNicknameDrawer(drawer, from: self) + } + + private func presentRequestDrawer(forContact contact: Contact) { + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: Localized.Ud.RequestDrawer.title, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + var subtitleFragment = "Share your information with #\(contact.username ?? "")" + + if let email = contact.email { + subtitleFragment.append(contentsOf: " (\(email))#") + } else if let phone = contact.phone { + subtitleFragment.append(contentsOf: " (\(Country.findFrom(phone).prefix) \(phone.dropLast(2)))#") + } else { + subtitleFragment.append(contentsOf: "#") } - private func setupBindings() { -// viewModel.hudPublisher -// .removeDuplicates() -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in -// hud.update(with: $0) -// -// if case .onAction = $0, let hudBtn = hud.actionButton { -// hudBtn.publisher(for: .touchUpInside) -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in viewModel.didTapCancelSearch() } -// .store(in: &self.hudCancellables) -// } else { -// hudCancellables.forEach { $0.cancel() } -// hudCancellables.removeAll() -// } -// } -// .store(in: &cancellables) - - viewModel.statePublisher - .map(\.item) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.updateUIForItem(item: $0) } - .store(in: &cancellables) - - viewModel.statePublisher - .map(\.country) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.countryButton.setFlag($0.flag, prefix: $0.prefix) } - .store(in: &cancellables) - - viewModel.statePublisher - .map(\.input) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.inputField.update(content: $0) } - .store(in: &cancellables) - - viewModel.statePublisher - .compactMap(\.snapshot) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.placeholderView.isHidden = true - screenView.emptyView.isHidden = $0.numberOfItems != 0 - - dataSource.apply($0, animatingDifferences: false) - }.store(in: &cancellables) - - screenView.placeholderView - .infoPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentSearchDisclaimer() } - .store(in: &cancellables) - - screenView.countryButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toCountries(from: self) { [weak self] country in - guard let self = self else { return } - self.viewModel.didPick(country: country) - } - }.store(in: &cancellables) - - screenView.inputField - .textPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didEnterInput($0) } - .store(in: &cancellables) - - screenView.inputField - .returnPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] _ in viewModel.didStartSearching() } - .store(in: &cancellables) - - screenView.inputField - .isEditingPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] isEditing in - UIView.animate(withDuration: 0.25) { - self.screenView.placeholderView.titleLabel.alpha = isEditing ? 0.1 : 1.0 - self.screenView.placeholderView.subtitleWithInfo.alpha = isEditing ? 0.1 : 1.0 - } - }.store(in: &cancellables) - - viewModel.successPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentSucessDrawerFor(contact: $0) } - .store(in: &cancellables) + subtitleFragment.append(contentsOf: " so they know its you.") + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitleFragment, + color: Asset.neutralDark.color, + spacingAfter: 31.5, + customAttributes: [ + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .foregroundColor: Asset.brandPrimary.color + ] + ) + + items.append(contentsOf: [ + drawerTitle, + drawerSubtitle + ]) + + if let email = email { + let drawerEmail = DrawerSwitch( + title: Localized.Ud.RequestDrawer.email, + content: email, + spacingAfter: phone != nil ? 23 : 31, + isInitiallyOn: isSharingEmail + ) + + items.append(drawerEmail) + + drawerEmail.isOnPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.isSharingEmail = $0 } + .store(in: &drawerCancellables) } - private func presentSearchDisclaimer() { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Ud.Placeholder.Drawer.action - ) - - let drawer = DrawerController([ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: Localized.Ud.Placeholder.Drawer.title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: Localized.Ud.Placeholder.Drawer.subtitle, - urlString: adrpURLString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &self.drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + if let phone = phone { + let drawerPhone = DrawerSwitch( + title: Localized.Ud.RequestDrawer.phone, + content: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", + spacingAfter: 31, + isInitiallyOn: isSharingPhone + ) - private func presentSucessDrawerFor(contact: Contact) { - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: Localized.Ud.NicknameDrawer.title, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Ud.NicknameDrawer.subtitle, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerSubtitle - ]) - - let drawerNicknameInput = DrawerInput( - placeholder: contact.username!, - validator: .init( - wrongIcon: .image(Asset.sharedError.image), - correctIcon: .image(Asset.sharedSuccess.image), - shouldAcceptPlaceholder: true - ), - spacingAfter: 29 - ) - - items.append(drawerNicknameInput) - - let drawerSaveButton = DrawerCapsuleButton( - model: .init( - title: Localized.Ud.NicknameDrawer.save, - style: .brandColored - ), spacingAfter: 5 - ) - - items.append(drawerSaveButton) - - let drawer = DrawerController(items) - var nickname: String? - var allowsSave = true - - drawerNicknameInput.validationPublisher - .receive(on: DispatchQueue.main) - .sink { allowsSave = $0 } - .store(in: &drawerCancellables) - - drawerNicknameInput.inputPublisher - .receive(on: DispatchQueue.main) - .sink { - guard !$0.isEmpty else { - nickname = contact.username - return - } - - nickname = $0 - } - .store(in: &drawerCancellables) - - drawerSaveButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard allowsSave else { return } - - drawer.dismiss(animated: true) { - self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact) - } - } - .store(in: &drawerCancellables) - - coordinator.toNicknameDrawer(drawer, from: self) - } - - private func presentRequestDrawer(forContact contact: Contact) { - var items: [DrawerItem] = [] + items.append(drawerPhone) - let drawerTitle = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: Localized.Ud.RequestDrawer.title, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - var subtitleFragment = "Share your information with #\(contact.username ?? "")" + drawerPhone.isOnPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.isSharingPhone = $0 } + .store(in: &drawerCancellables) + } - if let email = contact.email { - subtitleFragment.append(contentsOf: " (\(email))#") - } else if let phone = contact.phone { - subtitleFragment.append(contentsOf: " (\(Country.findFrom(phone).prefix) \(phone.dropLast(2)))#") - } else { - subtitleFragment.append(contentsOf: "#") + let drawerSendButton = DrawerCapsuleButton( + model: .init( + title: Localized.Ud.RequestDrawer.send, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerCancelButton = DrawerCapsuleButton( + model: .init( + title: Localized.Ud.RequestDrawer.cancel, + style: .simplestColoredBrand + ), spacingAfter: 5 + ) + + items.append(contentsOf: [drawerSendButton, drawerCancelButton]) + + drawerSendButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didTapRequest(contact: contact) } - - subtitleFragment.append(contentsOf: " so they know its you.") - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitleFragment, - color: Asset.neutralDark.color, - spacingAfter: 31.5, - customAttributes: [ - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .foregroundColor: Asset.brandPrimary.color - ] - ) - - items.append(contentsOf: [ - drawerTitle, - drawerSubtitle - ]) - - if let email = email { - let drawerEmail = DrawerSwitch( - title: Localized.Ud.RequestDrawer.email, - content: email, - spacingAfter: phone != nil ? 23 : 31, - isInitiallyOn: isSharingEmail - ) - - items.append(drawerEmail) - - drawerEmail.isOnPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.isSharingEmail = $0 } - .store(in: &drawerCancellables) + }.store(in: &drawerCancellables) + + drawerCancelButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() } + }.store(in: &drawerCancellables) - if let phone = phone { - let drawerPhone = DrawerSwitch( - title: Localized.Ud.RequestDrawer.phone, - content: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", - spacingAfter: 31, - isInitiallyOn: isSharingPhone - ) - - items.append(drawerPhone) - - drawerPhone.isOnPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.isSharingPhone = $0 } - .store(in: &drawerCancellables) - } - - let drawerSendButton = DrawerCapsuleButton( - model: .init( - title: Localized.Ud.RequestDrawer.send, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerCancelButton = DrawerCapsuleButton( - model: .init( - title: Localized.Ud.RequestDrawer.cancel, - style: .simplestColoredBrand - ), spacingAfter: 5 - ) - - items.append(contentsOf: [drawerSendButton, drawerCancelButton]) - let drawer = DrawerController(items) - - drawerSendButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didTapRequest(contact: contact) - } - }.store(in: &drawerCancellables) - - drawerCancelButton.action - .receive(on: DispatchQueue.main) - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + navigator.perform(PresentDrawer(items: items)) + } } extension SearchLeftController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let item = dataSource.itemIdentifier(for: indexPath) { - switch item { - case .stranger(let contact): - didTap(contact: contact) - case .connection(let contact): - didTap(contact: contact) - } - } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let item = dataSource.itemIdentifier(for: indexPath) { + switch item { + case .stranger(let contact): + didTap(contact: contact) + case .connection(let contact): + didTap(contact: contact) + } } + } - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { - (view as! UITableViewHeaderFooterView).textLabel?.textColor = Asset.neutralWeak.color - } + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + (view as! UITableViewHeaderFooterView).textLabel?.textColor = Asset.neutralWeak.color + } - private func didTap(contact: Contact) { - guard contact.authStatus == .stranger else { - coordinator.toContact(contact, from: self) - return - } - - presentRequestDrawer(forContact: contact) + private func didTap(contact: Contact) { + guard contact.authStatus == .stranger else { + navigator.perform(PresentContact(contact: contact)) + return } + + presentRequestDrawer(forContact: contact) + } } diff --git a/Sources/SearchFeature/Controllers/SearchRightController.swift b/Sources/SearchFeature/Controllers/SearchRightController.swift index 4541af0f..cbd97898 100644 --- a/Sources/SearchFeature/Controllers/SearchRightController.swift +++ b/Sources/SearchFeature/Controllers/SearchRightController.swift @@ -1,81 +1,88 @@ import UIKit import Combine +import XXNavigation import DependencyInjection final class SearchRightController: UIViewController { - @Dependency var coordinator: SearchCoordinating + @Dependency var navigator: Navigator - private lazy var screenView = SearchRightView() + private lazy var screenView = SearchRightView() - private var cancellables = Set<AnyCancellable>() - private let cameraController = CameraController() - private(set) var viewModel = SearchRightViewModel() + private var cancellables = Set<AnyCancellable>() + private let cameraController = CameraController() + private(set) var viewModel = SearchRightViewModel() - override func loadView() { - view = screenView - } + override func loadView() { + view = screenView + } - override func viewDidLoad() { - super.viewDidLoad() - screenView.layer.insertSublayer(cameraController.previewLayer, at: 0) - setupBindings() - } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + cameraController.previewLayer.frame = screenView.bounds + } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - cameraController.previewLayer.frame = screenView.bounds - } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.viewWillDisappear() + } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - viewModel.viewWillDisappear() - } + override func viewDidLoad() { + super.viewDidLoad() + screenView.layer.insertSublayer( + cameraController.previewLayer, at: 0 + ) - private func setupBindings() { - cameraController - .dataPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didScan(data: $0) } - .store(in: &cancellables) + cameraController + .dataPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didScan(data: $0) + }.store(in: &cancellables) - viewModel.cameraSemaphorePublisher - .removeDuplicates() - .receive(on: DispatchQueue.global()) - .sink { [unowned self] setOn in - if setOn { - cameraController.start() - } else { - cameraController.stop() - } - }.store(in: &cancellables) + viewModel + .cameraSemaphorePublisher + .removeDuplicates() + .receive(on: DispatchQueue.global()) + .sink { [unowned self] in + if $0 { + cameraController.start() + } else { + cameraController.stop() + } + }.store(in: &cancellables) - viewModel.foundPublisher - .receive(on: DispatchQueue.main) - .delay(for: 1, scheduler: DispatchQueue.main) - .sink { [unowned self] in coordinator.toContact($0, from: self) } - .store(in: &cancellables) + viewModel + .foundPublisher + .receive(on: DispatchQueue.main) + .delay(for: 1, scheduler: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentContact(contact: $0)) + }.store(in: &cancellables) - viewModel.statusPublisher - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) + viewModel + .statusPublisher + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) - screenView.actionButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch viewModel.statusSubject.value { - case .failed(.cameraPermission): - guard let url = URL(string: UIApplication.openSettingsURLString) else { return } - UIApplication.shared.open(url, options: [:]) - case .failed(.requestOpened): - coordinator.toRequests(from: self) - case .failed(.alreadyFriends): - coordinator.toContacts(from: self) - default: - break - } - }.store(in: &cancellables) - } + screenView + .actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch viewModel.statusSubject.value { + case .failed(.cameraPermission): + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url, options: [:]) + case .failed(.requestOpened): + navigator.perform(PresentRequests()) + case .failed(.alreadyFriends): + navigator.perform(PresentContactList()) + default: + break + } + }.store(in: &cancellables) + } } diff --git a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift deleted file mode 100644 index 053e5b65..00000000 --- a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift +++ /dev/null @@ -1,83 +0,0 @@ -import UIKit -import XXModels -import Countries -import Presentation -import ScrollViewController - -public protocol SearchCoordinating { - func toRequests(from: UIViewController) - func toContacts(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toNicknameDrawer(_: UIViewController, from: UIViewController) - func toCountries(from: UIViewController, _: @escaping (Country) -> Void) -} - -public struct SearchCoordinator { - var pushPresenter: Presenting = PushPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - - var contactsFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - - public init( - contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController - ) { - self.contactFactory = contactFactory - self.contactsFactory = contactsFactory - self.requestsFactory = requestsFactory - self.countriesFactory = countriesFactory - } -} - -extension SearchCoordinator: SearchCoordinating { - public func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - replacePresenter.present(screen, from: parent) - } - - public func toContacts(from parent: UIViewController) { - let screen = contactsFactory() - replacePresenter.present(screen, from: parent) - } - - public func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - public func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - public func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { - let screen = countriesFactory(onChoose) - pushPresenter.present(screen, from: parent) - } - - public func toNicknameDrawer(_ target: UIViewController, from parent: UIViewController) { - let screen = ScrollViewController.embedding(target) - fullscreenPresenter.present(screen, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift index b221c6fe..9306052b 100644 --- a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift @@ -39,7 +39,7 @@ final class SearchContainerViewModel { guard pushNotifications == false else { return } pushHandler.requestAuthorization { [weak self] result in - guard let self = self else { return } + guard let self else { return } switch result { case .success(let granted): diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index 969152bd..8f21ff47 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -116,7 +116,7 @@ final class SearchLeftViewModel { } retry(max: 5, retryStrategy: .delay(seconds: 2)) { [weak self] in - guard let self = self else { return } + guard let self else { return } do { let nrr = try self.messenger.cMix.get()!.getNodeRegistrationStatus() @@ -125,7 +125,7 @@ final class SearchLeftViewModel { throw NodeRegistrationError.unhealthyNet } }.finalCatch { [weak self] in - guard let self = self else { return } + guard let self else { return } if case .unhealthyNet = $0 as? NodeRegistrationError { self.hudController.show(.init(content: "Network is not healthy yet, try again within the next minute or so.")) @@ -147,7 +147,7 @@ final class SearchLeftViewModel { } backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { let report = try SearchUD.live( @@ -199,7 +199,7 @@ final class SearchLeftViewModel { contact.authStatus = .requesting backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.database.saveContact(contact) @@ -245,7 +245,7 @@ final class SearchLeftViewModel { contact.authStatus = .requesting backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try self.database.saveContact(contact) @@ -349,7 +349,7 @@ final class SearchLeftViewModel { Deferred { Future { promise in retry(max: timeout, retryStrategy: .delay(seconds: 1)) { [weak self] in - guard let self = self else { return } + guard let self else { return } _ = try self.messenger.cMix.get()!.getNodeRegistrationStatus() promise(.success(())) }.finalCatch { diff --git a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift index de6a06ea..d4361d50 100644 --- a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift @@ -46,7 +46,7 @@ final class SearchRightViewModel { func viewWillAppear() { permissions.requestCamera { [weak self] granted in - guard let self = self else { return } + guard let self else { return } if granted { self.statusSubject.value = .reading diff --git a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift index 7742ff1d..001c6687 100644 --- a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift +++ b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift @@ -44,7 +44,7 @@ final class SearchLeftPlaceholderView: UIView { .font: Fonts.Mulish.regular.font(size: 16.0) ], didTapInfo: { [weak self] in - guard let self = self else { return } + guard let self else { return } self.infoSubject.send(()) } ) diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift index 31674910..e46450c2 100644 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift @@ -2,112 +2,123 @@ import UIKit import Shared import Combine import Defaults +import XXNavigation import DrawerFeature import ScrollViewController import DependencyInjection public final class AccountDeleteController: UIViewController { - @KeyObject(.username, defaultValue: "") var username: String - - @Dependency var coordinator: SettingsCoordinating - - private lazy var screenView = AccountDeleteView() - private lazy var scrollViewController = ScrollViewController() - - private let viewModel = AccountDeleteViewModel() - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - - screenView.update(username: username) - - screenView.setInfoClosure { [weak self] in - self?.presentInfo( - title: Localized.Settings.Delete.Info.title, - subtitle: Localized.Settings.Delete.Info.subtitle - ) - } - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + @Dependency var navigator: Navigator + @KeyObject(.username, defaultValue: "") var username: String + + private lazy var screenView = AccountDeleteView() + private lazy var scrollViewController = ScrollViewController() + + private let viewModel = AccountDeleteViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + + screenView.update(username: username) + + screenView.setInfoClosure { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Settings.Delete.Info.title, + subtitle: Localized.Settings.Delete.Info.subtitle + ) } - - private func setupBindings() { - screenView.cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in dismiss(animated: true) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [unowned self] in screenView.update(status: $0 == username ? .valid("") : .invalid("")) } - .store(in: &cancellables) - - screenView.confirmButton.publisher(for: .touchUpInside) - .sink { [unowned self] in - DispatchQueue.global().async { [weak self] in - self?.viewModel.didTapDelete() - } - }.store(in: &cancellables) - - screenView.cancelButton.publisher(for: .touchUpInside) - .sink { [unowned self] in navigationController?.popViewController(animated: true) } - .store(in: &cancellables) - } - - private func presentInfo(title: String, subtitle: String) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action + } + + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } + + private func setupBindings() { + screenView + .cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dismiss(animated: true) + }.store(in: &cancellables) + + screenView + .inputField + .textPublisher + .sink { [unowned self] in + screenView.update( + status: $0 == username ? + .valid("") : .invalid("") ) - - let drawer = DrawerController([ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &cancellables) + + screenView + .confirmButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapDelete() + }.store(in: &cancellables) + + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + } + + private func presentInfo(title: String, subtitle: String) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ])) + } } diff --git a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift index 62dcd629..a0e33f9b 100644 --- a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift @@ -1,90 +1,111 @@ import UIKit import Shared import Combine +import XXNavigation import DependencyInjection public final class SettingsAdvancedController: UIViewController { - @Dependency private var coordinator: SettingsCoordinating - - private lazy var screenView = SettingsAdvancedView() - - private var cancellables = Set<AnyCancellable>() - private let viewModel = SettingsAdvancedViewModel() - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupBindings() - - viewModel.loadCachedSettings() - } - - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.Settings.Advanced.title - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) - - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupBindings() { - screenView.downloadLogsButton - .publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapDownloadLogs() } - .store(in: &cancellables) - - screenView.logRecordingSwitcher.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleRecordLogs() } - .store(in: &cancellables) - - screenView.showUsernamesSwitcher.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleShowUsernames() } - .store(in: &cancellables) - - screenView.crashReportingSwitcher.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleCrashReporting() } - .store(in: &cancellables) - - screenView.reportingSwitcher.switcherView - .publisher(for: .valueChanged) - .compactMap { [weak screenView] _ in screenView?.reportingSwitcher.switcherView.isOn } - .sink { [weak viewModel] isOn in viewModel?.didSetReporting(enabled: isOn) } - .store(in: &cancellables) - - viewModel.sharePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toActivityController(with: [$0], from: self) } - .store(in: &cancellables) - - viewModel.state - .removeDuplicates() - .map(\.isReportingOptional) - .sink { [unowned self] in screenView.reportingSwitcher.isHidden = !$0 } - .store(in: &cancellables) - - viewModel.state - .removeDuplicates() - .sink { [unowned self] state in - screenView.logRecordingSwitcher.switcherView.setOn(state.isRecordingLogs, animated: true) - screenView.crashReportingSwitcher.switcherView.setOn(state.isCrashReporting, animated: true) - screenView.showUsernamesSwitcher.switcherView.setOn(state.isShowingUsernames, animated: true) - screenView.reportingSwitcher.switcherView.setOn(state.isReportingEnabled, animated: true) - }.store(in: &cancellables) - } + @Dependency var navigator: Navigator + + private lazy var screenView = SettingsAdvancedView() + + private var cancellables = Set<AnyCancellable>() + private let viewModel = SettingsAdvancedViewModel() + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + + viewModel.loadCachedSettings() + } + + private func setupNavigationBar() { + let title = UILabel() + title.text = Localized.Settings.Advanced.title + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) + navigationItem.leftItemsSupplementBackButton = true + } + + private func setupBindings() { + screenView + .downloadLogsButton + .publisher(for: .touchUpInside) + .sink { [weak viewModel] in + viewModel?.didTapDownloadLogs() + }.store(in: &cancellables) + + screenView + .logRecordingSwitcher + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleRecordLogs() + }.store(in: &cancellables) + + screenView + .showUsernamesSwitcher + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleShowUsernames() + }.store(in: &cancellables) + + screenView + .crashReportingSwitcher + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleCrashReporting() + }.store(in: &cancellables) + + screenView + .reportingSwitcher + .switcherView + .publisher(for: .valueChanged) + .compactMap { [weak screenView] _ in + screenView?.reportingSwitcher.switcherView.isOn + }.sink { [weak viewModel] isOn in + viewModel?.didSetReporting(enabled: isOn) + }.store(in: &cancellables) + + viewModel + .sharePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentActivitySheet(items: [$0])) + }.store(in: &cancellables) + + viewModel + .state + .removeDuplicates() + .map(\.isReportingOptional) + .sink { [unowned self] in + screenView.reportingSwitcher.isHidden = !$0 + }.store(in: &cancellables) + + viewModel + .state + .removeDuplicates() + .sink { [unowned self] state in + screenView.logRecordingSwitcher.switcherView.setOn(state.isRecordingLogs, animated: true) + screenView.crashReportingSwitcher.switcherView.setOn(state.isCrashReporting, animated: true) + screenView.showUsernamesSwitcher.switcherView.setOn(state.isShowingUsernames, animated: true) + screenView.reportingSwitcher.switcherView.setOn(state.isReportingEnabled, animated: true) + }.store(in: &cancellables) + } } diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift index 5cce068e..08dce114 100644 --- a/Sources/SettingsFeature/Controllers/SettingsController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsController.swift @@ -1,13 +1,14 @@ import UIKit import Shared import Combine +import XXNavigation import DrawerFeature import DependencyInjection import ScrollViewController public final class SettingsController: UIViewController { + @Dependency var navigator: Navigator @Dependency var barStylist: StatusBarStylist - @Dependency var coordinator: SettingsCoordinating private lazy var scrollViewController = ScrollViewController() private lazy var screenView = SettingsView { @@ -80,47 +81,66 @@ public final class SettingsController: UIViewController { private func setupScrollView() { scrollViewController.view.backgroundColor = Asset.neutralWhite.color - addChild(scrollViewController) view.addSubview(scrollViewController.view) - - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() + } scrollViewController.didMove(toParent: self) scrollViewController.contentView = screenView } private func setupBindings() { - screenView.inAppNotifications.switcherView + screenView + .inAppNotifications + .switcherView .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleInAppNotifications() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didToggleInAppNotifications() + }.store(in: &cancellables) - screenView.dummyTraffic.switcherView + screenView + .dummyTraffic + .switcherView .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleDummyTraffic() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didToggleDummyTraffic() + }.store(in: &cancellables) - screenView.remoteNotifications.switcherView + screenView + .remoteNotifications + .switcherView .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didTogglePushNotifications() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didTogglePushNotifications() + }.store(in: &cancellables) - screenView.hideActiveApp.switcherView + screenView + .hideActiveApp + .switcherView .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleHideActiveApps() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didToggleHideActiveApps() + }.store(in: &cancellables) - screenView.icognitoKeyboard.switcherView + screenView + .icognitoKeyboard + .switcherView .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleIcognitoKeyboard() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didToggleIcognitoKeyboard() + }.store(in: &cancellables) - screenView.biometrics.switcherView + screenView + .biometrics + .switcherView .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleBiometrics() } - .store(in: &cancellables) + .sink { [weak viewModel] in + viewModel?.didToggleBiometrics() + }.store(in: &cancellables) - screenView.privacyPolicyButton + screenView + .privacyPolicyButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in @@ -133,7 +153,8 @@ public final class SettingsController: UIViewController { } }.store(in: &cancellables) - screenView.disclosuresButton + screenView + .disclosuresButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in @@ -146,32 +167,41 @@ public final class SettingsController: UIViewController { } }.store(in: &cancellables) - screenView.deleteButton + screenView + .deleteButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toDelete(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentSettingsAccountDelete()) + }.store(in: &cancellables) - screenView.accountBackupButton + screenView + .accountBackupButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toBackup(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentSettingsBackup()) + }.store(in: &cancellables) - screenView.advancedButton + screenView + .advancedButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toAdvanced(from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + navigator.perform(PresentSettingsAdvanced()) + }.store(in: &cancellables) - viewModel.state + viewModel + .state .map(\.isBiometricsPossible) .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.biometrics.switcherView.isEnabled = $0 } - .store(in: &cancellables) + .sink { [weak screenView] in + screenView?.biometrics.switcherView.isEnabled = $0 + }.store(in: &cancellables) - viewModel.state + viewModel + .state .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [unowned self] state in @@ -198,7 +228,27 @@ public final class SettingsController: UIViewController { cancelButton.setStyle(.seeThrough) cancelButton.setTitle(Localized.ChatList.Dashboard.cancel, for: .normal) - let drawer = DrawerController([ + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + action() + } + }.store(in: &drawerCancellables) + + cancelButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ DrawerImage( image: Asset.drawerNegative.image ), @@ -218,32 +268,11 @@ public final class SettingsController: UIViewController { spacing: 20.0, views: [actionButton, cancelButton] ) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - - action() - } - }.store(in: &drawerCancellables) - - cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) + navigator.perform(PresentMenu(currentItem: .settings)) } } @@ -258,8 +287,17 @@ extension SettingsController { style: .seeThrough, title: Localized.Settings.InfoDrawer.action ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - let drawer = DrawerController([ + navigator.perform(PresentDrawer(items: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -276,17 +314,6 @@ extension SettingsController { actionButton, FlexibleSpace() ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + ])) } } diff --git a/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift b/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift deleted file mode 100644 index 73de5491..00000000 --- a/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift +++ /dev/null @@ -1,70 +0,0 @@ -import UIKit -import Shared -import MenuFeature -import Presentation - -public protocol SettingsCoordinating { - func toBackup(from: UIViewController) - func toDelete(from: UIViewController) - func toAdvanced(from: UIViewController) - func toSideMenu(from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toActivityController(with: [Any], from: UIViewController) -} - -public struct SettingsCoordinator: SettingsCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var backupFactory: () -> UIViewController - var advancedFactory: () -> UIViewController - var accountDeleteFactory: () -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var activityControllerFactory: ([Any]) -> UIViewController - = { UIActivityViewController(activityItems: $0, applicationActivities: nil) } - - public init( - backupFactory: @escaping () -> UIViewController, - advancedFactory: @escaping () -> UIViewController, - accountDeleteFactory: @escaping () -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController - ) { - self.backupFactory = backupFactory - self.advancedFactory = advancedFactory - self.sideMenuFactory = sideMenuFactory - self.accountDeleteFactory = accountDeleteFactory - } -} - -public extension SettingsCoordinator { - func toAdvanced(from parent: UIViewController) { - let screen = advancedFactory() - pushPresenter.present(screen, from: parent) - } - - func toDelete(from parent: UIViewController) { - let screen = accountDeleteFactory() - pushPresenter.present(screen, from: parent) - } - - func toBackup(from parent: UIViewController) { - let screen = backupFactory() - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toActivityController(with items: [Any], from parent: UIViewController) { - let screen = activityControllerFactory(items) - modalPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.settings, parent) - sidePresenter.present(screen, from: parent) - } -} diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift index bb1b4cb3..55f48f6c 100644 --- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift @@ -1,5 +1,5 @@ -import Shared import Retry +import Shared import Combine import Defaults import Keychain @@ -51,7 +51,7 @@ final class AccountDeleteViewModel { )) } catch { DispatchQueue.main.async { [weak self] in - guard let self = self else { return } + guard let self else { return } self.hudController.show(.init(error: error)) } } diff --git a/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift index 3ec9be36..42814774 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift @@ -7,87 +7,87 @@ import ReportingFeature import DependencyInjection struct AdvancedViewState: Equatable { - var isRecordingLogs = false - var isCrashReporting = false - var isShowingUsernames = false - var isReportingEnabled = false - var isReportingOptional = false + var isRecordingLogs = false + var isCrashReporting = false + var isShowingUsernames = false + var isReportingEnabled = false + var isReportingOptional = false } final class SettingsAdvancedViewModel { - @KeyObject(.recordingLogs, defaultValue: true) var isRecordingLogs: Bool - @KeyObject(.crashReporting, defaultValue: true) var isCrashReporting: Bool + @KeyObject(.recordingLogs, defaultValue: true) var isRecordingLogs: Bool + @KeyObject(.crashReporting, defaultValue: true) var isCrashReporting: Bool - private var cancellables = Set<AnyCancellable>() - private let isShowingUsernamesKey = "isShowingUsernames" + private var cancellables = Set<AnyCancellable>() + private let isShowingUsernamesKey = "isShowingUsernames" - @Dependency private var logger: XXLogger - @Dependency private var crashReporter: CrashReporter - @Dependency private var reportingStatus: ReportingStatus + @Dependency private var logger: XXLogger + @Dependency private var crashReporter: CrashReporter + @Dependency private var reportingStatus: ReportingStatus - var sharePublisher: AnyPublisher<URL, Never> { shareRelay.eraseToAnyPublisher() } - private let shareRelay = PassthroughSubject<URL, Never>() + var sharePublisher: AnyPublisher<URL, Never> { shareRelay.eraseToAnyPublisher() } + private let shareRelay = PassthroughSubject<URL, Never>() - var state: AnyPublisher<AdvancedViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<AdvancedViewState, Never>(.init()) + var state: AnyPublisher<AdvancedViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<AdvancedViewState, Never>(.init()) - func loadCachedSettings() { - stateRelay.value.isRecordingLogs = isRecordingLogs - stateRelay.value.isCrashReporting = isCrashReporting - stateRelay.value.isReportingOptional = reportingStatus.isOptional() + func loadCachedSettings() { + stateRelay.value.isRecordingLogs = isRecordingLogs + stateRelay.value.isCrashReporting = isCrashReporting + stateRelay.value.isReportingOptional = reportingStatus.isOptional() - reportingStatus - .isEnabledPublisher() - .sink { [weak stateRelay] in stateRelay?.value.isReportingEnabled = $0 } - .store(in: &cancellables) + reportingStatus + .isEnabledPublisher() + .sink { [weak stateRelay] in stateRelay?.value.isReportingEnabled = $0 } + .store(in: &cancellables) - guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { - print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") - return - } - - guard let isShowingUsernames = defaults.value(forKey: isShowingUsernamesKey) as? Bool else { - defaults.set(false, forKey: isShowingUsernamesKey) - return - } + guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { + print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") + return + } - stateRelay.value.isShowingUsernames = isShowingUsernames + guard let isShowingUsernames = defaults.value(forKey: isShowingUsernamesKey) as? Bool else { + defaults.set(false, forKey: isShowingUsernamesKey) + return } - func didToggleShowUsernames() { - stateRelay.value.isShowingUsernames.toggle() + stateRelay.value.isShowingUsernames = isShowingUsernames + } - guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { - print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") - return - } + func didToggleShowUsernames() { + stateRelay.value.isShowingUsernames.toggle() - defaults.set(stateRelay.value.isShowingUsernames, forKey: isShowingUsernamesKey) + guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { + print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") + return } - func didToggleRecordLogs() { - if isRecordingLogs == true { - XXLogger.stop() - } else { - XXLogger.start() - } + defaults.set(stateRelay.value.isShowingUsernames, forKey: isShowingUsernamesKey) + } - isRecordingLogs.toggle() - stateRelay.value.isRecordingLogs.toggle() + func didToggleRecordLogs() { + if isRecordingLogs == true { + XXLogger.stop() + } else { + XXLogger.start() } - func didTapDownloadLogs() { - let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - shareRelay.send(url.appendingPathComponent("swiftybeaver.log")) - } + isRecordingLogs.toggle() + stateRelay.value.isRecordingLogs.toggle() + } - func didToggleCrashReporting() { - isCrashReporting.toggle() - stateRelay.value.isCrashReporting.toggle() - crashReporter.setEnabled(isCrashReporting) - } + func didTapDownloadLogs() { + let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + shareRelay.send(url.appendingPathComponent("swiftybeaver.log")) + } - func didSetReporting(enabled: Bool) { - reportingStatus.enable(enabled) - } + func didToggleCrashReporting() { + isCrashReporting.toggle() + stateRelay.value.isCrashReporting.toggle() + crashReporter.setEnabled(isCrashReporting) + } + + func didSetReporting(enabled: Bool) { + reportingStatus.enable(enabled) + } } diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index 3bea57be..4337264c 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -89,7 +89,7 @@ final class SettingsViewModel { } permissions.requestBiometrics { [weak self] result in - guard let self = self else { return } + guard let self else { return } switch result { case .success(let granted): @@ -112,7 +112,7 @@ final class SettingsViewModel { if enable == true { pushHandler.requestAuthorization { [weak self] result in - guard let self = self else { return } + guard let self else { return } switch result { case .success(let granted): @@ -129,7 +129,7 @@ final class SettingsViewModel { } } else { backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + guard let self else { return } do { try UnregisterForNotifications.live( @@ -140,6 +140,7 @@ final class SettingsViewModel { } catch { let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) self.hudController.show(.init(content: xxError)) + } self.pushNotifications = false diff --git a/Sources/SettingsFeature/Views/AccountDeleteView.swift b/Sources/SettingsFeature/Views/AccountDeleteView.swift index e4aef7f5..791bec29 100644 --- a/Sources/SettingsFeature/Views/AccountDeleteView.swift +++ b/Sources/SettingsFeature/Views/AccountDeleteView.swift @@ -3,118 +3,114 @@ import Shared import InputField final class AccountDeleteView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let iconImageView = UIImageView() - let inputField = InputField() - - let stackView = UIStackView() - let confirmButton = CapsuleButton() - let cancelButton = CapsuleButton() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - iconImageView.image = Asset.settingsDeleteLarge.image - - iconImageView.contentMode = .center - - inputField.setup( - style: .regular, - title: Localized.Settings.Delete.input, - placeholder: "", - leftView: .image(Asset.personGray.image), - subtitleColor: Asset.neutralDisabled.color, - allowsEmptySpace: false, - autocapitalization: .none - ) - - titleLabel.text = Localized.Settings.Delete.title - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) - - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .center - paragraph.lineHeightMultiple = 1.1 - - subtitleView.setup( - text: Localized.Settings.Delete.subtitle, - attributes: [ - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.regular.font(size: 16.0), - .paragraphStyle: paragraph - ], - didTapInfo: { self.didTapInfo?() } - ) - - confirmButton.setStyle(.red) - confirmButton.isEnabled = false - confirmButton.setTitle(Localized.Settings.Delete.delete, for: .normal) - cancelButton.setStyle(.simplestColoredRed) - cancelButton.setTitle(Localized.Settings.Delete.cancel, for: .normal) - - stackView.spacing = 12 - stackView.axis = .vertical - stackView.addArrangedSubview(confirmButton) - stackView.addArrangedSubview(cancelButton) - - addSubview(iconImageView) - addSubview(inputField) - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(stackView) - - iconImageView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) - make.centerX.equalToSuperview() - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(iconImageView.snp.bottom).offset(34) - make.centerX.equalToSuperview() - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(20) - make.left.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-50) - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(subtitleView.snp.bottom).offset(50) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } - - stackView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) - make.left.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-50) - make.bottom.equalToSuperview().offset(-44) - } + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let iconImageView = UIImageView() + let inputField = InputField() + + let stackView = UIStackView() + let confirmButton = CapsuleButton() + let cancelButton = CapsuleButton() + + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + iconImageView.image = Asset.settingsDeleteLarge.image + + iconImageView.contentMode = .center + + inputField.setup( + style: .regular, + title: Localized.Settings.Delete.input, + placeholder: "", + leftView: .image(Asset.personGray.image), + subtitleColor: Asset.neutralDisabled.color, + allowsEmptySpace: false, + autocapitalization: .none + ) + + titleLabel.text = Localized.Settings.Delete.title + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .center + paragraph.lineHeightMultiple = 1.1 + + subtitleView.setup( + text: Localized.Settings.Delete.subtitle, + attributes: [ + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.regular.font(size: 16.0), + .paragraphStyle: paragraph + ], + didTapInfo: { self.didTapInfo?() } + ) + + confirmButton.setStyle(.red) + confirmButton.isEnabled = false + confirmButton.setTitle(Localized.Settings.Delete.delete, for: .normal) + cancelButton.setStyle(.simplestColoredRed) + cancelButton.setTitle(Localized.Settings.Delete.cancel, for: .normal) + + stackView.spacing = 12 + stackView.axis = .vertical + stackView.addArrangedSubview(confirmButton) + stackView.addArrangedSubview(cancelButton) + + addSubview(iconImageView) + addSubview(inputField) + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(stackView) + + iconImageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(30) + $0.centerX.equalToSuperview() } + titleLabel.snp.makeConstraints { + $0.top.equalTo(iconImageView.snp.bottom).offset(34) + $0.centerX.equalToSuperview() + } + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + } + inputField.snp.makeConstraints { + $0.top.equalTo(subtitleView.snp.bottom).offset(50) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + } + stackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + $0.bottom.equalToSuperview().offset(-44) + } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func setInfoClosure(_ closure: @escaping () -> Void) { - didTapInfo = closure - } + func setInfoClosure(_ closure: @escaping () -> Void) { + didTapInfo = closure + } - func update(username: String) { - inputField.update(placeholder: username) - } + func update(username: String) { + inputField.update(placeholder: username) + } - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) - switch status { - case .valid: - confirmButton.isEnabled = true - case .invalid, .unknown: - confirmButton.isEnabled = false - } + switch status { + case .valid: + confirmButton.isEnabled = true + case .invalid, .unknown: + confirmButton.isEnabled = false } + } } diff --git a/Sources/SettingsFeature/Views/SettingsAdvancedView.swift b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift index 0e5e5d89..750a175c 100644 --- a/Sources/SettingsFeature/Views/SettingsAdvancedView.swift +++ b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift @@ -2,63 +2,60 @@ import UIKit import Shared final class SettingsAdvancedView: UIView { - let stackView = UIStackView() - let downloadLogsButton = UIButton() - let logRecordingSwitcher = SettingsSwitcher() - let crashReportingSwitcher = SettingsSwitcher() - let showUsernamesSwitcher = SettingsSwitcher() - let reportingSwitcher = SettingsSwitcher() - - init() { - super.init(frame: .zero) - - backgroundColor = Asset.neutralWhite.color - downloadLogsButton.setImage(Asset.settingsDownload.image, for: .normal) - - showUsernamesSwitcher.set( - title: Localized.Settings.Advanced.ShowUsername.title, - text: Localized.Settings.Advanced.ShowUsername.description, - icon: Asset.settingsHide.image - ) - - logRecordingSwitcher.set( - title: Localized.Settings.Advanced.Logs.title, - text: Localized.Settings.Advanced.Logs.description, - icon: Asset.settingsLogs.image, - extraAction: downloadLogsButton - ) - - crashReportingSwitcher.set( - title: Localized.Settings.Advanced.Crashes.title, - text: Localized.Settings.Advanced.Crashes.description, - icon: Asset.settingsCrash.image - ) - - reportingSwitcher.set( - title: Localized.Settings.Advanced.Reporting.title, - text: Localized.Settings.Advanced.Reporting.description, - icon: Asset.settingsCrash.image - ) - - stackView.axis = .vertical - stackView.addArrangedSubview(logRecordingSwitcher) - stackView.addArrangedSubview(crashReportingSwitcher) - stackView.addArrangedSubview(showUsernamesSwitcher) - stackView.addArrangedSubview(reportingSwitcher) - - stackView.setCustomSpacing(20, after: logRecordingSwitcher) - stackView.setCustomSpacing(10, after: crashReportingSwitcher) - stackView.setCustomSpacing(10, after: showUsernamesSwitcher) - stackView.setCustomSpacing(10, after: reportingSwitcher) - - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.top.equalToSuperview().offset(24) - $0.left.equalToSuperview().offset(16) - $0.right.equalToSuperview().offset(-16) - } + let stackView = UIStackView() + let downloadLogsButton = UIButton() + let logRecordingSwitcher = SettingsSwitcher() + let crashReportingSwitcher = SettingsSwitcher() + let showUsernamesSwitcher = SettingsSwitcher() + let reportingSwitcher = SettingsSwitcher() + + init() { + super.init(frame: .zero) + + backgroundColor = Asset.neutralWhite.color + downloadLogsButton.setImage(Asset.settingsDownload.image, for: .normal) + + showUsernamesSwitcher.set( + title: Localized.Settings.Advanced.ShowUsername.title, + text: Localized.Settings.Advanced.ShowUsername.description, + icon: Asset.settingsHide.image + ) + logRecordingSwitcher.set( + title: Localized.Settings.Advanced.Logs.title, + text: Localized.Settings.Advanced.Logs.description, + icon: Asset.settingsLogs.image, + extraAction: downloadLogsButton + ) + crashReportingSwitcher.set( + title: Localized.Settings.Advanced.Crashes.title, + text: Localized.Settings.Advanced.Crashes.description, + icon: Asset.settingsCrash.image + ) + reportingSwitcher.set( + title: Localized.Settings.Advanced.Reporting.title, + text: Localized.Settings.Advanced.Reporting.description, + icon: Asset.settingsCrash.image + ) + + stackView.axis = .vertical + stackView.addArrangedSubview(logRecordingSwitcher) + stackView.addArrangedSubview(crashReportingSwitcher) + stackView.addArrangedSubview(showUsernamesSwitcher) + stackView.addArrangedSubview(reportingSwitcher) + + stackView.setCustomSpacing(20, after: logRecordingSwitcher) + stackView.setCustomSpacing(10, after: crashReportingSwitcher) + stackView.setCustomSpacing(10, after: showUsernamesSwitcher) + stackView.setCustomSpacing(10, after: reportingSwitcher) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/SettingsFeature/Views/SettingsSwitcher.swift b/Sources/SettingsFeature/Views/SettingsSwitcher.swift index 8b928746..5a59b17f 100644 --- a/Sources/SettingsFeature/Views/SettingsSwitcher.swift +++ b/Sources/SettingsFeature/Views/SettingsSwitcher.swift @@ -2,218 +2,216 @@ import UIKit import Shared final class SettingsSwitcher: UIView { - let titleLabel = UILabel() - let textLabel = UILabel() - let iconImageView = UIImageView() - let separatorView = UIView() - let switcherView = UISwitch() - let stackView = UIStackView() - let verticalStackView = UIStackView() - - init() { - super.init(frame: .zero) - - textLabel.textColor = Asset.neutralWeak.color - titleLabel.textColor = Asset.neutralActive.color - switcherView.onTintColor = Asset.brandPrimary.color - separatorView.backgroundColor = Asset.neutralLine.color - - iconImageView.contentMode = .center - iconImageView.setContentHuggingPriority(.required, for: .horizontal) - - textLabel.numberOfLines = 0 - textLabel.font = Fonts.Mulish.regular.font(size: 12.0) - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - addSubview(stackView) - addSubview(separatorView) - - verticalStackView.spacing = 3 - verticalStackView.axis = .vertical - verticalStackView.addArrangedSubview(titleLabel) - verticalStackView.addArrangedSubview(textLabel) - - let icon = iconImageView.pinning(at: .top(0)) - - stackView.spacing = 8 - stackView.addArrangedSubview(icon) - stackView.addArrangedSubview(verticalStackView) - stackView.addArrangedSubview(switcherView.pinning(at: .top(0))) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(-20) - } - - separatorView.snp.makeConstraints { make in - make.height.equalTo(1) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } + let titleLabel = UILabel() + let textLabel = UILabel() + let iconImageView = UIImageView() + let separatorView = UIView() + let switcherView = UISwitch() + let stackView = UIStackView() + let verticalStackView = UIStackView() + + init() { + super.init(frame: .zero) + + textLabel.textColor = Asset.neutralWeak.color + titleLabel.textColor = Asset.neutralActive.color + switcherView.onTintColor = Asset.brandPrimary.color + separatorView.backgroundColor = Asset.neutralLine.color + + iconImageView.contentMode = .center + iconImageView.setContentHuggingPriority(.required, for: .horizontal) + + textLabel.numberOfLines = 0 + textLabel.font = Fonts.Mulish.regular.font(size: 12.0) + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + addSubview(stackView) + addSubview(separatorView) + + verticalStackView.spacing = 3 + verticalStackView.axis = .vertical + verticalStackView.addArrangedSubview(titleLabel) + verticalStackView.addArrangedSubview(textLabel) + + let icon = iconImageView.pinning(at: .top(0)) + + stackView.spacing = 8 + stackView.addArrangedSubview(icon) + stackView.addArrangedSubview(verticalStackView) + stackView.addArrangedSubview(switcherView.pinning(at: .top(0))) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(-20) + } + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func set( + title: String, + text: String? = nil, + icon: UIImage? = nil, + separator: Bool = true, + extraAction: UIButton? = nil + ) { + titleLabel.text = title + + if let content = text { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.5 + + textLabel.attributedText = NSAttributedString( + string: content, attributes: [.paragraphStyle: paragraphStyle] + ) + } else { + verticalStackView.removeArrangedSubview(textLabel) + } + + if let icon = icon { + iconImageView.image = icon + } else { + stackView.removeArrangedSubview(iconImageView) + } + + if let button = extraAction { + stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) } - required init?(coder: NSCoder) { nil } - - func set( - title: String, - text: String? = nil, - icon: UIImage? = nil, - separator: Bool = true, - extraAction: UIButton? = nil - ) { - titleLabel.text = title - - if let content = text { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.5 - - textLabel.attributedText = NSAttributedString( - string: content, attributes: [.paragraphStyle: paragraphStyle] - ) - } else { - verticalStackView.removeArrangedSubview(textLabel) - } - - if let icon = icon { - iconImageView.image = icon - } else { - stackView.removeArrangedSubview(iconImageView) - } - - if let button = extraAction { - stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) - } - - guard separator == true else { - separatorView.removeFromSuperview() - return - } + guard separator == true else { + separatorView.removeFromSuperview() + return } + } } final class SettingsInfoSwitcher: UIView { - let titleView = TextWithInfoView() - let textLabel = UILabel() - let iconImageView = UIImageView() - let separatorView = UIView() - let switcherView = UISwitch() - let stackView = UIStackView() - let verticalStackView = UIStackView() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - - textLabel.textColor = Asset.neutralWeak.color - switcherView.onTintColor = Asset.brandPrimary.color - separatorView.backgroundColor = Asset.neutralLine.color - - iconImageView.contentMode = .center - iconImageView.setContentHuggingPriority(.required, for: .horizontal) - - textLabel.numberOfLines = 0 - textLabel.font = Fonts.Mulish.regular.font(size: 12.0) - textLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - addSubview(stackView) - addSubview(separatorView) - - let titleContainer = UIView() - titleContainer.addSubview(titleView) - titleView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(-10) - make.left.equalToSuperview().offset(-4) - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(10) - } - - verticalStackView.spacing = 3 - verticalStackView.axis = .vertical - verticalStackView.addArrangedSubview(titleContainer.pinning(at: .left(0))) - verticalStackView.addArrangedSubview(textLabel) - - let icon = iconImageView.pinning(at: .top(0)) - - let switcherContainer = UIView() - switcherContainer.addSubview(switcherView) - switcherView.setContentCompressionResistancePriority(.required, for: .horizontal) - switcherContainer.setContentCompressionResistancePriority(.required, for: .horizontal) - - switcherView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - - stackView.spacing = 8 - stackView.addArrangedSubview(icon) - stackView.addArrangedSubview(verticalStackView) - stackView.addArrangedSubview(switcherContainer) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(-20) - } - - separatorView.snp.makeConstraints { make in - make.height.equalTo(1) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } + let titleView = TextWithInfoView() + let textLabel = UILabel() + let iconImageView = UIImageView() + let separatorView = UIView() + let switcherView = UISwitch() + let stackView = UIStackView() + let verticalStackView = UIStackView() + + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + + textLabel.textColor = Asset.neutralWeak.color + switcherView.onTintColor = Asset.brandPrimary.color + separatorView.backgroundColor = Asset.neutralLine.color + + iconImageView.contentMode = .center + iconImageView.setContentHuggingPriority(.required, for: .horizontal) + + textLabel.numberOfLines = 0 + textLabel.font = Fonts.Mulish.regular.font(size: 12.0) + textLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + addSubview(stackView) + addSubview(separatorView) + + let titleContainer = UIView() + titleContainer.addSubview(titleView) + titleView.snp.makeConstraints { + $0.top.equalToSuperview().offset(-10) + $0.left.equalToSuperview().offset(-4) + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(10) + } + + verticalStackView.spacing = 3 + verticalStackView.axis = .vertical + verticalStackView.addArrangedSubview(titleContainer.pinning(at: .left(0))) + verticalStackView.addArrangedSubview(textLabel) + + let icon = iconImageView.pinning(at: .top(0)) + + let switcherContainer = UIView() + switcherContainer.addSubview(switcherView) + switcherView.setContentCompressionResistancePriority(.required, for: .horizontal) + switcherContainer.setContentCompressionResistancePriority(.required, for: .horizontal) + + switcherView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() + } + + stackView.spacing = 8 + stackView.addArrangedSubview(icon) + stackView.addArrangedSubview(verticalStackView) + stackView.addArrangedSubview(switcherContainer) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(-20) + } + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func set( + title: String, + text: String? = nil, + icon: UIImage? = nil, + separator: Bool = true, + extraAction: UIButton? = nil, + didTapInfo: (() -> Void)? = nil + ) { + self.didTapInfo = didTapInfo + + titleView.setup( + text: title, + attributes: [ + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.semiBold.font(size: 14.0) as Any + ], didTapInfo: { self.didTapInfo?() } + ) + + if let content = text { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.5 + + textLabel.attributedText = NSAttributedString( + string: content, attributes: [.paragraphStyle: paragraphStyle] + ) + } else { + verticalStackView.removeArrangedSubview(textLabel) + } + + if let icon = icon { + iconImageView.image = icon + } else { + stackView.removeArrangedSubview(iconImageView) + } + + if let button = extraAction { + stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) } - required init?(coder: NSCoder) { nil } - - func set( - title: String, - text: String? = nil, - icon: UIImage? = nil, - separator: Bool = true, - extraAction: UIButton? = nil, - didTapInfo: (() -> Void)? = nil - ) { - self.didTapInfo = didTapInfo - - titleView.setup( - text: title, - attributes: [ - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.semiBold.font(size: 14.0) as Any - ], didTapInfo: { self.didTapInfo?() } - ) - - if let content = text { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.5 - - textLabel.attributedText = NSAttributedString( - string: content, attributes: [.paragraphStyle: paragraphStyle] - ) - } else { - verticalStackView.removeArrangedSubview(textLabel) - } - - if let icon = icon { - iconImageView.image = icon - } else { - stackView.removeArrangedSubview(iconImageView) - } - - if let button = extraAction { - stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) - } - - guard separator == true else { - separatorView.removeFromSuperview() - return - } + guard separator == true else { + separatorView.removeFromSuperview() + return } + } } diff --git a/Sources/SettingsFeature/Views/SettingsView.swift b/Sources/SettingsFeature/Views/SettingsView.swift index 8f17926c..450e8aa6 100644 --- a/Sources/SettingsFeature/Views/SettingsView.swift +++ b/Sources/SettingsFeature/Views/SettingsView.swift @@ -2,181 +2,176 @@ import UIKit import Shared final class SettingsView: UIView { - enum InfoTapped { - case dummyTraffic - case biometrics - case notifications - case icognitoKeyboard + enum InfoTapped { + case dummyTraffic + case biometrics + case notifications + case icognitoKeyboard + } + + let generalStack = UIStackView() + let generalTitle = UILabel() + let biometrics = SettingsInfoSwitcher() + let remoteNotifications = SettingsInfoSwitcher() + let dummyTraffic = SettingsInfoSwitcher() + let inAppNotifications = SettingsSwitcher() + + let chatStack = UIStackView() + let chatTitle = UILabel() + let hideActiveApp = SettingsSwitcher() + let icognitoKeyboard = SettingsInfoSwitcher() + + let otherStackView = UIStackView() + let privacyPolicyButton = RowButton() + let disclosuresButton = RowButton() + let advancedButton = RowButton() + let accountBackupButton = RowButton() + let deleteButton = RowButton() + + let didTap: (InfoTapped) -> Void + + init(didTap: @escaping (InfoTapped) -> Void) { + self.didTap = didTap + + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupGeneralStack() + setupChatStack() + setupOtherStack() + } + + required init?(coder: NSCoder) { nil } + + private func setupGeneralStack() { + generalTitle.text = Localized.Settings.general + generalTitle.textColor = Asset.neutralActive.color + generalTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) + + remoteNotifications.set( + title: Localized.Settings.RemoteNotifications.title, + text: Localized.Settings.RemoteNotifications.description, + icon: Asset.settingsNotifications.image + ) { self.didTap(.notifications) } + + inAppNotifications.set( + title: Localized.Settings.InAppNotifications.title, + text: Localized.Settings.InAppNotifications.description, + icon: Asset.settingsNotifications.image + ) + + dummyTraffic.set( + title: Localized.Settings.Traffic.title, + text: Localized.Settings.Traffic.subtitle, + icon: Asset.settingsBiometrics.image, + separator: false, + didTapInfo: { self.didTap(.dummyTraffic) } + ) + + biometrics.set( + title: Localized.Settings.Biometrics.title, + text: Localized.Settings.Biometrics.description, + icon: Asset.settingsBiometrics.image, + separator: false + ) { self.didTap(.biometrics) } + + generalStack.axis = .vertical + generalStack.addArrangedSubview(remoteNotifications) + generalStack.addArrangedSubview(dummyTraffic) + generalStack.addArrangedSubview(inAppNotifications) + generalStack.addArrangedSubview(biometrics) + + addSubview(generalTitle) + addSubview(generalStack) + + generalTitle.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.left.equalToSuperview().offset(21) } - let generalStack = UIStackView() - let generalTitle = UILabel() - let biometrics = SettingsInfoSwitcher() - let remoteNotifications = SettingsInfoSwitcher() - let dummyTraffic = SettingsInfoSwitcher() - let inAppNotifications = SettingsSwitcher() - - let chatStack = UIStackView() - let chatTitle = UILabel() - let hideActiveApp = SettingsSwitcher() - let icognitoKeyboard = SettingsInfoSwitcher() - - let otherStackView = UIStackView() - let privacyPolicyButton = RowButton() - let disclosuresButton = RowButton() - let advancedButton = RowButton() - let accountBackupButton = RowButton() - let deleteButton = RowButton() - - let didTap: (InfoTapped) -> Void - - init(didTap: @escaping (InfoTapped) -> Void) { - self.didTap = didTap - - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupGeneralStack() - setupChatStack() - setupOtherStack() + generalStack.snp.makeConstraints { + $0.top.equalTo(generalTitle.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) } - - required init?(coder: NSCoder) { nil } - - private func setupGeneralStack() { - generalTitle.text = Localized.Settings.general - generalTitle.textColor = Asset.neutralActive.color - generalTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) - - remoteNotifications.set( - title: Localized.Settings.RemoteNotifications.title, - text: Localized.Settings.RemoteNotifications.description, - icon: Asset.settingsNotifications.image - ) { self.didTap(.notifications) } - - inAppNotifications.set( - title: Localized.Settings.InAppNotifications.title, - text: Localized.Settings.InAppNotifications.description, - icon: Asset.settingsNotifications.image - ) - - dummyTraffic.set( - title: Localized.Settings.Traffic.title, - text: Localized.Settings.Traffic.subtitle, - icon: Asset.settingsBiometrics.image, - separator: false, - didTapInfo: { self.didTap(.dummyTraffic) } - ) - - biometrics.set( - title: Localized.Settings.Biometrics.title, - text: Localized.Settings.Biometrics.description, - icon: Asset.settingsBiometrics.image, - separator: false - ) { self.didTap(.biometrics) } - - generalStack.axis = .vertical - generalStack.addArrangedSubview(remoteNotifications) - generalStack.addArrangedSubview(dummyTraffic) - generalStack.addArrangedSubview(inAppNotifications) - generalStack.addArrangedSubview(biometrics) - - addSubview(generalTitle) - addSubview(generalStack) - - generalTitle.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.left.equalToSuperview().offset(21) - } - - generalStack.snp.makeConstraints { make in - make.top.equalTo(generalTitle.snp.bottom).offset(8) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } + } + + private func setupChatStack() { + chatTitle.text = Localized.Settings.chat + chatTitle.textColor = Asset.neutralActive.color + chatTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) + + hideActiveApp.set( + title: Localized.Settings.HideActiveApps.title, + text: Localized.Settings.HideActiveApps.description, + icon: Asset.settingsHide.image + ) + + icognitoKeyboard.set( + title: Localized.Settings.IcognitoKeyboard.title, + text: Localized.Settings.IcognitoKeyboard.description, + icon: Asset.settingsKeyboard.image + ) { self.didTap(.icognitoKeyboard) } + + chatStack.axis = .vertical + chatStack.addArrangedSubview(hideActiveApp) + chatStack.addArrangedSubview(icognitoKeyboard) + + addSubview(chatTitle) + addSubview(chatStack) + + chatTitle.snp.makeConstraints { + $0.top.equalTo(generalStack.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(21) } - - private func setupChatStack() { - chatTitle.text = Localized.Settings.chat - chatTitle.textColor = Asset.neutralActive.color - chatTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) - - hideActiveApp.set( - title: Localized.Settings.HideActiveApps.title, - text: Localized.Settings.HideActiveApps.description, - icon: Asset.settingsHide.image - ) - - icognitoKeyboard.set( - title: Localized.Settings.IcognitoKeyboard.title, - text: Localized.Settings.IcognitoKeyboard.description, - icon: Asset.settingsKeyboard.image - ) { self.didTap(.icognitoKeyboard) } - - chatStack.axis = .vertical - chatStack.addArrangedSubview(hideActiveApp) - chatStack.addArrangedSubview(icognitoKeyboard) - - addSubview(chatTitle) - addSubview(chatStack) - - chatTitle.snp.makeConstraints { make in - make.top.equalTo(generalStack.snp.bottom).offset(20) - make.left.equalToSuperview().offset(21) - } - - chatStack.snp.makeConstraints { make in - make.top.equalTo(chatTitle.snp.bottom).offset(8) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } + chatStack.snp.makeConstraints { + $0.top.equalTo(chatTitle.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) } - - private func setupOtherStack() { - privacyPolicyButton.setup( - title: Localized.Settings.privacyPolicy, - icon: Asset.settingsPrivacy.image, - separator: false - ) - - disclosuresButton.setup( - title: Localized.Settings.disclosures, - icon: Asset.settingsFolder.image - ) - - advancedButton.setup( - title: Localized.Settings.advanced, - icon: Asset.settingsAdvanced.image - ) - - accountBackupButton.setup( - title: Localized.Settings.Advanced.AccountBackup.title, - icon: Asset.settingsAdvanced.image, - style: .clean, - separator: false - ) - - deleteButton.setup( - title: Localized.Settings.delete, - icon: Asset.settingsDelete.image, - style: .delete, - separator: false - ) - - otherStackView.axis = .vertical - otherStackView.addArrangedSubview(privacyPolicyButton) - otherStackView.addArrangedSubview(disclosuresButton) - otherStackView.addArrangedSubview(accountBackupButton) - otherStackView.addArrangedSubview(advancedButton) - otherStackView.addArrangedSubview(deleteButton) - - addSubview(otherStackView) - - otherStackView.snp.makeConstraints { make in - make.top.equalTo(chatStack.snp.bottom).offset(15) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.bottom.lessThanOrEqualToSuperview().offset(-20) - } + } + + private func setupOtherStack() { + privacyPolicyButton.setup( + title: Localized.Settings.privacyPolicy, + icon: Asset.settingsPrivacy.image, + separator: false + ) + disclosuresButton.setup( + title: Localized.Settings.disclosures, + icon: Asset.settingsFolder.image + ) + advancedButton.setup( + title: Localized.Settings.advanced, + icon: Asset.settingsAdvanced.image + ) + accountBackupButton.setup( + title: Localized.Settings.Advanced.AccountBackup.title, + icon: Asset.settingsAdvanced.image, + style: .clean, + separator: false + ) + deleteButton.setup( + title: Localized.Settings.delete, + icon: Asset.settingsDelete.image, + style: .delete, + separator: false + ) + + otherStackView.axis = .vertical + otherStackView.addArrangedSubview(privacyPolicyButton) + otherStackView.addArrangedSubview(disclosuresButton) + otherStackView.addArrangedSubview(accountBackupButton) + otherStackView.addArrangedSubview(advancedButton) + otherStackView.addArrangedSubview(deleteButton) + + addSubview(otherStackView) + + otherStackView.snp.makeConstraints { + $0.top.equalTo(chatStack.snp.bottom).offset(15) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) + $0.bottom.lessThanOrEqualToSuperview().offset(-20) } + } } diff --git a/Sources/Shared/Controllers/RootViewController.swift b/Sources/Shared/Controllers/RootViewController.swift index 86f20217..ee367e2e 100644 --- a/Sources/Shared/Controllers/RootViewController.swift +++ b/Sources/Shared/Controllers/RootViewController.swift @@ -159,7 +159,7 @@ extension RootViewController { self.toastTimer?.invalidate() self.toastTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in - guard let self = self else { return } + guard let self else { return } self.dismiss(toastView: toastView) } } diff --git a/Sources/Shared/Models/Country.swift b/Sources/Shared/Models/Country.swift new file mode 100644 index 00000000..8d25ca75 --- /dev/null +++ b/Sources/Shared/Models/Country.swift @@ -0,0 +1,44 @@ +import Foundation + +public struct Country { + public var name: String + public var code: String + public var flag: String + public var regex: String + public var prefix: String + public var example: String + public var prefixWithFlag: String { "\(flag) \(prefix)" } + + public static func fromMyPhone() -> Self { + let all = all() + + guard let country = all.filter({ $0.code == Locale.current.regionCode }).first else { + return all.filter { $0.code == "US" }.first! + } + + return country + } + + public static func all() -> [Self] { + guard let url = Bundle.module.url(forResource: "country_codes", withExtension: "json"), + let data = try? Data(contentsOf: url), + let countries = try? JSONDecoder().decode([Country].self, from: data) else { + fatalError("Can't handle country codes json") + } + + return countries + } + + public static func findFrom(_ number: String) -> Self { + all().first { country in + let start = number.index(number.startIndex, offsetBy: number.count - 2) + let end = number.index(start, offsetBy: number.count - (number.count - 2)) + + return country.code == String(number[start ..< end]) + }! + } +} + +extension Country: Hashable {} +extension Country: Equatable {} +extension Country: Decodable {} diff --git a/Sources/Shared/Models/HUDModel.swift b/Sources/Shared/Models/HUDModel.swift index a68d3bc3..ddc8fb5c 100644 --- a/Sources/Shared/Models/HUDModel.swift +++ b/Sources/Shared/Models/HUDModel.swift @@ -5,6 +5,7 @@ public struct HUDModel { var content: String? var actionTitle: String? var hasDotAnimation: Bool + var isAutoDismissable: Bool var onTapClosure: (() -> Void)? public init( @@ -12,6 +13,7 @@ public struct HUDModel { content: String? = nil, actionTitle: String? = nil, hasDotAnimation: Bool = false, + isAutoDismissable: Bool = false, onTapClosure: (() -> Void)? = nil ) { self.title = title @@ -19,6 +21,7 @@ public struct HUDModel { self.actionTitle = actionTitle self.onTapClosure = onTapClosure self.hasDotAnimation = hasDotAnimation + self.isAutoDismissable = isAutoDismissable } public init( @@ -30,6 +33,7 @@ public struct HUDModel { self.actionTitle = actionTitle self.onTapClosure = onTapClosure self.title = Localized.Hud.Error.title + self.isAutoDismissable = onTapClosure == nil self.content = error.localizedDescription } } diff --git a/Sources/Shared/Models/MenuItem.swift b/Sources/Shared/Models/MenuItem.swift new file mode 100644 index 00000000..072b5033 --- /dev/null +++ b/Sources/Shared/Models/MenuItem.swift @@ -0,0 +1,11 @@ +public enum MenuItem { + case join + case scan + case chats + case share + case profile + case contacts + case requests + case settings + case dashboard +} diff --git a/Sources/Shared/Models/PermissionType.swift b/Sources/Shared/Models/PermissionType.swift new file mode 100644 index 00000000..43d28fbd --- /dev/null +++ b/Sources/Shared/Models/PermissionType.swift @@ -0,0 +1,5 @@ +public enum PermissionType { + case camera + case library + case microphone +} diff --git a/Sources/Countries/Resources/country_codes.json b/Sources/Shared/Resources/country_codes.json similarity index 100% rename from Sources/Countries/Resources/country_codes.json rename to Sources/Shared/Resources/country_codes.json diff --git a/Sources/TermsFeature/TermsConditionsController.swift b/Sources/TermsFeature/TermsConditionsController.swift index d426489d..8d722933 100644 --- a/Sources/TermsFeature/TermsConditionsController.swift +++ b/Sources/TermsFeature/TermsConditionsController.swift @@ -3,17 +3,17 @@ import WebKit import Shared import Combine import Defaults +import XXNavigation import DependencyInjection public final class TermsConditionsController: UIViewController { - @Dependency var coordinator: TermsCoordinator + @Dependency var navigator: Navigator @KeyObject(.username, defaultValue: nil) var username: String? @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool - private lazy var screenView = TermsConditionsView() - private var cancellables = Set<AnyCancellable>() + private lazy var screenView = TermsConditionsView() public override func loadView() { view = screenView @@ -30,7 +30,6 @@ public final class TermsConditionsController: UIViewController { public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - let gradient = CAGradientLayer() gradient.colors = [ UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, @@ -38,18 +37,16 @@ public final class TermsConditionsController: UIViewController { UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor ] - gradient.startPoint = CGPoint(x: 0, y: 0) gradient.endPoint = CGPoint(x: 1, y: 1) - gradient.frame = screenView.bounds screenView.layer.insertSublayer(gradient, at: 0) } public override func viewDidLoad() { super.viewDidLoad() - - screenView.radioComponent + screenView + .radioComponent .radioButton .publisher(for: .touchUpInside) .sink { [unowned self] in @@ -58,19 +55,20 @@ public final class TermsConditionsController: UIViewController { UIImpactFeedbackGenerator(style: .heavy).impactOccurred() }.store(in: &cancellables) - screenView.nextButton + screenView + .nextButton .publisher(for: .touchUpInside) .sink { [unowned self] in didAcceptTerms = true - - if let _ = username { - coordinator.presentChatList(self) + if username != nil { + navigator.perform(PresentChatList()) } else { - coordinator.presentUsername(self) + navigator.perform(PresentOnboardingUsername()) } }.store(in: &cancellables) - screenView.showTermsButton + screenView + .showTermsButton .publisher(for: .touchUpInside) .sink { [unowned self] _ in let webView = WKWebView() diff --git a/Sources/TermsFeature/TermsCoordinator.swift b/Sources/TermsFeature/TermsCoordinator.swift deleted file mode 100644 index 05e4fc62..00000000 --- a/Sources/TermsFeature/TermsCoordinator.swift +++ /dev/null @@ -1,25 +0,0 @@ -import UIKit -import Presentation - -public struct TermsCoordinator { - var presentChatList: (UIViewController) -> Void - var presentUsername: (UIViewController) -> Void -} - -public extension TermsCoordinator { - static func live( - usernameFactory: @escaping () -> UIViewController, - chatListFactory: @escaping () -> UIViewController - ) -> Self { - .init( - presentChatList: { parent in - let presenter = ReplacePresenter() - presenter.present(chatListFactory(), from: parent) - }, - presentUsername: { parent in - let presenter = PushPresenter() - presenter.present(usernameFactory(), from: parent) - } - ) - } -} diff --git a/Sources/VersionChecking/VersionChecking.swift b/Sources/VersionChecking/VersionChecking.swift index 820a2aac..5acc10b5 100644 --- a/Sources/VersionChecking/VersionChecking.swift +++ b/Sources/VersionChecking/VersionChecking.swift @@ -14,13 +14,7 @@ public struct VersionCheck { } public extension VersionCheck { - static let mock: Self = .init { $0(.outdated(.init( - appUrl: "https://testflight.apple.com/join/L1Rj0so3", - minimum: "2.0", - isRequired: false, - recommended: "5.0", - minimumMessage: "This app version is not supported anymore, please update to the latest version to keep enjoying our app" - ))) } + static let mock: Self = .init { $0(.upToDate) } static let live: Self = .init { completion in let request = URLRequest( diff --git a/Sources/XXNavigation/Actions/PresentChat.swift b/Sources/XXNavigation/Actions/PresentChat.swift deleted file mode 100644 index db7e63b6..00000000 --- a/Sources/XXNavigation/Actions/PresentChat.swift +++ /dev/null @@ -1,12 +0,0 @@ -import XXModels -import Navigation - -public struct PresentChat: Navigation.Action { - public var contact: Contact - public var animated: Bool = true - - public init(contact: Contact, animated: Bool = true) { - self.contact = contact - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentChatList.swift b/Sources/XXNavigation/Actions/PresentChatList.swift deleted file mode 100644 index 69f3f942..00000000 --- a/Sources/XXNavigation/Actions/PresentChatList.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentChatList: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentCountryList.swift b/Sources/XXNavigation/Actions/PresentCountryList.swift deleted file mode 100644 index adb7ee1d..00000000 --- a/Sources/XXNavigation/Actions/PresentCountryList.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentCountryList: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentDrawer.swift b/Sources/XXNavigation/Actions/PresentDrawer.swift deleted file mode 100644 index 01e878b3..00000000 --- a/Sources/XXNavigation/Actions/PresentDrawer.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Navigation -import DrawerFeature - -public struct PresentDrawer: Navigation.Action { - public var items: [DrawerItem] - public var animated: Bool = true - public var dismissable: Bool = true - - public init( - items: [DrawerItem], - animated: Bool = true, - dismissable: Bool = true - ) { - self.items = items - self.animated = animated - self.dismissable = dismissable - } -} diff --git a/Sources/XXNavigation/Actions/PresentGroupChat.swift b/Sources/XXNavigation/Actions/PresentGroupChat.swift deleted file mode 100644 index 39219036..00000000 --- a/Sources/XXNavigation/Actions/PresentGroupChat.swift +++ /dev/null @@ -1,12 +0,0 @@ -import XXModels -import Navigation - -public struct PresentGroupChat: Navigation.Action { - public var model: GroupInfo - public var animated: Bool = true - - public init(model: GroupInfo, animated: Bool = true) { - self.model = model - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentOnboardingCode.swift b/Sources/XXNavigation/Actions/PresentOnboardingCode.swift deleted file mode 100644 index 59f0926f..00000000 --- a/Sources/XXNavigation/Actions/PresentOnboardingCode.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Navigation - -public struct PresentOnboardingCode: Navigation.Action { - public var isEmail: Bool - public var content: String - public var animated: Bool = true - public var confirmationId: String - - public init( - isEmail: Bool, - content: String, - confirmationId: String, - animated: Bool = true - ) { - self.animated = animated - self.isEmail = isEmail - self.content = content - self.confirmationId = confirmationId - } -} diff --git a/Sources/XXNavigation/Actions/PresentOnboardingEmail.swift b/Sources/XXNavigation/Actions/PresentOnboardingEmail.swift deleted file mode 100644 index 6d418378..00000000 --- a/Sources/XXNavigation/Actions/PresentOnboardingEmail.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentOnboardingEmail: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentOnboardingPhone.swift b/Sources/XXNavigation/Actions/PresentOnboardingPhone.swift deleted file mode 100644 index 6a104049..00000000 --- a/Sources/XXNavigation/Actions/PresentOnboardingPhone.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentOnboardingPhone: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentOnboardingStart.swift b/Sources/XXNavigation/Actions/PresentOnboardingStart.swift deleted file mode 100644 index fc0bf229..00000000 --- a/Sources/XXNavigation/Actions/PresentOnboardingStart.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentOnboardingStart: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentOnboardingUsername.swift b/Sources/XXNavigation/Actions/PresentOnboardingUsername.swift deleted file mode 100644 index 5a828dc4..00000000 --- a/Sources/XXNavigation/Actions/PresentOnboardingUsername.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentOnboardingUsername: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentOnboardingWelcome.swift b/Sources/XXNavigation/Actions/PresentOnboardingWelcome.swift deleted file mode 100644 index 1c412089..00000000 --- a/Sources/XXNavigation/Actions/PresentOnboardingWelcome.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentOnboardingWelcome: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentRestoreList.swift b/Sources/XXNavigation/Actions/PresentRestoreList.swift deleted file mode 100644 index 8a7fbb5a..00000000 --- a/Sources/XXNavigation/Actions/PresentRestoreList.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Navigation - -public struct PresentRestoreList: Navigation.Action { - public var animated: Bool = true - - public init(animated: Bool = true) { - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentSearch.swift b/Sources/XXNavigation/Actions/PresentSearch.swift deleted file mode 100644 index 7cb33ba2..00000000 --- a/Sources/XXNavigation/Actions/PresentSearch.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Navigation - -public struct PresentSearch: Navigation.Action { - public var searching: String? - public var animated: Bool = true - - public init(searching: String? = nil, animated: Bool = true) { - self.searching = searching - self.animated = animated - } -} diff --git a/Sources/XXNavigation/Actions/PresentTermsAndConditions.swift b/Sources/XXNavigation/Actions/PresentTermsAndConditions.swift deleted file mode 100644 index 2df2ad38..00000000 --- a/Sources/XXNavigation/Actions/PresentTermsAndConditions.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Navigation - -public struct PresentTermsAndConditions: Navigation.Action { - public var animated: Bool = true - public var popAllowed: Bool = true - - public init( - animated: Bool = true, - popAllowed: Bool = true - ) { - self.animated = animated - self.popAllowed = popAllowed - } -} diff --git a/Sources/XXNavigation/Actions/PresentRequests.swift b/Sources/XXNavigation/Chat/PresentCamera.swift similarity index 52% rename from Sources/XXNavigation/Actions/PresentRequests.swift rename to Sources/XXNavigation/Chat/PresentCamera.swift index 0f9718f0..ad8bd440 100644 --- a/Sources/XXNavigation/Actions/PresentRequests.swift +++ b/Sources/XXNavigation/Chat/PresentCamera.swift @@ -1,7 +1,7 @@ import Navigation -public struct PresentRequests: Navigation.Action { - public var animated: Bool = true +public struct PresentCamera: Navigation.Action { + public var animated: Bool public init(animated: Bool = true) { self.animated = animated diff --git a/Sources/XXNavigation/Navigators/PresentChatNavigator.swift b/Sources/XXNavigation/Chat/PresentChat.swift similarity index 75% rename from Sources/XXNavigation/Navigators/PresentChatNavigator.swift rename to Sources/XXNavigation/Chat/PresentChat.swift index 2714bc55..52593d96 100644 --- a/Sources/XXNavigation/Navigators/PresentChatNavigator.swift +++ b/Sources/XXNavigation/Chat/PresentChat.swift @@ -3,6 +3,19 @@ import XXModels import Navigation import DependencyInjection +public struct PresentChat: Navigation.Action { + public var contact: Contact + public var animated: Bool + + public init( + contact: Contact, + animated: Bool = true + ) { + self.contact = contact + self.animated = animated + } +} + public struct PresentChatNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: (Contact) -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentChatListNavigator.swift b/Sources/XXNavigation/Chat/PresentChatList.swift similarity index 81% rename from Sources/XXNavigation/Navigators/PresentChatListNavigator.swift rename to Sources/XXNavigation/Chat/PresentChatList.swift index 0d83657f..8fc96301 100644 --- a/Sources/XXNavigation/Navigators/PresentChatListNavigator.swift +++ b/Sources/XXNavigation/Chat/PresentChatList.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentChatList: Navigation.Action { + public var animated: Bool = true + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentChatListNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentGroupChatNavigator.swift b/Sources/XXNavigation/Chat/PresentGroupChat.swift similarity index 75% rename from Sources/XXNavigation/Navigators/PresentGroupChatNavigator.swift rename to Sources/XXNavigation/Chat/PresentGroupChat.swift index a44485bc..7770475e 100644 --- a/Sources/XXNavigation/Navigators/PresentGroupChatNavigator.swift +++ b/Sources/XXNavigation/Chat/PresentGroupChat.swift @@ -3,6 +3,19 @@ import XXModels import Navigation import DependencyInjection +public struct PresentGroupChat: Navigation.Action { + public var model: GroupInfo + public var animated: Bool + + public init( + model: GroupInfo, + animated: Bool = true + ) { + self.model = model + self.animated = animated + } +} + public struct PresentGroupChatNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: (GroupInfo) -> UIViewController diff --git a/Sources/XXNavigation/Chat/PresentMemberList.swift b/Sources/XXNavigation/Chat/PresentMemberList.swift new file mode 100644 index 00000000..628c0613 --- /dev/null +++ b/Sources/XXNavigation/Chat/PresentMemberList.swift @@ -0,0 +1,15 @@ +import XXModels +import Navigation + +public struct PresentMemberList: Navigation.Action { + public var members: [Contact] + public var animated: Bool + + public init( + members: [Contact], + animated: Bool = true + ) { + self.members = members + self.animated = animated + } +} diff --git a/Sources/XXNavigation/Chat/PresentNewGroup.swift b/Sources/XXNavigation/Chat/PresentNewGroup.swift new file mode 100644 index 00000000..e04adf50 --- /dev/null +++ b/Sources/XXNavigation/Chat/PresentNewGroup.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentNewGroup: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentNewGroupNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentNewGroup, completion: @escaping () -> Void) { + let pushAction = Push(screen(), on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Chat/PresentWebsite.swift b/Sources/XXNavigation/Chat/PresentWebsite.swift new file mode 100644 index 00000000..cb46909b --- /dev/null +++ b/Sources/XXNavigation/Chat/PresentWebsite.swift @@ -0,0 +1,15 @@ +import Navigation +import Foundation + +public struct PresentWebsite: Navigation.Action { + public var url: URL + public var animated: Bool + + public init( + url: URL, + animated: Bool = true + ) { + self.url = url + self.animated = animated + } +} diff --git a/Sources/XXNavigation/Contact/PresentContact.swift b/Sources/XXNavigation/Contact/PresentContact.swift new file mode 100644 index 00000000..4addb00c --- /dev/null +++ b/Sources/XXNavigation/Contact/PresentContact.swift @@ -0,0 +1,36 @@ +import UIKit +import XXModels +import Navigation +import DependencyInjection + +public struct PresentContact: Navigation.Action { + public var contact: Contact + public var animated: Bool + + public init( + contact: Contact, + animated: Bool = true + ) { + self.contact = contact + self.animated = animated + } +} + +public struct PresentContactNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: (Contact) -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentContact, completion: @escaping () -> Void) { + let pushAction = Push(screen(action.contact), on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping (Contact) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Contact/PresentContactList.swift b/Sources/XXNavigation/Contact/PresentContactList.swift new file mode 100644 index 00000000..f6f328f4 --- /dev/null +++ b/Sources/XXNavigation/Contact/PresentContactList.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentContactList: Navigation.Action { + public var animated: Bool = true + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentContactListNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentContactList, completion: @escaping () -> Void) { + let setStackAction = SetStack([screen()], on: navigationController(), animated: action.animated) + navigator.perform(setStackAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Contact/PresentNickname.swift b/Sources/XXNavigation/Contact/PresentNickname.swift new file mode 100644 index 00000000..11d691e2 --- /dev/null +++ b/Sources/XXNavigation/Contact/PresentNickname.swift @@ -0,0 +1,17 @@ +import Navigation + +public struct PresentNickname: Navigation.Action { + public var prefilled: String? + public var completion: (String) -> Void + public var animated: Bool + + public init( + prefilled: String?, + completion: @escaping (String) -> Void, + animated: Bool = true + ) { + self.prefilled = prefilled + self.completion = completion + self.animated = animated + } +} diff --git a/Sources/XXNavigation/CustomActions/OpenLeft.swift b/Sources/XXNavigation/CustomActions/OpenLeft.swift new file mode 100644 index 00000000..4296f39e --- /dev/null +++ b/Sources/XXNavigation/CustomActions/OpenLeft.swift @@ -0,0 +1,310 @@ +import UIKit +import Navigation + +/// Open left view controller on provided parent view controller +public struct OpenLeft: Action { + /// - Parameters: + /// - viewController: View controller to present + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + _ viewController: UIViewController, + from parent: UIViewController, + animated: Bool = true + ) { + self.viewController = viewController + self.parent = parent + self.animated = animated + } + + /// View controller to present + public var viewController: UIViewController + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `OpenLeft` action +public struct OpenLeftNavigator: TypedNavigator { + let transitioningDelegate = SidePresenter() + + public init() {} + + public func perform(_ action: OpenLeft, completion: @escaping () -> Void) { + action.viewController.transitioningDelegate = transitioningDelegate + action.viewController.modalPresentationStyle = .overFullScreen + + action.parent.present( + action.viewController, + animated: action.animated, + completion: completion + ) + } +} + +final class SidePresenter: NSObject, UIViewControllerTransitioningDelegate { + public init(dismissInteractor: SideMenuDismissInteracting = SideMenuDismissInteractor(), + menuAnimator: SideMenuAnimating = SideMenuAnimator(), + viewAnimator: UIViewAnimating.Type = UIView.self) { + self.dismissInteractor = dismissInteractor + self.menuAnimator = menuAnimator + self.viewAnimator = viewAnimator + super.init() + } + + let dismissInteractor: SideMenuDismissInteracting + let menuAnimator: SideMenuAnimating + let viewAnimator: UIViewAnimating.Type + + // MARK: Presenting + + public func present(_ viewControllers: UIViewController..., from parent: UIViewController) { + guard let screen = viewControllers.first else { + fatalError("Tried to present empty list of view controllers") + } + + screen.modalPresentationStyle = .overFullScreen + screen.transitioningDelegate = self + parent.present(screen, animated: true) + } + + // MARK: UIViewControllerTransitioningDelegate + + public func animationController( + forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + SideMenuPresentTransition(dismissInteractor: dismissInteractor, + menuAnimator: menuAnimator, + viewAnimator: viewAnimator) + } + + public func animationController( + forDismissed dismissed: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + SideMenuDismissTransition(menuAnimator: menuAnimator, + viewAnimator: viewAnimator) + } + + public func interactionControllerForDismissal( + using animator: UIViewControllerAnimatedTransitioning + ) -> UIViewControllerInteractiveTransitioning? { + dismissInteractor.interactionInProgress ? dismissInteractor : nil + } +} + +public protocol SideMenuAnimating { + func animate(in containerView: UIView, to progress: CGFloat) +} + +public struct SideMenuAnimator: SideMenuAnimating { + public init() {} + + public func animate(in containerView: UIView, to progress: CGFloat) { + guard let fromView = containerView.viewWithTag(SideMenuPresentTransition.fromViewTag) + else { return } + + let cornerRadius = progress * 24 + let shadowOpacity = Float(progress) + let offsetX = containerView.bounds.size.width * 0.5 * progress + let offsetY = containerView.bounds.size.height * 0.08 * progress + let scale = 1 - (0.25 * progress) + + fromView.subviews.first?.layer.cornerRadius = cornerRadius + fromView.layer.shadowOpacity = shadowOpacity + fromView.transform = CGAffineTransform.identity + .translatedBy(x: offsetX, y: offsetY) + .scaledBy(x: scale, y: scale) + } +} + +import UIKit +import Shared + +public protocol SideMenuDismissInteracting: UIViewControllerInteractiveTransitioning { + var interactionInProgress: Bool { get } + + func setup(view: UIView, action: @escaping EmptyClosure) +} + +public final class SideMenuDismissInteractor: UIPercentDrivenInteractiveTransition, SideMenuDismissInteracting { + private var action: EmptyClosure? + private var shouldFinishTransition = false + + // MARK: SideMenuDismissInteracting + + public var interactionInProgress = false + + public func setup(view: UIView, action: @escaping EmptyClosure) { + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + view.addGestureRecognizer(panRecognizer) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) + view.addGestureRecognizer(tapRecognizer) + + self.action = action + } + + // MARK: Gesture handling + + @objc + private func handleTapGesture(_ recognizer: UITapGestureRecognizer) { + action?() + } + + @objc + private func handlePanGesture(_ recognizer: UIPanGestureRecognizer) { + guard let view = recognizer.view, + let containerView = view.superview + else { return } + + let viewWidth = containerView.bounds.size.width + guard viewWidth > 0 else { return } + + let translation = recognizer.translation(in: view) + let progress = min(1, max(0, -translation.x / (viewWidth * 0.8))) + + switch recognizer.state { + case .possible, .failed: + interactionInProgress = false + + case .began: + interactionInProgress = true + shouldFinishTransition = false + action?() + + case .changed: + shouldFinishTransition = progress >= 0.5 + update(progress) + + case .cancelled: + interactionInProgress = false + cancel() + + case .ended: + interactionInProgress = false + shouldFinishTransition ? finish() : cancel() + + @unknown default: + interactionInProgress = false + cancel() + } + } +} + +import UIKit + +final class SideMenuDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { + + init(menuAnimator: SideMenuAnimating, + viewAnimator: UIViewAnimating.Type) { + self.menuAnimator = menuAnimator + self.viewAnimator = viewAnimator + super.init() + } + + let menuAnimator: SideMenuAnimating + let viewAnimator: UIViewAnimating.Type + + // MARK: UIViewControllerAnimatedTransitioning + + func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.25 } + + func animateTransition(using context: UIViewControllerContextTransitioning) { + viewAnimator.animate( + withDuration: transitionDuration(using: context), + animations: { + self.menuAnimator.animate(in: context.containerView, to: 0) + }, + completion: { _ in + let isCancelled = context.transitionWasCancelled + context.completeTransition(isCancelled == false) + } + ) + } +} + +import UIKit +import Shared + +final class SideMenuPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { + static let fromViewTag = UUID().hashValue + + init( + dismissInteractor: SideMenuDismissInteracting, + menuAnimator: SideMenuAnimating, + viewAnimator: UIViewAnimating.Type + ) { + self.dismissInteractor = dismissInteractor + self.menuAnimator = menuAnimator + self.viewAnimator = viewAnimator + super.init() + } + + let dismissInteractor: SideMenuDismissInteracting + let menuAnimator: SideMenuAnimating + let viewAnimator: UIViewAnimating.Type + + // MARK: UIViewControllerAnimatedTransitioning + + func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.25 } + + func animateTransition(using context: UIViewControllerContextTransitioning) { + guard let fromVC = context.viewController(forKey: .from), + let fromSnapshot = fromVC.view.snapshotView(afterScreenUpdates: true), + let toVC = context.viewController(forKey: .to) + else { + context.completeTransition(false) + return + } + + context.containerView.addSubview(toVC.view) + toVC.view.frame = context.containerView.bounds + + let fromView = UIView() + fromView.tag = Self.fromViewTag + context.containerView.addSubview(fromView) + fromView.frame = context.containerView.bounds + fromView.layer.shadowColor = UIColor.black.cgColor + fromView.layer.shadowOpacity = 1 + fromView.layer.shadowOffset = .zero + fromView.layer.shadowRadius = 32 + fromView.addSubview(fromSnapshot) + fromSnapshot.frame = fromView.bounds + fromSnapshot.layer.cornerRadius = 0 + fromSnapshot.layer.masksToBounds = true + + dismissInteractor.setup( + view: fromView, + action: { fromVC.dismiss(animated: true) } + ) + + viewAnimator.animate( + withDuration: transitionDuration(using: context), + animations: { + self.menuAnimator.animate(in: context.containerView, to: 1) + }, + completion: { _ in + let isCancelled = context.transitionWasCancelled + context.completeTransition(isCancelled == false) + } + ) + } +} + +import UIKit +import Shared + +public protocol UIViewAnimating { + static func animate( + withDuration duration: TimeInterval, + animations: @escaping EmptyClosure, + completion: ((Bool) -> Void)? + ) +} + +extension UIView: UIViewAnimating {} diff --git a/Sources/XXNavigation/Export.swift b/Sources/XXNavigation/Export.swift new file mode 100644 index 00000000..dfaf1c7d --- /dev/null +++ b/Sources/XXNavigation/Export.swift @@ -0,0 +1 @@ +@_exported import Navigation diff --git a/Sources/XXNavigation/Navigators/PresentCountryListNavigator.swift b/Sources/XXNavigation/Navigators/PresentCountryListNavigator.swift deleted file mode 100644 index a5842927..00000000 --- a/Sources/XXNavigation/Navigators/PresentCountryListNavigator.swift +++ /dev/null @@ -1,23 +0,0 @@ -import UIKit -import Navigation -import DependencyInjection - -public struct PresentCountryListNavigator: TypedNavigator { - @Dependency var navigator: Navigator - var screen: () -> UIViewController - var navigationController: () -> UINavigationController - - public func perform(_ action: PresentCountryList, completion: @escaping () -> Void) { - if let topViewController = navigationController().topViewController { - let modalAction = PresentModal(screen(), from: topViewController) - } - } - - public init( - screen: @escaping () -> UIViewController, - navigationController: @escaping () -> UINavigationController - ) { - self.screen = screen - self.navigationController = navigationController - } -} diff --git a/Sources/XXNavigation/Navigators/PresentSearchNavigator.swift b/Sources/XXNavigation/Navigators/PresentSearchNavigator.swift deleted file mode 100644 index d4ce74c1..00000000 --- a/Sources/XXNavigation/Navigators/PresentSearchNavigator.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit -import Navigation -import DependencyInjection - -public struct PresentSearchNavigator: TypedNavigator { - @Dependency var navigator: Navigator - var screen: (String?) -> UIViewController - var navigationController: () -> UINavigationController - - public func perform(_ action: PresentSearch, completion: @escaping () -> Void) { - let setStackAction = SetStack([screen(action.searching)], on: navigationController(), animated: action.animated) - navigator.perform(setStackAction, completion: completion) - } - - public init( - screen: @escaping (String?) -> UIViewController, - navigationController: @escaping () -> UINavigationController - ) { - self.screen = screen - self.navigationController = navigationController - } -} diff --git a/Sources/XXNavigation/Navigators/PresentOnboardingCodeNavigator.swift b/Sources/XXNavigation/Onboarding/PresentOnboardingCode.swift similarity index 66% rename from Sources/XXNavigation/Navigators/PresentOnboardingCodeNavigator.swift rename to Sources/XXNavigation/Onboarding/PresentOnboardingCode.swift index 359dc109..be83f0d1 100644 --- a/Sources/XXNavigation/Navigators/PresentOnboardingCodeNavigator.swift +++ b/Sources/XXNavigation/Onboarding/PresentOnboardingCode.swift @@ -2,6 +2,25 @@ import UIKit import Navigation import DependencyInjection +public struct PresentOnboardingCode: Navigation.Action { + public var isEmail: Bool + public var content: String + public var confirmationId: String + public var animated: Bool + + public init( + isEmail: Bool, + content: String, + confirmationId: String, + animated: Bool = true + ) { + self.isEmail = isEmail + self.content = content + self.confirmationId = confirmationId + self.animated = animated + } +} + public struct PresentOnboardingCodeNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: (Bool, String, String) -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentOnboardingEmailNavigator.swift b/Sources/XXNavigation/Onboarding/PresentOnboardingEmail.swift similarity index 81% rename from Sources/XXNavigation/Navigators/PresentOnboardingEmailNavigator.swift rename to Sources/XXNavigation/Onboarding/PresentOnboardingEmail.swift index 190941d8..4d8905ad 100644 --- a/Sources/XXNavigation/Navigators/PresentOnboardingEmailNavigator.swift +++ b/Sources/XXNavigation/Onboarding/PresentOnboardingEmail.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentOnboardingEmail: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentOnboardingEmailNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentOnboardingPhoneNavigator.swift b/Sources/XXNavigation/Onboarding/PresentOnboardingPhone.swift similarity index 81% rename from Sources/XXNavigation/Navigators/PresentOnboardingPhoneNavigator.swift rename to Sources/XXNavigation/Onboarding/PresentOnboardingPhone.swift index 674a2ad3..e890add3 100644 --- a/Sources/XXNavigation/Navigators/PresentOnboardingPhoneNavigator.swift +++ b/Sources/XXNavigation/Onboarding/PresentOnboardingPhone.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentOnboardingPhone: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentOnboardingPhoneNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentOnboardingStartNavigator.swift b/Sources/XXNavigation/Onboarding/PresentOnboardingStart.swift similarity index 81% rename from Sources/XXNavigation/Navigators/PresentOnboardingStartNavigator.swift rename to Sources/XXNavigation/Onboarding/PresentOnboardingStart.swift index 050b6790..c7dfbc31 100644 --- a/Sources/XXNavigation/Navigators/PresentOnboardingStartNavigator.swift +++ b/Sources/XXNavigation/Onboarding/PresentOnboardingStart.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentOnboardingStart: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentOnboardingStartNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentOnboardingUsernameNavigator.swift b/Sources/XXNavigation/Onboarding/PresentOnboardingUsername.swift similarity index 81% rename from Sources/XXNavigation/Navigators/PresentOnboardingUsernameNavigator.swift rename to Sources/XXNavigation/Onboarding/PresentOnboardingUsername.swift index 964ee100..48fa7be5 100644 --- a/Sources/XXNavigation/Navigators/PresentOnboardingUsernameNavigator.swift +++ b/Sources/XXNavigation/Onboarding/PresentOnboardingUsername.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentOnboardingUsername: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentOnboardingUsernameNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentOnboardingWelcomeNavigator.swift b/Sources/XXNavigation/Onboarding/PresentOnboardingWelcome.swift similarity index 81% rename from Sources/XXNavigation/Navigators/PresentOnboardingWelcomeNavigator.swift rename to Sources/XXNavigation/Onboarding/PresentOnboardingWelcome.swift index e5fc1ed4..7a55f0e6 100644 --- a/Sources/XXNavigation/Navigators/PresentOnboardingWelcomeNavigator.swift +++ b/Sources/XXNavigation/Onboarding/PresentOnboardingWelcome.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentOnboardingWelcome: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentOnboardingWelcomeNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentTermsAndConditionsNavigator.swift b/Sources/XXNavigation/Onboarding/PresentTermsAndConditions.swift similarity index 76% rename from Sources/XXNavigation/Navigators/PresentTermsAndConditionsNavigator.swift rename to Sources/XXNavigation/Onboarding/PresentTermsAndConditions.swift index 61ad413e..a71b7f79 100644 --- a/Sources/XXNavigation/Navigators/PresentTermsAndConditionsNavigator.swift +++ b/Sources/XXNavigation/Onboarding/PresentTermsAndConditions.swift @@ -2,6 +2,19 @@ import UIKit import Navigation import DependencyInjection +public struct PresentTermsAndConditions: Navigation.Action { + public var popAllowed: Bool + public var animated: Bool + + public init( + popAllowed: Bool = true, + animated: Bool = true + ) { + self.popAllowed = popAllowed + self.animated = animated + } +} + public struct PresentTermsAndConditionsNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/PresentActivitySheet.swift b/Sources/XXNavigation/PresentActivitySheet.swift new file mode 100644 index 00000000..c13c1faa --- /dev/null +++ b/Sources/XXNavigation/PresentActivitySheet.swift @@ -0,0 +1,42 @@ +import UIKit +import XXModels +import Navigation +import DependencyInjection + +public struct PresentActivitySheet: Navigation.Action { + public var items: [Any] + public var animated: Bool + + public init( + items: [Any], + animated: Bool = true + ) { + self.items = items + self.animated = animated + } +} + +public struct PresentActivitySheetNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: ([Any]) -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentActivitySheet, completion: @escaping () -> Void) { + if let topViewController = navigationController().topViewController { + let modalAction = PresentModal( + screen(action.items), + from: topViewController, + animated: action.animated + ) + navigator.perform(modalAction, completion: completion) + } + } + + public init( + screen: @escaping ([Any]) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/PresentCountryList.swift b/Sources/XXNavigation/PresentCountryList.swift new file mode 100644 index 00000000..2a8504ea --- /dev/null +++ b/Sources/XXNavigation/PresentCountryList.swift @@ -0,0 +1,38 @@ +import UIKit +import Shared +import Navigation +import DependencyInjection + +public struct PresentCountryList: Navigation.Action { + public var completion: ((Country) -> Void) + public var animated: Bool + + public init( + completion: @escaping (Country) -> Void, + animated: Bool = true + ) { + self.animated = animated + self.completion = completion + } +} + +public struct PresentCountryListNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: (@escaping (Country) -> Void) -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentCountryList, completion: @escaping () -> Void) { + if let topViewController = navigationController().topViewController { + let modalAction = PresentModal(screen(action.completion), from: topViewController) + navigator.perform(modalAction, completion: completion) + } + } + + public init( + screen: @escaping (@escaping (Country) -> Void) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Navigators/PresentDrawerNavigator.swift b/Sources/XXNavigation/PresentDrawer.swift similarity index 73% rename from Sources/XXNavigation/Navigators/PresentDrawerNavigator.swift rename to Sources/XXNavigation/PresentDrawer.swift index aec72788..fe5812eb 100644 --- a/Sources/XXNavigation/Navigators/PresentDrawerNavigator.swift +++ b/Sources/XXNavigation/PresentDrawer.swift @@ -3,6 +3,22 @@ import Navigation import DrawerFeature import DependencyInjection +public struct PresentDrawer: Navigation.Action { + public var items: [DrawerItem] + public var dismissable: Bool + public var animated: Bool + + public init( + items: [DrawerItem], + dismissable: Bool = true, + animated: Bool = true + ) { + self.items = items + self.dismissable = dismissable + self.animated = animated + } +} + public struct PresentDrawerNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: ([DrawerItem]) -> UIViewController diff --git a/Sources/XXNavigation/PresentMenu.swift b/Sources/XXNavigation/PresentMenu.swift new file mode 100644 index 00000000..12976625 --- /dev/null +++ b/Sources/XXNavigation/PresentMenu.swift @@ -0,0 +1,42 @@ +import UIKit +import Shared +import Navigation +import DependencyInjection + +public struct PresentMenu: Navigation.Action { + public var currentItem: MenuItem + public var animated: Bool + + public init( + currentItem: MenuItem, + animated: Bool = true + ) { + self.currentItem = currentItem + self.animated = animated + } +} + +public struct PresentMenuNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var navigationController: () -> UINavigationController + var screen: (MenuItem) -> UIViewController + + public func perform(_ action: PresentMenu, completion: @escaping () -> Void) { + if let topViewController = navigationController().topViewController { + let openLeftAction = OpenLeft( + screen(action.currentItem), + from: topViewController, + animated: action.animated + ) + navigator.perform(openLeftAction, completion: completion) + } + } + + public init( + screen: @escaping (MenuItem) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/PresentPermissionRequest.swift b/Sources/XXNavigation/PresentPermissionRequest.swift new file mode 100644 index 00000000..f8d5e5e1 --- /dev/null +++ b/Sources/XXNavigation/PresentPermissionRequest.swift @@ -0,0 +1,38 @@ +import UIKit +import Shared +import Navigation +import DependencyInjection + +public struct PresentPermissionRequest: Navigation.Action { + public var type: PermissionType + public var animated: Bool + + public init( + type: PermissionType, + animated: Bool = true + ) { + self.type = type + self.animated = animated + } +} + +public struct PresentPermissionRequestNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: (PermissionType) -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentPermissionRequest, completion: @escaping () -> Void) { + if let topViewController = navigationController().topViewController { + let modalAction = PresentModal(screen(action.type), from: topViewController) + navigator.perform(modalAction, completion: completion) + } + } + + public init( + screen: @escaping (PermissionType) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/PresentPhotoLibrary.swift b/Sources/XXNavigation/PresentPhotoLibrary.swift new file mode 100644 index 00000000..d0170d0a --- /dev/null +++ b/Sources/XXNavigation/PresentPhotoLibrary.swift @@ -0,0 +1,34 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentPhotoLibrary: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentPhotoLibraryNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIImagePickerController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentPhotoLibrary, completion: @escaping () -> Void) { + if let topViewController = navigationController().topViewController { + let imagePicker = screen() + imagePicker.delegate = topViewController as? UIImagePickerControllerDelegate & UINavigationControllerDelegate + let modalAction = PresentModal(imagePicker, from: topViewController) + navigator.perform(modalAction, completion: completion) + } + } + + public init( + screen: @escaping () -> UIImagePickerController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/PresentScan.swift b/Sources/XXNavigation/PresentScan.swift new file mode 100644 index 00000000..def9482e --- /dev/null +++ b/Sources/XXNavigation/PresentScan.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentScan: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentScanNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentScan, completion: @escaping () -> Void) { + let setStackAction = SetStack([screen()], on: navigationController(), animated: action.animated) + navigator.perform(setStackAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/PresentSearch.swift b/Sources/XXNavigation/PresentSearch.swift new file mode 100644 index 00000000..d62a767b --- /dev/null +++ b/Sources/XXNavigation/PresentSearch.swift @@ -0,0 +1,43 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentSearch: Navigation.Action { + public var searching: String? + public var replacing: Bool + public var animated: Bool + + public init( + searching: String? = nil, + replacing: Bool = true, + animated: Bool = true + ) { + self.searching = searching + self.replacing = replacing + self.animated = animated + } +} + +public struct PresentSearchNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: (String?) -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentSearch, completion: @escaping () -> Void) { + let navAction: Action + if action.replacing { + navAction = SetStack([screen(action.searching)], on: navigationController(), animated: action.animated) + } else { + navAction = Push(screen(action.searching), on: navigationController(), animated: action.animated) + } + navigator.perform(navAction, completion: completion) + } + + public init( + screen: @escaping (String?) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Profile/PresentProfile.swift b/Sources/XXNavigation/Profile/PresentProfile.swift new file mode 100644 index 00000000..4eb70e97 --- /dev/null +++ b/Sources/XXNavigation/Profile/PresentProfile.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentProfile: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentProfileNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentProfile, completion: @escaping () -> Void) { + let setStackAction = SetStack([screen()], on: navigationController(), animated: action.animated) + navigator.perform(setStackAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Profile/PresentProfileCode.swift b/Sources/XXNavigation/Profile/PresentProfileCode.swift new file mode 100644 index 00000000..887c88b0 --- /dev/null +++ b/Sources/XXNavigation/Profile/PresentProfileCode.swift @@ -0,0 +1,42 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentProfileCode: Navigation.Action { + public var isEmail: Bool + public var content: String + public var confirmationId: String + public var animated: Bool + + public init( + isEmail: Bool, + content: String, + confirmationId: String, + animated: Bool = true + ) { + self.isEmail = isEmail + self.content = content + self.confirmationId = confirmationId + self.animated = animated + } +} + +public struct PresentProfileCodeNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: (Bool, String, String) -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentProfileCode, completion: @escaping () -> Void) { + let controller = screen(action.isEmail, action.content, action.confirmationId) + let pushAction = Push(controller, on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping (Bool, String, String) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Profile/PresentProfileEmail.swift b/Sources/XXNavigation/Profile/PresentProfileEmail.swift new file mode 100644 index 00000000..0a150af8 --- /dev/null +++ b/Sources/XXNavigation/Profile/PresentProfileEmail.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentProfileEmail: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentProfileEmailNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentProfileEmail, completion: @escaping () -> Void) { + let pushAction = Push(screen(), on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Profile/PresentProfilePhone.swift b/Sources/XXNavigation/Profile/PresentProfilePhone.swift new file mode 100644 index 00000000..37966c9d --- /dev/null +++ b/Sources/XXNavigation/Profile/PresentProfilePhone.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentProfilePhone: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentProfilePhoneNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentProfilePhone, completion: @escaping () -> Void) { + let pushAction = Push(screen(), on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/RestoreAndBackup/PresentPassphrase.swift b/Sources/XXNavigation/RestoreAndBackup/PresentPassphrase.swift new file mode 100644 index 00000000..b566ea3c --- /dev/null +++ b/Sources/XXNavigation/RestoreAndBackup/PresentPassphrase.swift @@ -0,0 +1,17 @@ +import Navigation + +public struct PresentPassphrase: Navigation.Action { + public var onCancel: () -> Void + public var onPasspharse: (String) -> Void + public var animated: Bool + + public init( + onCancel: @escaping () -> Void, + onPassphrase: @escaping (String) -> Void, + animated: Bool = true + ) { + self.onCancel = onCancel + self.onPasspharse = onPassphrase + self.animated = animated + } +} diff --git a/Sources/XXNavigation/Navigators/PresentRequestsNavigator.swift b/Sources/XXNavigation/RestoreAndBackup/PresentRequests.swift similarity index 82% rename from Sources/XXNavigation/Navigators/PresentRequestsNavigator.swift rename to Sources/XXNavigation/RestoreAndBackup/PresentRequests.swift index c39d439b..812da622 100644 --- a/Sources/XXNavigation/Navigators/PresentRequestsNavigator.swift +++ b/Sources/XXNavigation/RestoreAndBackup/PresentRequests.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentRequests: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentRequestsNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/Navigators/PresentRestoreListNavigator.swift b/Sources/XXNavigation/RestoreAndBackup/PresentRestoreList.swift similarity index 81% rename from Sources/XXNavigation/Navigators/PresentRestoreListNavigator.swift rename to Sources/XXNavigation/RestoreAndBackup/PresentRestoreList.swift index 0e9a783a..23b40ab0 100644 --- a/Sources/XXNavigation/Navigators/PresentRestoreListNavigator.swift +++ b/Sources/XXNavigation/RestoreAndBackup/PresentRestoreList.swift @@ -2,6 +2,14 @@ import UIKit import Navigation import DependencyInjection +public struct PresentRestoreList: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + public struct PresentRestoreListNavigator: TypedNavigator { @Dependency var navigator: Navigator var screen: () -> UIViewController diff --git a/Sources/XXNavigation/RestoreAndBackup/PresentSFTP.swift b/Sources/XXNavigation/RestoreAndBackup/PresentSFTP.swift new file mode 100644 index 00000000..d19e06c5 --- /dev/null +++ b/Sources/XXNavigation/RestoreAndBackup/PresentSFTP.swift @@ -0,0 +1,14 @@ +import Navigation + +public struct PresentSFTP: Navigation.Action { + public var completion: (String, String, String) -> Void + public var animated: Bool + + public init( + completion: @escaping (String, String, String) -> Void, + animated: Bool = true + ) { + self.completion = completion + self.animated = animated + } +} diff --git a/Sources/XXNavigation/Settings/PresentSettings.swift b/Sources/XXNavigation/Settings/PresentSettings.swift new file mode 100644 index 00000000..932a1868 --- /dev/null +++ b/Sources/XXNavigation/Settings/PresentSettings.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentSettings: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentSettingsNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentSettings, completion: @escaping () -> Void) { + let setStackAction = SetStack([screen()], on: navigationController(), animated: action.animated) + navigator.perform(setStackAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Settings/PresentSettingsAccountDelete.swift b/Sources/XXNavigation/Settings/PresentSettingsAccountDelete.swift new file mode 100644 index 00000000..74119fe2 --- /dev/null +++ b/Sources/XXNavigation/Settings/PresentSettingsAccountDelete.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentSettingsAccountDelete: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentSettingsAccountDeleteNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentSettingsAccountDelete, completion: @escaping () -> Void) { + let pushAction = Push(screen(), on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Settings/PresentSettingsAdvanced.swift b/Sources/XXNavigation/Settings/PresentSettingsAdvanced.swift new file mode 100644 index 00000000..fd258eba --- /dev/null +++ b/Sources/XXNavigation/Settings/PresentSettingsAdvanced.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentSettingsAdvanced: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentSettingsAdvancedNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentSettingsAdvanced, completion: @escaping () -> Void) { + let pushAction = Push(screen(), on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Sources/XXNavigation/Settings/PresentSettingsBackup.swift b/Sources/XXNavigation/Settings/PresentSettingsBackup.swift new file mode 100644 index 00000000..62a6e243 --- /dev/null +++ b/Sources/XXNavigation/Settings/PresentSettingsBackup.swift @@ -0,0 +1,30 @@ +import UIKit +import Navigation +import DependencyInjection + +public struct PresentSettingsBackup: Navigation.Action { + public var animated: Bool + + public init(animated: Bool = true) { + self.animated = animated + } +} + +public struct PresentSettingsBackupNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: () -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentSettingsBackup, completion: @escaping () -> Void) { + let pushAction = Push(screen(), on: navigationController(), animated: action.animated) + navigator.perform(pushAction, completion: completion) + } + + public init( + screen: @escaping () -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +} diff --git a/Tests/ChatFeatureTests/Coordinator/ChatCoordinatorSpec.swift b/Tests/ChatFeatureTests/Coordinator/ChatCoordinatorSpec.swift deleted file mode 100644 index afc03f67..00000000 --- a/Tests/ChatFeatureTests/Coordinator/ChatCoordinatorSpec.swift +++ /dev/null @@ -1,118 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ChatFeature - -final class ChatCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ChatCoordinator! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - - var retryController: UIViewController! - var contactController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - retryController = UIViewController() - contactController = UIViewController() - - sut = ChatCoordinator( - retryFactory: { retryController }, - contactFactory: { _ in contactController } - ) - - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - bottomPresenter = nil - retryController = nil - contactController = nil - } - - context("when presenting retry sheet") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toRetrySheet(from: parent) - } - - it("should present RetrySheetController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(retryController)) - } - } - - context("when presenting members list") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.toMembersList(target, from: parent) - } - - it("should present MembersController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting Popup") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting Contact") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toContact(.dummy, from: parent) - } - - it("should present ContactController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(contactController)) - } - } - - context("when presenting menu sheet") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.toMenuSheet(target, from: parent) - } - - it("should present SheetController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/ChatListFeatureTests/Coordinator/ChatListCoordinatorSpec.swift b/Tests/ChatListFeatureTests/Coordinator/ChatListCoordinatorSpec.swift deleted file mode 100644 index dc4a069c..00000000 --- a/Tests/ChatListFeatureTests/Coordinator/ChatListCoordinatorSpec.swift +++ /dev/null @@ -1,243 +0,0 @@ -import UIKit -import Quick -import Nimble -import MenuFeature -import TestHelpers - -@testable import ChatListFeature - -final class ChatListCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ChatListCoordinator! - var sider: PresenterDouble! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - - var scanScreen: UIViewController! - var searchScreen: UIViewController! - var profileScreen: UIViewController! - var settingsScreen: UIViewController! - var contactsScreen: UIViewController! - var requestsScreen: UIViewController! - - beforeEach { - sider = PresenterDouble() - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - - scanScreen = UIViewController() - searchScreen = UIViewController() - profileScreen = UIViewController() - settingsScreen = UIViewController() - contactsScreen = UIViewController() - requestsScreen = UIViewController() - - sut = ChatListCoordinator( - scanFactory: { scanScreen }, - searchFactory: { searchScreen }, - profileFactory: { profileScreen }, - settingsFactory: { settingsScreen }, - contactsFactory: { contactsScreen }, - requestsFactory: { requestsScreen } - ) - - sut.sider = sider - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - sider = nil - scanScreen = nil - searchScreen = nil - profileScreen = nil - settingsScreen = nil - contactsScreen = nil - requestsScreen = nil - bottomPresenter = nil - } - - context("when presenting chat") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.singleChatFactory = { _ in target } - sut.toSingleChat(with: .dummy, from: parent) - } - - it("should present ChatController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting side menu") { - var parent: Delegate! - var target: UIViewController! - - beforeEach { - parent = Delegate() - target = UIViewController() - sut.sideMenuFactory = { _ in target } - sut.toSideMenu(from: parent) - } - - it("should present side menu") { - expect(sider.didPresentFrom).to(be(parent)) - expect(sider.didPresentTarget).to(be(target)) - } - } - - context("when presenting search") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.searchFactory = { target } - sut.toSearch(from: parent) - } - - it("should present SearchController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting scan") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.scanFactory = { target } - sut.toScan(from: parent) - } - - it("should present ScanController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting profile") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.profileFactory = { target } - sut.toProfile(from: parent) - } - - it("should present ProfileController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting contacts") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactsFactory = { target } - sut.toContacts(from: parent) - } - - it("should present ContactListController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting settings") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.settingsFactory = { target } - sut.toSettings(from: parent) - } - - it("should present SettingsController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting group chat") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.groupChatFactory = { _ in target } - sut.toGroupChat(with: .init( - group: .dummy, - members: [], - lastMessage: nil - ), from: parent) - } - - it("should present GroupChatController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting requests") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.requestsFactory = { target } - sut.toRequests(from: parent) - } - - it("should present RequestsContainer") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} - -// MARK: - Delegate - -private final class Delegate: UIViewController, MenuDelegate { - func didSelect(item: MenuItem) {} -} diff --git a/Tests/ContactFeatureTests/Coordinator/ContactCoordinatorSpec.swift b/Tests/ContactFeatureTests/Coordinator/ContactCoordinatorSpec.swift deleted file mode 100644 index 1a4026f8..00000000 --- a/Tests/ContactFeatureTests/Coordinator/ContactCoordinatorSpec.swift +++ /dev/null @@ -1,129 +0,0 @@ -import UIKit -import Quick -import Nimble -import ChatFeature -import TestHelpers - -@testable import ContactFeature - -final class ContactCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ContactCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - var presenter: PresenterDouble! - var bottomPresenter: PresenterDouble! - - var requestsScreen: UIViewController! - - beforeEach { - pusher = PresenterDouble() - replacer = PresenterDouble() - presenter = PresenterDouble() - requestsScreen = UIViewController() - bottomPresenter = PresenterDouble() - - sut = ContactCoordinator(requestsFactory: { requestsScreen }) - - sut.pusher = pusher - sut.replacer = replacer - sut.presenter = presenter - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - presenter = nil - requestsScreen = nil - bottomPresenter = nil - } - - context("when presenting image picker") { - var parent: UIViewController! - var target: UIImagePickerController! - - beforeEach { - parent = UIViewController() - target = UIImagePickerController() - sut.imagePickerFactory = { target } - sut.toPhotos(from: parent) - } - - it("should present UIImagePickerController") { - expect(presenter.didPresentFrom).to(be(parent)) - expect(presenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting single chat") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.singleChatFactory = { _ in target } - sut.toSingleChat(with: .dummy, from: parent) - } - - it("should present ChatController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting nickname") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.nicknameFactory = { _,_ in target } - sut.toNickname(from: parent, prefilled: "", { _ in }) - } - - it("should present NickameController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting requests") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.requestsFactory = { target } - sut.toRequests(from: parent) - } - - it("should present RequestsController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/ContactListFeatureTests/Coordinator/ContactListCoordinatorSpec.swift b/Tests/ContactListFeatureTests/Coordinator/ContactListCoordinatorSpec.swift deleted file mode 100644 index ce494c1f..00000000 --- a/Tests/ContactListFeatureTests/Coordinator/ContactListCoordinatorSpec.swift +++ /dev/null @@ -1,162 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ContactListFeature - -final class ContactListCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ContactListCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - var bottomPresenter: PresenterDouble! - var fullscreenPresenter: PresenterDouble! - - var scanScreen: UIViewController! - var groupScreen: UIViewController! - var searchScreen: UIViewController! - var requestsScreen: UIViewController! - - beforeEach { - scanScreen = UIViewController() - searchScreen = UIViewController() - - sut = ContactListCoordinator( - scanFactory: { scanScreen }, - searchFactory: { searchScreen }, - newGroupFactory: { groupScreen }, - requestsFactory: { requestsScreen } - ) - - pusher = PresenterDouble() - replacer = PresenterDouble() - bottomPresenter = PresenterDouble() - fullscreenPresenter = PresenterDouble() - - sut.pusher = pusher - sut.replacer = replacer - sut.bottomPresenter = bottomPresenter - sut.fullscreenPresenter = fullscreenPresenter - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - scanScreen = nil - groupScreen = nil - searchScreen = nil - requestsScreen = nil - bottomPresenter = nil - fullscreenPresenter = nil - } - - context("when presenting contact details") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present contact details screen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting SearchScreen") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.searchFactory = { target } - sut.toSearch(from: parent) - } - - it("should present SearchScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting scan screen") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.scanFactory = { target } - sut.toScan(from: parent) - } - - it("should present qr screen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting new group") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.newGroupFactory = { target } - sut.toNewGroup(from: parent) - } - - it("should present new group") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting group chat") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.groupChatFactory = { _ in target } - sut.toGroupChat(with: .init( - group: .dummy, - members: [], - lastMessage: nil - ), from: parent) - } - - it("should present group chat") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting group popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.groupPopupFactory = { _,_ in target } - sut.toGroupPopup(with: 0, from: parent, { _,_ in }) - } - - it("should present group popup") { - expect(fullscreenPresenter.didPresentFrom).to(be(parent)) - } - } - } - } -} diff --git a/Tests/OnboardingFeatureTests/Coordinator/OnboardingCoordinatorSpec.swift b/Tests/OnboardingFeatureTests/Coordinator/OnboardingCoordinatorSpec.swift deleted file mode 100644 index fdf5e58c..00000000 --- a/Tests/OnboardingFeatureTests/Coordinator/OnboardingCoordinatorSpec.swift +++ /dev/null @@ -1,244 +0,0 @@ -import UIKit -import Quick -import Nimble -import Combine -import TestHelpers -import DependencyInjection - -@testable import OnboardingFeature - -final class OnboardingCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: OnboardingCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - var bottomPresenter: PresenterDouble! - var chatsController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - replacer = PresenterDouble() - bottomPresenter = PresenterDouble() - - chatsController = UIViewController() - - DependencyInjection.Container.shared - .register(StatusBarControllerDouble() as StatusBarStyleControlling) - - sut = OnboardingCoordinator(chatListFactory: { chatsController }) - - sut.pusher = pusher - sut.replacer = replacer - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - bottomPresenter = nil - chatsController = nil - } - - context("when presenting success") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.successFactory = { _ in target } - sut.toSuccess(isEmail: false, from: parent) - } - - it("should present OnboardingSuccessController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting username") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.usernameFactory = { _ in target } - sut.toUsername(with: "", from: parent) - } - - it("should present OnboardingUsernameController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting chats") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toChats(from: parent) - } - - it("should present ChatsController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(chatsController)) - } - } - - context("when presenting email") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.emailFactory = { target } - sut.toEmail(from: parent) - } - - it("should present OnboardingEmailController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting phone") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.phoneFactory = { target } - sut.toPhone(from: parent) - } - - it("should present OnboardingPhoneController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting countries") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.countriesFactory = { _ in target } - sut.toCountries(from: parent, { _ in }) - } - - it("should present CountriesController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting welcome") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.welcomeFactory = { target } - sut.toWelcome(from: parent) - } - - it("should present OnboardingWelcomeScreen") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting start") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.startFactory = { _ in target } - sut.toStart(with: "ndf", from: parent) - } - - it("should present OnboardingStartScreen") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting email confirmation") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.emailConfirmationFactory = { _,_ in target } - sut.toEmailConfirmation(with: .init(content: ""), from: parent, completion: { _ in }) - } - - it("should present OnboardingEmailConfirmationScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting phone confirmation") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.phoneConfirmationFactory = { _,_ in target } - sut.toPhoneConfirmation(with: .init(content: ""), from: parent, completion: { _ in }) - } - - it("should present OnboardingPhoneConfirmationScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} - -// MARK: - StatusBarControllerDouble - -private final class StatusBarControllerDouble: StatusBarStyleControlling { - var didSetStyle: UIStatusBarStyle? - - let style = CurrentValueSubject<UIStatusBarStyle, Never>(.lightContent) - var cancellables = Set<AnyCancellable>() - - init() { - style - .receive(on: DispatchQueue.main) - .sink { [unowned self] in didSetStyle = $0 } - .store(in: &cancellables) - } -} diff --git a/Tests/ProfileFeatureTests/Coordinator/ProfileCoordinatorSpec.swift b/Tests/ProfileFeatureTests/Coordinator/ProfileCoordinatorSpec.swift deleted file mode 100644 index 1352033a..00000000 --- a/Tests/ProfileFeatureTests/Coordinator/ProfileCoordinatorSpec.swift +++ /dev/null @@ -1,140 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ProfileFeature - -final class ProfileCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ProfileCoordinator! - var pusher: PresenterDouble! - var presenter: PresenterDouble! - var bottomPresenter: PresenterDouble! - - beforeEach { - sut = ProfileCoordinator() - pusher = PresenterDouble() - presenter = PresenterDouble() - bottomPresenter = PresenterDouble() - - sut.pusher = pusher - sut.presenter = presenter - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - presenter = nil - bottomPresenter = nil - } - - context("when presenting image picker") { - var parent: UIViewController! - var target: UIImagePickerController! - - beforeEach { - parent = UIViewController() - target = UIImagePickerController() - sut.imagePickerFactory = { target } - sut.toPhotos(from: parent) - } - - it("should present UIImagePickerController") { - expect(presenter.didPresentFrom).to(be(parent)) - expect(presenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting email") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.emailFactory = { target } - sut.toEmail(from: parent) - } - - it("should present ProfileEmailController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting phone") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.phoneFactory = { target } - sut.toPhone(from: parent) - } - - it("should present ProfilePhoneController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting countries") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.countriesFactory = { _ in target } - sut.toCountries(from: parent, { _ in }) - } - - it("should present CountriesController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting code") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.codeFactory = { _,_ in target } - - sut.toCode( - with: .init(content: ""), - from: parent - ) { _,_ in } - } - - it("should present CodeController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/RequestsFeatureTests/Coordinator/RequestsCoordinatorSpec.swift b/Tests/RequestsFeatureTests/Coordinator/RequestsCoordinatorSpec.swift deleted file mode 100644 index d27a5c47..00000000 --- a/Tests/RequestsFeatureTests/Coordinator/RequestsCoordinatorSpec.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Quick -import UIKit -import Nimble -import TestHelpers - -@testable import RequestsFeature - -final class RequestsCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: RequestsCoordinator! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - var searchController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - searchController = UIViewController() - - sut = RequestsCoordinator(searchFactory: { searchController }) - - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - bottomPresenter = nil - searchController = nil - } - - context("when presenting search") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toSearch(from: parent) - } - - it("should present SearchController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(searchController)) - } - } - - context("when presenting contact") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present ContactController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting VerifyingFactory") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.verifyingFactory = { target } - sut.toVerifying(from: parent) - } - - it("should present VerifyingScreen") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting nickname") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.nicknameFactory = { _,_ in target } - sut.toNickname(from: parent, prefilled: "", { _ in }) - } - - it("should present NicknameController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/ScanFeatureTests/Coordinator/ScanCoordinatorSpec.swift b/Tests/ScanFeatureTests/Coordinator/ScanCoordinatorSpec.swift deleted file mode 100644 index 1d35eb46..00000000 --- a/Tests/ScanFeatureTests/Coordinator/ScanCoordinatorSpec.swift +++ /dev/null @@ -1,87 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ScanFeature - -final class ScanCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ScanCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - - var contactsController: UIViewController! - var requestsController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - replacer = PresenterDouble() - contactsController = UIViewController() - requestsController = UIViewController() - - sut = ScanCoordinator( - contactsFactory: { contactsController }, - requestsFactory: { requestsController } - ) - - sut.pusher = pusher - sut.replacer = replacer - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - contactsController = nil - requestsController = nil - } - - context("when presenting add") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present ContactController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting contacts") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toContacts(from: parent) - } - - it("should present ContactListController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(contactsController)) - } - } - - context("when presenting requests") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toRequests(from: parent) - } - - it("should present RequestsController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(requestsController)) - } - } - } - } -} diff --git a/Tests/SearchFeatureTests/Coordinator/SearchCoordinatorSpec.swift b/Tests/SearchFeatureTests/Coordinator/SearchCoordinatorSpec.swift deleted file mode 100644 index cb8056fc..00000000 --- a/Tests/SearchFeatureTests/Coordinator/SearchCoordinatorSpec.swift +++ /dev/null @@ -1,80 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import SearchFeature - -final class SearchCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: SearchCoordinator! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - - beforeEach { - sut = SearchCoordinator() - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - bottomPresenter = nil - } - - context("when presenting add screen") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present AddScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting countries") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.countriesFactory = { _ in target } - sut.toCountries(from: parent, { _ in }) - } - - it("should present CountriesController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/SettingsFeatureTests/Coordinator/SettingsCoordinatorSpec.swift b/Tests/SettingsFeatureTests/Coordinator/SettingsCoordinatorSpec.swift deleted file mode 100644 index 610e7b9b..00000000 --- a/Tests/SettingsFeatureTests/Coordinator/SettingsCoordinatorSpec.swift +++ /dev/null @@ -1,79 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import SettingsFeature - -final class SettingsCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: SettingsCoordinator! - var pusher: PresenterDouble! - var presenter: PresenterDouble! - var bottomPresenter: PresenterDouble! - - beforeEach { - pusher = PresenterDouble() - presenter = PresenterDouble() - bottomPresenter = PresenterDouble() - - sut = SettingsCoordinator() - - sut.pusher = pusher - sut.presenter = presenter - sut.bottomPresenter = bottomPresenter - } - - context("when presenting advanced settings") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.advancedFactory = { target } - sut.toAdvanced(from: parent) - } - - it("should present AdvancedScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting Popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting ActivityViewController") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.activityControllerFactory = { _ in target } - sut.toActivityController(with: [0], from: parent) - } - - it("should present ActivityViewController") { - expect(presenter.didPresentFrom).to(be(parent)) - expect(presenter.didPresentTarget).to(be(target)) - } - } - } - } -} -- GitLab