From 75621c2badab65550e0b9290c63b573085ba561a Mon Sep 17 00:00:00 2001 From: AshCenso Date: Mon, 1 Oct 2018 18:26:53 +0300 Subject: [PATCH 001/138] in progress --- Nynja copy-Info.plist | 122 ++++++ Nynja.xcodeproj/project.pbxproj | 293 ++++++++++++- Nynja/AppDelegate.swift | 295 +++++++++++++ .../SwiftLibrary/Array/ArrayExtension.swift | 31 ++ Nynja/Library/Result/Result.swift | 154 +++++++ .../UI/UIImageView/UIImageExtensions.swift | 2 + .../UIViewControllerExtensions.swift | 25 ++ Nynja/Library/UI/View/UIViewExtensions.swift | 9 +- Nynja/Modules/Auth/AuthCoordinator.swift | 62 +++ .../Auth/AuthModule/AuthProtocols.swift | 34 ++ .../AuthModule/Entities/LoginOption.swift | 14 + .../Interactor/AuthInteractor.swift | 26 ++ .../AuthModule/Presenter/AuthPresenter.swift | 50 +++ .../AuthModule/View/AuthViewController.swift | 138 ++++++ .../View/Subviews/AuthHeaderView.swift | 66 +++ .../View/Subviews/EmailLoginView.swift | 152 +++++++ .../View/Subviews/LoginOptionsView.swift | 154 +++++++ .../View/Subviews/PhoneNumberLoginView.swift | 30 ++ .../AuthModule/Wireframe/AuthWireframe.swift | 38 ++ .../CodeConfirmationProtocols.swift | 47 ++ .../CodeConfirmationInteractor.swift | 50 +++ .../Presenter/CodeConfirmationPresenter.swift | 105 +++++ .../View/CodeConfirmationViewController.swift | 406 ++++++++++++++++++ .../Wireframe/CodeConfirmationWireframe.swift | 69 +++ .../CountrySelectorProtocols.swift | 36 ++ .../CountrySelector/Entities/Country.swift | 19 + .../CountrySelectorInteractor.swift | 54 +++ .../Presenter/CountrySelectorPresenter.swift | 69 +++ .../View/Cells/CountryTVCell.swift | 103 +++++ .../View/CountrySelectorViewController.swift | 197 +++++++++ .../View/Headers/CountryTVHeader.swift | 74 ++++ .../Wireframe/CountrySelectorWireframe.swift | 56 +++ .../LanguageSelectroInteractor.swift | 5 +- .../MessageInteractor+Translation.swift | 11 +- .../Contents.json | 23 + .../Icons_General_ic_accept_call.png | Bin 0 -> 468 bytes .../Icons_General_ic_accept_call@2x.png | Bin 0 -> 908 bytes .../Icons_General_ic_accept_call@3x.png | Bin 0 -> 1287 bytes .../Contents.json | 23 + .../Icons_General_ic_google.png | Bin 0 -> 740 bytes .../Icons_General_ic_google@2x.png | Bin 0 -> 1367 bytes .../Icons_General_ic_google@3x.png | Bin 0 -> 2015 bytes .../ic_facebook.imageset/Contents.json | 23 + .../ic_facebook.imageset/ic_facebook.png | Bin 0 -> 292 bytes .../ic_facebook.imageset/ic_facebook@2x.png | Bin 0 -> 407 bytes .../ic_facebook.imageset/ic_facebook@3x.png | Bin 0 -> 517 bytes .../logo-2.imageset/Contents.json | 23 + .../Assets.xcassets/logo-2.imageset/logo.png | Bin 0 -> 2758 bytes .../logo-2.imageset/logo@2x.png | Bin 0 -> 5377 bytes .../logo-2.imageset/logo@3x.png | Bin 0 -> 8577 bytes Nynja/Services/StorageService.swift | 12 +- .../TranslationService.swift | 23 +- 52 files changed, 3099 insertions(+), 24 deletions(-) create mode 100644 Nynja copy-Info.plist create mode 100644 Nynja/Library/Result/Result.swift create mode 100644 Nynja/Library/UI/UIViewControllerExtensions/UIViewControllerExtensions.swift create mode 100644 Nynja/Modules/Auth/AuthCoordinator.swift create mode 100644 Nynja/Modules/Auth/AuthModule/AuthProtocols.swift create mode 100644 Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift create mode 100644 Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift create mode 100644 Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift create mode 100644 Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift create mode 100644 Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift create mode 100644 Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift create mode 100644 Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift create mode 100644 Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift create mode 100644 Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift create mode 100644 Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift create mode 100644 Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift create mode 100644 Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift create mode 100644 Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift create mode 100644 Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/Entities/Country.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call.png create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@2x.png create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@3x.png create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google.png create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@2x.png create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@3x.png create mode 100644 Nynja/Resources/Assets.xcassets/ic_facebook.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook.png create mode 100644 Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@2x.png create mode 100644 Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@3x.png create mode 100644 Nynja/Resources/Assets.xcassets/logo-2.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/logo-2.imageset/logo.png create mode 100644 Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@2x.png create mode 100644 Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@3x.png diff --git a/Nynja copy-Info.plist b/Nynja copy-Info.plist new file mode 100644 index 000000000..5472ab2b7 --- /dev/null +++ b/Nynja copy-Info.plist @@ -0,0 +1,122 @@ + + + + + AppGroup + $(AppGroup) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(AppName) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 0.2.158 + ConfServerAddress + $(ConfServerAddress) + ConfServerPort + $(ConfServerPort) + ConfServerSecure + $(ConfServerSecure) + Config + $(Config) + Fabric + + APIKey + 595b68a8c4deb3533dcdfc24ca73fd3cffd99f3c + Kits + + + KitInfo + + KitName + Crashlytics + + + + LSApplicationQueriesSchemes + + cydia + comgooglemapsurl + comgooglemaps + googlechromes + + LSRequiresIPhoneOS + + ModelsVersion + $(ModelsVersion) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCameraUsageDescription + NYNJA needs it to allow you make photos and video calls. + NSContactsUsageDescription + NYNJA needs it to allow you add new contacts from your phone contact book. + NSLocationAlwaysUsageDescription + NYNJA needs to know your location so that you can be able to share it. + NSLocationWhenInUseUsageDescription + NYNJA needs to know your location so that you can be able to share it. + NSMicrophoneUsageDescription + NYNJA needs it to allow you send audio messages and make voice calls. + NSPhotoLibraryAddUsageDescription + NYNJA needs it to save photos and video to your device. + NSPhotoLibraryUsageDescription + NYNJA needs it so that you can use your local images. + ServerPort + $(ServerPort) + ServerURL + $(ServerURL) + UIAppFonts + + Avenir.ttc + LatoBlack.ttf + Myriad Pro Regular.ttf + NotoSans-Bold.ttf + NotoSans-Regular.ttf + NotoSans-Medium.ttf + NotoSans-Italic.ttf + + UIBackgroundModes + + audio + fetch + remote-notification + voip + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + isServerConnectionSecure + $(isServerConnectionSecure) + + diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 4f6497c72..048d4ece6 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -653,8 +653,34 @@ 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */; }; 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */; }; 5E278E14F45F56BACB71271C /* VideoPreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5541C91FE7845F3E5C7EB2 /* VideoPreviewWireframe.swift */; }; + 5E7E9FB9215BA0BE004D306B /* CountrySelectorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */; }; + 5E7E9FBC215BA19B004D306B /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FBB215BA19B004D306B /* Country.swift */; }; + 5E7E9FBE215BA51C004D306B /* CountrySelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */; }; + 5E7E9FC2215BA681004D306B /* CountryTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */; }; + 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */; }; 5EB13FDBA6153EE67366115F /* ScheduleMessageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */; }; 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD49CF323041B47A752603E /* MapSearchInteractor.swift */; }; + 5EEB73A4215D00E300D8ECE6 /* CountrySelectorInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */; }; + 5EEB73A6215D00F100D8ECE6 /* CountrySelectorWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */; }; + 5EEB73A8215D00FD00D8ECE6 /* CountrySelectorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A7215D00FD00D8ECE6 /* CountrySelectorPresenter.swift */; }; + 5EEB73AA215D406400D8ECE6 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */; }; + 5EEB73B2216046FE00D8ECE6 /* CodeConfirmationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */; }; + 5EEB73B4216047E000D8ECE6 /* CodeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */; }; + 5EEB73B621604CF600D8ECE6 /* CodeConfirmationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B521604CF600D8ECE6 /* CodeConfirmationWireframe.swift */; }; + 5EEB73B821604DD900D8ECE6 /* CodeConfirmationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B721604DD900D8ECE6 /* CodeConfirmationInteractor.swift */; }; + 5EEB73BA21604E2300D8ECE6 /* CodeConfirmationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B921604E2300D8ECE6 /* CodeConfirmationPresenter.swift */; }; + 5EEB73BD2161797900D8ECE6 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73BC2161797900D8ECE6 /* Result.swift */; }; + 5EEB73C5216199ED00D8ECE6 /* AuthProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73C4216199ED00D8ECE6 /* AuthProtocols.swift */; }; + 5EEB73C721619A5000D8ECE6 /* AuthWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73C621619A5000D8ECE6 /* AuthWireframe.swift */; }; + 5EEB73C92161CB8F00D8ECE6 /* AuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73C82161CB8F00D8ECE6 /* AuthInteractor.swift */; }; + 5EEB73CB2161CBF300D8ECE6 /* AuthPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73CA2161CBF300D8ECE6 /* AuthPresenter.swift */; }; + 5EEB73CD2161CC8A00D8ECE6 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73CC2161CC8A00D8ECE6 /* AuthViewController.swift */; }; + 5EEB73D02161CE2700D8ECE6 /* LoginOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */; }; + 5EEB73D22161CEA100D8ECE6 /* LoginOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D12161CEA100D8ECE6 /* LoginOption.swift */; }; + 5EEB73D42161D5C500D8ECE6 /* AuthHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */; }; + 5EEB73D62161DBF100D8ECE6 /* EmailLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D52161DBF100D8ECE6 /* EmailLoginView.swift */; }; + 5EEB73D82162227B00D8ECE6 /* PhoneNumberLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D72162227B00D8ECE6 /* PhoneNumberLoginView.swift */; }; + 5EEB73DE21623FF900D8ECE6 /* UIViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73DD21623FF900D8ECE6 /* UIViewControllerExtensions.swift */; }; 619C44B00CC7B169077CDEC2 /* EditProfileProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B377AA90A6B6BA0120C31F1 /* EditProfileProtocols.swift */; }; 628E2C26BE0854DB1DF64990 /* SplashWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22259D46BE5732B494C4C7D /* SplashWireframe.swift */; }; 63E6537BBBD814F6DF3DC589 /* Pods_Nynja.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35B1AB871FA278D900E65233 /* Pods_Nynja.framework */; }; @@ -2773,7 +2799,34 @@ 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallCreatorMediator.swift; sourceTree = ""; }; 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryInteractor.swift; sourceTree = ""; }; 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; + 5E7E9FB2215BA059004D306B /* Nynja copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Nynja copy-Info.plist"; path = "/Users/ash/Projects/NynjaIOSWallet/Nynja copy-Info.plist"; sourceTree = ""; }; + 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorProtocols.swift; sourceTree = ""; }; + 5E7E9FBB215BA19B004D306B /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; + 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewController.swift; sourceTree = ""; }; + 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVCell.swift; sourceTree = ""; }; + 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVHeader.swift; sourceTree = ""; }; 5EEA3D18EFB98D7959F993E4 /* AddParticipantsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsProtocols.swift; sourceTree = ""; }; + 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorInteractor.swift; sourceTree = ""; }; + 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorWireframe.swift; sourceTree = ""; }; + 5EEB73A7215D00FD00D8ECE6 /* CountrySelectorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorPresenter.swift; sourceTree = ""; }; + 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; + 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationProtocols.swift; sourceTree = ""; }; + 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationViewController.swift; sourceTree = ""; }; + 5EEB73B521604CF600D8ECE6 /* CodeConfirmationWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationWireframe.swift; sourceTree = ""; }; + 5EEB73B721604DD900D8ECE6 /* CodeConfirmationInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationInteractor.swift; sourceTree = ""; }; + 5EEB73B921604E2300D8ECE6 /* CodeConfirmationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationPresenter.swift; sourceTree = ""; }; + 5EEB73BC2161797900D8ECE6 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; + 5EEB73C4216199ED00D8ECE6 /* AuthProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProtocols.swift; sourceTree = ""; }; + 5EEB73C621619A5000D8ECE6 /* AuthWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWireframe.swift; sourceTree = ""; }; + 5EEB73C82161CB8F00D8ECE6 /* AuthInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthInteractor.swift; sourceTree = ""; }; + 5EEB73CA2161CBF300D8ECE6 /* AuthPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPresenter.swift; sourceTree = ""; }; + 5EEB73CC2161CC8A00D8ECE6 /* AuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; + 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsView.swift; sourceTree = ""; }; + 5EEB73D12161CEA100D8ECE6 /* LoginOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOption.swift; sourceTree = ""; }; + 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthHeaderView.swift; sourceTree = ""; }; + 5EEB73D52161DBF100D8ECE6 /* EmailLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailLoginView.swift; sourceTree = ""; }; + 5EEB73D72162227B00D8ECE6 /* PhoneNumberLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberLoginView.swift; sourceTree = ""; }; + 5EEB73DD21623FF900D8ECE6 /* UIViewControllerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtensions.swift; sourceTree = ""; }; 5F509C0C8B9C738DBC7ABE07 /* SecurityViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SecurityViewController.swift; sourceTree = ""; }; 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryPresenter.swift; sourceTree = ""; }; 61CB12AA514912C6B8E4F670 /* Pods-Nynja.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.devautotests.xcconfig"; sourceTree = ""; }; @@ -5636,6 +5689,7 @@ 3A768DE41ECB3E7600108F7C /* Library */ = { isa = PBXGroup; children = ( + 5EEB73BB2161797100D8ECE6 /* Result */, 4B7C73F5215A5522007924DB /* Debug */, B74BAFED21076ADB0049CD27 /* CircleMenuControl */, A46679EF20F10B2B00DBC6B4 /* RequestModelFactory */, @@ -5750,6 +5804,7 @@ 3A82187C1EDEEDF400337B05 /* UI */ = { isa = PBXGroup; children = ( + 5EEB73DC21623FED00D8ECE6 /* UIViewControllerExtensions */, 4B749EF2214FEABB002F3A33 /* LoginView */, 4BB0EFBA2151347900704136 /* AlertManager.swift */, 4BB0EFB62151347900704136 /* CustomPopup */, @@ -5828,6 +5883,7 @@ 767274D7745E7E490BE6C79C /* Pods */, E853D758816E611EE4809ED3 /* Frameworks */, 851EBD7D20B403B90065C644 /* Recovered References */, + 5E7E9FB2215BA059004D306B /* Nynja copy-Info.plist */, ); sourceTree = ""; }; @@ -6366,8 +6422,12 @@ 4B749F0E214FEFC8002F3A33 /* Auth */ = { isa = PBXGroup; children = ( + 5EEB73BE216199DE00D8ECE6 /* AuthModule */, + 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, + 5E7E9FB3215BA0AD004D306B /* CountrySelector */, 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, + 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, ); path = Auth; sourceTree = ""; @@ -6753,6 +6813,211 @@ path = View; sourceTree = ""; }; + 5E7E9FB3215BA0AD004D306B /* CountrySelector */ = { + isa = PBXGroup; + children = ( + 5E7E9FBA215BA186004D306B /* Entities */, + 5E7E9FB4215BA0AD004D306B /* Presenter */, + 5E7E9FB5215BA0AD004D306B /* Wireframe */, + 5E7E9FB6215BA0AD004D306B /* View */, + 5E7E9FB7215BA0AD004D306B /* Interactor */, + 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */, + ); + path = CountrySelector; + sourceTree = ""; + }; + 5E7E9FB4215BA0AD004D306B /* Presenter */ = { + isa = PBXGroup; + children = ( + 5EEB73A7215D00FD00D8ECE6 /* CountrySelectorPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 5E7E9FB5215BA0AD004D306B /* Wireframe */ = { + isa = PBXGroup; + children = ( + 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 5E7E9FB6215BA0AD004D306B /* View */ = { + isa = PBXGroup; + children = ( + 5E7E9FBF215BA66E004D306B /* Cells */, + 5E7E9FC0215BA66E004D306B /* Headers */, + 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 5E7E9FB7215BA0AD004D306B /* Interactor */ = { + isa = PBXGroup; + children = ( + 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 5E7E9FBA215BA186004D306B /* Entities */ = { + isa = PBXGroup; + children = ( + 5E7E9FBB215BA19B004D306B /* Country.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 5E7E9FBF215BA66E004D306B /* Cells */ = { + isa = PBXGroup; + children = ( + 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 5E7E9FC0215BA66E004D306B /* Headers */ = { + isa = PBXGroup; + children = ( + 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */, + ); + path = Headers; + sourceTree = ""; + }; + 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */ = { + isa = PBXGroup; + children = ( + 5EEB73AC216046EA00D8ECE6 /* Presenter */, + 5EEB73AD216046EA00D8ECE6 /* Wireframe */, + 5EEB73AE216046EA00D8ECE6 /* View */, + 5EEB73AF216046EA00D8ECE6 /* Interactor */, + 5EEB73B0216046EA00D8ECE6 /* Entities */, + 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */, + ); + path = CodeConfirmation; + sourceTree = ""; + }; + 5EEB73AC216046EA00D8ECE6 /* Presenter */ = { + isa = PBXGroup; + children = ( + 5EEB73B921604E2300D8ECE6 /* CodeConfirmationPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 5EEB73AD216046EA00D8ECE6 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 5EEB73B521604CF600D8ECE6 /* CodeConfirmationWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 5EEB73AE216046EA00D8ECE6 /* View */ = { + isa = PBXGroup; + children = ( + 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 5EEB73AF216046EA00D8ECE6 /* Interactor */ = { + isa = PBXGroup; + children = ( + 5EEB73B721604DD900D8ECE6 /* CodeConfirmationInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 5EEB73B0216046EA00D8ECE6 /* Entities */ = { + isa = PBXGroup; + children = ( + ); + path = Entities; + sourceTree = ""; + }; + 5EEB73BB2161797100D8ECE6 /* Result */ = { + isa = PBXGroup; + children = ( + 5EEB73BC2161797900D8ECE6 /* Result.swift */, + ); + name = Result; + path = Library/Result; + sourceTree = ""; + }; + 5EEB73BE216199DE00D8ECE6 /* AuthModule */ = { + isa = PBXGroup; + children = ( + 5EEB73BF216199DE00D8ECE6 /* Presenter */, + 5EEB73C0216199DE00D8ECE6 /* Wireframe */, + 5EEB73C1216199DE00D8ECE6 /* View */, + 5EEB73C2216199DE00D8ECE6 /* Interactor */, + 5EEB73C3216199DE00D8ECE6 /* Entities */, + 5EEB73C4216199ED00D8ECE6 /* AuthProtocols.swift */, + ); + path = AuthModule; + sourceTree = ""; + }; + 5EEB73BF216199DE00D8ECE6 /* Presenter */ = { + isa = PBXGroup; + children = ( + 5EEB73CA2161CBF300D8ECE6 /* AuthPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 5EEB73C0216199DE00D8ECE6 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 5EEB73C621619A5000D8ECE6 /* AuthWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 5EEB73C1216199DE00D8ECE6 /* View */ = { + isa = PBXGroup; + children = ( + 5EEB73CE2161CDF700D8ECE6 /* Subviews */, + 5EEB73CC2161CC8A00D8ECE6 /* AuthViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 5EEB73C2216199DE00D8ECE6 /* Interactor */ = { + isa = PBXGroup; + children = ( + 5EEB73C82161CB8F00D8ECE6 /* AuthInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 5EEB73C3216199DE00D8ECE6 /* Entities */ = { + isa = PBXGroup; + children = ( + 5EEB73D12161CEA100D8ECE6 /* LoginOption.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 5EEB73CE2161CDF700D8ECE6 /* Subviews */ = { + isa = PBXGroup; + children = ( + 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */, + 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */, + 5EEB73D52161DBF100D8ECE6 /* EmailLoginView.swift */, + 5EEB73D72162227B00D8ECE6 /* PhoneNumberLoginView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; + 5EEB73DC21623FED00D8ECE6 /* UIViewControllerExtensions */ = { + isa = PBXGroup; + children = ( + 5EEB73DD21623FF900D8ECE6 /* UIViewControllerExtensions.swift */, + ); + path = UIViewControllerExtensions; + sourceTree = ""; + }; 6002E0651297C852142C0DEF /* View */ = { isa = PBXGroup; children = ( @@ -13980,6 +14245,7 @@ 4B02130220372C5700650298 /* OtherItemView.swift in Sources */, 853801282052CCAD002C6960 /* SoundCellModel.swift in Sources */, A458FABB20EB87BF0075D55E /* ActionContainerContent.swift in Sources */, + 5EEB73DE21623FF900D8ECE6 /* UIViewControllerExtensions.swift in Sources */, 00E9824C205C1E19008BF03D /* SecurityItemsFactory.swift in Sources */, 26342CA920ECBAEF00D2196B /* TranscribeNetworkClient.swift in Sources */, 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */, @@ -14020,6 +14286,7 @@ 26342CAB20ECBB0100D2196B /* TranscribeNetworkService.swift in Sources */, FBCE83D020E52352003B7558 /* PaymentTableViewCell.swift in Sources */, A44B4D5820CE9BDF00CA700A /* AvatarCell.swift in Sources */, + 5E7E9FBC215BA19B004D306B /* Country.swift in Sources */, 4B8996F7204EF77100DCB183 /* FeedDAO.swift in Sources */, A418DA3820EE1AFD00FE780B /* CountAppearanceModel.swift in Sources */, B763DD9320AA1C3400A30B63 /* ContactCellLayout.swift in Sources */, @@ -14048,6 +14315,7 @@ F11786CE20A8E4FD007A9A1B /* MuteState.swift in Sources */, 85D66A1120BD965300FBD803 /* UserMentionCellModel.swift in Sources */, 0062D9472062EC4100B915AC /* InviteFriendsViewController.swift in Sources */, + 5EEB73B821604DD900D8ECE6 /* CodeConfirmationInteractor.swift in Sources */, 4B2D063C202E1A1500010A0C /* ContactsExpandedItemsFactory.swift in Sources */, 85D66A2120BD970400FBD803 /* BBCodeEntity.swift in Sources */, B74BB00221076AFA0049CD27 /* CircleMenuFactory.swift in Sources */, @@ -14114,6 +14382,7 @@ 3A2171511EFB25C400F34B8B /* BaseVC.swift in Sources */, 8580BAD920BD98E700239D9D /* ChatListMessageTableViewCell.swift in Sources */, 26C0C1DF2073D9B600C530DA /* Desc+DB.swift in Sources */, + 5EEB73BA21604E2300D8ECE6 /* CodeConfirmationPresenter.swift in Sources */, 2648C4132069B52100863614 /* ChangeNumberCodeViewLayout.swift in Sources */, 005B0B242029AC14000D6416 /* SaveDeleteView.swift in Sources */, E23A0140D090ECB141911B57 /* TutorialPresenter.swift in Sources */, @@ -14163,6 +14432,7 @@ 267BE90D2069413A00153FB8 /* Handlers.swift in Sources */, A45F111720B4218D00F45004 /* RepliedMessageModel.swift in Sources */, F10AFEB820F7B1B000C7CE83 /* WheelChatItemPreview.swift in Sources */, + 5E7E9FC2215BA681004D306B /* CountryTVCell.swift in Sources */, FBCE841320E525A6003B7558 /* NetworkRouter.swift in Sources */, A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, @@ -14187,6 +14457,7 @@ A48C153F20EF765E002DA994 /* MQTTServiceLink.swift in Sources */, A408A0BC20C174040029F54B /* ChannelsListProtocols.swift in Sources */, 3A2374D91F262A1600701045 /* ContactHandler.swift in Sources */, + 5EEB73AA215D406400D8ECE6 /* AuthCoordinator.swift in Sources */, 6D5168A21F30430900DA3728 /* SpeakerView.swift in Sources */, 8503B529205046A6006F0593 /* NotificationSettingsWireFrame.swift in Sources */, FBD885782147F9640099B8C3 /* FontsConstants.swift in Sources */, @@ -14244,6 +14515,7 @@ F117FBD520FF9DAF00BA1F82 /* MediaInfoView.swift in Sources */, A45F114B20B421E400F45004 /* ChatModel.swift in Sources */, FB0AD86B20F3A07100F052CE /* ImagePreviewTransitionAnimatable.swift in Sources */, + 5EEB73C721619A5000D8ECE6 /* AuthWireframe.swift in Sources */, 005B0B222029ABDA000D6416 /* DateTimeItemView.swift in Sources */, 4B5A714D204F069000A551F5 /* ChatService.swift in Sources */, 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */, @@ -14253,6 +14525,7 @@ 852003FE20D46680007C0036 /* StickerPack.swift in Sources */, B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */, 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */, + 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */, 264312EC210DE4040057E8B0 /* LanguageSectionCoordinator.swift in Sources */, 85579882209322A8007050B8 /* StickerMenuDataSource.swift in Sources */, 8506F001206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift in Sources */, @@ -14334,6 +14607,7 @@ A45F115F20B422AF00F45004 /* Room+DB.swift in Sources */, 85380EE62109FF340048042D /* IdentifierComponentValue.swift in Sources */, F10AFEB920F7B1B000C7CE83 /* WheelMapItemPreview.swift in Sources */, + 5EEB73D42161D5C500D8ECE6 /* AuthHeaderView.swift in Sources */, 267BE2AE1FE13AB600C47E18 /* ParticipantsInteractor.swift in Sources */, 850C301F204DA87A00DB26C2 /* PrivacyListWireFrame.swift in Sources */, 858BC123203320BB0022EB25 /* ForwardSelectorDataSource.swift in Sources */, @@ -14352,6 +14626,7 @@ F11786CD20A8E4FD007A9A1B /* CameraVideoPreviewInteractor.swift in Sources */, A4679BBA20B305360021FE9C /* LinkValidator.swift in Sources */, 4BEE89D69CACB85ABEE9046F /* QRCodeGeneratorPresenter.swift in Sources */, + 5EEB73B621604CF600D8ECE6 /* CodeConfirmationWireframe.swift in Sources */, 2605311B212740FD002E1CF1 /* LogOutputProtocols.swift in Sources */, FBCE841420E525A6003B7558 /* NetworkService.swift in Sources */, A409B1CF2108D48E0051C20B /* QueryFactory.swift in Sources */, @@ -14412,10 +14687,12 @@ F10B0E1720B4401500528E7A /* GalleryPresenter.swift in Sources */, B7EF8ED9210C71E800E0E981 /* InterpretationType.swift in Sources */, 6D5157D21F30B822002A27DB /* MicrophoneView.swift in Sources */, + 5EEB73D22161CEA100D8ECE6 /* LoginOption.swift in Sources */, 260313AF20A0A50D009AC66D /* TranslationService.swift in Sources */, A42D51AD206A361400EEB952 /* cur.swift in Sources */, 8504DEAB206937A2006722AC /* MediaFullWheelItemView.swift in Sources */, BF20ED73252DE6954B6CDCA8 /* QRCodeReaderViewController.swift in Sources */, + 5EEB73C92161CB8F00D8ECE6 /* AuthInteractor.swift in Sources */, 268C341C21075B4700F1472A /* Cancelable.swift in Sources */, 26541F722007B93400AAEACF /* DBMessageAction.swift in Sources */, 264638251FFFE78E002590E6 /* RepliesWireFrame.swift in Sources */, @@ -14455,6 +14732,7 @@ 859B863920486068003272B2 /* CarouselPickerViewControllerLayout.swift in Sources */, A43B25BF20AB1E9600FF8107 /* LengthInputValidator.swift in Sources */, 85D669E520BD956000FBD803 /* UIButtonExtensions.swift in Sources */, + 5EEB73A8215D00FD00D8ECE6 /* CountrySelectorPresenter.swift in Sources */, E74EC9ED1FC2DA6E007268E6 /* RoomTable.swift in Sources */, A42D52C8206A53AB00EEB952 /* Auth_Spec.swift in Sources */, 267BE90920693F4400153FB8 /* ProfileDAO.swift in Sources */, @@ -14524,11 +14802,13 @@ A42D52C4206A53AA00EEB952 /* error2_Spec.swift in Sources */, 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */, 00E9824E205C2604008BF03D /* SessionItemView.swift in Sources */, + 5EEB73D02161CE2700D8ECE6 /* LoginOptionsView.swift in Sources */, 8520040920D4F9B4007C0036 /* MessageStickerRepliedView.swift in Sources */, 00102F40202C8E5300A877A9 /* NynjaCalendarView.swift in Sources */, 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */, 0062D9432062EC4100B915AC /* ShareNynjaHeaderViewLayout.swift in Sources */, A42D52B9206A53AA00EEB952 /* Profile_Spec.swift in Sources */, + 5EEB73A4215D00E300D8ECE6 /* CountrySelectorInteractor.swift in Sources */, 26DCB25420692237001EF0AB /* Array+Feature.swift in Sources */, F1607B2E20B2DE8A00BDF60A /* CameraQRPreviewInteractor.swift in Sources */, B77C11EA2109254800CCB42E /* InterpretationTypeWireFrame.swift in Sources */, @@ -14683,6 +14963,7 @@ 8526187C20D05BF700824357 /* StickerGridPlaceholderCollectionViewCell.swift in Sources */, 26B32B601FE170FE00888A0A /* MigrationManager.swift in Sources */, 986BE2204D6D0813B13618B1 /* AddContactViaPhonePresenter.swift in Sources */, + 5E7E9FB9215BA0BE004D306B /* CountrySelectorProtocols.swift in Sources */, 265AEA171FE9AFD400AC4806 /* MemberModel.swift in Sources */, 2605311D21274116002E1CF1 /* LogOutputView.swift in Sources */, 263529152075729400DC6FBD /* Job+DB.swift in Sources */, @@ -14697,11 +14978,13 @@ A407348C20B712E9005762D5 /* UIView+Hierarchy.swift in Sources */, F117870F20ACF018007A9A1B /* CameraQualitySettingsViewController.swift in Sources */, E764919B1F7A5485001E741C /* MainWheelContainerDelegate.swift in Sources */, + 5EEB73A6215D00F100D8ECE6 /* CountrySelectorWireframe.swift in Sources */, A4330A6A2109EA850060BD93 /* DatabaseManager.swift in Sources */, 8E6C4BDE1FF40B97009C8374 /* GroupFilesCell.swift in Sources */, 26DCB24E2064B9DC001EF0AB /* ContactsTableDS.swift in Sources */, A45F114820B421AB00F45004 /* Contact+BaseChatModel.swift in Sources */, 6CED2C4CE125011A3A731D62 /* AddContactViaPhoneInteractor.swift in Sources */, + 5EEB73C5216199ED00D8ECE6 /* AuthProtocols.swift in Sources */, 260313AA20A0A4BA009AC66D /* ChatLanguageSettingsViewController.swift in Sources */, 859B862F204820DC003272B2 /* ThemePickerInteractor.swift in Sources */, 263A60AC1FB4F8F7006F9D52 /* ParticipantsDataSource.swift in Sources */, @@ -14836,6 +15119,7 @@ F10AFEB520F7B1B000C7CE83 /* WheelDefaultItemPreview.swift in Sources */, 858A72E5A4AE48CE24AFF649 /* MainInteractor.swift in Sources */, 8EC767DA200782CB00655F80 /* GroupImagesListVC.swift in Sources */, + 5EEB73BD2161797900D8ECE6 /* Result.swift in Sources */, E7A77FD81FACC360004AE609 /* UIDeviceExtension.swift in Sources */, 3AE0A8431F20321A008A04F3 /* CountryWheelItemView.swift in Sources */, A42D52D5206A53AB00EEB952 /* sequenceFlow_Spec.swift in Sources */, @@ -14916,6 +15200,7 @@ A432CF1620B4347D00993AFB /* FloatingPlaceholderProvider.swift in Sources */, E7302A931FC83477002892F8 /* DescExtension.swift in Sources */, 8509FC7B2158CCA800734D93 /* MessageInteractor+Reply.swift in Sources */, + 5E7E9FBE215BA51C004D306B /* CountrySelectorViewController.swift in Sources */, A42D51CB206A361400EEB952 /* Job.swift in Sources */, E7F68D271FA22C45009C98D1 /* EditProfileVCStrings.swift in Sources */, 8ECC06801FC5C80C002CF225 /* MessagesProcessingManager.swift in Sources */, @@ -14932,6 +15217,7 @@ FB16E79D20EFCF15009FA203 /* CryptoMoney.swift in Sources */, 4B8996F2204EF5E900DCB183 /* ChatCheckpointDAO.swift in Sources */, 265F5D2E209B8C1C008ACCC8 /* MessageEditActionDAO.swift in Sources */, + 5EEB73B2216046FE00D8ECE6 /* CodeConfirmationProtocols.swift in Sources */, 853FB0772049B7CA000996C5 /* TextCellViewModel.swift in Sources */, 5BC1D38120D3B54B002A44B3 /* CallInfoView.swift in Sources */, 264638291FFFE835002590E6 /* RepliesInteractor.swift in Sources */, @@ -14987,6 +15273,7 @@ 0062D93B2062EC4100B915AC /* PhoneContact.swift in Sources */, 85433F24204D596D00B373A7 /* WebFullScreenProtocols.swift in Sources */, 4BAB9CE42035CB0A00385520 /* ScheduleTarget.swift in Sources */, + 5EEB73CD2161CC8A00D8ECE6 /* AuthViewController.swift in Sources */, 8514F17B20EA219F00883513 /* ContextMenuConfiguration+GroupStorage.swift in Sources */, CC59F623F661C99492F9F415 /* ImagePreviewProtocols.swift in Sources */, E77764B41FBDA8B50042541D /* WheelContainerDelegate.swift in Sources */, @@ -15162,8 +15449,11 @@ FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, E79061B61FBF1C8C009FD83A /* DescTable.swift in Sources */, + 5EEB73D62161DBF100D8ECE6 /* EmailLoginView.swift in Sources */, + 5EEB73B4216047E000D8ECE6 /* CodeConfirmationViewController.swift in Sources */, 2910A0129CA29C35161DD692 /* EditPhotoInteractor.swift in Sources */, 705B483A1FCDEA2273CEFE2C /* EditPhotoWireframe.swift in Sources */, + 5EEB73D82162227B00D8ECE6 /* PhoneNumberLoginView.swift in Sources */, A43B25A420AB1DFA00FF8107 /* TextField.swift in Sources */, E743B58F1FB0A32700F72F92 /* ParticipantsHeaderView.swift in Sources */, A4679BAA20B2DD100021FE9C /* SubscribersTableDataSource.swift in Sources */, @@ -15426,6 +15716,7 @@ CA6AA942773DEBE97BDCFDD6 /* DateTimePickerViewController.swift in Sources */, 24AC9EAFA26353C7B95B60BF /* DateTimePickerPresenter.swift in Sources */, 85788C48204423A4003600C9 /* BuildNumberWireFrame.swift in Sources */, + 5EEB73CB2161CBF300D8ECE6 /* AuthPresenter.swift in Sources */, 5894F4C605B66B55F21D406E /* DateTimePickerInteractor.swift in Sources */, 8514DE912136A9CB00718DD8 /* StarActionDAOProtocol.swift in Sources */, 00E98254205C2726008BF03D /* SessionFooterView.swift in Sources */, @@ -15804,7 +16095,7 @@ INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; + OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=50 -Xfrontend -warn-long-function-bodies=50"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "7757f70a-8690-4f76-9822-0ac1e08381ea"; diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 5849d18a0..08a9ae742 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -92,6 +92,11 @@ private extension AppDelegate { SplashWireFrame().presentSplash(navigation: navigation) self.window?.rootViewController = navigation self.window?.makeKeyAndVisible() + +// let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) +// self.window?.rootViewController = navigation +// self.window?.makeKeyAndVisible() +// coordinator.start() } private func configureDependencies() { @@ -160,3 +165,293 @@ private extension AppDelegate { AWSServiceManager.default().defaultServiceConfiguration = defaultConfiguration } } + + + + + + +//// MARK: - Service factory +// +//protocol ServiceFactoryProtocol { +// func makeService1(/*Some parameters*/) -> Service1 +// func makeService2(/*Some parameters*/) -> Service2 +// // Another services +//} +// +//final class ServiceFactory: ServiceFactoryProtocol { +// func makeService1(/*Some parameters*/) -> Service1 { +// return Service1.shared +// } +// +// func makeService2(/*Some parameters*/) -> Service2 { +// let service = Service2(/*Some parameters*/) +// return service2 +// } +// +// // Another services +//} +// +//// MARK: - Modules stack +// +//protocol ModulesStackProtocol { +// func present(view: UIViewController) +// func back() +// func close() +//} +// +//final class ModulesStack: ModulesStackProtocol { +// private weak var navigationController: UINavigationController? +// private var viewControllers: [UIViewController] +// +// init(navigationController: UINavigationController?) { +// self.navigationController = navigationController +// viewControllers = [] +// } +// +// func present(view: UIViewController) { +// // Some code for presenting +// } +// +// func back() { +// // Some code for popping +// } +// +// func close() { +// // Some code for closing all stack +// } +//} +// +//// MARK: - Coordinators factory +// +//protocol CoordinatorsFactoryProtocol { +// func makeCoordinator1() -> CoordinatorProtocol +// func makeCoordinator2(/*Some parameters*/) -> CoordinatorProtocol +// // Another coordinators +//} +// +//final class CoordinatorsFactory: CoordinatorsFactoryProtocol { +// func makeCoordinator1() -> CoordinatorProtocol { +// return Coordinator1() +// } +// +// func makeCoordinator2(/*Some parameters*/) -> CoordinatorProtocol { +// return Coordinator2(/*Some parameters*/) +// } +// +// // Another coordinators +//} +// +//// MARK: - Coordinators stack +// +//protocol CoordinatorsStackProtocol { +// +//} +// +//final class CoordinatorsStack: CoordinatorsStackProtocol { +// +//} +// +//// MARK: - AppCoordinator +// +//protocol AppCoordinatorProtocol { +// +//} +// +//final class AppCoordinator: AppCoordinatorProtocol { +// +//} +// +//// MARK: - Coordinator +// +//protocol CoordinatorProtocol { +// func start() +// func end() +//} +// +//final class Coordinator: CoordinatorProtocol, WireframeCoordinatorProtocol { +// private weak var stack: ModulesStackProtocol +// private let serviceFactory: ServiceFactoryProtocol +// // Some properties +// +// init(stack: ModulesStackProtocol, serviceFactory: ServiceFactoryProtocol/*, Some parameters*/) { +// self.stack = stack +// } +// +// func start() { +// let wireframe = Wireframe(coordinator: self) +// let parameters = Wireframe.Parameters(someParameter: someParameter) +// let dependencies = Wireframe.Dependencies(someService1: serviceFactory.makeSomeService1) +// +// let view = wireframe.prepareModule(parameters: parameters, dependencies: dependencies) +// stack.present(view: view) +// } +// +// func end() { +// stack.close() +// // Some code +// } +// +// func wireframe(_ wireframe: Wireframe, finishedWithState state: Wireframe.State) { +// switch state { +// case .stateForOpen1: // Some code +// break +// case .stateForOpen2(/*Some parameters*/): stack.back() +// break +// } +// } +//} +// +//// MARK: - Wireframe +// +//protocol WireframeCoordinatorProtocol { +// func wireframe(_ wireframe: Wireframe, finishedWithState state: Wireframe.State) +//} +// +//protocol WireframeProtocol { +// associatedtype Parameters +// associatedtype Dependencies +// associatedtype State +// +// func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController +// func open1(/*Some parameters*/) +// func open2(/*Some parameters*/) +//} +// +//final class Wireframe: WireframeProtocol { +// struct Parameters { +// let someParameter: SomeType +// // Another parameters +// } +// +// struct Dependencies { +// let someService1: SomeService1 +// // Another dependencies +// } +// +// enum State { +// case stateForOpen1 +// case stateForOpen2(/*Some parameters*/) +// } +// +// private let coordinator: WireframeCoordinatorProtocol +// +// init(coordinator: WireframeCoordinatorProtocol) { +// self.coordinator = coordinator +// } +// +// func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { +// let view = View() +// let presenter = Presenter() +// let interactor = Interactor() +// +// let viewDependencies = View.Dependencies(presenter: presenter, someService1: dependencies.makeService1) +// let interactorDependencies = Interactor.Dependencies(presenter: presenter, someService1: dependencies.makeService1) +// let presenterDependencies = Presenter.Dependencies(interactor: interactor, someService1: dependencies.makeService1) +// +// // set some parameters from Parameters structure +// +// view.inject(viewDependencies) +// presenter.inject(presenterDependencies) +// interactor.inject(interactorDependencies) +// +// return view +// } +// +// func open1(/*Some parameters*/) { +// coordinator.wireframe(self, finishedWithState: .stateForOpen1) +// } +// +// func open2(/*Some parameters*/) { +// coordinator.wireframe(self, finishedWithState: .stateForOpen2(/*Some parameters*/)) +// } +//} +// +//// MARK: - Presenter +// +//protocol PresenterProtocol: SetInjectable { +// func someMethod1() +//} +// +//final class Presenter: PresenterProtocol { +// private let interactor: InteractorProtocol +// private weak var view: ViewProtocol? +// private let someService: SomeService1 +// // Another properties +// +// struct Dependencies { +// let interactor: InteractorProtocol +// let view: ViewProtocol +// let someService1: SomeService1 +// // Another dependencies +// } +// +// func inject(dependencies: Presenter.Dependencies) { +// interactor = dependencies.interactor +// view = dependencies.view +// someService = dependencies.someService1 +// // Another dependencies +// } +// +// func someMethod1() { +// // Some code +// } +//} +// +//// MARK: - Interactor +// +//protocol InteractorProtocol: SetInjectable { +// func someMethod1() +//} +// +//final class Interactor: InteractorProtocol { +// private weak var presenter: PresenterProtocol? +// private let someService1: SomeService1 +// // Another properties +// +// struct Dependencies { +// let presenter: PresenterProtocol +// let someService1: SomeService1 +// // Another dependencies +// } +// +// func inject(dependencies: Interactor.Dependencies) { +// presenter = dependencies.presenter +// someService = dependencies.someService1 +// // Another dependencies +// } +// +// func someMethod1() { +// // Some code +// } +//} +// +//// MARK: - View +// +//protocol ViewProtocol: SetInjectable { +// func someMethod1() +//} +// +//final class View: ViewProtocol { +// private weak var presenter: PresenterProtocol? +// private let someService1: SomeService1 +// // Another properties +// +// struct Dependencies { +// let presenter: PresenterProtocol +// let someService1: SomeService1 +// // Another dependencies +// } +// +// func inject(dependencies: View.Dependencies) { +// presenter = dependencies.presenter +// someService = dependencies.someService1 +// // Another dependencies +// } +// +// func someMethod1() { +// // Some code +// } +//} + + diff --git a/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift index 093d1a6db..32416474b 100644 --- a/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift +++ b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift @@ -45,3 +45,34 @@ extension Array { insert(element, at: newIndex) } } + +extension Array where Element : Equatable { + func next(after element: Element) -> Element? { + guard let indexOfCurrent = index(of: element) else { + return nil + } + + let indexForNewElement = index(after: indexOfCurrent) + + if indexForNewElement <= count - 1 { + return self[indexForNewElement] + } else { + return nil + } + + } + + func previous(before element: Element) -> Element? { + guard let indexOfCurrent = index(of: element) else { + return nil + } + + let indexForNewElement = index(before: indexOfCurrent) + + if indexForNewElement >= 0 { + return self[indexForNewElement] + } else { + return nil + } + } +} diff --git a/Nynja/Library/Result/Result.swift b/Nynja/Library/Result/Result.swift new file mode 100644 index 000000000..e5d11afa5 --- /dev/null +++ b/Nynja/Library/Result/Result.swift @@ -0,0 +1,154 @@ +// +// Result.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +import Foundation + + +public enum Result { + case success(Value) + case failure(Error) + + public var isSuccess: Bool { + switch self { + case .success: + return true + case .failure: + return false + } + } + + public var isFailure: Bool { + return !isSuccess + } + + public var value: Value? { + switch self { + case .success(let value): + return value + case .failure: + return nil + } + } + + public var error: Error? { + switch self { + case .success: + return nil + case .failure(let error): + return error + } + } +} + +// MARK: - CustomStringConvertible + +extension Result: CustomStringConvertible { + public var description: String { + switch self { + case .success: + return "SUCCESS" + case .failure: + return "FAILURE" + } + } +} + +// MARK: - CustomDebugStringConvertible + +extension Result: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .success(let value): + return "SUCCESS: \(value)" + case .failure(let error): + return "FAILURE: \(error)" + } + } +} + +// MARK: - Functional APIs + +extension Result { + public init(value: () throws -> Value) { + do { + self = try .success(value()) + } catch { + self = .failure(error) + } + } + + public func unwrap() throws -> Value { + switch self { + case .success(let value): + return value + case .failure(let error): + throw error + } + } + + public func map(_ transform: (Value) -> T) -> Result { + switch self { + case .success(let value): + return .success(transform(value)) + case .failure(let error): + return .failure(error) + } + } + + public func flatMap(_ transform: (Value) throws -> T) -> Result { + switch self { + case .success(let value): + do { + return try .success(transform(value)) + } catch { + return .failure(error) + } + case .failure(let error): + return .failure(error) + } + } + + public func mapError(_ transform: (Error) -> T) -> Result { + switch self { + case .failure(let error): + return .failure(transform(error)) + case .success: + return self + } + } + + public func flatMapError(_ transform: (Error) throws -> T) -> Result { + switch self { + case .failure(let error): + do { + return try .failure(transform(error)) + } catch { + return .failure(error) + } + case .success: + return self + } + } + + @discardableResult + public func onSuccess(_ closure: (Value) -> Void) -> Result { + if case let .success(value) = self { closure(value) } + + return self + } + + @discardableResult + public func onFailure(_ closure: (Error) -> Void) -> Result { + if case let .failure(error) = self { closure(error) } + + return self + } +} diff --git a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift index 880fffdfa..251dd9e73 100644 --- a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift +++ b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift @@ -212,6 +212,8 @@ extension UIImage { //MARK: - images extension UIImage { + static var logoImage: UIImage? { return UIImage(named: "logo-2") } + static var navigationViewLogoImage: UIImage? { return UIImage(named: "auth_light_logo") } static var closeImage: UIImage { return UIImage(named: "cancel")! } diff --git a/Nynja/Library/UI/UIViewControllerExtensions/UIViewControllerExtensions.swift b/Nynja/Library/UI/UIViewControllerExtensions/UIViewControllerExtensions.swift new file mode 100644 index 000000000..2e0a9e868 --- /dev/null +++ b/Nynja/Library/UI/UIViewControllerExtensions/UIViewControllerExtensions.swift @@ -0,0 +1,25 @@ +// +// UIViewControllerExtensions.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +extension UIViewController { + func enableKeyboardHidingWhenTappedAround() { + let tap: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(UIViewController.dismissKeyboard)) + + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + } + + @objc func dismissKeyboard() { + view.endEditing(true) + } +} diff --git a/Nynja/Library/UI/View/UIViewExtensions.swift b/Nynja/Library/UI/View/UIViewExtensions.swift index 89f6bddf6..7e4092f8a 100644 --- a/Nynja/Library/UI/View/UIViewExtensions.swift +++ b/Nynja/Library/UI/View/UIViewExtensions.swift @@ -32,7 +32,7 @@ extension UIView { return background } - static func makeHeaderView(on view: UIView, top: UIView, config: NavigationView.Config) -> NavigationView { + static func makeHeaderView(on view: UIView, top: UIView?, config: NavigationView.Config) -> NavigationView { let navigationView = NavigationView() navigationView.configure(config: config) @@ -42,7 +42,12 @@ extension UIView { navigationView.backgroundColor = UIColor.nynja.darkLight navigationView.snp.makeConstraints { (maker) in - maker.top.equalTo(top.snp.bottom) + if let top = top { + maker.top.equalTo(top.snp.bottom) + } else { + maker.top.equalToSuperview() + } + maker.left.right.equalToSuperview() } diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift new file mode 100644 index 000000000..25378a8ae --- /dev/null +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -0,0 +1,62 @@ +// +// AuthCoordinator.swift +// Nynja +// +// Created by Ash on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol { + private weak var navigation: UINavigationController? + private let serviceFactory: ServiceFactoryProtocol + + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { + self.navigation = navigation + self.serviceFactory = serviceFactory + } + + func start() { + let wireframe = AuthWireframe() + let view = wireframe.prepareModule(parameters: NSNull(), dependencies: AuthWireframe.Dependencies()) + + navigation?.pushViewController(view, animated: true) +// let wireframe = CodeConfirmationWireframe.init(coordinator: self) +// let view = wireframe.prepareModule( +// parameters: CodeConfirmationWireframe.Parameters( +// address: "Some", +// authType: .phoneNumber), +// dependencies: CodeConfirmationWireframe.Dependencies()) +// navigation?.pushViewController(view, animated: true) + +// let wireframe = CountrySelectorWireframe(coordinator: self) +// let view = wireframe.prepareModule( +// parameters: NSNull(), +// dependencies: CountrySelectorWireframe.Dependencies( +// storageService: serviceFactory.makeStorageService())) +// +// navigation?.pushViewController(view, animated: true) + } + + func end() { + + } +} + +// MARK: - CountrySelectorCoordinatorProtocol + +extension AuthCoordinator { + func wireframe(_ wireframe: CountrySelectorWireframe, endWithState state: CountrySelectorWireframe.State) { + + } +} + +// MARK: - CodeConfirmationCoordinatorProtocol + +extension AuthCoordinator { + func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { + + } +} diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift new file mode 100644 index 000000000..cdae88ecd --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -0,0 +1,34 @@ +// +// AuthProtocols.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AuthWireframeProtocol: WireframeProtocol { + +} + +protocol AuthViewProtocol: class where Self: UIViewController { + +} + +protocol AuthPresenterProtocol { + var loginOption: LoginOption { get } + + func loginViaFacebook() + func loginViaGoogle() + func switchLoginOption() + func loginViaEmail(_ email: String) +} + +protocol AuthInputInteractorProtocol { + +} + +protocol AuthOutputInteractorProtocol { + +} diff --git a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift new file mode 100644 index 000000000..e51440640 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift @@ -0,0 +1,14 @@ +// +// LoginOption.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum LoginOption { + case phoneNumber + case email +} diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift new file mode 100644 index 000000000..13265b1fd --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -0,0 +1,26 @@ +// +// AuthInteractor.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AuthInteractor: AuthInputInteractorProtocol, SetInjectable { + private var presenter: AuthOutputInteractorProtocol? +} + +// MARK: - SetInjectable + +extension AuthInteractor { + struct Dependencies { + let presenter: AuthOutputInteractorProtocol + } + + func inject(dependencies: AuthInteractor.Dependencies) { + presenter = dependencies.presenter + } +} diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift new file mode 100644 index 000000000..9fd623655 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -0,0 +1,50 @@ +// +// AuthPresenter.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, SetInjectable { + private var view: AuthViewProtocol? + private var interactor: AuthInteractor? + private var wireframe: AuthWireframe? + + var loginOption: LoginOption = .phoneNumber + + func loginViaFacebook() { + + } + + func loginViaGoogle() { + + } + + func switchLoginOption() { + + } + + func loginViaEmail(_ email: String) { + + } +} + +// MARK: - SetInjectable + +extension AuthPresenter { + struct Dependencies { + let view: AuthViewProtocol + let interactor: AuthInteractor + let wireframe: AuthWireframe + } + + func inject(dependencies: AuthPresenter.Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift new file mode 100644 index 000000000..6d66255b0 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -0,0 +1,138 @@ +// +// AuthViewController.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AuthViewController: UIViewController, AuthViewProtocol, SetInjectable, KeyboardInteractive { + private var presenter: AuthPresenterProtocol? + + private var headerView: AuthHeaderView! + + private var scrollView: UIScrollView! + private var emailLoginView: EmailLoginView? + private var phoneNumberLoginView: PhoneNumberLoginView? + + private var bottomView: LoginOptionsView! + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.nynja.backgroundColor + + guard let presenter = presenter else { + return + } + + headerView = makeHeaderView(on: view) + bottomView = makeBottomView(on: view, presenter: presenter) + scrollView = makeScrollView(on: view, top: headerView, bottom: bottomView) + emailLoginView = makeEmailLoginView(on: scrollView, presenter: presenter) + + enableKeyboardHidingWhenTappedAround() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + unregisterForKeyboardNotifications() + } + + func keyboardNotified(endFrame: CGRect) { + var bottomInset: CGFloat = 0 + + if endFrame.origin.y < UIScreen.main.bounds.size.height { + bottomInset = endFrame.height - bottomView.bounds.size.height + } + + scrollView.snp.updateConstraints { (make) in + make.bottom.equalTo(bottomView.snp.top).inset(-bottomInset) + } + } +} + +// MARK: - Actions + +private extension AuthViewController { +} + +// MARK: - SetInjectable + +extension AuthViewController { + struct Dependencies { + let presenter: AuthPresenterProtocol + } + + func inject(dependencies: AuthViewController.Dependencies) { + presenter = dependencies.presenter + } +} + +// MARK: - UI factory methods + +private extension AuthViewController { + func makeHeaderView(on view: UIView) -> AuthHeaderView { + let header = AuthHeaderView() + header.configure(config: NSNull()) + + view.addSubview(header) + header.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + } + + return header + } + + func makeScrollView(on view: UIView, top: UIView, bottom: UIView) -> UIScrollView { + let scrollView = UIScrollView() + view.addSubview(scrollView) + + scrollView.snp.makeConstraints { (make) in + make.left.right.equalToSuperview() + make.bottom.equalTo(bottom.snp.top) + make.top.equalTo(top.snp.bottom) + } + + return scrollView + } + + func makeEmailLoginView(on view: UIView, presenter: AuthPresenterProtocol) -> EmailLoginView { + let loginView = EmailLoginView() + view.addSubview(loginView) + + loginView.configure(config: EmailLoginView.Config(nextAction: presenter.loginViaEmail)) + + loginView.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + } + + return loginView + } + + func makeBottomView(on view: UIView, presenter: AuthPresenterProtocol) -> LoginOptionsView { + let bottom = LoginOptionsView() + bottom.configure(config: LoginOptionsView.Config( + loginOption: presenter.loginOption, + switchLoginAction: presenter.switchLoginOption, + facebookLoginAction: presenter.loginViaFacebook, + googleLoginAction: presenter.loginViaGoogle)) + + view.addSubview(bottom) + bottom.snp.makeConstraints { (make) in + make.bottom.left.right.equalToSuperview() + } + + return bottom + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift new file mode 100644 index 000000000..b218ddb9d --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift @@ -0,0 +1,66 @@ +// +// AuthHeaderView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AuthHeaderView: UIView, Configurable { + private var welcomeLabel: UILabel! + private var logoImageView: UIImageView! +} + +// MARK: - Configurable + +extension AuthHeaderView { + typealias Config = NSNull + + func configure(config: NSNull) { + backgroundColor = UIColor.nynja.clear + + welcomeLabel = makeWelcomeLabel(on: self) + logoImageView = makeLogoImageView(on: self, top: welcomeLabel) + } +} + +// MARK: - UI fabric methods + +private extension AuthHeaderView { + func makeWelcomeLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.text = "Welcome to".localized + + label.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(70) + make.centerX.equalToSuperview() + } + + return label + } + + func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { + let imageView = UIImageView() + view.addSubview(imageView) + + imageView.contentMode = .scaleAspectFill + imageView.image = UIImage.logoImage + + imageView.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom).offset(16) + make.bottom.equalToSuperview().offset(-16) + make.centerX.equalToSuperview() + make.width.equalToSuperview().multipliedBy(9/20) + } + + return imageView + } +} + diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift new file mode 100644 index 000000000..3bf99c68f --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -0,0 +1,152 @@ +// +// EmailLoginView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class EmailLoginView: UIView, Configurable { + private struct Validator { + func isValid(email: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + + return emailTest.evaluate(with: email) + } + } + + private final class TextFieldController: NSObject, UITextFieldDelegate { + private let validator: Validator + private let validationAction: (Bool) -> Void + + init(validator: Validator, validationAction: @escaping (Bool) -> Void) { + self.validator = validator + self.validationAction = validationAction + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let str = textField.text as NSString? { + let resultStr = str.replacingCharacters(in: range, with: string) + validationAction(validator.isValid(email: resultStr)) + } + + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.becomeFirstResponder() + return true + } + } + + private var inputField: UITextField! + private var detailsLabel: UILabel! + private var nextButton: UIButton! + + private var textFieldController: TextFieldController? + private var nextAction: ((String) -> Void)? +} + +// MARK: - Configurable + +extension EmailLoginView { + struct Config { + let nextAction: (String) -> Void + } + + func configure(config: EmailLoginView.Config) { + textFieldController = TextFieldController(validator: Validator()) { [weak self] (result) in + if result { + self?.nextButton.backgroundColor = UIColor.nynja.mainRed + } else { + self?.nextButton.backgroundColor = UIColor.nynja.darkRed + } + + self?.nextButton.isEnabled = result + } + + inputField = makeInputField(on: self) + inputField.delegate = textFieldController + detailsLabel = makeDetailsLabel(on: self, top: inputField) + nextButton = makeNextButton(on: self, top: detailsLabel) + + nextAction = config.nextAction + } +} + +// MARK: - ACtions + +private extension EmailLoginView { + @objc func next(sender: UIButton) { + nextAction?(inputField.text ?? "") + } +} + +// MARK: - UI fabric methdos + +private extension EmailLoginView { + func makeInputField(on view: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.attributedPlaceholder = NSAttributedString( + string: "Email".localized, + attributes: [NSAttributedStringKey.foregroundColor : UIColor.nynja.dustyGray]) + textField.textColor = UIColor.nynja.white + textField.font = FontFamily.NotoSans.medium.font(size: 16) + + textField.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(16) + make.height.equalTo(64) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + + return textField + } + + func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.text = "Enter your email adsress to receive the login code.".localized + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + + label.snp.makeConstraints { (make) in + make.height.equalTo(40) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom) + } + + return label + } + + func makeNextButton(on view: UIView, top: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.layer.cornerRadius = 22 + button.setTitle("next".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.backgroundColor = UIColor.nynja.darkRed + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + + button.isEnabled = false + + button.snp.makeConstraints { (make) in + make.height.equalTo(44) + make.bottom.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom).offset(88) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + + return button + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift new file mode 100644 index 000000000..591e9db1b --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift @@ -0,0 +1,154 @@ +// +// LoginOptionsView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class LoginOptionsView: UIView, Configurable { + private weak var switchLoginButton: UIButton! + private weak var loginWithFacebook: UIButton! + private weak var loginWithGoogle: UIButton! + + private var localState: LoginOption? + private var switchLoginAction: (() -> Void)? + private var facebookLoginAction: (() -> Void)? + private var googleLoginAction: (() -> Void)? +} + +// MARK: - Configurable + +extension LoginOptionsView { + struct Config { + let loginOption: LoginOption + let switchLoginAction: () -> Void + let facebookLoginAction: () -> Void + let googleLoginAction: () -> Void + } + + func configure(config: LoginOptionsView.Config) { + backgroundColor = UIColor.nynja.clear + + loginWithGoogle = makeLoginWithGoogleButton(on: self) + loginWithFacebook = makeLoginWithFacebookButton(on: self, bottom: loginWithGoogle) + switchLoginButton = makeSwitchLoginButton(on: self, bottom: loginWithFacebook) + + switchLoginAction = config.switchLoginAction + facebookLoginAction = config.facebookLoginAction + googleLoginAction = config.googleLoginAction + + localState = config.loginOption + switch config.loginOption { + case .email: + switchLoginButton.setTitle("Log in with phone number".localized.uppercased(), for: .normal) + switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) + case .phoneNumber: + switchLoginButton.setTitle("Log in with email".localized.uppercased(), for: .normal) + switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) + } + } +} + +// MARK: - Actions + +private extension LoginOptionsView { + @objc func switchLogin(sender: UIButton) { + guard let state = localState else { + return + } + + switch state { + case .email: + localState = .phoneNumber + sender.setTitle("Log in with email".localized.uppercased(), for: .normal) + sender.setImage(UIImage.backButtonImage, for: .normal) + case .phoneNumber: + localState = .email + sender.setTitle("Log in with phone number".localized.uppercased(), for: .normal) + sender.setImage(UIImage.backButtonImage, for: .normal) + } + + switchLoginAction?() + } + + @objc func loginWithFacebook(sender: UIButton) { + facebookLoginAction?() + } + + @objc func loginWithGoogle(sender: UIButton) { + googleLoginAction?() + } +} + +// MARK: - UI fabric methods + +private extension LoginOptionsView { + func makeLoginWithGoogleButton(on view: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.backgroundColor = UIColor.nynja.white + button.setTitle("Log in with Google".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.subtitleGray, for: .normal) + button.setImage(UIImage.backButtonImage, for: .normal) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + + button.layer.cornerRadius = 22 + + button.snp.makeConstraints { (make) in + make.bottom.equalToSuperview().offset(-30) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + } + + return button + } + + func makeLoginWithFacebookButton(on view: UIView, bottom: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.backgroundColor = UIColor.nynja.dodgerBlue + button.setTitle("Log in with Facebook".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.setImage(UIImage.backButtonImage, for: .normal) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + + button.layer.cornerRadius = 22 + + button.snp.makeConstraints { (make) in + make.bottom.equalTo(bottom.snp.top).offset(-16) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + } + + return button + } + + func makeSwitchLoginButton(on view: UIView, bottom: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.backgroundColor = UIColor.nynja.mainRed + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + + button.layer.cornerRadius = 22 + + button.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(30) + make.bottom.equalTo(bottom.snp.top).offset(-16) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + } + + return button + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift new file mode 100644 index 000000000..9f6bc653b --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -0,0 +1,30 @@ +// +// PhoneNumberLoginView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class PhoneNumberLoginView: UIView, Configurable { + private weak var countrySelector: UIButton? + private weak var countryCode: UITextField? + private weak var phoneNumber: UITextField? + + private weak var detailsLabel: UILabel? + private weak var nextButton: UIButton? +} + +// MARK: - Configurable + +extension PhoneNumberLoginView { + struct Config { + + } + + func configure(config: PhoneNumberLoginView.Config) { + + } +} diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift new file mode 100644 index 000000000..d4cb21e0d --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -0,0 +1,38 @@ +// +// AuthWireframe.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AuthWireframe: AuthWireframeProtocol { + typealias Parameters = NSNull + + struct Dependencies { + + } + + enum State { + + } + + func prepareModule(parameters: NSNull, dependencies: AuthWireframe.Dependencies) -> UIViewController { + let view = AuthViewController() + let presenter = AuthPresenter() + let interactor = AuthInteractor() + + let viewDep = AuthViewController.Dependencies(presenter: presenter) + let presenterDep = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) + let interactorDep = AuthInteractor.Dependencies(presenter: presenter) + + view.inject(dependencies: viewDep) + presenter.inject(dependencies: presenterDep) + interactor.inject(dependencies: interactorDep) + + return view + } +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift new file mode 100644 index 000000000..a2298304f --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift @@ -0,0 +1,47 @@ +// +// CodeConfirmationProtocols.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol CodeConfirmationWireframeProtocol: WireframeProtocol { + func codeValid() + func codeInvalid() + func back() +} + +protocol CodeConfirmationViewProtocol: class where Self: UIViewController { + func updateTimerLabel(text: String) + func showButtons() +} + +protocol CodeConfirmationPresenterProtocol: NavigationProtocol { + var isCanAskForCall: Bool { get } + var address: String { get } + var descriptionText: String { get } + + func viewDidLoad() + + func sendConfirmationCode(code: String, completion: (Result) -> Void) + + func resendCode() + func askForCall() +} + +protocol CodeConfirmationInputInteractorProtocol { + var address: String { get } + var authProviderType: AuthProviderType { get } + + func sendConfirmationCode(code: String, completion: (Result) -> Void) + + func resendCode() + func askForCall() +} + +protocol CodeConfirmationOutputInteractorProtocol { + +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift new file mode 100644 index 000000000..b10a83e5c --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -0,0 +1,50 @@ +// +// CodeConfirmationInteractor.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, SetInjectable { + private var presenter: CodeConfirmationOutputInteractorProtocol? + + private(set) var address: String + private(set) var authProviderType: AuthProviderType + + init(address: String, authProviderType: AuthProviderType) { + self.address = address + self.authProviderType = authProviderType + } + + func sendConfirmationCode(code: String, completion: (Result) -> Void) { + completion(.success(())) + +// struct InnerError: Error {} +// +// completion(.failure(InnerError())) + } + + func resendCode() { + + } + + func askForCall() { + + } +} + +// MARK: - SetInjectable + +extension CodeConfirmationInteractor { + struct Dependencies { + let presenter: CodeConfirmationOutputInteractorProtocol + } + + func inject(dependencies: CodeConfirmationInteractor.Dependencies) { + presenter = dependencies.presenter + } +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift new file mode 100644 index 000000000..5aa9d1db9 --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -0,0 +1,105 @@ +// +// CodeConfirmationPresenter.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeConfirmationOutputInteractorProtocol, SetInjectable { + private var view: CodeConfirmationViewProtocol? + private var interactor: CodeConfirmationInputInteractorProtocol? + private var wireframe: CodeConfirmationWireframe? + private var timerValue = 61 { + didSet { + view?.updateTimerLabel(text: "You should receive it within \(timerValue) seconds.") + + if timerValue == 0 { + view?.showButtons() + timer?.invalidate() + timer = nil + } + } + } + private var timer: Timer? + + var isCanAskForCall: Bool { + guard let interactor = interactor else { + return false + } + + return interactor.authProviderType == .phoneNumber + } + + var address: String { + return interactor?.address ?? "" + } + + var descriptionText: String { + guard let interactor = interactor else { + return "" + } + + if interactor.authProviderType == .phoneNumber { + return "We've sent code to your phone".localized + } else { + return "We've sent code to your email".localized + } + + } + + func viewDidLoad() { + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in + self.timerValue = self.timerValue - 1 + } + } + + func sendConfirmationCode(code: String, completion: (Result) -> Void) { + interactor?.sendConfirmationCode(code: code) { (result) in + result + .onSuccess { + wireframe?.codeValid() + completion(.success(())) + } + .onFailure { (error) in + wireframe?.codeInvalid() + completion(.failure(error)) + } + } + } + + func resendCode() { + interactor?.resendCode() + } + + func askForCall() { + guard isCanAskForCall else { + return + } + + interactor?.askForCall() + } + + func back() { + wireframe?.back() + } +} + +// MARK: - SetInjectable + +extension CodeConfirmationPresenter { + struct Dependencies { + let view: CodeConfirmationViewProtocol + let interactor: CodeConfirmationInputInteractorProtocol + let wireframe: CodeConfirmationWireframe + } + + func inject(dependencies: CodeConfirmationPresenter.Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift new file mode 100644 index 000000000..c8363f2f9 --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -0,0 +1,406 @@ +// +// CodeConfirmationViewController.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, SetInjectable { + private final class TextFieldsController: NSObject, UITextFieldDelegate { + private var textFields: [UITextField] = [] + var allFieldsFilledAction: ((_ code: String) -> Void)? + + func add(textFields: [UITextField]) { + self.textFields.forEach { $0.delegate = nil } + self.textFields = textFields + self.textFields.forEach { $0.delegate = self } + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string == "" { + textFields.previous(before: textField)?.becomeFirstResponder() + textField.text = string + return false + } + + var isNew = false + + if range.location > 0 || range.length > 0 { + if let newTextField = textFields.next(after: textField) { + newTextField.becomeFirstResponder() + newTextField.text = string + isNew = true + } else { + if isAllFieldsFilled() { + allFieldsFilledAction?(allValues()) + } + + textField.text = String(string.last ?? Character("")) + + return false + } + } + + if isAllFieldsFilled() { + allFieldsFilledAction?(allValues()) + } + + if string.count == 1 && !isNew { + textField.text = string + } + + return false + } + + func textFieldDidEndEditing(_ textField: UITextField) { + print("end") + } + + private func isAllFieldsFilled() -> Bool { + return textFields + .filter { ($0.text ?? "").count == 0 } + .count < 1 + } + + private func allValues() -> String { + return textFields + .map { $0.text } + .compactMap { $0 } + .joined() + } + } + + private var presenter: CodeConfirmationPresenterProtocol? + private weak var backButton: UIButton! + private weak var welcomeLabel: UILabel! + private weak var logoImageView: UIImageView! + private weak var addressLabel: UILabel! + + private weak var textFieldsContainer: UIView! + private var textFieldsController: TextFieldsController = TextFieldsController() + + private weak var textField1: UITextField! + private weak var textField2: UITextField! + private weak var textField3: UITextField! + private weak var textField4: UITextField! + private weak var textField5: UITextField! + private weak var textField6: UITextField! + + private weak var descriptionLabel: UILabel! + + private weak var timerLabel: UILabel? + + private weak var resendCodeButton: UIButton! + private weak var callMeButton: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.nynja.backgroundColor + + backButton = makeBackButton(on: view) + welcomeLabel = makeWelcomeLabel(on: view) + logoImageView = makeLogoImageView(on: view, top: welcomeLabel) + addressLabel = makeAddressLabel(on: view, top: logoImageView) + textFieldsContainer = makeTextFieldsContainer(on: view, top: addressLabel) + + textField1 = makeFirstTextField(on: textFieldsContainer) + textField2 = makeMiddleTextField(on: textFieldsContainer, left: textField1) + textField3 = makeMiddleTextField(on: textFieldsContainer, left: textField2) + textField4 = makeMiddleTextField(on: textFieldsContainer, left: textField3) + textField5 = makeMiddleTextField(on: textFieldsContainer, left: textField4) + textField6 = makeLastTextField(on: textFieldsContainer, left: textField5) + + descriptionLabel = makeDescriptionLabel(on: view, top: textFieldsContainer) + + timerLabel = makeTimerLabel(on: view) + + addressLabel.text = presenter?.address + descriptionLabel.text = presenter?.descriptionText + + textField1.becomeFirstResponder() + + view.layoutIfNeeded() + + appendBottomBorder(to: [textField1, textField2, textField3, textField4, textField5, textField6]) + + textFieldsController.add(textFields: [textField1, textField2, textField3, textField4, textField5, textField6]) + textFieldsController.allFieldsFilledAction = { [weak self] code in + self?.showHUD() + self?.presenter?.sendConfirmationCode(code: code) { _ in self?.hideHUD()} + } + + presenter?.viewDidLoad() + } + + func showHUD() { + + } + + func hideHUD() { + + } + + func updateTimerLabel(text: String) { + timerLabel?.text = text + } + + func showButtons() { + timerLabel?.isHidden = true + resendCodeButton = makeResendCodeButton(on: view) + + if presenter?.isCanAskForCall ?? false { + callMeButton = makeCallMeButton(on: view, top: resendCodeButton) + } + } +} + +extension CodeConfirmationViewController { + @objc func back(sender: UIButton) { + presenter?.back() + } + + @objc func resendCode(sender: UIButton) { + presenter?.resendCode() + } + + @objc func callMe(sender: UIButton) { + presenter?.askForCall() + } +} + +// MARK: - SetInjectable + +extension CodeConfirmationViewController { + struct Dependencies { + let presenter: CodeConfirmationPresenterProtocol + } + + func inject(dependencies: CodeConfirmationViewController.Dependencies) { + presenter = dependencies.presenter + } +} + +// MARK: - UI fabric methods + +private extension CodeConfirmationViewController { + func appendBottomBorder(to textFields: [UITextField]) { + textFields.forEach { + let bottomBorder = CALayer() + bottomBorder.frame = CGRect(x: 0, y: $0.bounds.size.height - 1, width: $0.bounds.size.width, height: 2) + + bottomBorder.backgroundColor = UIColor.nynja.mainRed.cgColor + + $0.layer.addSublayer(bottomBorder) + } + } + + func makeBackButton(on view: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setImage(UIImage.backButtonImage, for: .normal) + button.addTarget(self, action: #selector(back(sender:)), for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.left.equalToSuperview().offset(8) + make.top.equalToSuperview().offset(37) + make.width.height.equalTo(24) + } + + return button + } + + func makeWelcomeLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.text = "Welcome to".localized + + label.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(70) + make.centerX.equalToSuperview() + } + + return label + } + + func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { + let imageView = UIImageView() + view.addSubview(imageView) + + imageView.contentMode = .scaleAspectFill + imageView.image = UIImage.logoImage + + imageView.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom).offset(16) + make.centerX.equalToSuperview() + make.width.equalToSuperview().multipliedBy(9/20) + } + + return imageView + } + + func makeAddressLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom).offset(42) + make.centerX.equalToSuperview() + } + + return label + } + + func makeTextFieldsContainer(on view: UIView, top: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + container.snp.makeConstraints { (make) in + make.height.equalTo(64) + make.centerX.equalToSuperview() + make.top.equalTo(top.snp.bottom).offset(16) + } + + return container + } + + func makeFirstTextField(on view: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.keyboardType = .numberPad + textField.tintColor = UIColor.nynja.mainRed + textField.textAlignment = .center + textField.textColor = UIColor.nynja.white + + textField.snp.makeConstraints { (make) in + make.left.equalToSuperview() + make.centerY.equalToSuperview() + make.width.equalTo(36) + } + + return textField + } + + func makeMiddleTextField(on view: UIView, left: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.keyboardType = .numberPad + textField.tintColor = UIColor.nynja.mainRed + textField.textAlignment = .center + textField.textColor = UIColor.nynja.white + + textField.snp.makeConstraints { (make) in + make.left.equalTo(left.snp.right).offset(5) + make.centerY.equalToSuperview() + make.width.equalTo(36) + } + + return textField + } + + func makeLastTextField(on view: UIView, left: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.keyboardType = .numberPad + textField.tintColor = UIColor.nynja.mainRed + textField.textAlignment = .center + textField.textColor = UIColor.nynja.white + + textField.snp.makeConstraints { (make) in + make.left.equalTo(left.snp.right).offset(5) + make.centerY.equalToSuperview() + make.width.equalTo(36) + make.right.equalToSuperview() + } + + return textField + } + + func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.manatee + + label.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom) + make.centerX.equalToSuperview() + } + return label + } + + func makeTimerLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.textAlignment = .center + label.textColor = UIColor.nynja.white + label.font = FontFamily.NotoSans.regular.font(size: 16) + + label.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-300) + make.width.lessThanOrEqualToSuperview() + } + + return label + } + + func makeResendCodeButton(on view: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setTitle("Resend code".localized, for: .normal) + button.setTitleColor(UIColor.nynja.mainRed, for: .normal) + button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.addTarget(self, action: #selector(resendCode(sender:)), for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-300) + } + + return button + } + + func makeCallMeButton(on view: UIView, top: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setTitle("Call me".localized, for: .normal) + button.setTitleColor(UIColor.nynja.mainRed, for: .normal) + button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.addTarget(self, action: #selector(callMe(sender:)), for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.top.equalTo(top.snp.bottom).offset(10) + + } + + return button + } +} + +// MARK: - Layout + +extension CodeConfirmationViewController { + +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift new file mode 100644 index 000000000..b5b3c1d41 --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -0,0 +1,69 @@ +// +// CodeConfirmationWireframe.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum AuthProviderType { + case email + case phoneNumber +} + +protocol CodeConfirmationCoordinatorProtocol { + func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) +} + +final class CodeConfirmationWireframe: CodeConfirmationWireframeProtocol { + private let coordinator: CodeConfirmationCoordinatorProtocol + + init(coordinator: CodeConfirmationCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let address: String + let authType: AuthProviderType + } + + struct Dependencies { + + } + + enum State { + case validCode + case invalidCode + case back + } + + func prepareModule(parameters: CodeConfirmationWireframe.Parameters, dependencies: CodeConfirmationWireframe.Dependencies) -> UIViewController { + let view = CodeConfirmationViewController() + let presenter = CodeConfirmationPresenter() + let interactor = CodeConfirmationInteractor(address: parameters.address, authProviderType: parameters.authType) + + let viewDep = CodeConfirmationViewController.Dependencies(presenter: presenter) + let presenterDep = CodeConfirmationPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) + let interactorDep = CodeConfirmationInteractor.Dependencies(presenter: presenter) + + view.inject(dependencies: viewDep) + presenter.inject(dependencies: presenterDep) + interactor.inject(dependencies: interactorDep) + + return view + } + + func codeValid() { + coordinator.wireframe(self, didEndWith: .validCode) + } + + func codeInvalid() { + coordinator.wireframe(self, didEndWith: .invalidCode) + } + + func back() { + coordinator.wireframe(self, didEndWith: .back) + } +} diff --git a/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift b/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift new file mode 100644 index 000000000..9f85c4ed8 --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift @@ -0,0 +1,36 @@ +// +// CountrySelectorProtocols.swift +// Nynja +// +// Created by Ash on 9/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +protocol CountrySelectorWireframeProtocol: WireframeProtocol { + func selectCountry(_ country: Country) +} + +protocol CountrySelectorViewProtocol: class where Self: UIViewController { + func reloadData() +} + +protocol CountrySelectorPresenterProtocol: NavigationProtocol { + var sections: [CountriesSection] { get } + + func viewDidLoad() + + func applyFilter(_ filter: String) + func selectCountry(_ country: Country) +} + +protocol CountrySelectorInteractorOutputProtocol { + func filteredCountries(_ countries: [Country]) +} + +protocol CountrySelectorInteractorInputProtocol { + var filter: String { get set } + var filteredCountries: [Country] { get } +} diff --git a/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift b/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift new file mode 100644 index 000000000..27a13796c --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift @@ -0,0 +1,19 @@ +// +// Country.swift +// Nynja +// +// Created by Ash on 9/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct CountriesSection { + let symbol: String + var countries: [Country] +} + +struct Country: Equatable, Hashable { + let name: String + let code: String +} diff --git a/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift b/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift new file mode 100644 index 000000000..ac7643d15 --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift @@ -0,0 +1,54 @@ +// +// CountrySelectorInteractor.swift +// Nynja +// +// Created by Ash on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class CountrySelectorInteractor: CountrySelectorInteractorInputProtocol, SetInjectable { + private var presenter: CountrySelectorInteractorOutputProtocol? + private var storageService: StorageServiceProtocol? + + var filter: String = "" { + didSet { + guard filter != "" else { + return filteredCountries = countries + } + + filteredCountries = countries.filter { + $0.name.contains(substring: filter, options: .caseInsensitive) + } + } + } + + private var countries: [Country] { + get { + return (storageService?.countries.map { + Country(name: $0.name, code: $0.code) + } ?? []).sorted { $0.name < $1.name } + } + } + + private(set) var filteredCountries: [Country] = [] { + didSet { + presenter?.filteredCountries(filteredCountries) + } + } +} + +// MARK: - SetInjectable + +extension CountrySelectorInteractor { + struct Dependencies { + let presenter: CountrySelectorInteractorOutputProtocol + let storageService: StorageServiceProtocol + } + + func inject(dependencies: CountrySelectorInteractor.Dependencies) { + presenter = dependencies.presenter + storageService = dependencies.storageService + } +} diff --git a/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift b/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift new file mode 100644 index 000000000..b66cc50b5 --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift @@ -0,0 +1,69 @@ +// +// CountrySelectorPresenter.swift +// Nynja +// +// Created by Ash on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class CountrySelectorPresenter: CountrySelectorInteractorOutputProtocol, CountrySelectorPresenterProtocol, SetInjectable { + private weak var view: CountrySelectorViewProtocol? + private var interactor: CountrySelectorInteractorInputProtocol? + private var wireframe: CountrySelectorWireframe? + + private(set) var sections: [CountriesSection] = [] { + didSet { + view?.reloadData() + } + } + + func viewDidLoad() { + interactor?.filter = "" + } + + func applyFilter(_ filter: String) { + interactor?.filter = filter + } + + func selectCountry(_ country: Country) { + wireframe?.selectCountry(country) + } + + func back() { + wireframe?.back() + } +} + +// MARK: - SetInjectable + +extension CountrySelectorPresenter { + struct Dependencies { + let view: CountrySelectorViewProtocol + let interactor: CountrySelectorInteractorInputProtocol + let wireframe: CountrySelectorWireframe + } + + func inject(dependencies: CountrySelectorPresenter.Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} + +// MARK: - CountrySelectorInteractorOutputProtocol + +extension CountrySelectorPresenter { + func filteredCountries(_ countries: [Country]) { + let filterAction: (String, [Country]) -> [Country] = { symbol, countries in + countries.filter { $0.name.first == symbol.first } + } + + sections = countries + .compactMap { $0.name.first } + .map { String($0) } + .uniqueWithPreservedOrder() + .map { CountriesSection(symbol: $0, countries: filterAction($0, countries)) } + } +} diff --git a/Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift b/Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift new file mode 100644 index 000000000..433565986 --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift @@ -0,0 +1,103 @@ +// +// CountryTVCell.swift +// Nynja +// +// Created by Ash on 9/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class CountryTVCell: UITableViewCell, Configurable, IdentityProtocol { + static var identifier: String = "CountryTVCell" + + private var titleLabel: UILabel? + private var codeLabel: UILabel? +} + +// MARK: - Configurable + +extension CountryTVCell { + struct Config { + let name: String + let code: String + } + + func configure(config: CountryTVCell.Config) { + separatorInset = UIEdgeInsets( + top: separatorInset.top, + left: CGFloat(SelfLayout.separatorLeftInset), + bottom: separatorInset.bottom, + right: separatorInset.right) + + backgroundColor = UIColor.nynja.clear + contentView.backgroundColor = UIColor.nynja.clear + + titleLabel = titleLabel ?? makeTitleLabel(on: contentView) + codeLabel = codeLabel ?? makeCodeLabel(on: contentView) + + titleLabel?.text = config.name + codeLabel?.text = "+" + config.code + } +} + +// MARK: - ui factory methods + +private extension CountryTVCell { + func makeTitleLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.textAlignment = .left + label.textColor = UIColor.nynja.white + label.font = FontFamily.NotoSans.medium.font(size: CGFloat(TitleLabelLayout.fontSize)) + + label.snp.makeConstraints { (make) in + make.left.equalTo(TitleLabelLayout.left) + make.height.equalTo(TitleLabelLayout.height) + make.centerY.equalToSuperview() + } + + return label + } + + func makeCodeLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.textAlignment = .right + label.textColor = UIColor.nynja.dustyGray + label.font = FontFamily.NotoSans.medium.font(size: CGFloat(CodeLabelLayout.fontSize)) + + label.snp.makeConstraints { (make) in + make.right.equalTo(-CodeLabelLayout.right) + make.height.equalTo(CodeLabelLayout.height) + make.centerY.equalToSuperview() + } + + return label + } +} + +// MARK: - Layout + +private extension CountryTVCell { + enum TitleLabelLayout { + static let left = 68.0 + static let height = 22.0 + + static let fontSize = 16.0 + } + + enum CodeLabelLayout { + static let right = 16.0 + static let height = 20.0 + + static let fontSize = 14.0 + } + + enum SelfLayout { + static let separatorLeftInset = 68.0 + } +} diff --git a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift new file mode 100644 index 000000000..a254c6018 --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift @@ -0,0 +1,197 @@ +// +// CountrySelectorViewController.swift +// Nynja +// +// Created by Ash on 9/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class CountrySelectorViewController: UIViewController, CountrySelectorViewProtocol, SetInjectable, UITableViewDelegate, UITableViewDataSource { + private var presenter: CountrySelectorPresenterProtocol? + + private var headerView: NavigationView? + private var tableView: UITableView? + private var controlContainerView: NynjaControlContainerView? + + private var searchField: NynjaSearchField? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.nynja.darkLight + + headerView = makeHeaderView(on: view) + searchField = makeSearchField() + controlContainerView = makeControlContainerView(on: view, searchField: searchField!) + tableView = makeTableView(on: view, top: headerView!, bottom: controlContainerView!) + + presenter?.viewDidLoad() + } +} + +// MARK: - SetInjectable + +extension CountrySelectorViewController { + struct Dependencies { + let presenter: CountrySelectorPresenterProtocol + } + + func inject(dependencies: CountrySelectorViewController.Dependencies) { + presenter = dependencies.presenter + } +} + +// MARK: - CountrySelectorViewProtocol + +extension CountrySelectorViewController { + func reloadData() { + tableView?.reloadData() + } +} + +// MARK: - UITableViewDelegate + +extension CountrySelectorViewController { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let view = CountryHeaderTVView() + + if let section = presenter?.sections[section] { + view.configure(config: CountryHeaderTVView.Config(symbol: section.symbol)) + } + + return view + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 52 + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 36 + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let country = countryWithIndexPath(indexPath) { + presenter?.selectCountry(country) + } + } +} + +// MARK: - UITableViewDataSource + +extension CountrySelectorViewController { + func numberOfSections(in tableView: UITableView) -> Int { + return presenter?.sections.count ?? 0 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let sections = presenter?.sections else { + return 0 + } + + let currentSection = sections[section] + let countriesInSection = currentSection.countries + + return countriesInSection.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: CountryTVCell.identifier, for: indexPath) as? CountryTVCell else { + fatalError() + } + + if let country = countryWithIndexPath(indexPath) { + cell.configure(config: CountryTVCell.Config(name: country.name, code: country.code)) + } + + return cell + } +} + +private extension CountrySelectorViewController { + func sectionWithIndexPath(_ indexPath: IndexPath) -> CountriesSection? { + return presenter?.sections[indexPath.section] + } + + func countryWithIndexPath(_ indexPath: IndexPath) -> Country? { + return sectionWithIndexPath(indexPath)?.countries[indexPath.row] + } +} + +// MARK: - UI factory methods + +private extension CountrySelectorViewController { + func makeHeaderView(on view: UIView) -> NavigationView { + let config = NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: "select country".uppercased(), + navigationHandler: presenter, + backButtonImage: UIImage.backButtonImage) + + return UIView.makeHeaderView(on: view, top: nil, config: config) + } + + func makeTableView(on view: UIView, top: UIView, bottom: UIView) -> UITableView { + let tableView = UITableView() + view.addSubview(tableView) + + tableView.keyboardDismissMode = .interactive + tableView.backgroundColor = UIColor.nynja.clear + tableView.separatorStyle = .singleLine + tableView.tableFooterView = UIView() + tableView.showsHorizontalScrollIndicator = false + tableView.rowHeight = 52 + tableView.estimatedRowHeight = tableView.rowHeight + + tableView.delegate = self + tableView.dataSource = self + + tableView.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom) + make.bottom.equalTo(bottom.snp.top) + make.left.right.equalToSuperview() + } + + tableView.register(CountryTVCell.self, + forCellReuseIdentifier: CountryTVCell.identifier) + + return tableView + } + + func makeControlContainerView(on view: UIView, searchField: NynjaSearchField) -> NynjaControlContainerView { + let containerView = NynjaControlContainerView(contentView: searchField) + view.addSubview(containerView) + + containerView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + adjustVerticalInset(.bottom, make: make, offset: -28) + } + + containerView.addGradientView() + + return containerView + } + + func makeSearchField() -> NynjaSearchField { + let searchField = NynjaSearchField() + + searchField.searchTextChangeHandler = { [weak self] searchQuery in + let filter = searchQuery ?? "" + self?.presenter?.applyFilter(filter) + } + + return searchField + } +} + +// MARK: - Layout + +private extension CountrySelectorViewController { + enum ControlContainerLayout { + + } +} diff --git a/Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift b/Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift new file mode 100644 index 000000000..b436967eb --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift @@ -0,0 +1,74 @@ +// +// CountryTVHeader.swift +// Nynja +// +// Created by Ash on 9/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + + +final class CountryHeaderTVView: UIView, Configurable, IdentityProtocol { + static var identifier: String = "CountryHeaderTVView" + + private var title: UILabel? +} + +// MARK: - Configurable + +extension CountryHeaderTVView { + struct Config { + let symbol: String + } + + func configure(config: CountryHeaderTVView.Config) { + backgroundColor = UIColor.nynja.clear + + title = title ?? makeTitleLabel(on: self) + title?.text = config.symbol + } +} + +// MARK: - UI factory methods + +private extension CountryHeaderTVView { + func makeTitleLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.layer.cornerRadius = CGFloat(TitleLabelLayout.cornerRadius) + + label.backgroundColor = UIColor.nynja.mainRed + label.clipsToBounds = true + + label.textColor = UIColor.nynja.white + label.textAlignment = .center + label.font = FontFamily.NotoSans.medium.font(size: CGFloat(TitleLabelLayout.fontSize)) + + label.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + make.width.equalTo(TitleLabelLayout.width) + make.height.equalTo(TitleLabelLayout.height) + make.left.equalTo(TitleLabelLayout.left) + } + + return label + } +} + +// MARK: - Layout + +private extension CountryHeaderTVView { + enum TitleLabelLayout { + static let width = 28.0 + static let height = 28.0 + + static let cornerRadius = width / 2 + + static let left = 16.0 + + static let fontSize = 16.0 + } +} diff --git a/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift b/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift new file mode 100644 index 000000000..f742d2a08 --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift @@ -0,0 +1,56 @@ +// +// CountrySelectorWireframe.swift +// Nynja +// +// Created by Ash on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol CountrySelectorCoordinatorProtocol { + func wireframe(_ wireframe: CountrySelectorWireframe, endWithState state: CountrySelectorWireframe.State) +} + +final class CountrySelectorWireframe: CountrySelectorWireframeProtocol { + typealias Parameters = NSNull + + struct Dependencies { + let storageService: StorageServiceProtocol + } + + enum State { + case back + case endWith(country: Country) + } + + private var coordinator: CountrySelectorCoordinatorProtocol? + + init(coordinator: CountrySelectorCoordinatorProtocol) { + self.coordinator = coordinator + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let view = CountrySelectorViewController() + let presenter = CountrySelectorPresenter() + let interactor = CountrySelectorInteractor() + + let viewDep = CountrySelectorViewController.Dependencies(presenter: presenter) + let presenterDep = CountrySelectorPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) + let interactorDep = CountrySelectorInteractor.Dependencies(presenter: presenter, storageService: dependencies.storageService) + + view.inject(dependencies: viewDep) + presenter.inject(dependencies: presenterDep) + interactor.inject(dependencies: interactorDep) + + return view + } + + func back() { + coordinator?.wireframe(self, endWithState: .back) + } + + func selectCountry(_ country: Country) { + coordinator?.wireframe(self, endWithState: .endWith(country: country)) + } +} diff --git a/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift b/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift index 610754656..c940ee161 100644 --- a/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift +++ b/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift @@ -38,9 +38,8 @@ class LanguageSelectorInteractor: BaseInteractor, LanguageSelectorInteractorInpu //MARK: - LanguageSelectorInteractorInputProtocol func fetchLanguages() { - translationService.availableLanguages { [weak self] (languages, error) in - guard let sSelf = self, let languages = languages else { return } - sSelf.languages = languages + translationService.availableLanguages { [weak self] result in + result.onSuccess { self?.languages = $0 } } } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift index 50269bbcc..5e1820aa3 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift @@ -86,11 +86,12 @@ extension MessageInteractor { result[element.0] = element.1 }) - let translationTask = translationService.translate(text: result.text, from: nil, to: language.language, completion: { (translatedText, error) in - guard error == nil else { + let translationTask = translationService.translate(text: result.text, from: nil, to: language.language, completion: { result in + guard result.isSuccess else { return } - guard var translatedText = translatedText else { + + guard var translatedText = result.value else { assertionFailure("Something went wrong with translation on sending") return } @@ -290,8 +291,8 @@ extension MessageInteractor { result[element.0] = element.1 }) - let task = translationService.translate(text: result.text, from: nil, to: traslationLanguage, completion: { [weak self] (translatedText, error) in - guard var translatedText = translatedText else { + let task = translationService.translate(text: result.text, from: nil, to: traslationLanguage, completion: { [weak self] result in + guard var translatedText = result.value else { self?.cancelTranslating(localId: id) return } diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Contents.json new file mode 100644 index 000000000..599c82dcf --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "info" : { + "author" : "xcode", + "version" : "1" + }, + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "Icons_General_ic_accept_call.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Icons_General_ic_accept_call@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "Icons_General_ic_accept_call@3x.png" + } + ] +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call.png new file mode 100644 index 0000000000000000000000000000000000000000..a05e0dc946c154e0452985fc17903fc7553bf11d GIT binary patch literal 468 zcmV;_0W1EAP)Px$j!8s8R7ef2l`%^LK@f%Sf{2iSg|&jQlXPMb5&Q}I8?>{rF`b2#pq_^{o(^Egc~+i{p1>Xyl9p=HA0(zq$(6@2->O-dA2a*Aay>Nz>8EDhSK z`Xe+M&U129SsJ{lYLc|j=F>EN^yK&@)^|@?`!@WnzdB!)Cpc8n`r$i@7EW`Qec)bs ziZgh*iDH4tg3>_fm16ExV^RXg;^v4B+)RunRg7L84TO3IwHQt2F!~fnM?AqzyPCic zf+;ZI(tmLzHGmfH2+JTfMa=8q0Z3ec5u>RiNgI;wiBq%*FbU)gvE!PHVb$CSByHPO zj=3yl%fSmJKgggatBOGg9o%FQH`jp_(&Wgjh=03ukqqFwnd!FU<=5H7aE(k zN6;MSF|Z5dZ>z-@g1jWX*Ez`CPNH}SEP(@X17x4%d)NoP6Yv{)gjm1ACp~8X0000< KMNUMnLSTaA;L+>= literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@2x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8dbfc42202cde8d345ffce861e4fea6bde4afd33 GIT binary patch literal 908 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sjKx9jP7LeL$-D%z-2;3=T!DiB zM*+nlV3k=N1q{lZk|4ie21X`k7FITP4o+?!VG&U=NhvvHH9cc9a~oTGXID3`fZ&Me zgtW};yuy;wvhs?`>YBR7=C<~Z&aR#bQ)bMZyeck%Lkm3L1~t?NVWk40?s zUH?7thy8mMo8Mb1&OcV4n|qwGK>O6ajf=GcCh3bs7^W5;70_mzU-{r#=DIwK10{3r zx?B~NIJfvl^?SBwF>TlFuPy)rT%#ODg7I(j!hJPGk5B= p%8yF(w-seg(fzD+E%^5X_CKN7qA&Bx-vFbZ!PC{xWt~$(69DhuONIad literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@3x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..362d8469bfaf36974cd0f9011158ccc9f0432020 GIT binary patch literal 1287 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifV{wqX6T`Z5GB1JbJpn!;u0X;6 zqhM%;z^WTBrvT$`a!HV1FaskKGYcylI|mmJAHRU0u!yL*grt~P2`u*qcKg%}NWef~VQJyZ2ArXhy-gp@p@#M*mWn9No$BGp@+(fMfoo%n^hrSv-WzB4YX96}=aVJqU~QLp$+a3?HZ93KocnQY z!4kp4ymnKcc-S?ESRHL>^Zlv0{wvuJvGrcip2`KO(%UtHXs%H!mO z_bv?Gy8rf-8~t;p*)I{jy49}lNc|FV_3CwR{+a&~junmkK27RWJ}?t7c)I$ztaD0e F0su1TIN<;Q literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Contents.json new file mode 100644 index 000000000..24973f8b2 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "info" : { + "author" : "xcode", + "version" : "1" + }, + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "Icons_General_ic_google.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Icons_General_ic_google@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "Icons_General_ic_google@3x.png" + } + ] +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google.png new file mode 100644 index 0000000000000000000000000000000000000000..0387da2a5a379d1eddf3706a81564518a5ca32f2 GIT binary patch literal 740 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S3?yCqj{O5tjKx9jP7LeL$-D%z?E-v4T!DiB z|Nn0Q!G`~#p=mS!dv449@9F8gIqkojTjG!$VDo1$tHt~#pq6hlM1JpO{Zy$4Bz;zU z0Tlzan1C%cSrP2i`sKB=DM)ZdFi`QIJ#0YpfwsJIHUkPew|)UC0J;Qhbuds6q7h`# z%3z@E?>($Q?GSTbIhz8xP0v>T1cqQ*NswPK10xd?Gb=kg7dH>Dkf@}LoSeLZqN0+D znwFuJt-XVDL{f5UMqzPjc~wnqTjzv{6Q@j@Id9&AwVOBZ*s*K(?mc_{Qmp*-^Iw+7l1ZC@^oY%pB~O+ zDBvpJb;EUoxr?hvz?;1a9kC^jnq~(Mt4^^Dkwk0||?I8b_^U=vsy>Xl$?N)9+ALISWRhv!! z`kC~(S}r`z4l{pV$-LEO^5Nv3$A!BO7#E*=e67Lz*<`(^X{yDSR2!bo*U|lFyF(?G zh2yS(>9Q?SbsOJa=aUTUc%dxEaBI=;BAunP8rd~hR@Q%aEBW{L#^W3wkrPiV?!>&i q`_D*A_|N}@imw|&G;_{a9M!)Z>uB$IX}2ZNw+x=HelF{r5}E)KE?m_B literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@2x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4e67f67f272bfefb549bd56a60d7db5529d1dc0a GIT binary patch literal 1367 zcmX9;2~-nT6it967!6QG5Vag=trhIzl9>=A5;K#8y@4bsRur*`S{1QS*IL;U7HzDp z8!c_6Dog@I0~8UB7_p#8J!;jKa!zeai<*Q*vmhjxzJY(<>KVlGAqn5p?*ClOMfK}!anZ~488*=}jv*aWdE(3M z(e-ORElh#N=`l@-I=>p#$vq~9C6*VZUxleg@e+r}NE3l?V{&!!s501y#^tq1FPSKT~LV9N+Eq9Wf8ypFHnAR*^g`hPDjYxS7>GoaT{q!4REA@STF=q zTvuphEv*b_n0k;r1%^6}WC;j>PaZjz4FphtmdHE7;EKpXlL%WYodki*CZqzD9SVVL zb`JRtQhv@$O_oxqUfw=FG~X#q=2Vuyf52Wrq$Q zIdb%?<0np>Jaww_bk(`)YC@;0tE<0YFf=tcUu?P5YP{Ok)^^Qw{d#+Qdq+p-&6{0a zx4XOV{rcd+!{2*){^;%f^KoC_fBjFMKC@V?*1@6S;TJZW!{Ky}jEuZ|IXXHvHs*4> z$6vu1df_dBZ>T2@Unz^ay)lzd{jvYn0UN6S_THDuuFm|aPyZd_`Z4x*A%@7*1uHak zX5b=udNx->MNgfM$ckT~ndwtSWF?yP=9b{{hDCFA?i5)_s5+spJ9qKL;ah7M{l#a9 zf7W|Ro9?Q;-?*V*1s)Q8dT?H%{Aid!av_o`KmH((&@Dkf+K?uaDUxgVn{>0evxW;^ z&2U-xn&6Jw_*XswP1!MnhgZ!k;>GrqmF2!1UU-|jkax5o;5p{~eV8dCK;iUNbl#=D zFUg6(D?&pRU3}Ab0gA{y$F@*u7X`<(f2UNl0@qGUQRa&)rb}|X@Csh2VmjZHSlY0y zxIr?jw1il8V_IOIPpvt9ep$#_QGjA9e_O?yeECld4Si!|f@(nmBlnx+Z69;@*`F%8 zdGhJW%$jV z6fbMz2J<6&=}Ohyp!#Qpgs#{kdrXt=&RZ`&T(T9v6&YsBWW_$rmKU>ji4BELN2ew| zocCMsPx+nMp)JRA}DSntM=G=NZPI_nfo4$bxbe?N~39iHTy{G!a3>O93TVNt*$J745~D zHl2(%u^pX=la@{<<}$IJY0@@Hr!#4%O$9L-8UrF|6a_?*HdIY8sa4yFRF~!E5@dJJ zdHRRbF}MqyU3Ou4{$uBRzx{suUcUD|2SFI+%uk=AdyGs)locdX2_y=UL?BUwDj+Tr z{UW>!;A4pVO>F9+q(d#AhVmQ|YOtR5ne)6uo~;zw0&+nR2O>n?Cs?gPt4?TWIU6yM zUAY;#28^9Tu4M@K6-c9~%C4j%r&}it((&Mmd*JoFNOA+hDyf1ZWi#trKbbH{78k3I z-pfymAv+0bm;edj5mBB`%*y|nl$RS}g>9Ej6$Dpd5+uOeuv<%$|JrmTERfE1 z%QJOdKL~IZCQ$7Ar0f69QkH`CGGiyylI&+pS z*A1fUvcK0baFO zEZJrO0btD1@A&NcuK}ix<5)KZd>4=gQI#ge&^qJiXxDMosycDwi!e9`(C;>gheaft zq&mu8*PvOl<7%5O23Dj-t)%Hix^5f@wh0oEm!mAU-^A9QbeJ)?u`YAw5WU|cQRXLd zx>OBo66@N|hpZW3e!k7&c6I=qA26OGf3RAuJL29s)f4nR?3Ncvmfi|c>DE+a$Jd={ z4`#m+!`10^J`p&e3@Qprlj_gBHJ-eSscHK!fX!XG%T60WITXo@uWM}&I$`{J@z9L; zu9p^mL@!dz7X;~16m>)5k(TCA78=JxD`L%dHUUBE4c1+{TfTGScZ=fne#>kV70<0u z)+8Q1(}D<;&lN&a2s|oQWATi~&a2kDTw~62ROGQp1@!Al&oNsb4CC5WP^1v!MP)@p5}RA`j5`{SyBcs3P6j+@(QMahC%8o z)5c6<#h4en^+K{Oz3ca8X1Pevux5aUECFa@mcw*=?$wX5H~25k+3RWydsS6rU&E93 zYHfJY!4jGc0V=1)HS`iI!|pt6hQTM_bre7;>vE{jz@3~#5YXspc|_IGz5JBk+vX% z&_?3RCgb&z52O(1%yc}Rv|oL+C+j}oP|&~f(K7q8VC-_AbLCOTVN;CCt;ie}uBs?w zPWP>zl=i~z9E-!S6{PPk$(ZmM)Wvj~D3A`{oXoj-1tK4gWc2F5y^T-wXDlL-3;>4c z+a{XP5EhwTt46We|IiH7_--uTANFS*e|>P_Dv;LO1&Eu{_Lrnh6yVxt26I59noRo3 zbvx{xejznct3LmCKdsKMJM;e0?oL?a_p%l5ugY1jOiZNeyG<81-Y$y1&v>#2TLJk1 z04^AD9UHpy26_$a{iBRb8Shk_eJ0$1xz7Q=Id{lT~!2B0(-enlrXa0JEb47;z*r<>lDGOhJ-gVz>Fn|}XH2#!UQMfte zdHBN3yWL*>SRgNdDe_YJ*Tg$2WNPYpH;J+UCw+~;AD31Xm4zxt^PY2k#qfGhlALSW zi$J%{9{tNZJ{#%Kl(dTC7J%tNbTciQYT4RS^p}oc;MTlbuDKlNcff1g9Q*0d2iOY1!}#HI|w^OjsBKDKvSOl zzp^Te=MEXh2?FyYVC3(#cb4_4g9{fy(4%Tvdi}1bPe!S@=9Tf*#`#{i_XGkc>0uj) zMFwrolJ{&q1y3K{6IDOPwGx;z4{n<0_71%X;{Hj7>{diJwmx#8e(aGeKs6V>aV0tS z3z-7EHhF+(7m=lA0E6=6uCk_;EDl`0+;$xI%f3{{j1N46~NY+Oz-w002ovPDHLkV1oUV)6W0^ literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/Contents.json new file mode 100644 index 000000000..d0ace947d --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "info" : { + "author" : "xcode", + "version" : "1" + }, + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "ic_facebook.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "ic_facebook@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "ic_facebook@3x.png" + } + ] +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook.png b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..37ced60f93f204574657223149e2485c53dbf3a4 GIT binary patch literal 292 zcmV+<0o(qGP)Px#-bqA3R7eeDU>F6|41xdu|AzzdJ|O-8#31kwh!^6sg9V=?vK-K$6d->#vKWxf z2*gZiVklg^k}#D(!$Af$!1$!l#QH!2Xs9%j!5@I^5Fq{x#BY(r$zuY2pbo^yU^lTQ z5fwyO4Ev8oj65Nrvp)gx7i8xH*?)leBNX@GGntq;;KgD9Gf<2VD*hgeJY0y}Ac7kJ zWIRLWptFhbK{Qa4BoHG9#T6jG9ctJ?C`~>E3~TgMw2N5Ns1ZbX?HaWJZ}5O5M}vr9 q5D}BnK!$t+;(bV_T|it7>SSG;jTulc&#Ld;Ij-%eNnX z{`&pr?>`@wsr!Mt`aE46Lp+YZy}DEEkb*#KqDfYa;jA4!(rRv;pa1c%kUM^iZQayo zAHqexuU_V`^2YbC-FI*9I(Bc}bP0l+XkK9nJ7p literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@3x.png b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..865f8af06b3f92c1a584296e5c7d54e8414825ea GIT binary patch literal 517 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifV{wqX6T`Z5GB1IgjR8I(u0Z<# zkOJFh6wLuT+NvbTFPMRmlZ%H>NK#o_-@wSi%G%aBEIdA`ps=X4wxOqg`jTa9w(L51 z{LJ~wSFhiF{QSkM*Kgka`1$MipTGZlZmk3b!5&W+$B>A_Z*OcDI%FW=8klTgIb)8G zk!AXZ@Bi(41VwV36JG`%c%vIS=iS+JCpM|Pynd?Wcz3b{+xOdFi|%C@AGQ3*H_PV? zHwV+#4uybTg9)3N9eJ4?jRmHt2!uR4RAv(JwRC}1@v?0P&1Uhen$ff7>;fCEQ=DH+ zGnU>m&e&V?)V<|Ju&{`Ui=!M@*1`o^Tt$HaQmi)>UWoh@SZU>>VH-cE<>R$YD=u=W qOmJZ01QHnPx+NqC8 zJ7cwt#k7$9_T*clL(#NYV~vkigqCrwj&ETc9kog;6&(vks>oO-7@a%~iEOgD_w)~T z;byx@b~oAGjBx(@?!9~Nz32SC-#Pc(Z;6>_Xsyq~LD}iL?j~d*kx0bezJ2?p07^yV zDBHIG$;?}_(X|DET0p)FV3&YCvYe z3~O^;_fd=kVdiqJ^#ZfavaAiv+>x#hkJkDO5xK``Q;y?&-*w%mGF?zpQBfa?;^0|NtX;c)ouU@-WfEYzK%lzPn2^#*{P0bOey6p?R@ zFzg)w;{xbf>pMl{3bRc_jsp-%SBGU;mP%@~MPx%T7(6S}1(lYTO44Ft*=}wo-s|_l zx(dK&$$bJT5y&h+F4MqW0--@5gt|i@mkOjpASpnOE*qZ9XQH(y+w~EVO4{qBSS+@& zuCA^u3r9+lQ)0OUj)?3aqE`XD0^nsLdWnc$1h8+Y{S}bSPZN>LQ|#fU)vH&Rq|NAY znoO2r0M9V<+05KDiggZ*W|OC&ea}`*Tr>cAIlfk;0R1=qg}{a_m^c*>uYh(6WZsYf z1+)daW;R-Pj+SUr82^N5H2MNFf18=RN5Nn@1$sCf{z%o z5m^|wan$GYRV4wts*b)wh>n+Q=)97Cu3Wa``4Wlci4tbylgTZq|nAsXmSw}~Q##*X8H2v?a2d*ii++` z6aYlzEM`7CZJ?WsZaAS5fXS3$X1;DbgkVDX%>>$N&Uj8;M5N%Ui{m&8MC6P_HH(Pu8X|{(AHXw4RIEVg!tVP+n%ZTo5> z`iH^Pjm-RidC`m+GxB=zLD>B@0V-1TOtl~<( z&l=|YjvR+SYcrrPNX_>0c8Cl-{r#H~-*<#Up}byv_<;L|dYNo^8glHIwC(1) z?n?l!H4aaw_4f8Yp19RZN?i(B)+-pJ*Vfi95|NXPrlkO0Q%b#|lzJ1uTd`Q|Z2)h@ zpEm<|O>6yY!zUgQxpk=Wq9if=Na1ChWm%Wu;O*(x`uh4dGIMDPhv(F3C!?d8fTvS% z)0?x=wl&A>myz9E*L{?TerY)6!sh1Yhed>vQkOykU1zeVu@Yvslu~SFr9>1ZqMkUZ zwgPA&qEF&yo_6fN9qoRNkgvZB_h8O=F_s9>$+`}f$keIheQ)BSOMeSL9fXXk$b94jIdqS5G00BVy{mkEaRycxWO zPXM^daL{>eZEb_TVUNXPIcJ60Xnz@?_YdagXgwN}B6)C5R(7kct^EyvM-02&s1iBI3&vn|rj@A!Gk_nSdMss;a8?5mAG&_MbEJgyiyKai)xJFw`H-t6CA6pCV%9 zTrA*TgKHm5Tq@-l2wB;Uh{U$->xt-vRCW^)PXWPwg0artJWYG8h@2Ro^W({rCqI}K z1}v+Ah`vai9hFiy4C*%>mI*T%V0QqdT%2v&d10rcL5~2`13+28eR*=wglz4`%mYUr zdE_D@O8M`rNF;gBtO^*++(tx?7(Tg`nLQ~} zmrNNwA+u)9`YBQ#3iHG0)6vmQz&e1QorBKZd1Alf(b#R-vSq!NWi2G450d+unws(o z^hhLPN2Ag80K9RKv{{z*P_E}~Zf?Fl5k-h-pt!jBu~D6*wf?K&ldo#64}8MwA)*!l zFUFsv*-Wr_@nTh9U;l%yuC50F9A|`+&-3RlYOp|Md7F*s{e<0!=-=705Hru$THkAu zLLd;JeC@+^-I?3BZ~rrZxrX0vWadxvV8eL^_L@?C(~?nP!nWVgT0fN_Zz4Jkz`FR^ z?XVn*!ptXWt*Zbm5s}2dq4p9{rR%yIzHIwF4D>@#9LM>wh&*gG^#NFG+jb})OkN1_ z9CrJmClwVH>$h#&b`cTH5s|gFZQuW81^8h(#3d4m*y;YK^~)gt1I1GqhguovJOBUy M07*qoM6N<$f&>9ZM*si- literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@2x.png b/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e072d60ab4eed4f8400f7a476e7c8a44e2dba396 GIT binary patch literal 5377 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 = (Value?, ServiceError?) typealias Lang = String associatedtype ServiceError: Error @@ -48,7 +47,7 @@ extension TranslationService { URLSession.shared.dataTask(with: request) { (data, response, error) in guard isSuccessMethod(data, response, error) else { - return completion((nil, .failedRequest)) + return completion(.failure(ServiceError.failedRequest)) } let result = parseAvailableLanguagesMethod(data) @@ -69,7 +68,7 @@ extension TranslationService { URLSession.shared.dataTask(with: request) { (data, response, error) in guard isSuccessMethod(data, response, error) else { - return completion((nil, .failedRequest)) + return completion(.failure(ServiceError.failedRequest)) } let result = parseDetectedLanguagesMethod(data) @@ -95,7 +94,7 @@ extension TranslationService { let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in guard isSuccessMethod(data, response, error) else { - return completion((nil, .failedRequest)) + return completion(.failure(ServiceError.failedRequest)) } let result = parseTranslationMethod(data) @@ -168,7 +167,7 @@ private extension TranslationService { let data = data, let json = try? JSON(data: data)["data"].dictionaryValue, let languages = json["languages"]?.arrayValue - else { return (nil, .wrongJsonSchema) } + else { return .failure(ServiceError.wrongJsonSchema) } let result = languages .map { $0.dictionaryValue }.map({ pair -> LangExtended? in @@ -178,7 +177,7 @@ private extension TranslationService { return LangExtended(language: language, name: name) }).compactMap { $0 } - return (result, nil) + return .success(result) } func parseDetectedLanguages(data: Data?) -> Result { @@ -186,15 +185,15 @@ private extension TranslationService { let data = data, let json = try? JSON(data: data)["data"].dictionaryValue, let detections = json["detections"]?.arrayValue - else { return (nil, .wrongJsonSchema) } + else { return .failure(ServiceError.wrongJsonSchema) } let innerArray = detections.map { $0.arrayValue }.first let dicts = innerArray?.first?.dictionaryValue guard let language = dicts?["language"]?.stringValue - else { return (nil, .wrongJsonSchema) } + else { return .failure(ServiceError.wrongJsonSchema) } - return (language, nil) + return .success(language) } func parseTranslation(data: Data?) -> Result { @@ -202,15 +201,15 @@ private extension TranslationService { let data = data, let json = try? JSON(data: data)["data"].dictionaryValue, let translations = json["translations"]?.arrayValue - else { return (nil, .wrongJsonSchema) } + else { return .failure(ServiceError.wrongJsonSchema) } let translation = translations.first?.dictionaryValue guard let result = translation?["translatedText"]?.stringValue - else { return (nil, .wrongJsonSchema) } + else { return .failure(ServiceError.wrongJsonSchema) } - return (result.htmlDecoded, nil) + return .success(result.htmlDecoded ?? "") } } -- GitLab From aaa366d4765c03f0d6acecd018e772e67d587ac1 Mon Sep 17 00:00:00 2001 From: AshCenso Date: Tue, 2 Oct 2018 20:01:29 +0300 Subject: [PATCH 002/138] in progress --- Nynja.xcodeproj/project.pbxproj | 2 +- Nynja/AppDelegate.swift | 10 +- Nynja/Library/UI/View/UIViewExtensions.swift | 11 + Nynja/Modules/Auth/AuthCoordinator.swift | 38 ++- .../Auth/AuthModule/AuthProtocols.swift | 13 +- .../AuthModule/Presenter/AuthPresenter.swift | 23 +- .../AuthModule/View/AuthViewController.swift | 145 +++++++-- .../View/Subviews/AuthHeaderView.swift | 8 +- .../View/Subviews/EmailLoginView.swift | 113 ++++--- .../View/Subviews/LoginOptionsView.swift | 67 ++-- .../View/Subviews/PhoneNumberLoginView.swift | 307 +++++++++++++++++- .../AuthModule/Wireframe/AuthWireframe.swift | 24 +- .../View/CodeConfirmationViewController.swift | 9 +- .../CountrySelector/Entities/Country.swift | 2 + .../CountrySelectorInteractor.swift | 2 +- 15 files changed, 629 insertions(+), 145 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 048d4ece6..29139da60 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -16095,7 +16095,7 @@ INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=50 -Xfrontend -warn-long-function-bodies=50"; + OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "7757f70a-8690-4f76-9822-0ac1e08381ea"; diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 08a9ae742..55735f709 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -89,14 +89,12 @@ private extension AppDelegate { self.window = UIWindow(frame: UIScreen.main.bounds) let navigation = UINavigationController() navigation.isNavigationBarHidden = true - SplashWireFrame().presentSplash(navigation: navigation) +// SplashWireFrame().presentSplash(navigation: navigation) + let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) self.window?.rootViewController = navigation self.window?.makeKeyAndVisible() - -// let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) -// self.window?.rootViewController = navigation -// self.window?.makeKeyAndVisible() -// coordinator.start() + + coordinator.start() } private func configureDependencies() { diff --git a/Nynja/Library/UI/View/UIViewExtensions.swift b/Nynja/Library/UI/View/UIViewExtensions.swift index 7e4092f8a..f4b0607d5 100644 --- a/Nynja/Library/UI/View/UIViewExtensions.swift +++ b/Nynja/Library/UI/View/UIViewExtensions.swift @@ -11,6 +11,17 @@ import UIKit // MARK: - Factory methods extension UIView { + func appendBottomBorder(color: UIColor, width: CGFloat) { + let view = UIView() + view.backgroundColor = color + addSubview(view) + + view.snp.makeConstraints { (make) in + make.bottom.left.right.equalToSuperview() + make.height.equalTo(width) + } + } + static func makeStatusBarBackgroundView(on view: UIView) -> UIView { struct StatusBarBackgroundLayout { static let height = 20 diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 25378a8ae..4459a1558 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -9,35 +9,35 @@ import Foundation -final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol { +final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol { private weak var navigation: UINavigationController? private let serviceFactory: ServiceFactoryProtocol + private var selectCountryCallback: ((Result) -> Void)? + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { self.navigation = navigation self.serviceFactory = serviceFactory } func start() { - let wireframe = AuthWireframe() + let wireframe = AuthWireframe(coordinator: self) let view = wireframe.prepareModule(parameters: NSNull(), dependencies: AuthWireframe.Dependencies()) - - navigation?.pushViewController(view, animated: true) + // let wireframe = CodeConfirmationWireframe.init(coordinator: self) // let view = wireframe.prepareModule( // parameters: CodeConfirmationWireframe.Parameters( // address: "Some", // authType: .phoneNumber), // dependencies: CodeConfirmationWireframe.Dependencies()) -// navigation?.pushViewController(view, animated: true) // let wireframe = CountrySelectorWireframe(coordinator: self) // let view = wireframe.prepareModule( // parameters: NSNull(), // dependencies: CountrySelectorWireframe.Dependencies( // storageService: serviceFactory.makeStorageService())) -// -// navigation?.pushViewController(view, animated: true) + + navigation?.pushViewController(view, animated: true) } func end() { @@ -49,7 +49,12 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt extension AuthCoordinator { func wireframe(_ wireframe: CountrySelectorWireframe, endWithState state: CountrySelectorWireframe.State) { + switch state { + case .endWith(let country): selectCountryCallback?(.success(country)) + case .back: break + } + navigation?.popViewController(animated: true) } } @@ -60,3 +65,22 @@ extension AuthCoordinator { } } + +// MARK: - AuthCoordinatorProtocol + +extension AuthCoordinator { + func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) { + switch state { + case .continueLogin(let loginOption): break + case .getCountry(let callback): + selectCountryCallback = callback + let wireframe = CountrySelectorWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: NSNull(), + dependencies: CountrySelectorWireframe.Dependencies( + storageService: serviceFactory.makeStorageService())) + + navigation?.pushViewController(view, animated: true) + } + } +} diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index cdae88ecd..a7457e5b4 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -9,7 +9,8 @@ import Foundation protocol AuthWireframeProtocol: WireframeProtocol { - + func selectCountry(completion: @escaping (Result) -> Void) + func continueLogin(loginOption: LoginOption) } protocol AuthViewProtocol: class where Self: UIViewController { @@ -18,11 +19,15 @@ protocol AuthViewProtocol: class where Self: UIViewController { protocol AuthPresenterProtocol { var loginOption: LoginOption { get } + var country: Country { get } - func loginViaFacebook() - func loginViaGoogle() func switchLoginOption() - func loginViaEmail(_ email: String) + func loginViaFacebook(completion: (Result) -> Void) + func loginViaGoogle(completion: (Result) -> Void) + func loginViaEmail(_ email: String, completion: (Result) -> Void) + func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) + + func selectCountry(completion: @escaping (Result) -> Void) } protocol AuthInputInteractorProtocol { diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 9fd623655..8ae80365f 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -16,21 +16,36 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, var loginOption: LoginOption = .phoneNumber - func loginViaFacebook() { + var country: Country = Country(ISO: "ARG", name: "Argentina", code: "54", numberTemplate: "XX XX XXX XX") + + func switchLoginOption() { + switch loginOption { + case .email: loginOption = .phoneNumber + case .phoneNumber: loginOption = .email + } + } + + func loginViaFacebook(completion: (Result) -> Void) { } - func loginViaGoogle() { + func loginViaGoogle(completion: (Result) -> Void) { } - func switchLoginOption() { + func loginViaEmail(_ email: String, completion: (Result) -> Void) { } - func loginViaEmail(_ email: String) { + func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) { } + + func selectCountry(completion: @escaping (Result) -> Void) { + wireframe?.selectCountry { result in + completion(result) + } + } } // MARK: - SetInjectable diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 6d66255b0..dfc2acc32 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -12,27 +12,24 @@ import Foundation final class AuthViewController: UIViewController, AuthViewProtocol, SetInjectable, KeyboardInteractive { private var presenter: AuthPresenterProtocol? - private var headerView: AuthHeaderView! + private lazy var headerView: AuthHeaderView = makeHeaderView(on: view) - private var scrollView: UIScrollView! - private var emailLoginView: EmailLoginView? - private var phoneNumberLoginView: PhoneNumberLoginView? + private lazy var scrollView: UIScrollView = makeScrollView(on: view, top: headerView, bottom: bottomView) + private lazy var scrollContentView: UIView = makeScrollContentView(on: scrollView, baseView: view) - private var bottomView: LoginOptionsView! + private weak var emailLoginView: EmailLoginView? + private weak var phoneNumberLoginView: PhoneNumberLoginView? + + private lazy var bottomView: LoginOptionsView = makeBottomView(on: view, presenter: presenter!) override func viewDidLoad() { super.viewDidLoad() - + view.backgroundColor = UIColor.nynja.backgroundColor - guard let presenter = presenter else { - return - } + _ = [headerView, scrollView, scrollContentView, bottomView] - headerView = makeHeaderView(on: view) - bottomView = makeBottomView(on: view, presenter: presenter) - scrollView = makeScrollView(on: view, top: headerView, bottom: bottomView) - emailLoginView = makeEmailLoginView(on: scrollView, presenter: presenter) + showPhoneNumberLogin(animated: false) enableKeyboardHidingWhenTappedAround() } @@ -44,10 +41,13 @@ final class AuthViewController: UIViewController, AuthViewProtocol, SetInjectabl override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - unregisterForKeyboardNotifications() } - +} + +// MARK: - KeyboardInteractive + +extension AuthViewController { func keyboardNotified(endFrame: CGRect) { var bottomInset: CGFloat = 0 @@ -61,9 +61,51 @@ final class AuthViewController: UIViewController, AuthViewProtocol, SetInjectabl } } -// MARK: - Actions +// MARK: - Private private extension AuthViewController { + func showPhoneNumberLogin(animated: Bool) { + guard let presenter = presenter else { + return + } + + phoneNumberLoginView = makePhoneNumberLoginView(on: scrollContentView, presenter: presenter, country: presenter.country ) + + if animated { + animateChangingViews(first: emailLoginView, second: phoneNumberLoginView) + } else { + emailLoginView?.removeFromSuperview() + } + } + + func showEmailLogin(animated: Bool) { + guard let presenter = presenter else { + return + } + + emailLoginView = makeEmailLoginView(on: scrollContentView, presenter: presenter) + + if animated { + animateChangingViews(first: phoneNumberLoginView, second: emailLoginView) + } else { + phoneNumberLoginView?.removeFromSuperview() + } + } + + func animateChangingViews(first: UIView?, second: UIView?) { + second?.alpha = 0 + view.layoutIfNeeded() + + UIView.animate( + withDuration: 0.25, + animations: { [weak self] in + first?.alpha = 0 + second?.alpha = 1 + self?.view.layoutIfNeeded() + }) { _ in + first?.removeFromSuperview() + } + } } // MARK: - SetInjectable @@ -106,11 +148,55 @@ private extension AuthViewController { return scrollView } + func makeScrollContentView(on view: UIView, baseView: UIView) -> UIView { + let contentView = UIView() + view.addSubview(contentView) + + contentView.backgroundColor = UIColor.nynja.clear + + contentView.snp.makeConstraints { (make) in + make.right.left.top.bottom.equalToSuperview() + make.width.equalTo(baseView.snp.width) + make.height.equalTo(baseView.snp.height) + } + + return contentView + } + func makeEmailLoginView(on view: UIView, presenter: AuthPresenterProtocol) -> EmailLoginView { let loginView = EmailLoginView() view.addSubview(loginView) - loginView.configure(config: EmailLoginView.Config(nextAction: presenter.loginViaEmail)) + loginView.configure(config: EmailLoginView.Config(nextAction: { + presenter.loginViaEmail($0) { (result) in + print(#function) + } + })) + + loginView.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + } + + return loginView + } + + func makePhoneNumberLoginView(on view: UIView, presenter: AuthPresenterProtocol, country: Country) -> PhoneNumberLoginView { + let loginView = PhoneNumberLoginView() + view.addSubview(loginView) + + loginView.configure(config: PhoneNumberLoginView.Config( + country: country, + countrySelectorAction: { + presenter.selectCountry { (result) in + result.onSuccess { loginView.updateCountry($0) } + } + }, + nextAction: { + presenter.loginViaPhoneNumber($0) { (result) in + print(#function) + } + })) loginView.snp.makeConstraints { (make) in make.top.left.right.equalToSuperview() @@ -122,11 +208,30 @@ private extension AuthViewController { func makeBottomView(on view: UIView, presenter: AuthPresenterProtocol) -> LoginOptionsView { let bottom = LoginOptionsView() + bottom.configure(config: LoginOptionsView.Config( loginOption: presenter.loginOption, - switchLoginAction: presenter.switchLoginOption, - facebookLoginAction: presenter.loginViaFacebook, - googleLoginAction: presenter.loginViaGoogle)) + switchLoginAction: { [weak self] () -> LoginOption in + presenter.switchLoginOption() + let loginOption = presenter.loginOption + + switch loginOption { + case .email: self?.showEmailLogin(animated: true) + case .phoneNumber: self?.showPhoneNumberLogin(animated: true) + } + + return loginOption + }, + facebookLoginAction: { + presenter.loginViaFacebook { (result) in + print(#function) + } + }, + googleLoginAction: { + presenter.loginViaGoogle { (result) in + print(#function) + } + })) view.addSubview(bottom) bottom.snp.makeConstraints { (make) in diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift index b218ddb9d..a39f0aa29 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift @@ -9,8 +9,8 @@ import Foundation final class AuthHeaderView: UIView, Configurable { - private var welcomeLabel: UILabel! - private var logoImageView: UIImageView! + private lazy var welcomeLabel: UILabel = makeWelcomeLabel(on: self) + private lazy var logoImageView: UIImageView = makeLogoImageView(on: self, top: welcomeLabel) } // MARK: - Configurable @@ -20,9 +20,7 @@ extension AuthHeaderView { func configure(config: NSNull) { backgroundColor = UIColor.nynja.clear - - welcomeLabel = makeWelcomeLabel(on: self) - logoImageView = makeLogoImageView(on: self, top: welcomeLabel) + _ = [welcomeLabel, logoImageView] } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index 3bf99c68f..47b61f39b 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -10,42 +10,10 @@ import Foundation final class EmailLoginView: UIView, Configurable { - private struct Validator { - func isValid(email: String) -> Bool { - let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) - - return emailTest.evaluate(with: email) - } - } - - private final class TextFieldController: NSObject, UITextFieldDelegate { - private let validator: Validator - private let validationAction: (Bool) -> Void - - init(validator: Validator, validationAction: @escaping (Bool) -> Void) { - self.validator = validator - self.validationAction = validationAction - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if let str = textField.text as NSString? { - let resultStr = str.replacingCharacters(in: range, with: string) - validationAction(validator.isValid(email: resultStr)) - } - - return true - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.becomeFirstResponder() - return true - } - } - - private var inputField: UITextField! - private var detailsLabel: UILabel! - private var nextButton: UIButton! + private lazy var inputFieldContainer: UIView = makeInputFieldContainer(on: self) + private lazy var inputField: UITextField = makeInputField(on: inputFieldContainer) + private lazy var detailsLabel: UILabel = makeDetailsLabel(on: self, top: inputFieldContainer) + private lazy var nextButton: UIButton = makeNextButton(on: self, top: detailsLabel) private var textFieldController: TextFieldController? private var nextAction: ((String) -> Void)? @@ -69,12 +37,9 @@ extension EmailLoginView { self?.nextButton.isEnabled = result } - inputField = makeInputField(on: self) - inputField.delegate = textFieldController - detailsLabel = makeDetailsLabel(on: self, top: inputField) - nextButton = makeNextButton(on: self, top: detailsLabel) - nextAction = config.nextAction + + _ = [inputFieldContainer, inputField, detailsLabel, nextButton] } } @@ -89,6 +54,22 @@ private extension EmailLoginView { // MARK: - UI fabric methdos private extension EmailLoginView { + func makeInputFieldContainer(on view: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + + container.backgroundColor = UIColor.nynja.clear + + container.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(16) + make.height.equalTo(44) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + + return container + } + func makeInputField(on view: UIView) -> UITextField { let textField = UITextField() view.addSubview(textField) @@ -99,11 +80,12 @@ private extension EmailLoginView { textField.textColor = UIColor.nynja.white textField.font = FontFamily.NotoSans.medium.font(size: 16) + textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + textField.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(16) - make.height.equalTo(64) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) + make.centerY.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() } return textField @@ -150,3 +132,44 @@ private extension EmailLoginView { return button } } + +// MARK: - Validator + +private extension EmailLoginView { + struct Validator { + func isValid(email: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + + return emailTest.evaluate(with: email) + } + } +} + +// MARK: - TextFieldController + +private extension EmailLoginView { + final class TextFieldController: NSObject, UITextFieldDelegate { + private let validator: Validator + private let validationAction: (Bool) -> Void + + init(validator: Validator, validationAction: @escaping (Bool) -> Void) { + self.validator = validator + self.validationAction = validationAction + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let str = textField.text as NSString? { + let resultStr = str.replacingCharacters(in: range, with: string) + validationAction(validator.isValid(email: resultStr)) + } + + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.becomeFirstResponder() + return true + } + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift index 591e9db1b..4f1c500e4 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift @@ -10,12 +10,11 @@ import Foundation final class LoginOptionsView: UIView, Configurable { - private weak var switchLoginButton: UIButton! - private weak var loginWithFacebook: UIButton! - private weak var loginWithGoogle: UIButton! + private lazy var switchLoginButton: UIButton = makeSwitchLoginButton(on: self, bottom: loginWithFacebook) + private lazy var loginWithFacebook: UIButton = makeLoginWithFacebookButton(on: self, bottom: loginWithGoogle) + private lazy var loginWithGoogle: UIButton = makeLoginWithGoogleButton(on: self) - private var localState: LoginOption? - private var switchLoginAction: (() -> Void)? + private var switchLoginAction: (() -> LoginOption)? private var facebookLoginAction: (() -> Void)? private var googleLoginAction: (() -> Void)? } @@ -25,31 +24,21 @@ final class LoginOptionsView: UIView, Configurable { extension LoginOptionsView { struct Config { let loginOption: LoginOption - let switchLoginAction: () -> Void + let switchLoginAction: () -> LoginOption let facebookLoginAction: () -> Void let googleLoginAction: () -> Void } func configure(config: LoginOptionsView.Config) { backgroundColor = UIColor.nynja.clear - - loginWithGoogle = makeLoginWithGoogleButton(on: self) - loginWithFacebook = makeLoginWithFacebookButton(on: self, bottom: loginWithGoogle) - switchLoginButton = makeSwitchLoginButton(on: self, bottom: loginWithFacebook) - + switchLoginAction = config.switchLoginAction facebookLoginAction = config.facebookLoginAction googleLoginAction = config.googleLoginAction - localState = config.loginOption - switch config.loginOption { - case .email: - switchLoginButton.setTitle("Log in with phone number".localized.uppercased(), for: .normal) - switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) - case .phoneNumber: - switchLoginButton.setTitle("Log in with email".localized.uppercased(), for: .normal) - switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) - } + updateSwitchButton(loginOption: config.loginOption) + + _ = [switchLoginButton, loginWithFacebook, loginWithGoogle] } } @@ -57,22 +46,11 @@ extension LoginOptionsView { private extension LoginOptionsView { @objc func switchLogin(sender: UIButton) { - guard let state = localState else { + guard let loginOption = switchLoginAction?() else { return } - - switch state { - case .email: - localState = .phoneNumber - sender.setTitle("Log in with email".localized.uppercased(), for: .normal) - sender.setImage(UIImage.backButtonImage, for: .normal) - case .phoneNumber: - localState = .email - sender.setTitle("Log in with phone number".localized.uppercased(), for: .normal) - sender.setImage(UIImage.backButtonImage, for: .normal) - } - - switchLoginAction?() + + updateSwitchButton(loginOption: loginOption) } @objc func loginWithFacebook(sender: UIButton) { @@ -84,6 +62,21 @@ private extension LoginOptionsView { } } +// MARK: - Private + +private extension LoginOptionsView { + func updateSwitchButton(loginOption: LoginOption) { + switch loginOption { + case .email: + switchLoginButton.setTitle("Log in with phone number".localized.uppercased(), for: .normal) + switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) + case .phoneNumber: + switchLoginButton.setTitle("Log in with email".localized.uppercased(), for: .normal) + switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) + } + } +} + // MARK: - UI fabric methods private extension LoginOptionsView { @@ -99,6 +92,8 @@ private extension LoginOptionsView { button.layer.cornerRadius = 22 + button.addTarget(self, action: #selector(loginWithGoogle(sender:)), for: .touchUpInside) + button.snp.makeConstraints { (make) in make.bottom.equalToSuperview().offset(-30) make.left.equalToSuperview().offset(16) @@ -121,6 +116,8 @@ private extension LoginOptionsView { button.layer.cornerRadius = 22 + button.addTarget(self, action: #selector(loginWithFacebook(sender:)), for: .touchUpInside) + button.snp.makeConstraints { (make) in make.bottom.equalTo(bottom.snp.top).offset(-16) make.left.equalToSuperview().offset(16) @@ -141,6 +138,8 @@ private extension LoginOptionsView { button.layer.cornerRadius = 22 + button.addTarget(self, action: #selector(switchLogin(sender:)), for: .touchUpInside) + button.snp.makeConstraints { (make) in make.top.equalToSuperview().offset(30) make.bottom.equalTo(bottom.snp.top).offset(-16) diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index 9f6bc653b..bc9364173 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -8,23 +8,318 @@ import Foundation + final class PhoneNumberLoginView: UIView, Configurable { - private weak var countrySelector: UIButton? - private weak var countryCode: UITextField? - private weak var phoneNumber: UITextField? + private lazy var countrySelector: UIButton = makeCountrySelector(on: self) + + private lazy var countryCodeContainer: UIView = makeCountryCodeContainer(on: self, top: countrySelector) + private lazy var countryCodeLabel: UILabel = makeCountryCodeLabel(on: countryCodeContainer) + + private var phoneNumberTextFieldController: TextFieldController? - private weak var detailsLabel: UILabel? - private weak var nextButton: UIButton? + private lazy var phoneNumberContainer: UIView = makePhoneNumberContainer(on: self, left: countryCodeLabel) + private lazy var phoneNumberTextField: UITextField = makePhoneNumberTextField(on: phoneNumberContainer) + + private lazy var detailsLabel: UILabel = makeDetailsLabel(on: self, top: countryCodeContainer) + private lazy var nextButton: UIButton = makeNextButton(on: self, top: detailsLabel) + + private var country: Country? + private var countrySelectorAction: (() -> Void)? + private var nextAction: ((String) -> Void)? } // MARK: - Configurable extension PhoneNumberLoginView { struct Config { - + let country: Country + let countrySelectorAction: () -> Void + let nextAction: (String) -> Void } func configure(config: PhoneNumberLoginView.Config) { + country = config.country + countrySelectorAction = config.countrySelectorAction + + phoneNumberTextFieldController = TextFieldController() + phoneNumberTextFieldController?.isFullFilelledAction = { [weak self] result in + if result { + self?.nextButton.backgroundColor = UIColor.nynja.mainRed + } else { + self?.nextButton.backgroundColor = UIColor.nynja.darkRed + } + + self?.nextButton.isEnabled = result + } + + phoneNumberTextField.delegate = phoneNumberTextFieldController + updateCountry(config.country) + + _ = [countrySelector, countryCodeContainer, countryCodeLabel, phoneNumberContainer, phoneNumberTextField, detailsLabel, nextButton] + } +} + +// MARK: - Public + +extension PhoneNumberLoginView { + func updateCountry(_ country: Country) { + self.country = country + + phoneNumberTextFieldController?.template = country.numberTemplate + + countrySelector.setTitle(country.name, for: .normal) + countryCodeLabel.text = "+" + country.code + + phoneNumberTextField.text = "".updateWithMask(placeHolder: country.numberTemplate) + } +} + +// MARK: - Actions + +private extension PhoneNumberLoginView { + @objc func changeCountry(sender: UIButton) { + countrySelectorAction?() + } + + @objc func next(sender: UIButton) { + let number = (country?.code ?? "") + (phoneNumberTextField.text ?? "") + nextAction?(number) + } +} + +// MARK: - UI fabric methods + +extension PhoneNumberLoginView { + func makeCountrySelector(on view: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.setTitleColor(UIColor.nynja.white, for: .normal) + + button.addTarget(self, action: #selector(changeCountry(sender:)), for: .touchUpInside) + + button.titleLabel?.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + + button.titleLabel?.snp.makeConstraints { (make) in + make.width.equalToSuperview() + } + + button.snp.makeConstraints { (make) in + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + make.top.equalToSuperview().offset(10) + } + + return button + } + + func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + + container.backgroundColor = UIColor.nynja.clear + + container.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom) + make.left.equalToSuperview().offset(16) + make.width.equalTo(100) + make.height.equalTo(64) + } + + return container + } + + func makeCountryCodeLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.snp.makeConstraints { (make) in + make.left.equalToSuperview() + make.right.equalToSuperview().offset(-16) + make.centerY.equalToSuperview() + } + + return label + } + + func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + + container.backgroundColor = UIColor.nynja.clear + + container.snp.makeConstraints { (make) in + make.height.equalTo(left.snp.height) + make.right.equalToSuperview().offset(-16) + make.left.equalTo(left.snp.right) + make.centerY.equalTo(left.snp.centerY) + } + + return container + } + + func makePhoneNumberTextField(on view: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + + textField.font = FontFamily.NotoSans.medium.font(size: 16) + textField.textColor = UIColor.nynja.white + textField.keyboardType = .numberPad + + textField.snp.makeConstraints { (make) in + make.centerY.right.equalToSuperview() + make.left.equalToSuperview().offset(16) + } + + return textField + } + + func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.text = "Please choose your country code and enter your phone number.".localized + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + label.numberOfLines = 0 + + label.snp.makeConstraints { (make) in + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom) + } + + return label + } + + func makeNextButton(on view: UIView, top: UIView) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.layer.cornerRadius = 22 + button.setTitle("next".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.backgroundColor = UIColor.nynja.darkRed + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + + button.isEnabled = false + + button.addTarget(self, action: #selector(next(sender:)), for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.height.equalTo(44) + make.bottom.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom).offset(24) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + + return button + } +} + +// MARK: - Text field controller + +extension PhoneNumberLoginView { + final class TextFieldController: NSObject, UITextFieldDelegate { + var template: String? + var isFullFilelledAction: ((Bool) -> Void)? + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard let template = template else { + return false + } + + textField.text = textAfterUpdate(textField: textField, range: range, replacementString: string) + .updateWithMask(placeHolder: template) + + let offset = string != "" ? + cursorOffsetForNonEmptyString(textField: textField, range: range) : + cursorOffsetForEmptyString(textField: textField, range: range) + + updateCursorPosition(on: textField, position: range.location + offset) + + isFullFilelledAction?(isFullfiled(textField: textField, template: template)) + + return false + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + let position = calculatedCursorPosition(on: textField) + updateCursorPosition(on: textField, position: position) + } + + // MARK: - Private + + private func textAfterUpdate(textField: UITextField, range: NSRange, replacementString string: String) -> String { + let text = textField.text ?? "" + + let updatedRange = newRange(text: text, oldRange: range, replacementString: string) + + return (text as NSString) + .replacingCharacters(in: updatedRange, with: string) + .replacingOccurrences(of: " ", with: "") + } + + private func newRange(text: String, oldRange: NSRange,replacementString string: String) -> NSRange { + if string == "", Array(text)[oldRange.location] == " " { + var range = oldRange + range.location = range.location - 1 + return range + } + + return oldRange + } + + private func cursorOffsetForNonEmptyString(textField: UITextField, range: NSRange) -> Int { + guard let text = textField.text else { + return 1 + } + + let index = range.location + 1 + let arr = Array(text) + + if arr.count > index, arr[index] == " " { + return 2 + } + + return 1 + } + + private func cursorOffsetForEmptyString(textField: UITextField, range: NSRange) -> Int { + guard let text = textField.text else { + return 0 + } + + return Array(text)[range.location] == " " ? -1 : 0 + } + + private func updateCursorPosition(on textField: UITextField, position: Int) { + guard let newPosition = textField.position(from: textField.beginningOfDocument, offset: position) else { + return + } + + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + private func calculatedCursorPosition(on textField: UITextField) -> Int { + return textField.text? + .enumerated() + .filter { $1 != " " && $1 != "\u{2013}" } + .last? + .offset ?? 0 + } + private func isFullfiled(textField: UITextField, template: String) -> Bool { + return (textField.text ?? "").filter { $0 != "\u{2013}" }.count == template.count + } } } diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index d4cb21e0d..ce0861f5e 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -8,16 +8,24 @@ import Foundation +protocol AuthCoordinatorProtocol { + func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) +} final class AuthWireframe: AuthWireframeProtocol { - typealias Parameters = NSNull + private let coordinator: AuthCoordinatorProtocol - struct Dependencies { - + init(coordinator: AuthCoordinatorProtocol) { + self.coordinator = coordinator } + typealias Parameters = NSNull + + struct Dependencies {} + enum State { - + case continueLogin(loginOption: LoginOption) + case getCountry(callback: (Result) -> Void) } func prepareModule(parameters: NSNull, dependencies: AuthWireframe.Dependencies) -> UIViewController { @@ -35,4 +43,12 @@ final class AuthWireframe: AuthWireframeProtocol { return view } + + func selectCountry(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .getCountry(callback: completion)) + } + + func continueLogin(loginOption: LoginOption) { + coordinator.wireframe(self, didEndWithState: .continueLogin(loginOption: loginOption)) + } } diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift index c8363f2f9..0b5ace9b9 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -189,14 +189,7 @@ extension CodeConfirmationViewController { private extension CodeConfirmationViewController { func appendBottomBorder(to textFields: [UITextField]) { - textFields.forEach { - let bottomBorder = CALayer() - bottomBorder.frame = CGRect(x: 0, y: $0.bounds.size.height - 1, width: $0.bounds.size.width, height: 2) - - bottomBorder.backgroundColor = UIColor.nynja.mainRed.cgColor - - $0.layer.addSublayer(bottomBorder) - } + textFields.forEach { $0.appendBottomBorder(color: UIColor.nynja.mainRed, width: 2) } } func makeBackButton(on view: UIView) -> UIButton { diff --git a/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift b/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift index 27a13796c..870fd83da 100644 --- a/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift +++ b/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift @@ -14,6 +14,8 @@ struct CountriesSection { } struct Country: Equatable, Hashable { + let ISO: String let name: String let code: String + let numberTemplate: String } diff --git a/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift b/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift index ac7643d15..b03587e42 100644 --- a/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift +++ b/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift @@ -27,7 +27,7 @@ final class CountrySelectorInteractor: CountrySelectorInteractorInputProtocol, S private var countries: [Country] { get { return (storageService?.countries.map { - Country(name: $0.name, code: $0.code) + Country(ISO: $0.ISO, name: $0.name, code: $0.code, numberTemplate: $0.placeHolder) } ?? []).sorted { $0.name < $1.name } } } -- GitLab From c29827e514d792ac862809dcd69f92021d7f1775 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 12:51:24 +0300 Subject: [PATCH 003/138] [NY-4699] Implemented AvatarStatusView with clipped round corder for status view. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 48 ++++++- .../AvatarStatusView/AvatarStatusView.swift | 136 ++++++++++++++++++ 2 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index d227aa966..413b47d08 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 8514D51C20EE41E90002378A /* UIWindowExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51B20EE41E90002378A /* UIWindowExtensions.swift */; }; 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */; }; 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */; }; + 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -63,6 +64,7 @@ 8514D51B20EE41E90002378A /* UIWindowExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIWindowExtensions.swift; sourceTree = ""; }; 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+HitTestDelegate.swift"; sourceTree = ""; }; 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaContextMenuUserInfo.swift; sourceTree = ""; }; + 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStatusView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C90742AD8E6E2E817F7DB1E9 /* Pods-NynjaUIKit.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.channels.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.channels.xcconfig"; sourceTree = ""; }; @@ -273,6 +275,7 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( + 85409FFD2181C8AF003A010F /* AvatarStatusView */, 8514D50120EE40530002378A /* ContextMenu */, ); path = Views; @@ -287,6 +290,14 @@ path = UIWindow; sourceTree = ""; }; + 85409FFD2181C8AF003A010F /* AvatarStatusView */ = { + isa = PBXGroup; + children = ( + 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */, + ); + path = AvatarStatusView; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -412,6 +423,7 @@ 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */, 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, + 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, @@ -505,7 +517,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -586,7 +602,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -667,7 +687,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -748,7 +772,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -829,7 +857,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -910,7 +942,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift new file mode 100644 index 000000000..5f5fe2676 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift @@ -0,0 +1,136 @@ +// +// AvatarStatusView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class AvatarStatusView: UIView { + + public var angle: CGFloat = .pi / 4 { + didSet { + setNeedsLayout() + } + } + + public var statusRadius: CGFloat = 24 { + didSet { + setNeedsLayout() + } + } + + public var statusPadding: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + public var imageRadius: CGFloat { + return bounds.height / 2 + } + + + // MARK: - Views + + public let imageView = UIImageView() + + private let statusView = UIImageView() + + private let maskLayer = CAShapeLayer() + + + // MARK: - Init + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + backgroundColor = .clear + + imageView.layer.masksToBounds = true + addSubview(imageView) + + statusView.layer.masksToBounds = true + addSubview(statusView) + } + + + // MARK: - Layout + + public override func layoutSubviews() { + super.layoutSubviews() + + imageView.frame = bounds + imageView.layer.cornerRadius = bounds.height / 2 + + let clipCircleCenter = statusPosition(in: bounds) + let clipCircleSize = statusRadius + statusPadding * 2 + let clipCircleFrame = CGRect( + x: clipCircleCenter.x - clipCircleSize / 2, + y: clipCircleCenter.y - clipCircleSize / 2, + width: clipCircleSize, + height: clipCircleSize + ) + updateClipMask(with: clipCircleFrame) + updateStatusView(with: clipCircleFrame) + } + + private func updateClipMask(with frame: CGRect) { + let statusCirclePath = UIBezierPath(ovalIn: frame) + + let path = UIBezierPath(rect: bounds) + path.append(statusCirclePath.reversing()) + + maskLayer.path = path.cgPath + + imageView.layer.mask = maskLayer + } + + private func updateStatusView(with frame: CGRect) { + let size = frame.width - statusPadding * 2 + + statusView.frame = CGRect(x: frame.minX + statusPadding, + y: frame.minY + statusPadding, + width: size, + height: size) + + statusView.layer.cornerRadius = size / 2 + } + + private func statusPosition(in bounds: CGRect) -> CGPoint { + return CGPoint(x: imageRadius * cos(angle) + bounds.width / 2, + y: imageRadius * sin(angle) + bounds.height / 2) + } +} + +extension AvatarStatusView { + + public enum StatusAppearance { + case color(UIColor) + case image(UIImage) + } + + public func update(_ statusAppearance: StatusAppearance) { + switch statusAppearance { + case let .color(color): + statusView.backgroundColor = color + statusView.image = nil + case let .image(image): + statusView.backgroundColor = nil + statusView.image = image + } + } +} -- GitLab From bdfda3853c303285dfa697673750fd300f46301d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 15:14:30 +0300 Subject: [PATCH 004/138] [NY-4699] Implemented typing animation using CAReplicationLayer. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 18 ++- .../AvatarStatusView.swift | 0 .../Views/Typing/TypingAnimatableView.swift | 124 ++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) rename Frameworks/NynjaUIKit/NynjaUIKit/Views/{AvatarStatusView => Avatar}/AvatarStatusView.swift (100%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 413b47d08..5f5185912 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */; }; 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */; }; 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */; }; + 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -65,6 +66,7 @@ 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+HitTestDelegate.swift"; sourceTree = ""; }; 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaContextMenuUserInfo.swift; sourceTree = ""; }; 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStatusView.swift; sourceTree = ""; }; + 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingAnimatableView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C90742AD8E6E2E817F7DB1E9 /* Pods-NynjaUIKit.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.channels.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.channels.xcconfig"; sourceTree = ""; }; @@ -275,7 +277,8 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( - 85409FFD2181C8AF003A010F /* AvatarStatusView */, + 85409FFD2181C8AF003A010F /* Avatar */, + 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, ); path = Views; @@ -290,12 +293,20 @@ path = UIWindow; sourceTree = ""; }; - 85409FFD2181C8AF003A010F /* AvatarStatusView */ = { + 85409FFD2181C8AF003A010F /* Avatar */ = { isa = PBXGroup; children = ( 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */, ); - path = AvatarStatusView; + path = Avatar; + sourceTree = ""; + }; + 8540A0062181EA0D003A010F /* Typing */ = { + isa = PBXGroup; + children = ( + 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */, + ); + path = Typing; sourceTree = ""; }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { @@ -424,6 +435,7 @@ 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, + 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift new file mode 100644 index 000000000..e99a0607a --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift @@ -0,0 +1,124 @@ +// +// TypingAnimatableView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class TypingAnimatableView: UIView { + + public var itemsCount: Int = 3 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var padding: CGFloat = 4 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemSize: CGFloat = 4 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemColor: UIColor = .lightGray { + didSet { + setupColor() + } + } + + public override var intrinsicContentSize: CGSize { + let width = itemSize * CGFloat(itemsCount) + padding * CGFloat(itemsCount - 1) + return CGSize(width: width, height: itemSize) + } + + + // MARK: - Layers + + public override class var layerClass: AnyClass { + return CAReplicatorLayer.self + } + + private var animationLayer: CAReplicatorLayer { + return layer as! CAReplicatorLayer + } + + private let itemLayer = CAShapeLayer() + + + // MARK: - Init + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + animationLayer.addSublayer(itemLayer) + animationLayer.masksToBounds = true + setupColor() + } + + private func setupColor() { + itemLayer.backgroundColor = itemColor.cgColor + } + + + // MARK: - Layout + + public override func layoutSubviews() { + super.layoutSubviews() + + itemLayer.frame.size = CGSize(width: itemSize, height: itemSize) + itemLayer.cornerRadius = itemSize / 2 + + animationLayer.instanceCount = itemsCount + animationLayer.instanceTransform = CATransform3DMakeTranslation(itemSize + padding, 0, 0) + animationLayer.instanceAlphaOffset = Float(Animation.toValue - Animation.fromValue) / Float(itemsCount - 1) + animationLayer.instanceDelay = Animation.duration / Double(itemsCount) + + addAnimation() + } + + + // MARK: - Animation + + private func addAnimation() { + guard itemLayer.animation(forKey: Animation.key) == nil else { + return + } + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = Animation.fromValue + animation.toValue = Animation.toValue + animation.duration = Animation.duration + animation.autoreverses = true + animation.repeatCount = .infinity + + itemLayer.add(animation, forKey: Animation.key) + } + + private enum Animation { + static let key = "typing" + static let duration = 0.5 + static let fromValue = 0.5 + static let toValue = 1.0 + } +} -- GitLab From 0f9229ab15724526a5ee2b8625bfc3d4783a8a53 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 15:26:34 +0300 Subject: [PATCH 005/138] [NY-4699] Added base view. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 20 ++++++++++++ .../CoreAnimation/CALayer+Animation.swift | 16 ++++++++++ .../Views/Avatar/AvatarStatusView.swift | 19 +++-------- .../NynjaUIKit/Views/BaseView.swift | 32 +++++++++++++++++++ .../ContextMenu/View/NynjaContextMenu.swift | 18 ++--------- .../Views/Typing/TypingAnimatableView.swift | 20 +++--------- .../NynjaUIKit/Views/Typing/TypingView.swift | 19 +++++++++++ 7 files changed, 98 insertions(+), 46 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 5f5185912..064940d21 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -34,6 +34,9 @@ 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */; }; 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */; }; 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */; }; + 8540A00A2181EB87003A010F /* TypingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0092181EB87003A010F /* TypingView.swift */; }; + 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00B2181EBD2003A010F /* BaseView.swift */; }; + 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -67,6 +70,9 @@ 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaContextMenuUserInfo.swift; sourceTree = ""; }; 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStatusView.swift; sourceTree = ""; }; 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingAnimatableView.swift; sourceTree = ""; }; + 8540A0092181EB87003A010F /* TypingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingView.swift; sourceTree = ""; }; + 8540A00B2181EBD2003A010F /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; + 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Animation.swift"; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C90742AD8E6E2E817F7DB1E9 /* Pods-NynjaUIKit.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.channels.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.channels.xcconfig"; sourceTree = ""; }; @@ -269,6 +275,7 @@ 8514D51920EE41AC0002378A /* Extensions */ = { isa = PBXGroup; children = ( + 8540A00D2181ED10003A010F /* CoreAnimation */, 8514D51F20EE47350002378A /* UIWindow */, ); path = Extensions; @@ -277,6 +284,7 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( + 8540A00B2181EBD2003A010F /* BaseView.swift */, 85409FFD2181C8AF003A010F /* Avatar */, 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, @@ -305,10 +313,19 @@ isa = PBXGroup; children = ( 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */, + 8540A0092181EB87003A010F /* TypingView.swift */, ); path = Typing; sourceTree = ""; }; + 8540A00D2181ED10003A010F /* CoreAnimation */ = { + isa = PBXGroup; + children = ( + 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */, + ); + path = CoreAnimation; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -433,6 +450,7 @@ 8514D4E820EE2D970002378A /* UITableView+ViewModels.swift in Sources */, 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */, 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, + 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */, @@ -450,7 +468,9 @@ 8514D4E920EE2D970002378A /* UICollectionView+ViewModel.swift in Sources */, 8514D51120EE40540002378A /* ContextMenuItem.swift in Sources */, 8514D51820EE40540002378A /* NynjaContextMenuItemsFactory.swift in Sources */, + 8540A00A2181EB87003A010F /* TypingView.swift in Sources */, 8514D51520EE40540002378A /* NynjaContextMenuItemCollectionViewCell.swift in Sources */, + 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */, 8514D4E120EE2D970002378A /* SelectableCellViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift new file mode 100644 index 000000000..df70d9566 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift @@ -0,0 +1,16 @@ +// +// CAAnimationExtensions.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +extension CALayer { + + public func hasAnimation(forKey key: String) -> Bool { + return animation(forKey: key) != nil + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index 5f5fe2676..bad89a549 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -8,7 +8,7 @@ import UIKit -public final class AvatarStatusView: UIView { +public final class AvatarStatusView: BaseView { public var angle: CGFloat = .pi / 4 { didSet { @@ -42,22 +42,11 @@ public final class AvatarStatusView: UIView { private let maskLayer = CAShapeLayer() - // MARK: - Init - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - // MARK: - Setup - private func setup() { + public override func setup() { + super.setup() + backgroundColor = .clear imageView.layer.masksToBounds = true diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift new file mode 100644 index 000000000..dc6523cbb --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift @@ -0,0 +1,32 @@ +// +// BaseView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public class BaseView: UIView { + + // MARK: - Init + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + public func setup() { + // should be implemented in childs + } +} + diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift index ecea64cfb..23e4a01d3 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift @@ -18,7 +18,7 @@ public protocol NynjaContextMenuDelegate: class { userInfo: NynjaContextMenuUserInfo?) } -public final class NynjaContextMenu: UIView { +public final class NynjaContextMenu: BaseView { // MARK: - Properties @@ -99,22 +99,10 @@ public final class NynjaContextMenu: UIView { }() - // MARK: - Init - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - // MARK: - Setup - private func setup() { + public override func setup() { + super.setup() clipsToBounds = true contentView.layer.cornerRadius = cornerRadius contentView.clipsToBounds = true diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift index e99a0607a..c77b7a9de 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift @@ -8,7 +8,7 @@ import UIKit -public final class TypingAnimatableView: UIView { +public final class TypingAnimatableView: BaseView { public var itemsCount: Int = 3 { didSet { @@ -56,22 +56,10 @@ public final class TypingAnimatableView: UIView { private let itemLayer = CAShapeLayer() - // MARK: - Init - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - // MARK: - Setup - private func setup() { + public override func setup() { + super.setup() animationLayer.addSublayer(itemLayer) animationLayer.masksToBounds = true setupColor() @@ -102,7 +90,7 @@ public final class TypingAnimatableView: UIView { // MARK: - Animation private func addAnimation() { - guard itemLayer.animation(forKey: Animation.key) == nil else { + guard !itemLayer.hasAnimation(forKey: Animation.key) else { return } let animation = CABasicAnimation(keyPath: "opacity") diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift new file mode 100644 index 000000000..a91a77e08 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -0,0 +1,19 @@ +// +// TypingView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +public final class TypingView: BaseView { + + // MARK: - Setup + + public override func setup() { + super.setup() + } +} -- GitLab From 91073edbfc6d1d86d94e886c2884673e60c5ed92 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 18:05:57 +0300 Subject: [PATCH 006/138] [NY-4699] Implemented typing view. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index a91a77e08..4fc71b153 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -11,9 +11,111 @@ import SnapKit public final class TypingView: BaseView { + // MARK: - Appearance + + public struct Appearance { + public enum Indicator { + case dots(UIColor) + case circle(UIColor) + } + public let indicator: Indicator + public let textColor: UIColor + public let textFont: UIFont + public let senderInfo: String + public let typingInfo: String + public let isTypingInfoPinned: Bool + } + + + // MARK: - Views + + private(set) lazy var indicatorContainer: UIView = { + let view = UIView() + view.setContentHuggingPriority(.required, for: .horizontal) + addSubview(view) + return view + }() + + private(set) lazy var senderInfoLabel: UILabel = { + let label = UILabel() + addSubview(label) + return label + }() + + // MARK: - Setup public override func setup() { super.setup() + + indicatorContainer.snp.makeConstraints { maker in + maker.top.bottom.left.equalToSuperview() + maker.width.equalTo(Constraints.indicator.containerWidth.adjustedByWidth) + } + + senderInfoLabel.snp.makeConstraints { maker in + maker.top.bottom.right.equalToSuperview() + maker.left.equalTo(indicatorContainer.snp.right).offset(Constraints.senderInfo.leftOffset.adjustedByWidth) + } + } + + + // MARK: - Layout + + public func update(_ appearance: Appearance) { + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + + switch appearance.indicator { + case let .dots(color): + setupDotsIndicator(color: color) + case let .circle(color): + setupCircleIndicator(color: color) + } + + senderInfoLabel.font = appearance.textFont + senderInfoLabel.textColor = appearance.textColor + senderInfoLabel.text = "\(appearance.senderInfo) \(appearance.typingInfo)" + } + + private func setupDotsIndicator(color: UIColor) { + let indicatorView = TypingAnimatableView() + indicatorView.itemColor = color + + indicatorContainer.addSubview(indicatorView) + + indicatorView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview() + } + } + + private func setupCircleIndicator(color: UIColor) { + let indicatorView = UIView() + indicatorView.backgroundColor = color + + indicatorContainer.addSubview(indicatorView) + + let size = Constraints.indicator.circleSize.adjustedByWidth + + indicatorView.layer.cornerRadius = size / 2 + indicatorView.snp.makeConstraints { maker in + maker.center.equalToSuperview() + maker.width.height.equalTo(size) + } + } + + + // MARK: - Constraints + + private enum Constraints { + + enum indicator { + static let containerWidth: CGFloat = 16 + static let circleSize: CGFloat = 8 + } + + enum senderInfo { + static let leftOffset: CGFloat = 8 + } } } -- GitLab From 2ccc203fb95ae5acfb1b587a88342d2378acec2a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 18:12:27 +0300 Subject: [PATCH 007/138] [NY-4699] Added RoundView. Rename TypingAnimatableView to TypingIndicatorView. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 20 +++++++++++++---- ...leView.swift => TypingIndicatorView.swift} | 4 ++-- .../NynjaUIKit/Views/Typing/TypingView.swift | 5 ++--- .../NynjaUIKit/Views/Utils/RoundView.swift | 22 +++++++++++++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) rename Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/{TypingAnimatableView.swift => TypingIndicatorView.swift} (97%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 064940d21..1d07d6b75 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -33,10 +33,11 @@ 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */; }; 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */; }; 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */; }; - 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */; }; + 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */; }; 8540A00A2181EB87003A010F /* TypingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0092181EB87003A010F /* TypingView.swift */; }; 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00B2181EBD2003A010F /* BaseView.swift */; }; 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */; }; + 8540A019218213E2003A010F /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A018218213E2003A010F /* RoundView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -69,10 +70,11 @@ 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+HitTestDelegate.swift"; sourceTree = ""; }; 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaContextMenuUserInfo.swift; sourceTree = ""; }; 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStatusView.swift; sourceTree = ""; }; - 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingAnimatableView.swift; sourceTree = ""; }; + 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; 8540A0092181EB87003A010F /* TypingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingView.swift; sourceTree = ""; }; 8540A00B2181EBD2003A010F /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Animation.swift"; sourceTree = ""; }; + 8540A018218213E2003A010F /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C90742AD8E6E2E817F7DB1E9 /* Pods-NynjaUIKit.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.channels.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.channels.xcconfig"; sourceTree = ""; }; @@ -285,6 +287,7 @@ isa = PBXGroup; children = ( 8540A00B2181EBD2003A010F /* BaseView.swift */, + 8540A01A218213E8003A010F /* Utils */, 85409FFD2181C8AF003A010F /* Avatar */, 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, @@ -312,7 +315,7 @@ 8540A0062181EA0D003A010F /* Typing */ = { isa = PBXGroup; children = ( - 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */, + 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */, 8540A0092181EB87003A010F /* TypingView.swift */, ); path = Typing; @@ -326,6 +329,14 @@ path = CoreAnimation; sourceTree = ""; }; + 8540A01A218213E8003A010F /* Utils */ = { + isa = PBXGroup; + children = ( + 8540A018218213E2003A010F /* RoundView.swift */, + ); + path = Utils; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -453,12 +464,13 @@ 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, - 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */, + 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, 8514D51420EE40540002378A /* NynjaContextMenuItemCellModel.swift in Sources */, 8514D51220EE40540002378A /* ContextMenuRow.swift in Sources */, + 8540A019218213E2003A010F /* RoundView.swift in Sources */, 8514D51620EE40540002378A /* NynjaContextMenuArrowView.swift in Sources */, 8514D4E620EE2D970002378A /* Reusable.swift in Sources */, 8514D4E320EE2D970002378A /* SupplementaryViewModel.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift similarity index 97% rename from Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift index c77b7a9de..5a67c92a5 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift @@ -1,5 +1,5 @@ // -// TypingAnimatableView.swift +// TypingIndicatorView.swift // NynjaUIKit // // Created by Anton Poltoratskyi on 25.10.2018. @@ -8,7 +8,7 @@ import UIKit -public final class TypingAnimatableView: BaseView { +public final class TypingIndicatorView: BaseView { public var itemsCount: Int = 3 { didSet { diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 4fc71b153..9e8be914d 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -78,7 +78,7 @@ public final class TypingView: BaseView { } private func setupDotsIndicator(color: UIColor) { - let indicatorView = TypingAnimatableView() + let indicatorView = TypingIndicatorView() indicatorView.itemColor = color indicatorContainer.addSubview(indicatorView) @@ -90,14 +90,13 @@ public final class TypingView: BaseView { } private func setupCircleIndicator(color: UIColor) { - let indicatorView = UIView() + let indicatorView = RoundView() indicatorView.backgroundColor = color indicatorContainer.addSubview(indicatorView) let size = Constraints.indicator.circleSize.adjustedByWidth - indicatorView.layer.cornerRadius = size / 2 indicatorView.snp.makeConstraints { maker in maker.center.equalToSuperview() maker.width.height.equalTo(size) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift new file mode 100644 index 000000000..9e8c2b633 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift @@ -0,0 +1,22 @@ +// +// RoundView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class RoundView: BaseView { + + override func setup() { + super.setup() + layer.masksToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = min(bounds.width, bounds.height) / 2 + } +} -- GitLab From a25fd4713f28a147f317ae956cec61242107f473 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 18:17:12 +0300 Subject: [PATCH 008/138] [NY-4699] Minor refactor. --- .../NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 9e8be914d..33ab72b25 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -29,14 +29,14 @@ public final class TypingView: BaseView { // MARK: - Views - private(set) lazy var indicatorContainer: UIView = { + private lazy var indicatorContainer: UIView = { let view = UIView() view.setContentHuggingPriority(.required, for: .horizontal) addSubview(view) return view }() - private(set) lazy var senderInfoLabel: UILabel = { + private lazy var senderInfoLabel: UILabel = { let label = UILabel() addSubview(label) return label -- GitLab From 3c95ccbc32566d0e391c6286f14a843385b4b633 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 19:19:32 +0300 Subject: [PATCH 009/138] [NY-4699] Fixed public init --- .../NynjaUIKit/Views/Typing/TypingView.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 33ab72b25..c71ecb7c8 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -24,6 +24,20 @@ public final class TypingView: BaseView { public let senderInfo: String public let typingInfo: String public let isTypingInfoPinned: Bool + + public init(indicator: Indicator, + textColor: UIColor, + textFont: UIFont, + senderInfo: String, + typingInfo: String, + isTypingInfoPinned: Bool) { + self.indicator = indicator + self.textColor = textColor + self.textFont = textFont + self.senderInfo = senderInfo + self.typingInfo = typingInfo + self.isTypingInfoPinned = isTypingInfoPinned + } } -- GitLab From fdbb132aea0713fd9173fec98b64ab1afef9edac Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 19:20:46 +0300 Subject: [PATCH 010/138] NY-4699] Minor refactor in AvatarView --- .../View/Views/AvatarView/AvatarView.swift | 46 ++++++++++++------- .../Views/AvatarView/AvatarViewLayout.swift | 18 +++----- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index e8f8185ea..47fe32217 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -7,23 +7,27 @@ // import SnapKit +import NynjaUIKit -class AvatarView: BaseView { +final class AvatarView: BaseView { override var activatedViews: [UIView] { return [statusLabel, separatorView] } var status: String? { - didSet { statusLabel.text = status - statusLabel.accessibilityValue = status + didSet { + statusLabel.text = status + statusLabel.accessibilityValue = status } } private var muteWidthConstraint: Constraint? private var muteLeftConstraint: Constraint? + // MARK: - Views + private lazy var imageView: UIImageView = { let img = UIImageView() img.contentMode = .scaleAspectFill @@ -41,10 +45,10 @@ class AvatarView: BaseView { return img }() - private lazy var labelsView: UIView = { + private lazy var titleContainerView: UIView = { let view = UIView() - let horizontalInset = Constraints.labelsView.horizontalInset.adjustedByWidth + let horizontalInset = Constraints.titleContainerView.horizontalInset.adjustedByWidth self.addSubview(view) view.snp.makeConstraints { (make) in @@ -60,7 +64,8 @@ class AvatarView: BaseView { let height = Constraints.titleLabel.height.adjustedByWidth let us = UILabel(height: height, color: UIColor.nynja.white, fontName: FontFamily.NotoSans.medium.name) us.accessibilityIdentifier = "chat_title" - self.labelsView.addSubview(us) + + titleContainerView.addSubview(us) us.snp.makeConstraints({ (make) in make.height.equalTo(height) make.top.left.equalToSuperview() @@ -74,7 +79,7 @@ class AvatarView: BaseView { let side = Constraints.muteImageView.side.adjustedByWidth - self.labelsView.addSubview(imageView) + titleContainerView.addSubview(imageView) imageView.snp.makeConstraints { make in make.height.equalTo(side) muteWidthConstraint = make.width.equalTo(0).constraint @@ -91,7 +96,8 @@ class AvatarView: BaseView { let height = Constraints.statusLabel.height.adjustedByWidth let sl = UILabel(height: height, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.regular.name) sl.accessibilityIdentifier = "chat_status" - self.labelsView.addSubview(sl) + + titleContainerView.addSubview(sl) sl.snp.makeConstraints({ (make) in make.height.equalTo(height) make.top.equalTo(muteImageView.snp.bottom) @@ -101,16 +107,25 @@ class AvatarView: BaseView { return sl }() - private lazy var separatorView: UIView = { - let view = UIView() + private lazy var typingView: TypingView = { + let typingView = TypingView() - view.backgroundColor = UIColor.nynja.backgroundGray + titleContainerView.addSubview(typingView) + typingView.snp.makeConstraints { maker in + maker.top.bottom.equalTo(statusLabel) + maker.left.right.equalToSuperview() + } - self.addSubview(view) - view.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.separatorView.height) + return typingView + }() + + private lazy var separatorView: SeparatorView = { + let view = SeparatorView() + + addSubview(view) + view.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() - }) + } return view }() @@ -135,5 +150,4 @@ class AvatarView: BaseView { muteLeftConstraint?.update(offset: leftInset) } } - } diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift index 7ee65abf2..bdac3dfd9 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift @@ -8,35 +8,29 @@ extension AvatarView { - struct Constraints { + enum Constraints { - struct imageView { + enum imageView { static let width: CGFloat = 32.0 static let leftInset = 16.0 } - struct labelsView { + enum titleContainerView { static let horizontalInset = 16.0 } - struct titleLabel { + enum titleLabel { static let height: CGFloat = 22.0 } - struct muteImageView { + enum muteImageView { static let side = 12.0 static let leftInset = 8.0 } - struct statusLabel { + enum statusLabel { static let height: CGFloat = 20.0 } - - struct separatorView { - static let height = 1.0 - } - } - } -- GitLab From 9a1f52441f450fac844fc0f7ef2fe8b679fb1de8 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 19:21:39 +0300 Subject: [PATCH 011/138] [NY-4699] Temp stub for displaying typing --- .../View/Views/AvatarView/AvatarView.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index 47fe32217..f78e6cb7a 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -17,6 +17,25 @@ final class AvatarView: BaseView { var status: String? { didSet { + if let title = status?.replacingOccurrences(of: "...", with: ""), title.contains("typing") { + statusLabel.isHidden = true + typingView.isHidden = false + + let appearance = TypingView.Appearance(indicator: .dots(UIColor.white), + textColor: titleLabel.textColor, + textFont: statusLabel.font, + senderInfo: "Typing", + typingInfo: "", + isTypingInfoPinned: false + ) + typingView.update(appearance) + + } else { + statusLabel.isHidden = false + typingView.isHidden = true + + statusLabel.text = status + } statusLabel.text = status statusLabel.accessibilityValue = status } -- GitLab From 45c26b684f03d7cf11a5fad8b072ab604054c5a4 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 13:18:54 +0300 Subject: [PATCH 012/138] [NY-4699] Display group member's alias not only for text messages. --- Nynja.xcodeproj/project.pbxproj | 8 +- .../Cell/ChatListMessageAccessoryView.swift | 6 - .../Cell/ChatListMessageContentView.swift | 116 ++---------- .../Cell/ChatListMessageTableViewCell.swift | 7 - .../Cell/ChatListMessageTextView.swift | 178 ++++++++++++++++++ .../Model/ChatListMessageCellModel.swift | 34 +--- 6 files changed, 207 insertions(+), 142 deletions(-) create mode 100644 Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTextView.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index fd27189e9..645c418e5 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1104,6 +1104,7 @@ 85E1DD2520BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */; }; 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2620BEE961008AD211 /* ScalableCell.swift */; }; 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; + 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3296,6 +3297,7 @@ 85D77806211D9B980044E72F /* ScrollPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollPosition.swift; sourceTree = ""; }; 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageVC+StickerInputModuleDelegate.swift"; sourceTree = ""; }; 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; + 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -8641,9 +8643,10 @@ 8580BAD020BD98E600239D9D /* Cell */ = { isa = PBXGroup; children = ( - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */, 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */, + 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, + 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */, 8580BAD220BD98E600239D9D /* CounterView.swift */, ); path = Cell; @@ -8652,8 +8655,8 @@ 8580BAD520BD98E600239D9D /* Model */ = { isa = PBXGroup; children = ( - 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, 8580BAD620BD98E600239D9D /* ChatListMessageCellModel.swift */, + 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, ); path = Model; sourceTree = ""; @@ -15141,6 +15144,7 @@ A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, + 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift index 57514705a..0faef603f 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift @@ -110,12 +110,6 @@ final class ChatListMessageAccessoryView: BaseView { } } } - - func reset() { - timeLabel.text = nil - mentionIndicatorView.isHidden = true - counterView.isHidden = true - } } // MARK: - Layout diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index c0c44cc22..5117e1d81 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -11,17 +11,8 @@ import SnapKit final class ChatListMessageContentView: BaseView { - private static let contentFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, - height: Constraints.contentLabel.height.adjustedByWidth)! - - private static let contentBoldFont = UIFont.makeFont(with: FontFamily.NotoSans.bold.name, - height: Constraints.contentLabel.height.adjustedByWidth)! - // MARK: - Views - private var contentLeftSuperviewConstraint: Constraint? - private var contentLeftContentTypeConstraint: Constraint? - private(set) lazy var titleLabel: UILabel = { let height = Constraints.titleLabel.height.adjustedByWidth let color = UIColor.nynja.white @@ -36,42 +27,18 @@ final class ChatListMessageContentView: BaseView { return label }() - - private(set) lazy var contentLabel: AlignableLabel = { - let height = Constraints.contentLabel.height.adjustedByWidth - let contentIconInset = Constraints.contentLabel.contentTypeIconInset.adjustedByWidth - let color = UIColor.nynja.manatee - - let label = AlignableLabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name) - label.verticalAlignement = .top - label.numberOfLines = 1 - - addSubview(label) - label.snp.makeConstraints { maker in - maker.top.equalTo(titleLabel.snp.bottom) - contentLeftSuperviewConstraint = maker.left.equalToSuperview() - .constraint - contentLeftContentTypeConstraint = maker.left.equalTo(contentTypeImageView.snp.right).offset(contentIconInset) - .constraint - maker.bottom.right.equalToSuperview() - maker.height.equalTo(height) - } - - return label - }() - - private(set) lazy var contentTypeImageView: UIImageView = { - let size = Constraints.contentTypeImageView.size.adjustedByWidth - let imageView = UIImageView() + + private(set) lazy var textView: ChatListMessageTextView = { + let textView = ChatListMessageTextView() - addSubview(imageView) - imageView.snp.makeConstraints { maker in - maker.left.equalToSuperview() + addSubview(textView) + textView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.width.height.equalTo(size) + maker.bottom.left.right.equalToSuperview() + // FIXME: right constraint must be setuped in parent view } - return imageView + return textView }() @@ -79,20 +46,8 @@ final class ChatListMessageContentView: BaseView { override func baseSetup() { super.baseSetup() - contentLabel.isHidden = false - hideImageView() - } - - func showImageView() { - contentTypeImageView.isHidden = false - contentLeftSuperviewConstraint?.deactivate() - contentLeftContentTypeConstraint?.activate() - } - - func hideImageView() { - contentTypeImageView.isHidden = true - contentLeftSuperviewConstraint?.activate() - contentLeftContentTypeConstraint?.deactivate() + titleLabel.isHidden = false + textView.isHidden = false } func setupTitle(_ title: String?) { @@ -100,60 +55,17 @@ final class ChatListMessageContentView: BaseView { titleLabel.accessibilityValue = title } - func setupContentTypeImage(_ image: UIImage?) { - contentTypeImageView.image = image - } - - func setupContent(_ text: String?) { - contentLabel.text = text - } - - func setupContent(sender: String, text: String) { - let defaultAttributes: [NSAttributedStringKey: Any] = [ - .foregroundColor: UIColor.nynja.manatee, - .font: type(of: self).contentFont - ] - let boldAttributes: [NSAttributedStringKey: Any] = [ - .foregroundColor: UIColor.nynja.manatee, - .font: type(of: self).contentBoldFont - ] - - let boldText = "\(sender):" - let resultText = "\(boldText) \(text)" - - let attributedText = NSMutableAttributedString(string: resultText, attributes: defaultAttributes) - - let boldRange = (boldText.startIndex.. Date: Fri, 26 Oct 2018 14:52:28 +0300 Subject: [PATCH 013/138] [NY-4699] Show typing UI on chat list. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 4 ++ .../Typing/TypingBoldIndicatorView.swift | 58 +++++++++++++++++++ .../NynjaUIKit/Views/Typing/TypingView.swift | 26 +++++---- .../Cell/ChatListMessageContentView.swift | 44 +++++++++++++- .../Model/ChatListMessageCellModel.swift | 2 + 5 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 1d07d6b75..8c3037efe 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00B2181EBD2003A010F /* BaseView.swift */; }; 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */; }; 8540A019218213E2003A010F /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A018218213E2003A010F /* RoundView.swift */; }; + 85EB37F621832D41003A2D6F /* TypingBoldIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F521832D41003A2D6F /* TypingBoldIndicatorView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -75,6 +76,7 @@ 8540A00B2181EBD2003A010F /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Animation.swift"; sourceTree = ""; }; 8540A018218213E2003A010F /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; + 85EB37F521832D41003A2D6F /* TypingBoldIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingBoldIndicatorView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C90742AD8E6E2E817F7DB1E9 /* Pods-NynjaUIKit.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.channels.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.channels.xcconfig"; sourceTree = ""; }; @@ -316,6 +318,7 @@ isa = PBXGroup; children = ( 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */, + 85EB37F521832D41003A2D6F /* TypingBoldIndicatorView.swift */, 8540A0092181EB87003A010F /* TypingView.swift */, ); path = Typing; @@ -481,6 +484,7 @@ 8514D51120EE40540002378A /* ContextMenuItem.swift in Sources */, 8514D51820EE40540002378A /* NynjaContextMenuItemsFactory.swift in Sources */, 8540A00A2181EB87003A010F /* TypingView.swift in Sources */, + 85EB37F621832D41003A2D6F /* TypingBoldIndicatorView.swift in Sources */, 8514D51520EE40540002378A /* NynjaContextMenuItemCollectionViewCell.swift in Sources */, 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */, 8514D4E120EE2D970002378A /* SelectableCellViewModel.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift new file mode 100644 index 000000000..7316394e6 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift @@ -0,0 +1,58 @@ +// +// TypingBoldIndicatorView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +public final class TypingBoldIndicatorView: BaseView { + + public var horizontalInset: CGFloat = 4 { + didSet { + indicatorView.snp.updateConstraints { maker in + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + } + } + } + + public var circleSize: CGFloat = 8 { + didSet { + indicatorView.snp.updateConstraints { maker in + maker.width.height.equalTo(circleSize) + } + } + } + + public var circleColor: UIColor = .lightGray { + didSet { + indicatorView.backgroundColor = circleColor + } + } + + + // MARK: - Views + + private let indicatorView = RoundView() + + + // MARK: - Setup + + public override func setup() { + super.setup() + + backgroundColor = .clear + indicatorView.backgroundColor = backgroundColor + + addSubview(indicatorView) + indicatorView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + maker.width.height.equalTo(circleSize) + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index c71ecb7c8..45009aa61 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -21,14 +21,14 @@ public final class TypingView: BaseView { public let indicator: Indicator public let textColor: UIColor public let textFont: UIFont - public let senderInfo: String + public let senderInfo: String? public let typingInfo: String public let isTypingInfoPinned: Bool public init(indicator: Indicator, textColor: UIColor, textFont: UIFont, - senderInfo: String, + senderInfo: String?, typingInfo: String, isTypingInfoPinned: Bool) { self.indicator = indicator @@ -52,6 +52,7 @@ public final class TypingView: BaseView { private lazy var senderInfoLabel: UILabel = { let label = UILabel() + label.setContentCompressionResistancePriority(.required, for: .horizontal) addSubview(label) return label }() @@ -64,7 +65,6 @@ public final class TypingView: BaseView { indicatorContainer.snp.makeConstraints { maker in maker.top.bottom.left.equalToSuperview() - maker.width.equalTo(Constraints.indicator.containerWidth.adjustedByWidth) } senderInfoLabel.snp.makeConstraints { maker in @@ -88,32 +88,34 @@ public final class TypingView: BaseView { senderInfoLabel.font = appearance.textFont senderInfoLabel.textColor = appearance.textColor - senderInfoLabel.text = "\(appearance.senderInfo) \(appearance.typingInfo)" + senderInfoLabel.text = appearance.senderInfo + .flatMap { "\($0) \(appearance.typingInfo)" } ?? appearance.typingInfo } private func setupDotsIndicator(color: UIColor) { let indicatorView = TypingIndicatorView() indicatorView.itemColor = color + indicatorView.setContentCompressionResistancePriority(.required, for: .horizontal) + indicatorView.setContentHuggingPriority(.required, for: .horizontal) indicatorContainer.addSubview(indicatorView) indicatorView.snp.makeConstraints { maker in maker.centerY.equalToSuperview() - maker.left.equalToSuperview() + maker.left.right.equalToSuperview() } } private func setupCircleIndicator(color: UIColor) { - let indicatorView = RoundView() - indicatorView.backgroundColor = color - + let indicatorView = TypingBoldIndicatorView() indicatorContainer.addSubview(indicatorView) - let size = Constraints.indicator.circleSize.adjustedByWidth + indicatorView.circleColor = color + indicatorView.circleSize = Constraints.indicator.circleSize.adjustedByWidth + indicatorView.horizontalInset = Constraints.indicator.padding.adjustedByWidth indicatorView.snp.makeConstraints { maker in - maker.center.equalToSuperview() - maker.width.height.equalTo(size) + maker.edges.equalToSuperview() } } @@ -123,8 +125,8 @@ public final class TypingView: BaseView { private enum Constraints { enum indicator { - static let containerWidth: CGFloat = 16 static let circleSize: CGFloat = 8 + static let padding: CGFloat = 4 } enum senderInfo { diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index 5117e1d81..4ad6ce2d7 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageContentView: BaseView { @@ -35,12 +36,26 @@ final class ChatListMessageContentView: BaseView { textView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) maker.bottom.left.right.equalToSuperview() - // FIXME: right constraint must be setuped in parent view } return textView }() + private(set) lazy var typingView: TypingView = { + let height = Constraints.typingView.height.adjustedByWidth + + let typingView = TypingView() + + addSubview(typingView) + typingView.snp.makeConstraints { maker in + maker.top.equalTo(titleLabel.snp.bottom) + maker.left.right.equalToSuperview() + maker.height.equalTo(height) + } + + return typingView + }() + // MARK: - Setup @@ -48,6 +63,7 @@ final class ChatListMessageContentView: BaseView { super.baseSetup() titleLabel.isHidden = false textView.isHidden = false + typingView.isHidden = false } func setupTitle(_ title: String?) { @@ -59,13 +75,39 @@ final class ChatListMessageContentView: BaseView { textView.setup(sender: sender, image: image, text: text) } + func showTyping(sender: String?) { + typingView.isHidden = false + textView.isHidden = true + + let appearance = TypingView.Appearance(indicator: .dots(UIColor.nynja.white), + textColor: titleLabel.textColor, + textFont: typingFont, + senderInfo: sender, + typingInfo: "typing", + isTypingInfoPinned: false) + + typingView.update(appearance) + } + + func hideTyping() { + typingView.isHidden = true + textView.isHidden = false + } + // MARK: - Layout + private let typingFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, + height: Constraints.typingView.height.adjustedByWidth)! + private enum Constraints { enum titleLabel { static let height: CGFloat = 22 } + + enum typingView { + static let height: CGFloat = 20 + } } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 8af6bacaa..37acf4f1d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -60,6 +60,8 @@ final class ChatListMessageCellModel: CellViewModel { let type = SendMessageType(rawValue: mime) else { return } + cell.messageContentView.showTyping(sender: sender) + switch type { case .text: setupText(for: message, in: cell) -- GitLab From 753aaa2bfb2e64017b4bf154f243221102eac196 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 16:33:56 +0300 Subject: [PATCH 014/138] [NY-4699] Temp fix for continue animation of typing after view controller dissapeared. --- Nynja/Modules/ChatsList/View/ChatsListViewController.swift | 6 ++++++ .../Modules/GroupsList/View/GroupsListViewController.swift | 2 ++ Nynja/Modules/Profile/View/ProfileViewController.swift | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index 7f512269c..b5bd6f8a1 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -78,6 +78,12 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable setupUI() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // FIXME: update visile cells in other way + tableView.reloadData() + } + override func prepareForDissappear() { super.prepareForDissappear() if !swipeBackHelper.isSwipeActive { diff --git a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift index 780533112..81fac1e09 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift @@ -88,6 +88,8 @@ final class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipab override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) presenter.viewWillAppear() + // FIXME: update visile cells in other way + tableView.reloadData() } diff --git a/Nynja/Modules/Profile/View/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index d5459dd4d..43067852b 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -58,6 +58,12 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { super.initialize() setupUI() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // FIXME: update visile cells in other way + tableView.reloadData() + } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() -- GitLab From 71d224345475338356d7ef2c8bbcf1367fe4fe68 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 17:22:48 +0300 Subject: [PATCH 015/138] [NY-4699] Implemented UI for displaying statuses on p2p chat list. --- .../Views/Avatar/AvatarStatusView.swift | 12 +++--- .../Cell/ChatListMessageTableViewCell.swift | 42 +++++++------------ .../Model/ChatListMessageCellModel.swift | 27 +++++++++--- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index bad89a549..bcd7ffdc8 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -16,13 +16,13 @@ public final class AvatarStatusView: BaseView { } } - public var statusRadius: CGFloat = 24 { + public var statusIconSize: CGFloat = 24 { didSet { setNeedsLayout() } } - public var statusPadding: CGFloat = 8 { + public var statusIconPadding: CGFloat = 8 { didSet { setNeedsLayout() } @@ -66,7 +66,7 @@ public final class AvatarStatusView: BaseView { imageView.layer.cornerRadius = bounds.height / 2 let clipCircleCenter = statusPosition(in: bounds) - let clipCircleSize = statusRadius + statusPadding * 2 + let clipCircleSize = statusIconSize + statusIconPadding * 2 let clipCircleFrame = CGRect( x: clipCircleCenter.x - clipCircleSize / 2, y: clipCircleCenter.y - clipCircleSize / 2, @@ -89,10 +89,10 @@ public final class AvatarStatusView: BaseView { } private func updateStatusView(with frame: CGRect) { - let size = frame.width - statusPadding * 2 + let size = frame.width - statusIconPadding * 2 - statusView.frame = CGRect(x: frame.minX + statusPadding, - y: frame.minY + statusPadding, + statusView.frame = CGRect(x: frame.minX + statusIconPadding, + y: frame.minY + statusIconPadding, width: size, height: size) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index a76ae6f8a..0230b9458 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -8,26 +8,30 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageTableViewCell: UITableViewCell { // MARK: - Views - private(set) lazy var avatarImageView: UIImageView = { + private(set) lazy var avatarImageView: AvatarStatusView = { let size = Constraints.avatarImageView.size.adjustedByWidth + let statusIconPadding = Constraints.avatarImageView.statusIconPadding.adjustedByWidth + let statusIconSize = Constraints.avatarImageView.statusIconSize.adjustedByWidth - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - applyCorners(to: imageView, radius: size / 2) + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconPadding = statusIconPadding + avatarView.statusIconSize = statusIconSize - contentView.addSubview(imageView) - imageView.snp.makeConstraints { maker in + contentView.addSubview(avatarView) + avatarView.snp.makeConstraints { maker in maker.width.height.equalTo(size) maker.left.equalToSuperview().offset(Constraints.avatarImageView.leftInset.adjustedByWidth) maker.centerY.equalToSuperview() } - return imageView + return avatarView }() private(set) lazy var messageContentView: ChatListMessageContentView = { @@ -43,7 +47,6 @@ final class ChatListMessageTableViewCell: UITableViewCell { view.snp.makeConstraints { maker in maker.left.equalTo(avatarImageView.snp.right).offset(leftInset) maker.right.lessThanOrEqualTo(messageAccessoryView.snp.left).offset(-rightInset) - maker.width.equalTo(width).priority(.high) maker.centerY.equalToSuperview() } @@ -71,8 +74,7 @@ final class ChatListMessageTableViewCell: UITableViewCell { private lazy var separatorView: SeparatorView = { let view = SeparatorView() - view.color = UIColor.nynja.backgroundGray - + contentView.addSubview(view) view.snp.makeConstraints { maker in maker.horizontalInset(Constraints.separatorView.horizontalInset.adjustedByWidth) @@ -104,22 +106,6 @@ final class ChatListMessageTableViewCell: UITableViewCell { messageContentView.isHidden = false separatorView.isHidden = false } - - - // MARK: - Life Cycle - - override func layoutSubviews() { - super.layoutSubviews() - applyCorners(to: avatarImageView, radius: Constraints.avatarImageView.cornerRadius.adjustedByWidth) - } - - - // MARK: - Layout - - private func applyCorners(to imageView: UIImageView, radius: CGFloat) { - let borderColor = UIColor.nynja.almostBlack - imageView.roundCornersImage(borderWidth: 2, cornerRadius: radius, borderColor: borderColor) - } } // MARK: - Layout @@ -132,10 +118,12 @@ extension ChatListMessageTableViewCell { enum avatarImageView { static let size: CGFloat = 48.0 - static let cornerRadius: CGFloat = size / 2 static let verticalInset: CGFloat = 8.0 static let leftInset: CGFloat = 16.0 + + static let statusIconPadding: CGFloat = 2.0 + static let statusIconSize: CGFloat = 8.0 } enum messageContentView { diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 37acf4f1d..370f62e03 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -34,10 +34,25 @@ final class ChatListMessageCellModel: CellViewModel { } func setup(cell: ChatListMessageTableViewCell) { - // Avatar - cell.avatarImageView.setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) + setupAvatar(in: cell) + setupAccessory(in: cell) + setupMessage(model.message, in: cell) + } + + + // MARK: - Avatar + + private func setupAvatar(in cell: Cell) { + cell.avatarImageView.imageView + .setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) - // Accessory + cell.avatarImageView.update(.color(UIColor.nynja.callGreen)) + } + + + // MARK: - Accessory + + private func setupAccessory(in cell: Cell) { let unreadCount = min(Int(model.unreadMessagesCount), type(of: self).unreadCounterLimit) let shouldDisplayMention = model.hasMentions @@ -45,11 +60,11 @@ final class ChatListMessageCellModel: CellViewModel { if let createdDate = model.message?.createdDate { cell.messageAccessoryView.setup(date: createdDate) } - - // Message Data - setupMessage(model.message, in: cell) } + + // MARK: - Message + private func setupMessage(_ message: Message?, in cell: Cell) { cell.messageContentView.setupTitle(model.title) -- GitLab From 0a5463e9e7c3d8e3b26a9dfe9c24da4409b0915f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 17:33:37 +0300 Subject: [PATCH 016/138] [NY-4699] Don't display status in rooms list. --- .../Views/Avatar/AvatarStatusView.swift | 36 +++++++++++++------ .../Model/ChatListMessageCellModel.swift | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index bcd7ffdc8..3e41e2794 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -41,6 +41,10 @@ public final class AvatarStatusView: BaseView { private let maskLayer = CAShapeLayer() + private var isMaskActive: Bool { + return !statusView.isHidden + } + // MARK: - Setup @@ -65,16 +69,20 @@ public final class AvatarStatusView: BaseView { imageView.frame = bounds imageView.layer.cornerRadius = bounds.height / 2 - let clipCircleCenter = statusPosition(in: bounds) - let clipCircleSize = statusIconSize + statusIconPadding * 2 - let clipCircleFrame = CGRect( - x: clipCircleCenter.x - clipCircleSize / 2, - y: clipCircleCenter.y - clipCircleSize / 2, - width: clipCircleSize, - height: clipCircleSize - ) - updateClipMask(with: clipCircleFrame) - updateStatusView(with: clipCircleFrame) + if isMaskActive { + let clipCircleCenter = statusPosition(in: bounds) + let clipCircleSize = statusIconSize + statusIconPadding * 2 + let clipCircleFrame = CGRect( + x: clipCircleCenter.x - clipCircleSize / 2, + y: clipCircleCenter.y - clipCircleSize / 2, + width: clipCircleSize, + height: clipCircleSize + ) + updateClipMask(with: clipCircleFrame) + updateStatusView(with: clipCircleFrame) + } else { + imageView.layer.mask = nil + } } private func updateClipMask(with frame: CGRect) { @@ -110,16 +118,24 @@ extension AvatarStatusView { public enum StatusAppearance { case color(UIColor) case image(UIImage) + case none } public func update(_ statusAppearance: StatusAppearance) { switch statusAppearance { case let .color(color): + statusView.isHidden = false statusView.backgroundColor = color statusView.image = nil case let .image(image): + statusView.isHidden = false statusView.backgroundColor = nil statusView.image = image + case .none: + statusView.isHidden = true + statusView.backgroundColor = nil + statusView.image = nil } + setNeedsLayout() } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 370f62e03..8d12b5eed 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -46,7 +46,7 @@ final class ChatListMessageCellModel: CellViewModel { cell.avatarImageView.imageView .setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) - cell.avatarImageView.update(.color(UIColor.nynja.callGreen)) + cell.avatarImageView.update(model is Contact ? .color(UIColor.nynja.callGreen) : .none) } -- GitLab From 16516ff8daf4fa6094b804f5e39e0d4d51a3cd76 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 19:23:26 +0300 Subject: [PATCH 017/138] [NY-4699] Implemented base stub for AccountStatusProvider. --- Nynja.xcodeproj/project.pbxproj | 24 ++++++++++ Nynja/Statuses/AccountStatus.swift | 17 +++++++ Nynja/Statuses/AccountStatusProvider.swift | 40 +++++++++++++++++ Nynja/Statuses/Observable.swift | 52 ++++++++++++++++++++++ Nynja/Statuses/ObservableContainer.swift | 44 ++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 Nynja/Statuses/AccountStatus.swift create mode 100644 Nynja/Statuses/AccountStatusProvider.swift create mode 100644 Nynja/Statuses/Observable.swift create mode 100644 Nynja/Statuses/ObservableContainer.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 645c418e5..e84f958e0 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1105,6 +1105,10 @@ 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2620BEE961008AD211 /* ScalableCell.swift */; }; 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; + 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */; }; + 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; + 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; + 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3298,6 +3302,10 @@ 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageVC+StickerInputModuleDelegate.swift"; sourceTree = ""; }; 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; + 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatusProvider.swift; sourceTree = ""; }; + 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; + 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -5973,6 +5981,7 @@ 8509AC61206A54420089089B /* ResponseResult.swift */, B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, + 85EB37F9218365A6003A2D6F /* Statuses */, ); name = Services; sourceTree = ""; @@ -9076,6 +9085,17 @@ path = BBCode; sourceTree = ""; }; + 85EB37F9218365A6003A2D6F /* Statuses */ = { + isa = PBXGroup; + children = ( + 85EB37FE21837304003A2D6F /* AccountStatus.swift */, + 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, + 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, + 85EB37FC21837253003A2D6F /* Observable.swift */, + ); + path = Statuses; + sourceTree = ""; + }; 85EF7C342090DEFF0090C418 /* Models */ = { isa = PBXGroup; children = ( @@ -14976,6 +14996,7 @@ 4B4266C1204D917800194BC1 /* ActionsView+Layout.swift in Sources */, AF440BA5CEBE5170D082FF60 /* LoginProtocols.swift in Sources */, 85C16C3520D2520E00EDB77E /* StickersDownloadingService.swift in Sources */, + 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */, 6D6234F81F1E158600EF375F /* HistoryCell.swift in Sources */, FEA655F62167777E00B44029 /* PaymentInteractor.swift in Sources */, E74EC9EF1FC2DE23007268E6 /* MemberTable.swift in Sources */, @@ -15117,6 +15138,7 @@ A49B81B320B4BB6400980D36 /* NynjaMTIConfig.swift in Sources */, 8EC2AF6B20053FC300807B20 /* GroupCollectionCell.swift in Sources */, 4B8996D8204EDA7700DCB183 /* JobDAOProtocol.swift in Sources */, + 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */, A4CE80C320C95E7F00400713 /* CollectionDisplayMode.swift in Sources */, E74E53951FB45D6800463242 /* ScrollBar.swift in Sources */, FEA655CD2167777E00B44029 /* SeedVerificationWalletProtocols.swift in Sources */, @@ -15145,6 +15167,7 @@ 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, + 85EB37FD21837253003A2D6F /* Observable.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15308,6 +15331,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, + 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, diff --git a/Nynja/Statuses/AccountStatus.swift b/Nynja/Statuses/AccountStatus.swift new file mode 100644 index 000000000..f9f0890d4 --- /dev/null +++ b/Nynja/Statuses/AccountStatus.swift @@ -0,0 +1,17 @@ +// +// AccountStatus.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +typealias AccountId = String + +enum AccountStatus { + case active + case inactive + case busy + case offline + case none +} diff --git a/Nynja/Statuses/AccountStatusProvider.swift b/Nynja/Statuses/AccountStatusProvider.swift new file mode 100644 index 000000000..d8996ab5f --- /dev/null +++ b/Nynja/Statuses/AccountStatusProvider.swift @@ -0,0 +1,40 @@ +// +// AccountStatusProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AccountStatusObservable: class { + typealias Callback = (AccountId, AccountStatus) -> Void + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: AccountId) + func notify(_ key: AccountId, with value: AccountStatus) +} + +protocol AccountStatusProvider: AccountStatusObservable { + func status(for accountId: AccountId) -> AccountStatus + func update(_ status: AccountStatus, for accountId: AccountId) +} + +final class AccountStatusProviderImpl: AccountStatusProvider, ObservableContainer { + + private var data: [AccountId: AccountStatus] = [:] + + private(set) var observable = Observable() + + func status(for accountId: AccountId) -> AccountStatus { + return data[accountId] ?? .none + } + + func update(_ status: AccountStatus, for accountId: AccountId) { + data[accountId] = status + observable.notify(accountId, with: status) + } +} diff --git a/Nynja/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift new file mode 100644 index 000000000..5857ca9f6 --- /dev/null +++ b/Nynja/Statuses/Observable.swift @@ -0,0 +1,52 @@ +// +// Observable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class Observable { + + private typealias Observers = [AnyWeakSubscriber] + + private struct Handler { + var callback: (Key, Value) -> Void + } + + private var allObservers: Observers = [] + + private var observers: [Key: Observers] = [:] + + func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + allObservers.append(container) + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + var newObservers = observers[key] ?? [] + newObservers.append(container) + observers[key] = newObservers + } + + func removeObserver(_ observer: AnyObject) { + allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + } + + func notify(_ key: Key, with value: Value) { + for observer in allObservers { + observer.handler.callback(key, value) + } + observers[key]?.forEach { $0.handler.callback(key, value) } + } +} diff --git a/Nynja/Statuses/ObservableContainer.swift b/Nynja/Statuses/ObservableContainer.swift new file mode 100644 index 000000000..9b89e430f --- /dev/null +++ b/Nynja/Statuses/ObservableContainer.swift @@ -0,0 +1,44 @@ +// +// ObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ObservableContainer: class { + associatedtype Key: Hashable + associatedtype Value + typealias Callback = (Key, Value) -> Void + + var observable: Observable { get } + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: Key) + func notify(_ key: Key, with value: Value) +} + +extension ObservableContainer { + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) { + observable.addObserver(observer, callback: callback) + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) { + observable.addObserver(observer, for: key, callback: callback) + } + + func removeObserver(_ observer: AnyObject) { + observable.removeObserver(observer) + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + observable.removeObserver(observer, for: key) + } + + func notify(_ key: Key, with value: Value) { + observable.notify(key, with: value) + } +} -- GitLab From 454c2211a6f5db1e312fc5ed4ebdafacbecd1881 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 10:31:38 +0200 Subject: [PATCH 018/138] [NY-4699] Added TypingStatusProvider. --- Nynja.xcodeproj/project.pbxproj | 8 +++++ Nynja/Statuses/TypingActionStatus.swift | 23 +++++++++++++ Nynja/Statuses/TypingStatusProvider.swift | 40 +++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 Nynja/Statuses/TypingActionStatus.swift create mode 100644 Nynja/Statuses/TypingStatusProvider.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index e84f958e0..0f640fc47 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -949,6 +949,7 @@ 85482848204EA56600DCBEC8 /* PrivacyListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */; }; 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */; }; 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548340D207769E800604051 /* DocumentInteractionInput.swift */; }; + 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingStatusProvider.swift */; }; 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */; }; 854A4B2D2080D68200759152 /* CellWithArrowCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */; }; 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */; }; @@ -1109,6 +1110,7 @@ 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; + 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3157,6 +3159,7 @@ 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyListDataSource.swift; sourceTree = ""; }; 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastScrollable.swift; sourceTree = ""; }; 8548340D207769E800604051 /* DocumentInteractionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentInteractionInput.swift; sourceTree = ""; }; + 854834172186FADB002064E1 /* TypingStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusProvider.swift; sourceTree = ""; }; 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowCellModel.swift; sourceTree = ""; }; 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; @@ -3306,6 +3309,7 @@ 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; + 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingActionStatus.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -9089,7 +9093,9 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, + 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, + 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, 85EB37FC21837253003A2D6F /* Observable.swift */, ); @@ -15057,6 +15063,7 @@ 6D485DDF1F0ACA4700E12FB1 /* UIImageView+Rounded.swift in Sources */, 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */, 40C2631343E285717633ADFA /* LoginPresenter.swift in Sources */, + 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */, A42D51A7206A361400EEB952 /* log.swift in Sources */, DAE89B7EFAB308A6B48AF5EC /* LoginInteractor.swift in Sources */, C940514A204C7FAF00D72B04 /* DataAndStorageViewController.swift in Sources */, @@ -16213,6 +16220,7 @@ 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, + 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */, F11DF06520BD96D000F3E005 /* GalleryFilterType.swift in Sources */, FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, diff --git a/Nynja/Statuses/TypingActionStatus.swift b/Nynja/Statuses/TypingActionStatus.swift new file mode 100644 index 000000000..7654b102f --- /dev/null +++ b/Nynja/Statuses/TypingActionStatus.swift @@ -0,0 +1,23 @@ +// +// TypingActionStatus.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum TypingActionStatus { + typealias Sender = String + + enum Record { + case voice, video + } + enum Mime { + case file + case image + } + case typing([Sender]) + case sending([Sender], Mime) + case recording([Sender], Record) + case none +} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift new file mode 100644 index 000000000..eaf60ecf3 --- /dev/null +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -0,0 +1,40 @@ +// +// TypingStatusProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol TypingStatusObservable: class { + typealias Callback = (AccountId, TypingActionStatus) -> Void + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: AccountId) + func notify(_ key: AccountId, with value: TypingActionStatus) +} + +protocol TypingStatusProvider: TypingStatusObservable { + func status(for accountId: AccountId) -> TypingActionStatus + func update(_ status: TypingActionStatus, for accountId: AccountId) +} + +final class TypingStatusProviderImpl: TypingStatusProvider, ObservableContainer { + + private var data: [AccountId: TypingActionStatus] = [:] + + private(set) var observable = Observable() + + func status(for accountId: AccountId) -> TypingActionStatus { + return data[accountId] ?? .none + } + + func update(_ status: TypingActionStatus, for accountId: AccountId) { + data[accountId] = status + observable.notify(accountId, with: status) + } +} -- GitLab From 6d3fc1dd02e4ef8022643bd18603f4bbdfbc74e9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 10:54:23 +0200 Subject: [PATCH 019/138] [NY-4699] Added status to AvatarStatusView --- .../Views/Avatar/AvatarStatusView.swift | 4 ++-- .../View/DetailsView/ProfileDetailsView.swift | 24 +++++++++++-------- .../ProfileDetailsViewLayout.swift | 24 ++++++++++--------- .../Profile/View/ProfileViewController.swift | 8 +++---- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index 3e41e2794..b2bec5db6 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -16,13 +16,13 @@ public final class AvatarStatusView: BaseView { } } - public var statusIconSize: CGFloat = 24 { + public var statusIconSize: CGFloat = 8 { didSet { setNeedsLayout() } } - public var statusIconPadding: CGFloat = 8 { + public var statusIconPadding: CGFloat = 2 { didSet { setNeedsLayout() } diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift index f956cc96b..cb5b5b71f 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift @@ -6,25 +6,27 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class ProfileDetailsView: UIView { // MARK: Views - lazy var avatarImageView: UIImageView = { + lazy var avatarImageView: AvatarStatusView = { let width = Constraints.avatarImageView.width.adjustedByWidth - let imageView = UIImageView(frame: CGRect(x: 0, y:0, width: width, height: width)) - - imageView.contentMode = .scaleAspectFill - imageView.roundImageView() - - self.addSubview(imageView) - imageView.snp.makeConstraints({ (make) in + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconSize = CGFloat(Constraints.avatarImageView.statusIconSize.adjustedByWidth) + avatarView.statusIconPadding = CGFloat(Constraints.avatarImageView.statusIconPadding.adjustedByWidth) + + addSubview(avatarView) + avatarView.snp.makeConstraints { make in make.width.height.equalTo(width) make.top.equalToSuperview().offset(Constraints.avatarImageView.topInset.adjustedByHeight) make.left.equalToSuperview().offset(Constraints.avatarImageView.leftInset.adjustedByWidth) - }) + } - return imageView + return avatarView }() lazy var infoView: UIView = { @@ -196,6 +198,8 @@ class ProfileDetailsView: UIView { phoneLabel.isHidden = false walletButton.isHidden = false infoView.isHidden = false + + avatarImageView.update(.color(UIColor.nynja.callGreen)) } // MARK: Recognizer diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift index 38591f0c8..33c9b7617 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift @@ -8,21 +8,23 @@ extension ProfileDetailsView { - struct Constraints { + enum Constraints { - struct avatarImageView { + enum avatarImageView { static let width = 95.0 + static let statusIconSize = 16.0 + static let statusIconPadding = 4.0 static let topInset = 30.0 static let leftInset = 16.0 } // MARK: Info View - struct infoView { + enum infoView { static let leftInset = 16.0 } - struct nameLabel { + enum nameLabel { static let width = 220.0 static let height = 33.0 @@ -32,7 +34,7 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct surnameLabel { + enum surnameLabel { static let width = 197.0 static let height = 33.0 @@ -41,14 +43,14 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct usernameLabel { + enum usernameLabel { static let width = 223.0 static let height = 22.0 static let heightProportion = height / width } - struct phoneLabel { + enum phoneLabel { static let width = 223.0 static let height = 22.0 @@ -57,14 +59,14 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct balanceLabel { + enum balanceLabel { static let width = 44.0 static let height = 17.0 static let topInset = 4.0 static let heightProportion = height / width } - struct fundsLabel { + enum fundsLabel { static let width = 66.0 static let height = 17.0 @@ -72,13 +74,13 @@ extension ProfileDetailsView { } // MARK: QR Button - struct qrButton { + enum qrButton { static let width: CGFloat = 40.0 static let rightInset = 16.0 } - struct addWalletButton { + enum addWalletButton { static let width = 50 static let height = 36 } diff --git a/Nynja/Modules/Profile/View/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index 43067852b..33f2fc770 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -114,9 +114,9 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { //MARK: - ProfileViewProtocol func setup(contact: Contact) { - detailsView.avatarImageView.setImage(url: contact.avatarUrl, - placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image, - accessibilityPrefix: "profile_details_view") + detailsView.avatarImageView.imageView.setImage(url: contact.avatarUrl, + placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image, + accessibilityPrefix: "profile_details_view") detailsView.nameLabel.text = contact.names detailsView.surnameLabel.text = contact.surnames @@ -156,7 +156,7 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { } @objc private func avatarTapped() { - presenter.showImagePreview(from: detailsView.avatarImageView) + presenter.showImagePreview(from: detailsView.avatarImageView.imageView) } @objc private func walletButtonAction() { -- GitLab From 6e007c5e248a981984ce05dd7e787af16f4660c8 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 11:05:15 +0200 Subject: [PATCH 020/138] [NY-4699] Minor UI fix. --- .../NynjaUIKit/Views/Typing/TypingIndicatorView.swift | 6 +++--- .../NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift index 5a67c92a5..a3946fc2d 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift @@ -17,7 +17,7 @@ public final class TypingIndicatorView: BaseView { } } - public var padding: CGFloat = 4 { + public var itemPadding: CGFloat = 4 { didSet { invalidateIntrinsicContentSize() setNeedsLayout() @@ -38,7 +38,7 @@ public final class TypingIndicatorView: BaseView { } public override var intrinsicContentSize: CGSize { - let width = itemSize * CGFloat(itemsCount) + padding * CGFloat(itemsCount - 1) + let width = itemSize * CGFloat(itemsCount) + itemPadding * CGFloat(itemsCount - 1) return CGSize(width: width, height: itemSize) } @@ -79,7 +79,7 @@ public final class TypingIndicatorView: BaseView { itemLayer.cornerRadius = itemSize / 2 animationLayer.instanceCount = itemsCount - animationLayer.instanceTransform = CATransform3DMakeTranslation(itemSize + padding, 0, 0) + animationLayer.instanceTransform = CATransform3DMakeTranslation(itemSize + itemPadding, 0, 0) animationLayer.instanceAlphaOffset = Float(Animation.toValue - Animation.fromValue) / Float(itemsCount - 1) animationLayer.instanceDelay = Animation.duration / Double(itemsCount) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 45009aa61..9258ab028 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -95,6 +95,9 @@ public final class TypingView: BaseView { private func setupDotsIndicator(color: UIColor) { let indicatorView = TypingIndicatorView() indicatorView.itemColor = color + indicatorView.itemSize = Constraints.indicator.dotsSize.adjustedByWidth + indicatorView.itemPadding = Constraints.indicator.dotsPadding.adjustedByWidth + indicatorView.setContentCompressionResistancePriority(.required, for: .horizontal) indicatorView.setContentHuggingPriority(.required, for: .horizontal) @@ -126,6 +129,8 @@ public final class TypingView: BaseView { enum indicator { static let circleSize: CGFloat = 8 + static let dotsSize: CGFloat = 3 + static let dotsPadding: CGFloat = 4 static let padding: CGFloat = 4 } -- GitLab From 321ff20518eb9d0d99bb423416c76982f7398e43 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 11:41:42 +0200 Subject: [PATCH 021/138] [NY-4699] Fixed offset in typing view. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 20 +++++++++++-------- .../Cell/ChatListMessageContentView.swift | 2 +- .../View/Views/AvatarView/AvatarView.swift | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 9258ab028..5ffd98f8d 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -103,10 +103,7 @@ public final class TypingView: BaseView { indicatorContainer.addSubview(indicatorView) - indicatorView.snp.makeConstraints { maker in - maker.centerY.equalToSuperview() - maker.left.right.equalToSuperview() - } + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) } private func setupCircleIndicator(color: UIColor) { @@ -115,10 +112,15 @@ public final class TypingView: BaseView { indicatorView.circleColor = color indicatorView.circleSize = Constraints.indicator.circleSize.adjustedByWidth - indicatorView.horizontalInset = Constraints.indicator.padding.adjustedByWidth + indicatorView.horizontalInset = Constraints.indicator.horizontalInset.adjustedByWidth - indicatorView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) + } + + private func makeIndicatorViewConstraints() -> (ConstraintMaker) -> Void { + return { maker in + maker.centerY.equalToSuperview().offset(Constraints.indicator.centerVerticalOffset.adjustedByWidth) + maker.left.right.equalToSuperview() } } @@ -131,7 +133,9 @@ public final class TypingView: BaseView { static let circleSize: CGFloat = 8 static let dotsSize: CGFloat = 3 static let dotsPadding: CGFloat = 4 - static let padding: CGFloat = 4 + + static let horizontalInset: CGFloat = 4 + static let centerVerticalOffset: CGFloat = 1 } enum senderInfo { diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index 4ad6ce2d7..71c5eda1e 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -79,7 +79,7 @@ final class ChatListMessageContentView: BaseView { typingView.isHidden = false textView.isHidden = true - let appearance = TypingView.Appearance(indicator: .dots(UIColor.nynja.white), + let appearance = TypingView.Appearance(indicator: .circle(UIColor.nynja.white), textColor: titleLabel.textColor, textFont: typingFont, senderInfo: sender, diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index f78e6cb7a..2d9495146 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -24,7 +24,7 @@ final class AvatarView: BaseView { let appearance = TypingView.Appearance(indicator: .dots(UIColor.white), textColor: titleLabel.textColor, textFont: statusLabel.font, - senderInfo: "Typing", + senderInfo: "typing", typingInfo: "", isTypingInfoPinned: false ) -- GitLab From 617a97977337098be957d535629a414584022aa8 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 13:19:09 +0200 Subject: [PATCH 022/138] [NY-4699] Fixed avatar view. --- Nynja/Modules/Message/View/MessageVC.swift | 8 +-- .../Message/View/MessageVCLayout.swift | 5 -- .../View/Views/AvatarView/AvatarView.swift | 62 +++++++++++-------- .../Views/AvatarView/AvatarViewLayout.swift | 7 ++- 4 files changed, 43 insertions(+), 39 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 515bdd20e..fb54b6343 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -88,12 +88,10 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw av.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed))) self.view.addSubview(av) - av.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.avatarView.height.adjustedByWidth) - - self.adjustVerticalInset(.top, make: make, offset: Constraints.avatarView.topInset.adjustedByWidth) + av.snp.makeConstraints { make in + adjustVerticalInset(.top, make: make) make.left.right.equalToSuperview() - }) + } return av }() diff --git a/Nynja/Modules/Message/View/MessageVCLayout.swift b/Nynja/Modules/Message/View/MessageVCLayout.swift index 0f7309484..008ace7bc 100644 --- a/Nynja/Modules/Message/View/MessageVCLayout.swift +++ b/Nynja/Modules/Message/View/MessageVCLayout.swift @@ -10,11 +10,6 @@ extension MessageVC { enum Constraints { - enum avatarView { - static let topInset = 8.0 - static let height = 48.0 - } - enum tableView { static let defaultVerticalInset = CGFloat(4.adjustedByWidth) static let bottomInset = CGFloat(gradientView.height) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index 2d9495146..bf039f735 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -29,7 +29,6 @@ final class AvatarView: BaseView { isTypingInfoPinned: false ) typingView.update(appearance) - } else { statusLabel.isHidden = false typingView.isHidden = true @@ -47,21 +46,27 @@ final class AvatarView: BaseView { // MARK: - Views - private lazy var imageView: UIImageView = { - let img = UIImageView() - img.contentMode = .scaleAspectFill + private lazy var avatarView: AvatarStatusView = { + let statusIconPadding = Constraints.imageView.statusIconPadding.adjustedByWidth + let statusIconSize = Constraints.imageView.statusIconSize.adjustedByWidth + + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconPadding = statusIconPadding + avatarView.statusIconSize = statusIconSize let width = Constraints.imageView.width.adjustedByWidth - img.roundCorners(radius: width / 2) + let verticalInset = Constraints.imageView.vertivalInset.adjustedByWidth - self.addSubview(img) - img.snp.makeConstraints({ (make) in + addSubview(avatarView) + avatarView.snp.makeConstraints { make in make.width.height.equalTo(width) + make.top.equalToSuperview().offset(verticalInset) + make.bottom.equalToSuperview().offset(-verticalInset) make.left.equalTo(Constraints.imageView.leftInset.adjustedByWidth) - make.centerY.equalToSuperview() - }) + } - return img + return avatarView }() private lazy var titleContainerView: UIView = { @@ -69,10 +74,10 @@ final class AvatarView: BaseView { let horizontalInset = Constraints.titleContainerView.horizontalInset.adjustedByWidth - self.addSubview(view) - view.snp.makeConstraints { (make) in + addSubview(view) + view.snp.makeConstraints { make in make.centerY.equalToSuperview() - make.left.equalTo(imageView.snp.right).offset(horizontalInset) + make.left.equalTo(avatarView.snp.right).offset(horizontalInset) make.right.equalTo(-horizontalInset) } @@ -81,16 +86,17 @@ final class AvatarView: BaseView { private lazy var titleLabel: UILabel = { let height = Constraints.titleLabel.height.adjustedByWidth - let us = UILabel(height: height, color: UIColor.nynja.white, fontName: FontFamily.NotoSans.medium.name) - us.accessibilityIdentifier = "chat_title" - titleContainerView.addSubview(us) - us.snp.makeConstraints({ (make) in + let label = UILabel(height: height, color: UIColor.nynja.white, fontName: FontFamily.NotoSans.medium.name) + label.accessibilityIdentifier = "chat_title" + + titleContainerView.addSubview(label) + label.snp.makeConstraints { make in make.height.equalTo(height) make.top.left.equalToSuperview() - }) + } - return us + return label }() private lazy var muteImageView: UIImageView = { @@ -113,17 +119,18 @@ final class AvatarView: BaseView { private lazy var statusLabel: UILabel = { let height = Constraints.statusLabel.height.adjustedByWidth - let sl = UILabel(height: height, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.regular.name) - sl.accessibilityIdentifier = "chat_status" - titleContainerView.addSubview(sl) - sl.snp.makeConstraints({ (make) in + let label = UILabel(height: height, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.regular.name) + label.accessibilityIdentifier = "chat_status" + + titleContainerView.addSubview(label) + label.snp.makeConstraints { make in make.height.equalTo(height) - make.top.equalTo(muteImageView.snp.bottom) + make.top.equalTo(titleLabel.snp.bottom) make.left.right.bottom.equalToSuperview() - }) + } - return sl + return label }() private lazy var typingView: TypingView = { @@ -155,7 +162,8 @@ final class AvatarView: BaseView { func setup(with viewModel: AvatarViewModel) { titleLabel.text = viewModel.title titleLabel.accessibilityValue = viewModel.title - imageView.setImage(url: viewModel.avatarUrl, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) + avatarView.imageView.setImage(url: viewModel.avatarUrl, + placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) setupMuteImageView(viewModel.isMuted) } diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift index bdac3dfd9..7a5ca994f 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift @@ -11,9 +11,12 @@ extension AvatarView { enum Constraints { enum imageView { - static let width: CGFloat = 32.0 - + static let width: CGFloat = 40.0 + static let vertivalInset: CGFloat = 8.0 static let leftInset = 16.0 + + static let statusIconPadding: CGFloat = 2.0 + static let statusIconSize: CGFloat = 8.0 } enum titleContainerView { -- GitLab From 0d9a82c436a2389669f272cedf530096f4ebfb8f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 13:20:52 +0200 Subject: [PATCH 023/138] [NY-4699] No status mask for avatar by default. --- .../NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index b2bec5db6..3706b9a21 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -58,6 +58,8 @@ public final class AvatarStatusView: BaseView { statusView.layer.masksToBounds = true addSubview(statusView) + + update(.none) } -- GitLab From 20d15ae4c5c78419e91d8b2ca8262f4e0fa58b96 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 14:37:55 +0200 Subject: [PATCH 024/138] [NY-4699] Use ActionStatus instead of TypingActionStatus. --- Nynja.xcodeproj/project.pbxproj | 4 ---- Nynja/Statuses/TypingActionStatus.swift | 23 ----------------------- Nynja/Statuses/TypingStatusProvider.swift | 18 +++++++++--------- 3 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 Nynja/Statuses/TypingActionStatus.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 0f640fc47..6dca8052b 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1110,7 +1110,6 @@ 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; - 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3309,7 +3308,6 @@ 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; - 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingActionStatus.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -9093,7 +9091,6 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, - 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, @@ -15063,7 +15060,6 @@ 6D485DDF1F0ACA4700E12FB1 /* UIImageView+Rounded.swift in Sources */, 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */, 40C2631343E285717633ADFA /* LoginPresenter.swift in Sources */, - 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */, A42D51A7206A361400EEB952 /* log.swift in Sources */, DAE89B7EFAB308A6B48AF5EC /* LoginInteractor.swift in Sources */, C940514A204C7FAF00D72B04 /* DataAndStorageViewController.swift in Sources */, diff --git a/Nynja/Statuses/TypingActionStatus.swift b/Nynja/Statuses/TypingActionStatus.swift deleted file mode 100644 index 7654b102f..000000000 --- a/Nynja/Statuses/TypingActionStatus.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// TypingActionStatus.swift -// Nynja -// -// Created by Anton Poltoratskyi on 26.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -enum TypingActionStatus { - typealias Sender = String - - enum Record { - case voice, video - } - enum Mime { - case file - case image - } - case typing([Sender]) - case sending([Sender], Mime) - case recording([Sender], Record) - case none -} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index eaf60ecf3..58b2ed044 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -9,31 +9,31 @@ import Foundation protocol TypingStatusObservable: class { - typealias Callback = (AccountId, TypingActionStatus) -> Void + typealias Callback = (AccountId, ActionStatus) -> Void func addObserver(_ observer: AnyObject, callback: @escaping Callback) func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) func removeObserver(_ observer: AnyObject) func removeObserver(_ observer: AnyObject, for key: AccountId) - func notify(_ key: AccountId, with value: TypingActionStatus) + func notify(_ key: AccountId, with value: ActionStatus) } protocol TypingStatusProvider: TypingStatusObservable { - func status(for accountId: AccountId) -> TypingActionStatus - func update(_ status: TypingActionStatus, for accountId: AccountId) + func status(for accountId: AccountId) -> ActionStatus + func update(_ status: ActionStatus, for accountId: AccountId) } final class TypingStatusProviderImpl: TypingStatusProvider, ObservableContainer { - private var data: [AccountId: TypingActionStatus] = [:] + private var data: [AccountId: ActionStatus] = [:] - private(set) var observable = Observable() + private(set) var observable = Observable() - func status(for accountId: AccountId) -> TypingActionStatus { - return data[accountId] ?? .none + func status(for accountId: AccountId) -> ActionStatus { + return data[accountId] ?? .done } - func update(_ status: TypingActionStatus, for accountId: AccountId) { + func update(_ status: ActionStatus, for accountId: AccountId) { data[accountId] = status observable.notify(accountId, with: status) } -- GitLab From 0249bf38382122019b9d47855b480c873af816c5 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 14:44:33 +0200 Subject: [PATCH 025/138] [NY-4699] Make Observable thread-safe --- Nynja/Statuses/Observable.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Nynja/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift index 5857ca9f6..96da0c491 100644 --- a/Nynja/Statuses/Observable.swift +++ b/Nynja/Statuses/Observable.swift @@ -16,6 +16,8 @@ final class Observable { var callback: (Key, Value) -> Void } + private let lock = NSLock() + private var allObservers: Observers = [] private var observers: [Key: Observers] = [:] @@ -23,30 +25,45 @@ final class Observable { func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { let handler = Handler(callback: callback) let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + allObservers.append(container) + + lock.unlock() } func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { let handler = Handler(callback: callback) let container = AnyWeakSubscriber(object: observer, handler: handler) + lock.lock() + var newObservers = observers[key] ?? [] newObservers.append(container) observers[key] = newObservers + + lock.unlock() } func removeObserver(_ observer: AnyObject) { + lock.lock() allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() } func removeObserver(_ observer: AnyObject, for key: Key) { + lock.lock() observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() } func notify(_ key: Key, with value: Value) { + lock.lock() for observer in allObservers { observer.handler.callback(key, value) } observers[key]?.forEach { $0.handler.callback(key, value) } + lock.unlock() } } -- GitLab From c599ead463c4fe474a42097864139e907ded2390 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 15:19:29 +0200 Subject: [PATCH 026/138] [NY-4699] Make handlers not static. --- .../Handlers/Base/HandlerFactory.swift | 15 ++++--- .../Services/Handlers/ContactHandler.swift | 10 +++-- .../Services/Handlers/MessageHandler.swift | 11 +++-- .../Services/Handlers/ProfileHandler.swift | 12 ++++-- .../ForwardSelectorInteractor.swift | 8 ++-- Nynja/AuthHandler.swift | 12 ++++-- Nynja/ChatService/SenderService.swift | 4 +- Nynja/ExtendedStarHandler.swift | 12 +++++- Nynja/HandlerFactory.swift | 34 +++++++-------- Nynja/JobHandler.swift | 14 +++++-- Nynja/LinkHandler.swift | 19 ++++++--- Nynja/MemberHandler.swift | 15 +++++-- .../Interactor/AddContactInteractor.swift | 2 +- .../AddContactByUsernameInteractor.swift | 2 +- .../AddContactViaPhoneInteractor.swift | 2 +- .../Login/Interactor/LoginInteractor.swift | 2 +- .../Interactor/VerifyNumberInteractor.swift | 2 +- .../Interactor/NewChannelInteractor.swift | 2 +- .../Interactor/ContactsInteractor.swift | 2 +- .../Interactor/EditUsernameInteractor.swift | 2 +- .../Interactor/InviteFriendsInteractor.swift | 2 +- .../Interactor/MessageInteractor.swift | 10 ++--- .../Interactor/ParticipantsInteractor.swift | 2 +- .../Interactor/QRCodeReaderInteractor.swift | 2 +- .../ScheduleMessageInteractor.swift | 2 +- .../Interactor/SecurityInteractor.swift | 4 +- .../HandleServices/ContactHandler.swift | 25 +++++++---- .../HandleServices/HistoryHandler.swift | 37 +++++++++------- .../HandleServices/MessageHandler.swift | 41 ++++++++++-------- .../HandleServices/ProfileHandler.swift | 34 ++++++++------- .../Services/HandleServices/RoomHandler.swift | 42 +++++++++++-------- .../HandleServices/RosterHandler.swift | 14 +++++-- .../HandleServices/SearchHandler.swift | 13 +++++- .../Services/HandleServices/StarHandler.swift | 13 +++++- .../HandleServices/TypingHandler.swift | 16 +++++-- Nynja/Services/MQTT/MQTTService.swift | 2 +- .../Services/Handlers/Base/BaseHandler.swift | 18 ++++---- Shared/Services/Handlers/ErrorsHandler.swift | 7 +++- Shared/Services/Handlers/IoHandler.swift | 16 ++++--- 39 files changed, 301 insertions(+), 181 deletions(-) diff --git a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift index 200518760..f4a7d5195 100644 --- a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift +++ b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift @@ -8,21 +8,20 @@ final class HandlerFactory { - static func handler(for handlerType: Handlers) -> BaseHandler.Type { + static func handler(for handlerType: Handlers) -> BaseHandler { switch handlerType { case .profile: - return ProfileHandler.self + return ProfileHandler.shared case .contact: - return ContactHandler.self + return ContactHandler.shared case .message: - return MessageHandler.self + return MessageHandler.shared case .io: - return IoHandler.self + return IoHandler.shared case .errors: - return ErrorsHandler.self + return ErrorsHandler.shared case .auth: - return AuthHandler.self + return AuthHandler.shared } } - } diff --git a/Nynja-Share/Services/Handlers/ContactHandler.swift b/Nynja-Share/Services/Handlers/ContactHandler.swift index a2041fabf..bdf7650af 100644 --- a/Nynja-Share/Services/Handlers/ContactHandler.swift +++ b/Nynja-Share/Services/Handlers/ContactHandler.swift @@ -16,11 +16,15 @@ extension ContactHandlerDelegate { } // TODO: need to think about this. It is share extension. -class ContactHandler: BaseHandler { +final class ContactHandler: BaseHandler { - static weak var delegate: ContactHandlerDelegate? + static let shared = ContactHandler() - static func executeHandle(data: BertTuple) { + private init() {} + + weak var delegate: ContactHandlerDelegate? + + func executeHandle(data: BertTuple) { guard let contact = get_Contact().parse(bert: data) as? Contact, contact.originalStatus != nil else { return } diff --git a/Nynja-Share/Services/Handlers/MessageHandler.swift b/Nynja-Share/Services/Handlers/MessageHandler.swift index b0daa836e..3c2b31d72 100644 --- a/Nynja-Share/Services/Handlers/MessageHandler.swift +++ b/Nynja-Share/Services/Handlers/MessageHandler.swift @@ -16,10 +16,15 @@ extension MessageHandlerDelegate { func getMessageSuccess(message: Message) {} } -class MessageHandler:BaseHandler { - static weak var delegate : MessageHandlerDelegate? +final class MessageHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = MessageHandler() + + private init() {} + + weak var delegate: MessageHandlerDelegate? + + func executeHandle(data: BertTuple) { if let message = get_Message().parse(bert: data) as? Message { delegate?.getMessageSuccess(message: message) } diff --git a/Nynja-Share/Services/Handlers/ProfileHandler.swift b/Nynja-Share/Services/Handlers/ProfileHandler.swift index 200095636..fbda22977 100644 --- a/Nynja-Share/Services/Handlers/ProfileHandler.swift +++ b/Nynja-Share/Services/Handlers/ProfileHandler.swift @@ -18,10 +18,15 @@ extension ProfileHandlerDelegate { func removeProfileSuccess() {} } -class ProfileHandler:BaseHandler { - static weak var delegate :ProfileHandlerDelegate? +final class ProfileHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = ProfileHandler() + + private init() {} + + weak var delegate: ProfileHandlerDelegate? + + func executeHandle(data: BertTuple) { if let profile = get_Profile().parse(bert: data) as? Profile { if let status = profile.status?.string { switch status { @@ -38,5 +43,4 @@ class ProfileHandler:BaseHandler { } } } - } diff --git a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift index 384c34370..a3181e534 100644 --- a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift +++ b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift @@ -66,10 +66,10 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P dependencies: .init(mqttService: MQTTService.sharedInstance, storageService: StorageService.sharedInstance) ) - ProfileHandler.delegate = self - MessageHandler.delegate = self - ContactHandler.delegate = self - IoHandler.delegate = self + ProfileHandler.shared.delegate = self + MessageHandler.shared.delegate = self + ContactHandler.shared.delegate = self + IoHandler.shared.delegate = self setupAmazon() notifyHostApplication() } diff --git a/Nynja/AuthHandler.swift b/Nynja/AuthHandler.swift index c5ec8f5f2..ca8b88591 100644 --- a/Nynja/AuthHandler.swift +++ b/Nynja/AuthHandler.swift @@ -18,11 +18,15 @@ extension AuthHandlerDelegate { func processDelete(auth: Auth) {} } -class AuthHandler: BaseHandler { +final class AuthHandler: BaseHandler { - static weak var delegate: AuthHandlerDelegate? + static let shared = AuthHandler() - static func executeHandle(data: BertTuple) { + private init() {} + + weak var delegate: AuthHandlerDelegate? + + func executeHandle(data: BertTuple) { guard let auth = get_Auth().parse(bert: data) as? Auth else {return} guard let type = StringAtom.string(auth.type) else {return} if type == "deleted" { @@ -36,7 +40,7 @@ class AuthHandler: BaseHandler { } } - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { let auths = data.elements.compactMap { get_Auth().parse(bert: $0) as? Auth } delegate?.processGetAll(auths: auths) } diff --git a/Nynja/ChatService/SenderService.swift b/Nynja/ChatService/SenderService.swift index c82826118..7c1d01954 100644 --- a/Nynja/ChatService/SenderService.swift +++ b/Nynja/ChatService/SenderService.swift @@ -16,7 +16,7 @@ final class SenderService: InitializeInjectable { init(dependencies: Dependencies) { mqttService = dependencies.mqttService - IoHandler.delegate = self + IoHandler.shared.delegate = self } @@ -35,7 +35,7 @@ final class SenderService: InitializeInjectable { } func updateSubscribes() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } } diff --git a/Nynja/ExtendedStarHandler.swift b/Nynja/ExtendedStarHandler.swift index 0322062f2..f0dc2beb4 100644 --- a/Nynja/ExtendedStarHandler.swift +++ b/Nynja/ExtendedStarHandler.swift @@ -10,7 +10,16 @@ import Foundation final class ExtendedStarHandler: BaseHandler { - static func executeHandle(data: BertList) { + // MARK: - Singleton + + static let shared = ExtendedStarHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertList) { let extendedStars = data.elements.compactMap { get_ExtendedStar().parse(bert: $0) as? ExtendedStar } let stars = extendedStars.compactMap { extendedStar -> DBStar? in @@ -24,5 +33,4 @@ final class ExtendedStarHandler: BaseHandler { try? StorageService.sharedInstance.perform(action: .save, with: stars) } - } diff --git a/Nynja/HandlerFactory.swift b/Nynja/HandlerFactory.swift index c94fbcbee..4f855ddb8 100644 --- a/Nynja/HandlerFactory.swift +++ b/Nynja/HandlerFactory.swift @@ -8,40 +8,40 @@ final class HandlerFactory { - static func handler(for handlerType: Handlers) -> BaseHandler.Type { + static func handler(for handlerType: Handlers) -> BaseHandler { switch handlerType { case .io: - return IoHandler.self + return IoHandler.shared case .profile: - return ProfileHandler.self + return ProfileHandler.shared case .roster: - return RosterHandler.self + return RosterHandler.shared case .contact: - return ContactHandler.self + return ContactHandler.shared case .history: - return HistoryHandler.self + return HistoryHandler.shared case .message: - return MessageHandler.self + return MessageHandler.shared case .search: - return SearchHandler.self + return SearchHandler.shared case .room: - return RoomHandler.self + return RoomHandler.shared case .member: - return MemberHandler.self + return MemberHandler.shared case .typing: - return TypingHandler.self + return TypingHandler.shared case .star: - return StarHandler.self + return StarHandler.shared case .job: - return JobHandler.self + return JobHandler.shared case .extendedStar: - return ExtendedStarHandler.self + return ExtendedStarHandler.shared case .auth: - return AuthHandler.self + return AuthHandler.shared case .link: - return LinkHandler.self + return LinkHandler.shared case .errors: - return ErrorsHandler.self + return ErrorsHandler.shared } } diff --git a/Nynja/JobHandler.swift b/Nynja/JobHandler.swift index cbf72bb79..c9b40eaf4 100644 --- a/Nynja/JobHandler.swift +++ b/Nynja/JobHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class JobHandler: BaseHandler { +final class JobHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = JobHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let job = get_Job().parse(bert: data) as? Job, let status = StringAtom.string(job.status) else { return @@ -23,6 +32,5 @@ class JobHandler: BaseHandler { break } } - } diff --git a/Nynja/LinkHandler.swift b/Nynja/LinkHandler.swift index feebe3ae1..8ca0a5dbc 100644 --- a/Nynja/LinkHandler.swift +++ b/Nynja/LinkHandler.swift @@ -11,12 +11,20 @@ protocol LinkHandlerDelegate: class { func linkIsAvailable(_ link: Link) } - final class LinkHandler: BaseHandler { - static weak var delegate: LinkHandlerDelegate? + // MARK: - Singleton + + static let shared = LinkHandler() + + private init() {} + - static func executeHandle(data: BertTuple, codes: StatusCodes) { + // MARK: - Handler + + weak var delegate: LinkHandlerDelegate? + + func executeHandle(data: BertTuple, codes: StatusCodes) { guard let link = get_Link().parse(bert: data) as? Link else { return } @@ -28,7 +36,7 @@ final class LinkHandler: BaseHandler { } } - private static func handle(link: Link) { + private func handle(link: Link) { guard let status = link.originalStatus else { return } @@ -44,12 +52,11 @@ final class LinkHandler: BaseHandler { } } - private static func handle(codes: StatusCodes, link: Link) { + private func handle(codes: StatusCodes, link: Link) { let statusCodeManager = StatusCodeManager.shared codes .filter { StatusCode.linkCodes.contains($0) } .forEach { statusCodeManager.notify(model: link, code: $0) } } - } diff --git a/Nynja/MemberHandler.swift b/Nynja/MemberHandler.swift index 361d24a8b..f81c1b922 100644 --- a/Nynja/MemberHandler.swift +++ b/Nynja/MemberHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class MemberHandler: BaseHandler { +final class MemberHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = MemberHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let member = get_Member().parse(bert: data) as? Member, let status = (member.status as? StringAtom)?.string else { return @@ -24,8 +33,6 @@ class MemberHandler: BaseHandler { default: return } - } - } diff --git a/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift b/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift index 170fa07f9..f21673f79 100644 --- a/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift +++ b/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift @@ -11,7 +11,7 @@ class AddContactInteractor: AddContactInteractorInputProtocol, IoHandlerDelegate weak var presenter: AddContactInteractorOutputProtocol! init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } func addContact(contact: Contact) { diff --git a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift index 21d47a38e..2639da682 100644 --- a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift +++ b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift @@ -20,7 +20,7 @@ final class AddContactByUsernameInteractor: AddContactByUsernameInteractorInputP init() { mqttService = MQTTService.sharedInstance mqttService.addSubscriber(self) - IoHandler.delegate = self + IoHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift index b3e89801c..4a9c81ff8 100644 --- a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift +++ b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift @@ -20,7 +20,7 @@ final class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProto // MARK: - Init init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift index c0e1a555b..3c107f5cd 100644 --- a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift +++ b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift @@ -22,7 +22,7 @@ class LoginInteractor: BaseInteractor, LoginInteractorInputProtocol, IoHandlerDe // MARK: - Configure func configure() { - IoHandler.delegate = self + IoHandler.shared.delegate = self mqttService.addSubscriber(self) mqttService.tryReconnect() } diff --git a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift index c525eddce..a3d25180d 100644 --- a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift +++ b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift @@ -37,7 +37,7 @@ final class VerifyNumberInteractor: BaseInteractor, VerifyNumberInteractorInputP // MARK: - Config func configure() { - IoHandler.delegate = self + IoHandler.shared.delegate = self mqttService.addSubscriber(self) setupObservers() } diff --git a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift index 943525191..c00bf609c 100644 --- a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift +++ b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift @@ -33,7 +33,7 @@ final class NewChannelInteractor: BaseInteractor, NewChannelInteractorInputProto override init() { super.init() - LinkHandler.delegate = self + LinkHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift index 5b0a313da..f6839b618 100644 --- a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift +++ b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift @@ -19,7 +19,7 @@ class ContactsInteractor: BaseInteractor, ContactsInteractorInputProtocol, IoHan init(mode: ContactViewMode) { contactViewMode = mode super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } //MARK: - BaseInteractor diff --git a/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift b/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift index cb9d7bd25..f7319c2cc 100644 --- a/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift +++ b/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift @@ -25,7 +25,7 @@ class EditUsernameInteractor: BaseInteractor, EditUsernameInteractorInputProtoco override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } func save(username: String) { diff --git a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift index c2e141fa1..b7d58124b 100644 --- a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift +++ b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift @@ -24,7 +24,7 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } //MARK: - BaseInteractor diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index dcd8bbb7e..7f65539b7 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -185,8 +185,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H mqttService.addSubscriber(self) ConnectionService.shared.addSubscriber(self) - MessageHandler.addSubscriber(self) - HistoryHandler.addSubscriber(self) + MessageHandler.shared.addSubscriber(self) + HistoryHandler.shared.addSubscriber(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() @@ -196,8 +196,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) - MessageHandler.removeSubscriber(self) - HistoryHandler.removeSubscriber(self) + MessageHandler.shared.removeSubscriber(self) + HistoryHandler.shared.removeSubscriber(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -222,7 +222,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H processingManager.delegate = self - TypingHandler.delegate = self + TypingHandler.shared.delegate = self isAfterConnectionAppeared = false diff --git a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift index babd3b0c4..0097dde47 100644 --- a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift +++ b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift @@ -17,7 +17,7 @@ class ParticipantsInteractor: BaseInteractor, ParticipantsInteractorInputProtoco override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } diff --git a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift index 60cf171cd..b5fb466d4 100644 --- a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift +++ b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift @@ -14,7 +14,7 @@ class QRCodeReaderInteractor: QRCodeReaderInteractorInputProtocol, IoHandlerDele var status = "" init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } func getContactByPhone(number: String) { diff --git a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift index dec068752..788fc8f48 100644 --- a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift +++ b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift @@ -58,7 +58,7 @@ final class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractor required init(mode: ScheduledMessageMode) { self.mode = mode super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } func fetchInfo() { diff --git a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift index e497dfc85..f7c596eec 100644 --- a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift +++ b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift @@ -15,8 +15,8 @@ class SecurityInteractor: SecurityInteractorInputProtocol, AuthHandlerDelegate, private var timer : Timer? init() { - AuthHandler.delegate = self - IoHandler.delegate = self + AuthHandler.shared.delegate = self + IoHandler.shared.delegate = self } //MARK: - SecurityInteractorInputProtocol diff --git a/Nynja/Services/HandleServices/ContactHandler.swift b/Nynja/Services/HandleServices/ContactHandler.swift index 8a6aa1cfe..74b1f09b4 100644 --- a/Nynja/Services/HandleServices/ContactHandler.swift +++ b/Nynja/Services/HandleServices/ContactHandler.swift @@ -8,18 +8,25 @@ import Foundation -class ContactHandler: BaseHandler { +final class ContactHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = ContactHandler() + + private init() {} + // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return .sharedInstance } // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let contact = get_Contact().parse(bert: data) as? Contact, let status = contact.originalStatus else { return @@ -46,11 +53,11 @@ class ContactHandler: BaseHandler { // MARK: - Statuses - private static func handleDeleted(_ contact: Contact) { + private func handleDeleted(_ contact: Contact) { try? storageService.perform(action: .delete, with: contact) } - private static func handleInternal(_ contact: Contact) { + private func handleInternal(_ contact: Contact) { var columns: Set = [.presence] if contact.updated != 0 { columns.insert(.update) @@ -58,11 +65,11 @@ class ContactHandler: BaseHandler { ContactDAO.updateColumns(columns, contact: contact) } - private static func handleLastMessage(_ contact: Contact) { + private func handleLastMessage(_ contact: Contact) { ContactDAO.updateColumns([.unread], contact: contact) } - private static func handleFriend(_ contact: Contact, data: BertTuple) { + private func handleFriend(_ contact: Contact, data: BertTuple) { guard let phoneId = contact.phone_id, let prevContact = ContactDAO.findContactBy(phoneId: phoneId), @@ -87,7 +94,7 @@ class ContactHandler: BaseHandler { } } - private static func handleAuthorization(_ contact: Contact, data: BertTuple) { + private func handleAuthorization(_ contact: Contact, data: BertTuple) { do { try storageService.perform(action: .save, with: contact) NotificationManager.shared.handle(bert: data, type: .request) @@ -96,7 +103,7 @@ class ContactHandler: BaseHandler { } } - private static func handleBan(_ contact: Contact) { + private func handleBan(_ contact: Contact) { guard let phoneId = contact.phone_id, let prevContact = ContactDAO.findContactBy(phoneId: phoneId) else { diff --git a/Nynja/Services/HandleServices/HistoryHandler.swift b/Nynja/Services/HandleServices/HistoryHandler.swift index 4b36b4288..f46014cf4 100644 --- a/Nynja/Services/HandleServices/HistoryHandler.swift +++ b/Nynja/Services/HandleServices/HistoryHandler.swift @@ -22,19 +22,26 @@ extension HistoryHandlerDelegate { final class HistoryHandler: BaseHandler { + // MARK: - Singleton + + static let shared = HistoryHandler() + + private init() {} + + // MARK: - Subscribers - private static let subscribersLock = NSLock() + private let subscribersLock = NSLock() - private static var subscribers = [WeakRef]() + private var subscribers = [WeakRef]() - private static func notify(block: (HistoryHandlerDelegate) -> Void) { + private func notify(block: (HistoryHandlerDelegate) -> Void) { subscribersLock.lock() subscribers.forEach { ($0.value as? HistoryHandlerDelegate).map { block($0) } } subscribersLock.unlock() } - static func addSubscriber(_ subscriber: HistoryHandlerDelegate) { + func addSubscriber(_ subscriber: HistoryHandlerDelegate) { subscribersLock.lock() defer { subscribersLock.unlock() } @@ -45,7 +52,7 @@ final class HistoryHandler: BaseHandler { subscribers.append(ref) } - static func removeSubscriber(_ subscriber: HistoryHandlerDelegate) { + func removeSubscriber(_ subscriber: HistoryHandlerDelegate) { subscribersLock.lock() subscribers = subscribers.filter { $0.value != nil && $0.value !== subscriber } subscribersLock.unlock() @@ -54,22 +61,22 @@ final class HistoryHandler: BaseHandler { // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return StorageService.sharedInstance } - static var messageEditService: MessageEditServiceProtocol { + var messageEditService: MessageEditServiceProtocol { return MessageEditService(dependencies: .init(storageService: storageService)) } - static let stickersDownloadingService: StickersDownloadingService = { + let stickersDownloadingService: StickersDownloadingService = { return StickersDownloadingService() }() // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let history = get_History().parse(bert: data) as? History else { return } @@ -102,7 +109,7 @@ final class HistoryHandler: BaseHandler { // MARK: -- Messages /// The first message is new, the last is old. - private static func updateMessageHistory(_ messages: [Message]) { + private func updateMessageHistory(_ messages: [Message]) { var stackForSave = [Message](reserveCapacity: messages.count) var stackForDelete = [Message]() @@ -178,7 +185,7 @@ final class HistoryHandler: BaseHandler { systemClearMessage: systemClearMessage) } - private static func saveMessageHistory(stackForSave: [Message], + private func saveMessageHistory(stackForSave: [Message], stackForDelete: [Message], repliedMessages: [MessageServerId: [Message]], visibleRepliedMessages: Set, @@ -207,7 +214,7 @@ final class HistoryHandler: BaseHandler { try? MessageActionDAO.delete(deletedActions) } - private static func fetchType(from feed: AnyObject?) -> FetchType? { + private func fetchType(from feed: AnyObject?) -> FetchType? { switch feed { case let feed as muc: guard let name = feed.name else { @@ -225,14 +232,14 @@ final class HistoryHandler: BaseHandler { } /// Mark messages with 'serverId' <= id as trusted - private static func markHistoryAsTrusted(before id: MessageServerId, in fetchType: FetchType) { + private func markHistoryAsTrusted(before id: MessageServerId, in fetchType: FetchType) { try? MessageDAO.trustMessages(before: id, in: fetchType) } // MARK: -- Jobs - private static func updateJobsHistory(_ jobs: [Job]) { + private func updateJobsHistory(_ jobs: [Job]) { let stackForSave = jobs.filter { StringAtom.string($0.status) == "pending" } let deleteStatuses = ["delete", "complete"] @@ -247,7 +254,7 @@ final class HistoryHandler: BaseHandler { // MARK: -- Stickers - private static func updateStickerPacks(_ stickerPacks: [StickerPack]) { + private func updateStickerPacks(_ stickerPacks: [StickerPack]) { for package in stickerPacks { try? storageService.perform(action: .save, with: package) } diff --git a/Nynja/Services/HandleServices/MessageHandler.swift b/Nynja/Services/HandleServices/MessageHandler.swift index e9bd99580..fddb6043c 100644 --- a/Nynja/Services/HandleServices/MessageHandler.swift +++ b/Nynja/Services/HandleServices/MessageHandler.swift @@ -10,26 +10,33 @@ import Foundation final class MessageHandler: BaseHandler { + // MARK: - Singleton + + static let shared = MessageHandler() + + private init() {} + + // MARK: - Dependencies - private static var storageService: StorageService { + private var storageService: StorageService { return StorageService.sharedInstance } - private static var notificationManager: NotificationManager { + private var notificationManager: NotificationManager { return NotificationManager.shared } - private static var systemSoundManager: SystemSoundManager { + private var systemSoundManager: SystemSoundManager { return SystemSoundManager.sharedInstance } // MARK: - Subscribers - static var subscribers: [MessageHandlerSubscriberReference] = [] + private var subscribers: [MessageHandlerSubscriberReference] = [] - static func addSubscriber(_ subscriber: MessageHandlerSubscriber) { + func addSubscriber(_ subscriber: MessageHandlerSubscriber) { guard !subscribers.contains(where: { $0.subscriber === subscriber }) else { return } @@ -37,14 +44,14 @@ final class MessageHandler: BaseHandler { subscribers.append(ref) } - static func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { + func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { subscribers = subscribers.filter { $0.subscriber != nil && $0.subscriber !== subscriber } } // MARK: - Execute - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let message = get_Message().parse(bert: data) as? Message else { return } let types = message.types @@ -66,11 +73,11 @@ final class MessageHandler: BaseHandler { } } - private static func clearHistory(_ message: Message) { + private func clearHistory(_ message: Message) { ChatService.clearHistory(message) } - private static func updateReader(from message: Message) { + private func updateReader(from message: Message) { let shouldUpdateOwnReader = self.shouldUpdateOwnReader(from: message) let shouldUpdateOtherReader = !shouldUpdateOwnReader || message.isInOwnChat @@ -83,11 +90,11 @@ final class MessageHandler: BaseHandler { } } - private static func shouldUpdateOwnReader(from message: Message) -> Bool { + private func shouldUpdateOwnReader(from message: Message) -> Bool { return message.isOwn } - private static func deleteMessage(_ message: Message) { + private func deleteMessage(_ message: Message) { do { try save(message) ChatService.removeMessage(message) @@ -96,7 +103,7 @@ final class MessageHandler: BaseHandler { } } - private static func editMessage(_ message: Message) { + private func editMessage(_ message: Message) { do { try save(message) try ChatService.editMessage(message) @@ -105,7 +112,7 @@ final class MessageHandler: BaseHandler { } } - private static func updateMessage(_ message: Message) { + private func updateMessage(_ message: Message) { do { try save(message) try ChatService.updateMessage(message) @@ -115,7 +122,7 @@ final class MessageHandler: BaseHandler { } } - private static func saveMessage(_ message: Message, data: BertTuple) { + private func saveMessage(_ message: Message, data: BertTuple) { guard !shouldSkipMessage(message) else { return } @@ -159,7 +166,7 @@ final class MessageHandler: BaseHandler { } } - private static func shouldSkipMessage(_ message: Message) -> Bool { + private func shouldSkipMessage(_ message: Message) -> Bool { if let desc = message.files?.first, desc.mime == SendMessageType.audioCall.rawValue, desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, @@ -172,7 +179,7 @@ final class MessageHandler: BaseHandler { return false } - private static func save(_ message: Message) throws { + private func save(_ message: Message) throws { if let repliedMessage = message.repliedMessage { repliedMessage.localStatus = try MessageDAO.localStatusForRepliedMessage(repliedMessage) } @@ -182,7 +189,7 @@ final class MessageHandler: BaseHandler { /// Play sound for incoming messages if chat isn't muted. /// Play outcoming message only if chat screen is open. - private static func playSoundIfNeeded(for message: Message) { + private func playSoundIfNeeded(for message: Message) { guard message.isDelivered, let currentPhoneId = storageService.phoneId else { return } diff --git a/Nynja/Services/HandleServices/ProfileHandler.swift b/Nynja/Services/HandleServices/ProfileHandler.swift index b1b89d482..af9514889 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -6,34 +6,40 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class ProfileHandler: BaseHandler { +final class ProfileHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = ProfileHandler() + + private init() {} // MARK: - Dependencies - static var mqttService: MQTTService { + var mqttService: MQTTService { return MQTTService.sharedInstance } - static var historyFactory: HistoryRequestModelFactoryProtocol { + var historyFactory: HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } - static var storageService: StorageService { + var storageService: StorageService { return StorageService.sharedInstance } - static var messageBackgroundTaskHandler: BackgroundTaskHandler { + var messageBackgroundTaskHandler: BackgroundTaskHandler { return MessageBackgroundTaskHandler() } - static var alertManager: AlertManager { + var alertManager: AlertManager { return AlertManager.sharedInstance } // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let profile = get_Profile().parse(bert: data) as? Profile, let status = profile.status?.string else { return @@ -60,7 +66,7 @@ class ProfileHandler: BaseHandler { // MARK: Get & Init - private static func handleGetInit(_ profile: Profile) { + private func handleGetInit(_ profile: Profile) { do { guard let roster = (profile.rosters as? [Roster])?.first else { return @@ -82,7 +88,7 @@ class ProfileHandler: BaseHandler { } catch { } } - private static func prepareForReceived(_ newRoster: Roster) { + private func prepareForReceived(_ newRoster: Roster) { let currentRoster = RosterDAO.currentRoster func shouldSave(_ message: Message?) -> Bool { @@ -130,11 +136,11 @@ class ProfileHandler: BaseHandler { } } - private static func configureTestFairy(with roster: Roster) { + private func configureTestFairy(with roster: Roster) { TestFairy.setUserId("\(roster.myContact?.phone_id ?? "")_\(roster.myContact?.fullName ?? "")") } - private static func configureNynjaCommunicatorService(_ profile: Profile) { + private func configureNynjaCommunicatorService(_ profile: Profile) { guard let rosterId = (profile.rosters?.first as? Roster)?.myContact?.phone_id else { return } @@ -142,7 +148,7 @@ class ProfileHandler: BaseHandler { NynjaCommunicatorService.sharedInstance.initialize() } - private static func requestJobs(with phoneId: String) { + private func requestJobs(with phoneId: String) { do { let historyModel = try historyFactory.makeAllJobsRequest(rosterId: phoneId) mqttService.sendHistoryRequest(with: historyModel) @@ -151,7 +157,7 @@ class ProfileHandler: BaseHandler { } } - private static func requestStickerPacks(with rosterId: String) { + private func requestStickerPacks(with rosterId: String) { do { let historyModel = try historyFactory.makeStickerPackagesRequest(rosterId: rosterId) mqttService.sendHistoryRequest(with: historyModel) @@ -163,7 +169,7 @@ class ProfileHandler: BaseHandler { // MARK: Remove - private static func handleRemove(_ profile: Profile) { + private func handleRemove(_ profile: Profile) { try? storageService.perform(action: .delete, with: profile) storageService.phone = nil alertManager.showAlertOk(message: String.localizable.authAttemptsRemoved) diff --git a/Nynja/Services/HandleServices/RoomHandler.swift b/Nynja/Services/HandleServices/RoomHandler.swift index 6193bb931..a1ca2f862 100644 --- a/Nynja/Services/HandleServices/RoomHandler.swift +++ b/Nynja/Services/HandleServices/RoomHandler.swift @@ -6,22 +6,29 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class RoomHandler: BaseHandler { +final class RoomHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = RoomHandler() + + private init() {} + // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return .sharedInstance } - static var notificationManager: NotificationManager { + var notificationManager: NotificationManager { return .shared } // MARK: - Handler - static func executeHandle(data: BertTuple, codes: StatusCodes) { + func executeHandle(data: BertTuple, codes: StatusCodes) { guard let room = get_Room().parse(bert: data) as? Room else { return } @@ -32,7 +39,7 @@ class RoomHandler: BaseHandler { } } - private static func handle(room: Room, data: BertTuple) { + private func handle(room: Room, data: BertTuple) { guard let status = room.originalStatus else { return } @@ -62,7 +69,7 @@ class RoomHandler: BaseHandler { } } - private static func handle(codes: StatusCodes, room: Room) { + private func handle(codes: StatusCodes, room: Room) { let statusCodeManager = StatusCodeManager.shared codes.forEach { statusCodeManager.notify(model: room, code: $0) } } @@ -71,7 +78,7 @@ class RoomHandler: BaseHandler { // MARK: - Statuses // MARK: - Add Member - private static func handleAddMember(_ room: Room) { + private func handleAddMember(_ room: Room) { trustLastMessageIfNeeded(for: room) guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { @@ -91,7 +98,7 @@ class RoomHandler: BaseHandler { // MARK: - Add Member Channel - private static func handleChannelAddMember(_ room: Room, oldRoom: Room) { + private func handleChannelAddMember(_ room: Room, oldRoom: Room) { if let features = room.settings { oldRoom.settings = features } @@ -102,7 +109,7 @@ class RoomHandler: BaseHandler { // MARK: - Add Member Room - private static func handleGroupAddMember(_ room: Room, oldRoom: Room) { + private func handleGroupAddMember(_ room: Room, oldRoom: Room) { addNotExistedMembers(from: room, to: oldRoom) filterAdmins(using: room, in: oldRoom) @@ -117,7 +124,7 @@ class RoomHandler: BaseHandler { try? storageService.perform(action: .save, with: oldRoom) } - private static func addNotExistedMembers(from room: Room, to oldRoom: Room) { + private func addNotExistedMembers(from room: Room, to oldRoom: Room) { var notExistedMembers: [Member] = [] room.members?.forEach { member in @@ -133,7 +140,7 @@ class RoomHandler: BaseHandler { oldRoom.members = members } - private static func filterAdmins(using room: Room, in oldRoom: Room) { + private func filterAdmins(using room: Room, in oldRoom: Room) { oldRoom.admins = oldRoom.admins?.filter { admin in guard let members = room.members else { return true @@ -142,7 +149,7 @@ class RoomHandler: BaseHandler { } } - private static func addNotExistedAdmins(from room: Room, to oldRoom: Room) { + private func addNotExistedAdmins(from room: Room, to oldRoom: Room) { var notExistsAdmins: [Member] = [] room.admins?.forEach { member in @@ -158,7 +165,7 @@ class RoomHandler: BaseHandler { oldRoom.admins = admins } - private static func filterMembers(using room: Room, in oldRoom: Room) { + private func filterMembers(using room: Room, in oldRoom: Room) { oldRoom.members = oldRoom.members?.filter { member in guard let admins = room.admins else { return true @@ -170,7 +177,7 @@ class RoomHandler: BaseHandler { // MARK: - Remove Member - private static func handleRemoveMember(_ room: Room) { + private func handleRemoveMember(_ room: Room) { trustLastMessageIfNeeded(for: room) if let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) { @@ -187,7 +194,7 @@ class RoomHandler: BaseHandler { // MARK: - Leave - private static func handleLeave(_ room: Room) { + private func handleLeave(_ room: Room) { trustLastMessageIfNeeded(for: room) guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { @@ -213,7 +220,7 @@ class RoomHandler: BaseHandler { // MARK: - Last Message - private static func trustLastMessageIfNeeded(for room: Room) { + private func trustLastMessageIfNeeded(for room: Room) { guard let lastMessage = room.last_msg else { return } @@ -223,10 +230,9 @@ class RoomHandler: BaseHandler { // MARK: - Update - private static func updateReadersUnreadAndStatus(from room: Room, oldRoom: Room) { + private func updateReadersUnreadAndStatus(from room: Room, oldRoom: Room) { oldRoom.unread = room.unread oldRoom.readers = room.readers oldRoom.status = room.status } - } diff --git a/Nynja/Services/HandleServices/RosterHandler.swift b/Nynja/Services/HandleServices/RosterHandler.swift index 419d89e79..bab717eca 100644 --- a/Nynja/Services/HandleServices/RosterHandler.swift +++ b/Nynja/Services/HandleServices/RosterHandler.swift @@ -8,9 +8,18 @@ import Foundation -class RosterHandler: BaseHandler { +final class RosterHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = RosterHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let roster = get_Roster().parse(bert: data) as? Roster, let status = (roster.status as? StringAtom)?.string else { return @@ -24,5 +33,4 @@ class RosterHandler: BaseHandler { try? StorageService.sharedInstance.perform(action: .save, with: roster) } } - } diff --git a/Nynja/Services/HandleServices/SearchHandler.swift b/Nynja/Services/HandleServices/SearchHandler.swift index 9d8570b15..be743eb61 100644 --- a/Nynja/Services/HandleServices/SearchHandler.swift +++ b/Nynja/Services/HandleServices/SearchHandler.swift @@ -8,9 +8,18 @@ import Foundation -class SearchHandler: BaseHandler { +final class SearchHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = SearchHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let search = get_Search().parse(bert: data) as? Search, let ref = search.ref, let refType = SearchModelReference(rawValue: ref) else { diff --git a/Nynja/Services/HandleServices/StarHandler.swift b/Nynja/Services/HandleServices/StarHandler.swift index 416651962..c555a00d5 100644 --- a/Nynja/Services/HandleServices/StarHandler.swift +++ b/Nynja/Services/HandleServices/StarHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class StarHandler: BaseHandler { +final class StarHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = StarHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let star = get_Star().parse(bert: data) as? Star, let status = star.starStatus else { return } diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index e9023cba8..e14773beb 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -12,14 +12,22 @@ protocol TypingHandlerDelegate: class { func getTyping(typing: Typing) } -class TypingHandler: BaseHandler { +final class TypingHandler: BaseHandler { - static weak var delegate: TypingHandlerDelegate? + // MARK: - Singleton - static func executeHandle(data: BertTuple) { + static let shared = TypingHandler() + + private init() {} + + + // MARK: - Handler + + weak var delegate: TypingHandlerDelegate? + + func executeHandle(data: BertTuple) { if let typing = get_Typing().parse(bert: data) as? Typing { delegate?.getTyping(typing: typing) } } - } diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index 902a3814a..60445c01d 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -168,7 +168,7 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ConnectionServiceDelegate self.state = .notAuthenticated(isLoggedOutFromServer: true) - IoHandler.delegate?.sessionNotFound() + IoHandler.shared.delegate?.sessionNotFound() notifySubscribers { (delegate) in delegate.mqttServiceDidReceiveAuthenticationFailure(self) } diff --git a/Shared/Services/Handlers/Base/BaseHandler.swift b/Shared/Services/Handlers/Base/BaseHandler.swift index cdf0cebee..3021b29cd 100644 --- a/Shared/Services/Handlers/Base/BaseHandler.swift +++ b/Shared/Services/Handlers/Base/BaseHandler.swift @@ -6,23 +6,23 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol BaseHandler { - static func executeHandle(data: BertTuple) - static func executeHandle(data: BertList) +protocol BaseHandler: class { + func executeHandle(data: BertTuple) + func executeHandle(data: BertList) - static func executeHandle(data: BertTuple, codes: StatusCodes) - static func executeHandle(data: BertList, codes: StatusCodes) + func executeHandle(data: BertTuple, codes: StatusCodes) + func executeHandle(data: BertList, codes: StatusCodes) } extension BaseHandler { - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { executeHandle(data: data, codes: []) } - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { executeHandle(data: data, codes: []) } - static func executeHandle(data: BertTuple, codes: StatusCodes) {} - static func executeHandle(data: BertList, codes: StatusCodes) {} + func executeHandle(data: BertTuple, codes: StatusCodes) {} + func executeHandle(data: BertList, codes: StatusCodes) {} } diff --git a/Shared/Services/Handlers/ErrorsHandler.swift b/Shared/Services/Handlers/ErrorsHandler.swift index d284a35cb..d85e43db2 100644 --- a/Shared/Services/Handlers/ErrorsHandler.swift +++ b/Shared/Services/Handlers/ErrorsHandler.swift @@ -8,7 +8,11 @@ final class ErrorsHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = ErrorsHandler() + + private init() {} + + func executeHandle(data: BertTuple) { guard let errors = get_errors().parse(bert: data) as? errors, let dataTuple = data.elements.last as? BertTuple, let handlerKind = dataTuple.handlerKind else { @@ -20,5 +24,4 @@ final class ErrorsHandler: BaseHandler { let handler = HandlerFactory.handler(for: handlerKind) handler.executeHandle(data: dataTuple, codes: Set(codes)) } - } diff --git a/Shared/Services/Handlers/IoHandler.swift b/Shared/Services/Handlers/IoHandler.swift index f6fd64282..478cce95e 100644 --- a/Shared/Services/Handlers/IoHandler.swift +++ b/Shared/Services/Handlers/IoHandler.swift @@ -66,23 +66,27 @@ extension IoHandlerDelegate { } -class IoHandler:BaseHandler { +final class IoHandler: BaseHandler { - static weak var delegate: IoHandlerDelegate? + static let shared = IoHandler() - static var storageService: StorageService { + private init() {} + + weak var delegate: IoHandlerDelegate? + + var storageService: StorageService { return .sharedInstance } - static var mqttService: MQTTService { + var mqttService: MQTTService { return .sharedInstance } - static var keychainService: KeychainService { + var keychainService: KeychainService { return .standard } - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { if let IO = get_io().parse(bert: data) as? io { var code: String? = nil if let value = ((IO.code as? ok)?.code as? StringAtom)?.string { -- GitLab From 9787372cd62cffd36e750f73e17399afe66dc1c4 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 15:36:55 +0200 Subject: [PATCH 027/138] [NY-4699] Rename Observable to KeyedObservable --- Nynja.xcodeproj/project.pbxproj | 24 ++++--- Nynja/Observable/KeyedObservable.swift | 69 +++++++++++++++++++ .../KeyedObservableContainer.swift} | 8 +-- Nynja/Statuses/AccountStatusProvider.swift | 4 +- Nynja/Statuses/Observable.swift | 4 +- Nynja/Statuses/TypingStatusProvider.swift | 4 +- 6 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 Nynja/Observable/KeyedObservable.swift rename Nynja/{Statuses/ObservableContainer.swift => Observable/KeyedObservableContainer.swift} (87%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 6dca8052b..5bc4514ed 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1107,8 +1107,8 @@ 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */; }; - 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; - 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; + 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; + 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3305,8 +3305,8 @@ 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatusProvider.swift; sourceTree = ""; }; - 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; - 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; + 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; @@ -5983,6 +5983,7 @@ 8509AC61206A54420089089B /* ResponseResult.swift */, B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, + 8548341921874434002064E1 /* Observable */, 85EB37F9218365A6003A2D6F /* Statuses */, ); name = Services; @@ -8375,6 +8376,15 @@ path = Documents; sourceTree = ""; }; + 8548341921874434002064E1 /* Observable */ = { + isa = PBXGroup; + children = ( + 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, + 85EB37FC21837253003A2D6F /* KeyedObservable.swift */, + ); + path = Observable; + sourceTree = ""; + }; 854A4B392080E5D500759152 /* TableView */ = { isa = PBXGroup; children = ( @@ -9093,8 +9103,6 @@ 85EB37FE21837304003A2D6F /* AccountStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, - 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, - 85EB37FC21837253003A2D6F /* Observable.swift */, ); path = Statuses; sourceTree = ""; @@ -15170,7 +15178,7 @@ 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, - 85EB37FD21837253003A2D6F /* Observable.swift in Sources */, + 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15334,7 +15342,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, - 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */, + 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift new file mode 100644 index 000000000..00924123e --- /dev/null +++ b/Nynja/Observable/KeyedObservable.swift @@ -0,0 +1,69 @@ +// +// KeyedObservable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class KeyedObservable { + + private typealias Observers = [AnyWeakSubscriber] + + private struct Handler { + var callback: (Key, Value) -> Void + } + + private let lock = NSLock() + + private var allObservers: Observers = [] + + private var observers: [Key: Observers] = [:] + + func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + allObservers.append(container) + + lock.unlock() + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + var newObservers = observers[key] ?? [] + newObservers.append(container) + observers[key] = newObservers + + lock.unlock() + } + + func removeObserver(_ observer: AnyObject) { + lock.lock() + allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + lock.lock() + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() + } + + func notify(_ key: Key, with value: Value) { + lock.lock() + for observer in allObservers { + observer.handler.callback(key, value) + } + observers[key]?.forEach { $0.handler.callback(key, value) } + lock.unlock() + } +} diff --git a/Nynja/Statuses/ObservableContainer.swift b/Nynja/Observable/KeyedObservableContainer.swift similarity index 87% rename from Nynja/Statuses/ObservableContainer.swift rename to Nynja/Observable/KeyedObservableContainer.swift index 9b89e430f..ce8dfedc0 100644 --- a/Nynja/Statuses/ObservableContainer.swift +++ b/Nynja/Observable/KeyedObservableContainer.swift @@ -1,17 +1,17 @@ // -// ObservableContainer.swift +// KeyedObservableContainer.swift // Nynja // // Created by Anton Poltoratskyi on 26.10.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol ObservableContainer: class { +protocol KeyedObservableContainer: class { associatedtype Key: Hashable associatedtype Value typealias Callback = (Key, Value) -> Void - var observable: Observable { get } + var observable: KeyedObservable { get } func addObserver(_ observer: AnyObject, callback: @escaping Callback) func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) @@ -20,7 +20,7 @@ protocol ObservableContainer: class { func notify(_ key: Key, with value: Value) } -extension ObservableContainer { +extension KeyedObservableContainer { func addObserver(_ observer: AnyObject, callback: @escaping Callback) { observable.addObserver(observer, callback: callback) diff --git a/Nynja/Statuses/AccountStatusProvider.swift b/Nynja/Statuses/AccountStatusProvider.swift index d8996ab5f..65d15f633 100644 --- a/Nynja/Statuses/AccountStatusProvider.swift +++ b/Nynja/Statuses/AccountStatusProvider.swift @@ -23,11 +23,11 @@ protocol AccountStatusProvider: AccountStatusObservable { func update(_ status: AccountStatus, for accountId: AccountId) } -final class AccountStatusProviderImpl: AccountStatusProvider, ObservableContainer { +final class AccountStatusProviderImpl: AccountStatusProvider, KeyedObservableContainer { private var data: [AccountId: AccountStatus] = [:] - private(set) var observable = Observable() + private(set) var observable = KeyedObservable() func status(for accountId: AccountId) -> AccountStatus { return data[accountId] ?? .none diff --git a/Nynja/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift index 96da0c491..00924123e 100644 --- a/Nynja/Statuses/Observable.swift +++ b/Nynja/Statuses/Observable.swift @@ -1,5 +1,5 @@ // -// Observable.swift +// KeyedObservable.swift // Nynja // // Created by Anton Poltoratskyi on 26.10.2018. @@ -8,7 +8,7 @@ import Foundation -final class Observable { +final class KeyedObservable { private typealias Observers = [AnyWeakSubscriber] diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 58b2ed044..18145e837 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -23,11 +23,11 @@ protocol TypingStatusProvider: TypingStatusObservable { func update(_ status: ActionStatus, for accountId: AccountId) } -final class TypingStatusProviderImpl: TypingStatusProvider, ObservableContainer { +final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer { private var data: [AccountId: ActionStatus] = [:] - private(set) var observable = Observable() + private(set) var observable = KeyedObservable() func status(for accountId: AccountId) -> ActionStatus { return data[accountId] ?? .done -- GitLab From c767dc59df561030b3cae9b43ce48fa339c0414a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 16:13:58 +0200 Subject: [PATCH 028/138] [NY-4699] Subscribe to TypingHandler as observer. --- Nynja.xcodeproj/project.pbxproj | 10 ++++- .../Interactor/MessageInteractor.swift | 9 ++-- Nynja/Observable/Observable.swift | 41 +++++++++++++++++++ Nynja/Observable/ObservableContainer.swift | 31 ++++++++++++++ .../HandleServices/TypingHandler.swift | 16 +++++--- 5 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 Nynja/Observable/Observable.swift create mode 100644 Nynja/Observable/ObservableContainer.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 5bc4514ed..6ec362192 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -950,6 +950,8 @@ 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */; }; 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548340D207769E800604051 /* DocumentInteractionInput.swift */; }; 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingStatusProvider.swift */; }; + 8548341B2187449F002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* Observable.swift */; }; + 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* ObservableContainer.swift */; }; 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */; }; 854A4B2D2080D68200759152 /* CellWithArrowCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */; }; 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */; }; @@ -3159,6 +3161,8 @@ 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastScrollable.swift; sourceTree = ""; }; 8548340D207769E800604051 /* DocumentInteractionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentInteractionInput.swift; sourceTree = ""; }; 854834172186FADB002064E1 /* TypingStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusProvider.swift; sourceTree = ""; }; + 8548341A2187449F002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 8548341C218744AC002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowCellModel.swift; sourceTree = ""; }; 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; @@ -8379,8 +8383,10 @@ 8548341921874434002064E1 /* Observable */ = { isa = PBXGroup; children = ( - 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, + 8548341A2187449F002064E1 /* Observable.swift */, + 8548341C218744AC002064E1 /* ObservableContainer.swift */, 85EB37FC21837253003A2D6F /* KeyedObservable.swift */, + 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, ); path = Observable; sourceTree = ""; @@ -15565,6 +15571,7 @@ 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, + 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */, 4B1D7DFE2029C41C00703228 /* AboutItemsFactory.swift in Sources */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */, @@ -15828,6 +15835,7 @@ E70938371FBEDA2B006CCDC6 /* ProfileTable.swift in Sources */, A416DA602075341C00FBF1BA /* CLLocationCoordinate2D+Payload.swift in Sources */, A4679BAE20B2DD100021FE9C /* SubscribersSelectorInteractor.swift in Sources */, + 8548341B2187449F002064E1 /* Observable.swift in Sources */, FEA655FD2167777F00B44029 /* TransferDetailsInteractor.swift in Sources */, E70F78B91FD6C64E00385565 /* ChatCheckpointTable.swift in Sources */, 4B06D30620287060003B275B /* WCDataManagerProtocol.swift in Sources */, diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 7f65539b7..07e5f9f89 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -187,17 +187,18 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H ConnectionService.shared.addSubscriber(self) MessageHandler.shared.addSubscriber(self) HistoryHandler.shared.addSubscriber(self) + TypingHandler.shared.addObserver(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() } - - + deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) MessageHandler.shared.removeSubscriber(self) HistoryHandler.shared.removeSubscriber(self) + TypingHandler.shared.removeObserver(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -222,8 +223,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H processingManager.delegate = self - TypingHandler.shared.delegate = self - isAfterConnectionAppeared = false prepareInitialValues() @@ -934,7 +933,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } // MARK: - TypingHandlerDelegate - func getTyping(typing: Typing) { + func didReceiveTyping(_ typing: Typing) { guard self.contact?.phone_id == typing.phone_id, let typingModelType = typing.type else { return } diff --git a/Nynja/Observable/Observable.swift b/Nynja/Observable/Observable.swift new file mode 100644 index 000000000..fb149c81c --- /dev/null +++ b/Nynja/Observable/Observable.swift @@ -0,0 +1,41 @@ +// +// Observable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class Observable { + + private typealias Observers = [WeakRef] + + private let lock = NSLock() + + private var observers: Observers = [] + + func addObserver(_ observer: T) { + let container = WeakRef(value: observer as AnyObject) + + lock.lock() + + observers.append(container) + + lock.unlock() + } + + func removeObserver(_ observer: T) { + let observer = observer as AnyObject + lock.lock() + observers.removeAll { $0.value === observer || $0.value == nil } + lock.unlock() + } + + func notify(_ block: (T) -> Void) { + lock.lock() + observers.forEach { ($0.value as? T).flatMap(block) } + lock.unlock() + } +} diff --git a/Nynja/Observable/ObservableContainer.swift b/Nynja/Observable/ObservableContainer.swift new file mode 100644 index 000000000..d995d3c90 --- /dev/null +++ b/Nynja/Observable/ObservableContainer.swift @@ -0,0 +1,31 @@ +// +// ObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ObservableContainer: class { + associatedtype Observer + var observable: Observable { get } + + func addObserver(_ observer: Observer) + func removeObserver(_ observer: Observer) + func notify(_ block: (Observer) -> Void) +} + +extension ObservableContainer { + + func addObserver(_ observer: Observer) { + observable.addObserver(observer) + } + + func removeObserver(_ observer: Observer) { + observable.removeObserver(observer) + } + + func notify(_ block: (Observer) -> Void) { + observable.notify(block) + } +} diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index e14773beb..7774b0c4e 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -9,10 +9,10 @@ import Foundation protocol TypingHandlerDelegate: class { - func getTyping(typing: Typing) + func didReceiveTyping(_ typing: Typing) } -final class TypingHandler: BaseHandler { +final class TypingHandler: BaseHandler, ObservableContainer { // MARK: - Singleton @@ -21,13 +21,17 @@ final class TypingHandler: BaseHandler { private init() {} - // MARK: - Handler + // MARK: - ObservableContainer + + let observable = Observable() - weak var delegate: TypingHandlerDelegate? + + // MARK: - Handler func executeHandle(data: BertTuple) { - if let typing = get_Typing().parse(bert: data) as? Typing { - delegate?.getTyping(typing: typing) + guard let typing = get_Typing().parse(bert: data) as? Typing else { + return } + notify { $0.didReceiveTyping(typing) } } } -- GitLab From 689db962787f13d1450169471d2c02ba6c49cf8a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 16:42:57 +0200 Subject: [PATCH 029/138] [NY-4699] Use TypingStatusProvider in MessageInteractor. --- .../Interactor/MessageInteractor.swift | 38 ++++++++++--------- Nynja/Observable/KeyedObservable.swift | 3 ++ Nynja/Statuses/TypingStatusProvider.swift | 37 +++++++++++++++++- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 07e5f9f89..a06a64fa3 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -10,7 +10,7 @@ import UIKit import CoreLocation -final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, TypingHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { +final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { private var callService = NynjaCommunicatorService.sharedInstance @@ -98,6 +98,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let stickersProvider: StickersProviding private var presenceProvider: PresenceStatusProvider! + + private let typingStatusProvider: TypingStatusProvider private let historyRequestFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() @@ -176,6 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) + typingStatusProvider = TypingStatusProviderImpl(dependencies: TypingHandler.shared) super.init() @@ -184,10 +187,24 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } mqttService.addSubscriber(self) + + if let chatId = chat.id { + typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingStatus in + guard let `self` = self else { return } + + self.presenter?.actionStatusChanged(typingStatus) + + if case .done = typingStatus { + return + } + dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in + self?.presenter?.restoreStatus() + } + } + } ConnectionService.shared.addSubscriber(self) MessageHandler.shared.addSubscriber(self) HistoryHandler.shared.addSubscriber(self) - TypingHandler.shared.addObserver(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() @@ -196,9 +213,9 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) + typingStatusProvider.removeObserver(self) MessageHandler.shared.removeSubscriber(self) HistoryHandler.shared.removeSubscriber(self) - TypingHandler.shared.removeObserver(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -931,21 +948,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H self.autoTranslateReceiptMessagesIfNeeded() } } - - // MARK: - TypingHandlerDelegate - func didReceiveTyping(_ typing: Typing) { - guard self.contact?.phone_id == typing.phone_id, let typingModelType = typing.type else { - return - } - - let actionStatus = ActionStatus(typingModelType: typingModelType) - presenter?.actionStatusChanged(actionStatus) - if typingModelType != .done { - dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in - self?.presenter?.restoreStatus() - } - } - } func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { if service == .networking { diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift index 00924123e..6c1da682a 100644 --- a/Nynja/Observable/KeyedObservable.swift +++ b/Nynja/Observable/KeyedObservable.swift @@ -49,6 +49,9 @@ final class KeyedObservable { func removeObserver(_ observer: AnyObject) { lock.lock() allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + for (key, _) in observers { + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + } lock.unlock() } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 18145e837..ecf1c9a46 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -23,12 +23,34 @@ protocol TypingStatusProvider: TypingStatusObservable { func update(_ status: ActionStatus, for accountId: AccountId) } -final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer { - +final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { + private var data: [AccountId: ActionStatus] = [:] private(set) var observable = KeyedObservable() + + // MARK: - Dependencies + + typealias Dependencies = TypingHandler + + private let typingHandler: TypingHandler + + + // MARK: - Init + + init(dependencies: Dependencies) { + typingHandler = dependencies + typingHandler.addObserver(self) + } + + deinit { + typingHandler.removeObserver(self) + } + + + // MARK: - TypingStatusProvider + func status(for accountId: AccountId) -> ActionStatus { return data[accountId] ?? .done } @@ -37,4 +59,15 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta data[accountId] = status observable.notify(accountId, with: status) } + + + // MARK: - TypingHandlerDelegate + + func didReceiveTyping(_ typing: Typing) { + guard let accountId = typing.phone_id, let typingType = typing.type else { + return + } + let status = ActionStatus(typingModelType: typingType) + notify(accountId, with: status) + } } -- GitLab From f858f2b16da9afb47fd72785a157a00b9e76b320 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 18:14:41 +0200 Subject: [PATCH 030/138] [NY-4699] Move logic of timer refresh to separate method in PresenceStatusProvider. --- .../Interactor/MessageInteractor.swift | 1 + .../Interactor/PresenceStatusProvider.swift | 57 ++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index a06a64fa3..5e276b318 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -975,6 +975,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private func chatUpdated(from oldChat: ChatModel, to newChat: ChatModel) { presenter?.chatUpdated(newChat) if let status = presenceProvider.presence(for: newChat) { + presenceProvider.refreshTimer(for: newChat) presenter?.presenceStatusChanged(status) } diff --git a/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift b/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift index c53f3c856..1c207c69c 100644 --- a/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift +++ b/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift @@ -46,42 +46,23 @@ class PresenceStatusProvider { } private func presence(contact: Contact) -> PresenceStatus { - invalidatePresenceTimer() - guard contact.presenceStatus != "online" else { return .active } - let minutesDiff = minutesAfterOffline(contact.updated) - if minutesDiff >= minutes { - return .inactive - } else { - let interval = timeInterval - minutesDiff * 60 - presenceTimer = TimerHandler(interval: interval, repeats: false) { [weak self] _ in - self?.didContactBecomeInactive() - } - - return .active - } + return minutesDiff >= minutes ? .inactive : .active } private func presence(group: Room) -> PresenceStatus { - invalidatePresenceTimer() - let allMembers = group.allMembers ?? [] - let activeCount = allMembers.reduce(0) { (temp, member) in + let activeCount = allMembers.reduce(0) { count, member in let minutesDiff = minutesAfterOffline(member.updated) if StringAtom.string(member.presence) == "online" || minutesDiff < minutes { - return temp + 1 + return count + 1 } - - return temp - } - - presenceTimer = TimerHandler(interval: timeInterval, repeats: false) { [weak self] _ in - self?.checkCountOfActiveMembers() + return count } return .room(allMembers.count, activeCount) @@ -102,15 +83,38 @@ class PresenceStatusProvider { // MARK: - Timer - @objc private func didContactBecomeInactive() { + func refreshTimer(for chat: ChatModel) { invalidatePresenceTimer() - handler(.inactive) + + switch chat { + case let chat as Contact: + let minutesDiff = minutesAfterOffline(chat.updated) + if minutesDiff < minutes { + let interval = timeInterval - minutesDiff * 60 + presenceTimer = TimerHandler(interval: interval, repeats: false) { [weak self] _ in + self?.didContactBecomeInactive() + } + } + case let chat as Room where chat.kind == .group: + presenceTimer = TimerHandler(interval: timeInterval, repeats: false) { [weak self] _ in + self?.checkCountOfActiveMembers() + } + default: + break + } } - @objc private func checkCountOfActiveMembers() { + private func didContactBecomeInactive() { invalidatePresenceTimer() + handler(.inactive) + } + + private func checkCountOfActiveMembers() { if let chat = self.chat, let status = presence(for: chat) { + refreshTimer(for: chat) handler(status) + } else { + invalidatePresenceTimer() } } @@ -118,5 +122,4 @@ class PresenceStatusProvider { presenceTimer?.invalidate() presenceTimer = nil } - } -- GitLab From e8480f794e5a2f4f940cf1e7fc4a890ba6a91898 Mon Sep 17 00:00:00 2001 From: AshCenso Date: Tue, 30 Oct 2018 11:10:45 +0200 Subject: [PATCH 031/138] in progress --- Nynja.xcodeproj/project.pbxproj | 128 +++++ .../UI/UIImageView/UIImageExtensions.swift | 18 + Nynja/Library/UI/View/UIViewExtensions.swift | 54 +- Nynja/Modules/Auth/AuthCoordinator.swift | 108 +++- .../Auth/AuthModule/AuthProtocols.swift | 6 +- .../AuthModule/Entities/LoginOption.swift | 6 +- .../Interactor/AuthInteractor.swift | 20 + .../AuthModule/Presenter/AuthPresenter.swift | 43 +- .../AuthModule/View/AuthViewController.swift | 172 +----- .../View/Subviews/AuthHeaderView.swift | 55 +- .../View/Subviews/EmailLoginView.swift | 103 +--- .../View/Subviews/LoginOptionsView.swift | 106 +--- .../View/Subviews/PhoneNumberLoginView.swift | 183 +------ .../View/ViewsFactory/AuthViewsFactory.swift | 501 ++++++++++++++++++ .../AuthModule/Wireframe/AuthWireframe.swift | 4 +- .../CodeConfirmationProtocols.swift | 4 +- .../CodeConfirmation/Entities/AuthType.swift | 15 + .../CodeConfirmationInteractor.swift | 8 +- .../Presenter/CodeConfirmationPresenter.swift | 27 +- .../View/CodeConfirmationViewController.swift | 429 ++++----------- .../CodeConfirmationViewsFactory.swift | 225 ++++++++ .../Wireframe/CodeConfirmationWireframe.swift | 11 +- .../View/CountrySelectorViewController.swift | 171 +++--- .../View/CountrySelectorViewsFactory.swift | 96 ++++ .../Wireframe/CountrySelectorWireframe.swift | 5 +- .../CreateProfileProtocols.swift | 64 +++ .../Interactor/CreateProfileInteractor.swift | 110 ++++ .../Presenter/CreateProfilePresenter.swift | 71 +++ .../View/CreateProfileViewController.swift | 104 ++++ .../Subviews/CreateProfileContentView.swift | 144 +++++ .../CreateProfileViewsFactory.swift | 299 +++++++++++ .../Wireframe/CreateProfileWireframe.swift | 57 ++ .../Icons_General_ic_accept_call@2x.png | Bin 908 -> 911 bytes .../Icons_General_ic_accept_call@3x.png | Bin 1287 -> 1379 bytes .../Contents.json | 23 + .../Icons_General_ic_accept_call.png | Bin 0 -> 377 bytes .../Icons_General_ic_accept_call@2x.png | Bin 0 -> 777 bytes .../Icons_General_ic_accept_call@3x.png | Bin 0 -> 1142 bytes .../ic_empty_avatar.imageset/Contents.json | 23 + .../ic_empty_avatar.png | Bin 0 -> 6390 bytes .../ic_empty_avatar@2x.png | Bin 0 -> 17828 bytes .../ic_empty_avatar@3x.png | Bin 0 -> 30061 bytes .../Contents.json | 20 +- ..._Right_Overrides_Checkbox_ic_unchecked.png | Bin 0 -> 696 bytes ...ght_Overrides_Checkbox_ic_unchecked@2x.png | Bin 0 -> 1470 bytes ...ght_Overrides_Checkbox_ic_unchecked@3x.png | Bin 0 -> 2368 bytes 46 files changed, 2408 insertions(+), 1005 deletions(-) create mode 100644 Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift create mode 100644 Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift create mode 100644 Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift create mode 100644 Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift create mode 100644 Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift create mode 100644 Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift create mode 100644 Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift create mode 100644 Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift create mode 100644 Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift create mode 100644 Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift create mode 100644 Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call.png create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@2x.png create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@3x.png create mode 100644 Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar.png create mode 100644 Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@2x.png create mode 100644 Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@3x.png create mode 100644 Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked.png create mode 100644 Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@2x.png create mode 100644 Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@3x.png diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 29139da60..d3f83f3b4 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -651,6 +651,17 @@ 5BC1D38420D3B670002A44B3 /* CallCreatorMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */; }; 5C468A609C445962C0D19DD3 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D6900257B1AB2CA0BC834EB /* HistoryViewController.swift */; }; 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */; }; + 5E07BC3D216DFD08000E4558 /* AuthViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */; }; + 5E07BC40216E09F0000E4558 /* CodeConfirmationViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */; }; + 5E07BC42216E30A8000E4558 /* CountrySelectorViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC41216E30A8000E4558 /* CountrySelectorViewsFactory.swift */; }; + 5E07BC44216F56AF000E4558 /* AuthType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC43216F56AF000E4558 /* AuthType.swift */; }; + 5E07BC4D216F64EC000E4558 /* CreateProfileProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */; }; + 5E07BC4F216F659E000E4558 /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */; }; + 5E07BC51216F6617000E4558 /* CreateProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */; }; + 5E07BC53216F6661000E4558 /* CreateProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC52216F6661000E4558 /* CreateProfilePresenter.swift */; }; + 5E07BC55216F66F3000E4558 /* CreateProfileViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC54216F66F3000E4558 /* CreateProfileViewsFactory.swift */; }; + 5E07BC57216F6722000E4558 /* CreateProfileWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */; }; + 5E0B9FF22170BCE600A95467 /* CreateProfileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */; }; 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */; }; 5E278E14F45F56BACB71271C /* VideoPreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5541C91FE7845F3E5C7EB2 /* VideoPreviewWireframe.swift */; }; 5E7E9FB9215BA0BE004D306B /* CountrySelectorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */; }; @@ -659,6 +670,7 @@ 5E7E9FC2215BA681004D306B /* CountryTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */; }; 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */; }; 5EB13FDBA6153EE67366115F /* ScheduleMessageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */; }; + 5EC8C841216648B6003D4731 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EC8C840216648B6003D4731 /* ViewController.swift */; }; 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD49CF323041B47A752603E /* MapSearchInteractor.swift */; }; 5EEB73A4215D00E300D8ECE6 /* CountrySelectorInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */; }; 5EEB73A6215D00F100D8ECE6 /* CountrySelectorWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */; }; @@ -2798,6 +2810,17 @@ 5BC1D38020D3B54B002A44B3 /* CallInfoViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInfoViewLayout.swift; sourceTree = ""; }; 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallCreatorMediator.swift; sourceTree = ""; }; 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryInteractor.swift; sourceTree = ""; }; + 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewsFactory.swift; sourceTree = ""; }; + 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationViewsFactory.swift; sourceTree = ""; }; + 5E07BC41216E30A8000E4558 /* CountrySelectorViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewsFactory.swift; sourceTree = ""; }; + 5E07BC43216F56AF000E4558 /* AuthType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthType.swift; sourceTree = ""; }; + 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileProtocols.swift; sourceTree = ""; }; + 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = ""; }; + 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileInteractor.swift; sourceTree = ""; }; + 5E07BC52216F6661000E4558 /* CreateProfilePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfilePresenter.swift; sourceTree = ""; }; + 5E07BC54216F66F3000E4558 /* CreateProfileViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewsFactory.swift; sourceTree = ""; }; + 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileWireframe.swift; sourceTree = ""; }; + 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileContentView.swift; sourceTree = ""; }; 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; 5E7E9FB2215BA059004D306B /* Nynja copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Nynja copy-Info.plist"; path = "/Users/ash/Projects/NynjaIOSWallet/Nynja copy-Info.plist"; sourceTree = ""; }; 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorProtocols.swift; sourceTree = ""; }; @@ -2805,6 +2828,7 @@ 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewController.swift; sourceTree = ""; }; 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVCell.swift; sourceTree = ""; }; 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVHeader.swift; sourceTree = ""; }; + 5EC8C840216648B6003D4731 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 5EEA3D18EFB98D7959F993E4 /* AddParticipantsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsProtocols.swift; sourceTree = ""; }; 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorInteractor.swift; sourceTree = ""; }; 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorWireframe.swift; sourceTree = ""; }; @@ -5906,6 +5930,7 @@ 3A768DE41ECB3E7600108F7C /* Library */, 3ABCE9021EC9357900A80B15 /* Resources */, 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */, + 5EC8C840216648B6003D4731 /* ViewController.swift */, 3AC07E2E1F05572400ADBE26 /* Nynja-Bridging-Header.h */, F11786B320A8A5EB007A9A1B /* Coordinators */, 49E75E252CE2F3C96A626230 /* Modules */, @@ -6422,6 +6447,7 @@ 4B749F0E214FEFC8002F3A33 /* Auth */ = { isa = PBXGroup; children = ( + 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, 5E7E9FB3215BA0AD004D306B /* CountrySelector */, @@ -6813,6 +6839,92 @@ path = View; sourceTree = ""; }; + 5E07BC3B216DFCFA000E4558 /* ViewsFactory */ = { + isa = PBXGroup; + children = ( + 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */, + ); + path = ViewsFactory; + sourceTree = ""; + }; + 5E07BC3E216E09DF000E4558 /* ViewsFactory */ = { + isa = PBXGroup; + children = ( + 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */, + ); + path = ViewsFactory; + sourceTree = ""; + }; + 5E07BC45216F64DB000E4558 /* CreateProfile */ = { + isa = PBXGroup; + children = ( + 5E07BC46216F64DB000E4558 /* Presenter */, + 5E07BC47216F64DB000E4558 /* Wireframe */, + 5E07BC48216F64DB000E4558 /* View */, + 5E07BC4A216F64DB000E4558 /* Interactor */, + 5E07BC4B216F64DB000E4558 /* Entities */, + 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */, + ); + path = CreateProfile; + sourceTree = ""; + }; + 5E07BC46216F64DB000E4558 /* Presenter */ = { + isa = PBXGroup; + children = ( + 5E07BC52216F6661000E4558 /* CreateProfilePresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 5E07BC47216F64DB000E4558 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 5E07BC48216F64DB000E4558 /* View */ = { + isa = PBXGroup; + children = ( + 5E0B9FF02170BCD400A95467 /* Subviews */, + 5E07BC49216F64DB000E4558 /* ViewsFactory */, + 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 5E07BC49216F64DB000E4558 /* ViewsFactory */ = { + isa = PBXGroup; + children = ( + 5E07BC54216F66F3000E4558 /* CreateProfileViewsFactory.swift */, + ); + path = ViewsFactory; + sourceTree = ""; + }; + 5E07BC4A216F64DB000E4558 /* Interactor */ = { + isa = PBXGroup; + children = ( + 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 5E07BC4B216F64DB000E4558 /* Entities */ = { + isa = PBXGroup; + children = ( + ); + path = Entities; + sourceTree = ""; + }; + 5E0B9FF02170BCD400A95467 /* Subviews */ = { + isa = PBXGroup; + children = ( + 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; 5E7E9FB3215BA0AD004D306B /* CountrySelector */ = { isa = PBXGroup; children = ( @@ -6848,6 +6960,7 @@ 5E7E9FBF215BA66E004D306B /* Cells */, 5E7E9FC0215BA66E004D306B /* Headers */, 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */, + 5E07BC41216E30A8000E4558 /* CountrySelectorViewsFactory.swift */, ); path = View; sourceTree = ""; @@ -6916,6 +7029,7 @@ 5EEB73AE216046EA00D8ECE6 /* View */ = { isa = PBXGroup; children = ( + 5E07BC3E216E09DF000E4558 /* ViewsFactory */, 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */, ); path = View; @@ -6932,6 +7046,7 @@ 5EEB73B0216046EA00D8ECE6 /* Entities */ = { isa = PBXGroup; children = ( + 5E07BC43216F56AF000E4558 /* AuthType.swift */, ); path = Entities; sourceTree = ""; @@ -6977,6 +7092,7 @@ 5EEB73C1216199DE00D8ECE6 /* View */ = { isa = PBXGroup; children = ( + 5E07BC3B216DFCFA000E4558 /* ViewsFactory */, 5EEB73CE2161CDF700D8ECE6 /* Subviews */, 5EEB73CC2161CC8A00D8ECE6 /* AuthViewController.swift */, ); @@ -14034,6 +14150,7 @@ A40F18BB20BFD9C60091B09E /* EmptyStateViewModel.swift in Sources */, A42D52B6206A53AA00EEB952 /* Service_Spec.swift in Sources */, 4B8996C8204ECE9B00DCB183 /* ContactDAO.swift in Sources */, + 5E07BC42216E30A8000E4558 /* CountrySelectorViewsFactory.swift in Sources */, 4B7C73F3215A5509007924DB /* LogWriter.swift in Sources */, 262D43872033417F002F1E45 /* FriendExtansion+BERT.swift in Sources */, FE58F9B1208F00FE004AFDD3 /* MessageEditActionTable.swift in Sources */, @@ -14120,6 +14237,7 @@ 001F0CF5202C38FA006B4304 /* TimeZoneCell.swift in Sources */, C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */, 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */, + 5E07BC44216F56AF000E4558 /* AuthType.swift in Sources */, A49381AA21355EE1006D28DD /* MessageInteractor+Forward.swift in Sources */, 26FA4210201821B400E6F6EC /* StarHandler.swift in Sources */, FBCE83E120E52496003B7558 /* ProfileServices.swift in Sources */, @@ -14129,6 +14247,7 @@ E77764B61FBDA8E30042541D /* WheelContainerDataSource.swift in Sources */, A4B544E820EFB15C00EB7B0F /* errors.swift in Sources */, A42D51B0206A361400EEB952 /* Profile.swift in Sources */, + 5E07BC4F216F659E000E4558 /* CreateProfileViewController.swift in Sources */, 85D669FF20BD963C00FBD803 /* InputScheduleMessage.swift in Sources */, 269D9DEE1FC3987200324263 /* URLExtensions.swift in Sources */, 3A19FEAD1F3B7F1D00ACE750 /* MessageHandler.swift in Sources */, @@ -14284,6 +14403,7 @@ A43B259D20AB1DFA00FF8107 /* PhoneField.swift in Sources */, 267BE90820693DE700153FB8 /* DBManagerProtocol.swift in Sources */, 26342CAB20ECBB0100D2196B /* TranscribeNetworkService.swift in Sources */, + 5E07BC57216F6722000E4558 /* CreateProfileWireframe.swift in Sources */, FBCE83D020E52352003B7558 /* PaymentTableViewCell.swift in Sources */, A44B4D5820CE9BDF00CA700A /* AvatarCell.swift in Sources */, 5E7E9FBC215BA19B004D306B /* Country.swift in Sources */, @@ -14300,6 +14420,7 @@ A42D51BA206A361400EEB952 /* io.swift in Sources */, 3A1EB9A51F3A848A00658E93 /* HistoryHandler.swift in Sources */, 850FC5F42032F4CE00832D87 /* ForwardTargets.swift in Sources */, + 5E07BC4D216F64EC000E4558 /* CreateProfileProtocols.swift in Sources */, 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */, A4679BA920B2DD100021FE9C /* SubscribersTableDelegate.swift in Sources */, 2648C41E2069B5B300863614 /* ChangeNumberItemsFactory.swift in Sources */, @@ -14436,6 +14557,7 @@ FBCE841320E525A6003B7558 /* NetworkRouter.swift in Sources */, A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, + 5E0B9FF22170BCE600A95467 /* CreateProfileContentView.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, @@ -14566,6 +14688,7 @@ 4B8996E4204EEC5A00DCB183 /* MessageDAOProtocol.swift in Sources */, E76491961F7A529D001E741C /* WheelContainer.swift in Sources */, 2648C4122069B52100863614 /* ChangeNumberStep3ViewController.swift in Sources */, + 5E07BC55216F66F3000E4558 /* CreateProfileViewsFactory.swift in Sources */, 8E9601931FF295DF00E0C21D /* ItemsSelector.swift in Sources */, A42D51C9206A361400EEB952 /* writer.swift in Sources */, 2648C3E62069B49000863614 /* UITextField+Extension.swift in Sources */, @@ -14602,6 +14725,7 @@ C940514C204C7FAF00D72B04 /* DataAndStorageInteractor.swift in Sources */, F1A9FA3590CC1F834B727955 /* AddContactPresenter.swift in Sources */, 6DD72F601F1547AC008CFF83 /* GCD.swift in Sources */, + 5E07BC3D216DFD08000E4558 /* AuthViewsFactory.swift in Sources */, A49CC1D820E4AB2C00879D41 /* DisplayModeConfigFactory.swift in Sources */, 859C429F2056829300AE3797 /* NotificationSettings.swift in Sources */, A45F115F20B422AF00F45004 /* Room+DB.swift in Sources */, @@ -14632,6 +14756,7 @@ A409B1CF2108D48E0051C20B /* QueryFactory.swift in Sources */, A42D52B7206A53AA00EEB952 /* reader_Spec.swift in Sources */, F119E66A20D24B960043A532 /* MultiplePreviewProtocols.swift in Sources */, + 5E07BC53216F6661000E4558 /* CreateProfilePresenter.swift in Sources */, 850FC611203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift in Sources */, 0062D93D2062EC4100B915AC /* InviteFriendsWireframe.swift in Sources */, 9BD8E3F120EF7898001384EC /* CallInProgressViewController.swift in Sources */, @@ -15087,6 +15212,7 @@ 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */, + 5E07BC40216E09F0000E4558 /* CodeConfirmationViewsFactory.swift in Sources */, F10AFEBC20F7B1D200C7CE83 /* WheelPreviewFactory.swift in Sources */, 4B3F055F2043F871002E0F54 /* ScheduleMessageConfiguration.swift in Sources */, A42D51A1206A361400EEB952 /* Friend.swift in Sources */, @@ -15527,6 +15653,7 @@ C9B8BEFC204DDDA20018748C /* CheckmarkCellLayout.swift in Sources */, 4B8BEDE1204979AA00C7D625 /* ImagesView.swift in Sources */, A45F113720B4218D00F45004 /* ReplyPreview.swift in Sources */, + 5E07BC51216F6617000E4558 /* CreateProfileInteractor.swift in Sources */, A481BD1C20EE72CB008FFED8 /* ReplyCounterDelegate.swift in Sources */, A42D52D4206A53AB00EEB952 /* Message_Spec.swift in Sources */, 26C0C1E22073DA2E00C530DA /* P2P+DB.swift in Sources */, @@ -15548,6 +15675,7 @@ 6547BE911E492D790E0D4390 /* EditGroupNameInteractor.swift in Sources */, 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */, 263A60AE1FB51C22006F9D52 /* MemberExtension.swift in Sources */, + 5EC8C841216648B6003D4731 /* ViewController.swift in Sources */, E709383F1FBEE41D006CCDC6 /* Describable.swift in Sources */, A42D52DA206A53AB00EEB952 /* Desc_Spec.swift in Sources */, A433D9A120A5C18C00C946F9 /* ContactsProvider.swift in Sources */, diff --git a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift index 251dd9e73..14ac8363a 100644 --- a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift +++ b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift @@ -207,6 +207,24 @@ extension UIImage { return croppedImage } + + static func makeImageFromColor(_ color: UIColor) -> UIImage? { + let rect = CGRect(x: 0, y: 0, width: 1, height: 1) + + UIGraphicsBeginImageContext(rect.size) + + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + + context.setFillColor(color.cgColor) + context.fill(rect) + + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return img + } } //MARK: - images diff --git a/Nynja/Library/UI/View/UIViewExtensions.swift b/Nynja/Library/UI/View/UIViewExtensions.swift index f4b0607d5..8e913d253 100644 --- a/Nynja/Library/UI/View/UIViewExtensions.swift +++ b/Nynja/Library/UI/View/UIViewExtensions.swift @@ -42,26 +42,50 @@ extension UIView { return background } + + static func makeHeaderView(on view: UIView, config: NavigationView.Config) -> NavigationView { + let navigationView = makeHeaderViewWithoutConstraints(on: view, config: config) + + navigationView.snp.makeConstraints { (maker) in + maker.top.equalToSuperview() + maker.left.right.equalToSuperview() + } + + return navigationView + } - static func makeHeaderView(on view: UIView, top: UIView?, config: NavigationView.Config) -> NavigationView { - let navigationView = NavigationView() - - navigationView.configure(config: config) - - view.addSubview(navigationView) - - navigationView.backgroundColor = UIColor.nynja.darkLight - + static func makeHeaderView(on view: UIView, top: UIView, config: NavigationView.Config) -> NavigationView { + let navigationView = makeHeaderViewWithoutConstraints(on: view, config: config) + navigationView.snp.makeConstraints { (maker) in - if let top = top { - maker.top.equalTo(top.snp.bottom) - } else { - maker.top.equalToSuperview() - } + maker.top.equalTo(top.snp.bottom) maker.left.right.equalToSuperview() } - + + return navigationView + } + + static func makeHeaderView(on view: UIView, top: UILayoutGuide, config: NavigationView.Config) -> NavigationView { + let navigationView = makeHeaderViewWithoutConstraints(on: view, config: config) + + navigationView.snp.makeConstraints { (maker) in + maker.top.equalTo(top.snp.bottom) + maker.left.right.equalToSuperview() + } + + return navigationView + } + + private static func makeHeaderViewWithoutConstraints(on view: UIView, config: NavigationView.Config) -> NavigationView { + let navigationView = NavigationView() + + navigationView.configure(config: config) + + view.addSubview(navigationView) + + navigationView.backgroundColor = UIColor.nynja.darkLight + return navigationView } } diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 4459a1558..66298e0d4 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -7,9 +7,9 @@ // import Foundation +import SDWebImage - -final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol { +final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol, CreateProfileCoordinatorProtocol { private weak var navigation: UINavigationController? private let serviceFactory: ServiceFactoryProtocol @@ -24,19 +24,6 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt let wireframe = AuthWireframe(coordinator: self) let view = wireframe.prepareModule(parameters: NSNull(), dependencies: AuthWireframe.Dependencies()) -// let wireframe = CodeConfirmationWireframe.init(coordinator: self) -// let view = wireframe.prepareModule( -// parameters: CodeConfirmationWireframe.Parameters( -// address: "Some", -// authType: .phoneNumber), -// dependencies: CodeConfirmationWireframe.Dependencies()) - -// let wireframe = CountrySelectorWireframe(coordinator: self) -// let view = wireframe.prepareModule( -// parameters: NSNull(), -// dependencies: CountrySelectorWireframe.Dependencies( -// storageService: serviceFactory.makeStorageService())) - navigation?.pushViewController(view, animated: true) } @@ -62,7 +49,22 @@ extension AuthCoordinator { extension AuthCoordinator { func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { + switch state { + case .back: navigation?.popViewController(animated: true) + case .invalidCode: break + case .validCode(let type): handleType(type) + } + } + + private func handleType(_ type: AuthenticationType) { + let view = CreateProfileWireframe(coordinator: self).prepareModule(parameters: CreateProfileWireframe.Parameters(), dependencies: CreateProfileWireframe.Dependencies()) + + navigation?.pushViewController(view, animated: true) + switch type { + case .login: break + case .register: break + } } } @@ -71,7 +73,7 @@ extension AuthCoordinator { extension AuthCoordinator { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) { switch state { - case .continueLogin(let loginOption): break + case .continueLogin(let loginOption): continueLoginProcess(with: loginOption) case .getCountry(let callback): selectCountryCallback = callback let wireframe = CountrySelectorWireframe(coordinator: self) @@ -83,4 +85,78 @@ extension AuthCoordinator { navigation?.pushViewController(view, animated: true) } } + + private func continueLoginProcess(with loginOption: LoginOption) { + switch loginOption { + case .email, .phoneNumber: showConfirmationPopup(loginOption: loginOption) + default: break + } + } + + private func showConfirmationPopup(loginOption: LoginOption) { + let popup = UIAlertController(title: titleForPopup(loginOption: loginOption), message: messageForPopup(loginOption: loginOption), preferredStyle: .alert) + + let modify = UIAlertAction(title: "Modify".localized, style: .cancel, handler: nil) + let confirm = UIAlertAction.init(title: "Confirm".localized, style: .default) { [weak self] (action) in + guard let `self` = self else { + return + } + + switch loginOption { + case .email(let email): + let wireframe = CodeConfirmationWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: CodeConfirmationWireframe.Parameters(address: email, authType: .email), + dependencies: CodeConfirmationWireframe.Dependencies()) + + self.navigation?.pushViewController(view, animated: true) + case .phoneNumber(let number): + let wireframe = CodeConfirmationWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: CodeConfirmationWireframe.Parameters(address: number, authType: .phoneNumber), + dependencies: CodeConfirmationWireframe.Dependencies()) + + self.navigation?.pushViewController(view, animated: true) + default: break + } + } + + [modify, confirm].forEach { popup.addAction($0) } + + navigation?.present(popup, animated: true, completion: nil) + } + + private func titleForPopup(loginOption: LoginOption) -> String { + switch loginOption { + case .email(let email): return "Please confirm the email you entered is correct".localized + case .phoneNumber(let number): return "Please confirm the number you entered is correct".localized + default: return "" + } + } + + private func messageForPopup(loginOption: LoginOption) -> String { + switch loginOption { + case .email(let email): return email + case .phoneNumber(let number): return "+" + number + default: return "" + } + } } + +// MARK: - CreateProfileCoordinatorProtocol + +extension AuthCoordinator { + func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) { + switch state { + case .back: navigation?.popViewController(animated: true) + case .next: end() + case .chooseAvatar(let completion): + let dependencies = SelectAvatarFlowCoordinator.Dependencies(source: .gallery, rootViewController: navigation!.viewControllers.last!, serviceFactory: serviceFactory) { (url) in + completion(UIImage.sd_image(with: try? Data(contentsOf: url))) + } + let chooseAvatarCoordinator = SelectAvatarFlowCoordinator.init(dependencies: dependencies) + chooseAvatarCoordinator.start() + } + } +} + diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index a7457e5b4..3d87a582f 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -31,7 +31,11 @@ protocol AuthPresenterProtocol { } protocol AuthInputInteractorProtocol { - + associatedtype Code = String + func loginViaFacebook(completion: (Result) -> Void) + func loginViaGoogle(completion: (Result) -> Void) + func loginViaEmail(_ email: String, completion: (Result) -> Void) + func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) } protocol AuthOutputInteractorProtocol { diff --git a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift index e51440640..8e95c2273 100644 --- a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift +++ b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift @@ -9,6 +9,8 @@ import Foundation enum LoginOption { - case phoneNumber - case email + case phoneNumber(number: String) + case email(email: String) + case facebook(code: String) + case google(code: String) } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 13265b1fd..7e435f3f4 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -24,3 +24,23 @@ extension AuthInteractor { presenter = dependencies.presenter } } + +// MARK: - AuthInputInteractorProtocol + +extension AuthInteractor { + func loginViaFacebook(completion: (Result) -> Void) { + completion(.success("Some code")) + } + + func loginViaGoogle(completion: (Result) -> Void) { + completion(.success("Some code")) + } + + func loginViaEmail(_ email: String, completion: (Result) -> Void) { + completion(.success(())) + } + + func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) { + completion(.success(())) + } +} diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 8ae80365f..862e48d8c 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -14,31 +14,60 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, private var interactor: AuthInteractor? private var wireframe: AuthWireframe? - var loginOption: LoginOption = .phoneNumber + var loginOption: LoginOption = .phoneNumber(number: "") var country: Country = Country(ISO: "ARG", name: "Argentina", code: "54", numberTemplate: "XX XX XXX XX") func switchLoginOption() { switch loginOption { - case .email: loginOption = .phoneNumber - case .phoneNumber: loginOption = .email + case .email: loginOption = .phoneNumber(number: "") + case .phoneNumber: loginOption = .email(email: "") + default: break } } func loginViaFacebook(completion: (Result) -> Void) { - + interactor?.loginViaFacebook { + $0.onSuccess { + completion(.success(())) + wireframe?.continueLogin(loginOption: .facebook(code: $0)) + }.onFailure { + completion(.failure($0)) + } + } } func loginViaGoogle(completion: (Result) -> Void) { - + interactor?.loginViaGoogle { + $0.onSuccess { + completion(.success(())) + wireframe?.continueLogin(loginOption: .google(code: $0)) + }.onFailure { + completion(.failure($0)) + } + } } func loginViaEmail(_ email: String, completion: (Result) -> Void) { - + interactor?.loginViaEmail(email) { + $0.onSuccess { + completion(.success(())) + wireframe?.continueLogin(loginOption: .email(email: email)) + }.onFailure { + completion(.failure($0)) + } + } } func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) { - + interactor?.loginViaPhoneNumber(phoneNumber) { + $0.onSuccess { + completion(.success(())) + wireframe?.continueLogin(loginOption: .phoneNumber(number: phoneNumber)) + }.onFailure { + completion(.failure($0)) + } + } } func selectCountry(completion: @escaping (Result) -> Void) { diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index dfc2acc32..09f9d62cb 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -9,19 +9,34 @@ import Foundation -final class AuthViewController: UIViewController, AuthViewProtocol, SetInjectable, KeyboardInteractive { - private var presenter: AuthPresenterProtocol? +final class AuthViewController: UIViewController, AuthViewProtocol, InitializeInjectable, KeyboardInteractive { + private let presenter: AuthPresenterProtocol + private let viewsFactory: AuthViewsFactoryProtocol - private lazy var headerView: AuthHeaderView = makeHeaderView(on: view) + private lazy var headerView: AuthHeaderView = viewsFactory.makeHeaderView(on: view) - private lazy var scrollView: UIScrollView = makeScrollView(on: view, top: headerView, bottom: bottomView) - private lazy var scrollContentView: UIView = makeScrollContentView(on: scrollView, baseView: view) + private lazy var scrollView: UIScrollView = viewsFactory.makeScrollView(on: view, top: headerView, bottom: bottomView) + private lazy var scrollContentView: UIView = viewsFactory.makeScrollContentView(on: scrollView, baseView: view) private weak var emailLoginView: EmailLoginView? private weak var phoneNumberLoginView: PhoneNumberLoginView? - private lazy var bottomView: LoginOptionsView = makeBottomView(on: view, presenter: presenter!) + private lazy var bottomView: LoginOptionsView = viewsFactory.makeBottomView(on: view, + presenter: presenter, + showEmailLoginAction: showEmailLogin, + showPhoneNumberLoginAction: showPhoneNumberLogin) + init(dependencies: AuthViewController.Dependencies) { + presenter = dependencies.presenter + viewsFactory = dependencies.viewsFactory + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() @@ -43,6 +58,10 @@ final class AuthViewController: UIViewController, AuthViewProtocol, SetInjectabl super.viewDidDisappear(animated) unregisterForKeyboardNotifications() } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } } // MARK: - KeyboardInteractive @@ -65,11 +84,7 @@ extension AuthViewController { private extension AuthViewController { func showPhoneNumberLogin(animated: Bool) { - guard let presenter = presenter else { - return - } - - phoneNumberLoginView = makePhoneNumberLoginView(on: scrollContentView, presenter: presenter, country: presenter.country ) + phoneNumberLoginView = viewsFactory.makePhoneNumberLoginView(on: scrollContentView, presenter: presenter, country: presenter.country ) if animated { animateChangingViews(first: emailLoginView, second: phoneNumberLoginView) @@ -79,11 +94,7 @@ private extension AuthViewController { } func showEmailLogin(animated: Bool) { - guard let presenter = presenter else { - return - } - - emailLoginView = makeEmailLoginView(on: scrollContentView, presenter: presenter) + emailLoginView = viewsFactory.makeEmailLoginView(on: scrollContentView, presenter: presenter) if animated { animateChangingViews(first: phoneNumberLoginView, second: emailLoginView) @@ -108,136 +119,11 @@ private extension AuthViewController { } } -// MARK: - SetInjectable +// MARK: - InitializeInjectable extension AuthViewController { struct Dependencies { let presenter: AuthPresenterProtocol - } - - func inject(dependencies: AuthViewController.Dependencies) { - presenter = dependencies.presenter - } -} - -// MARK: - UI factory methods - -private extension AuthViewController { - func makeHeaderView(on view: UIView) -> AuthHeaderView { - let header = AuthHeaderView() - header.configure(config: NSNull()) - - view.addSubview(header) - header.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - } - - return header - } - - func makeScrollView(on view: UIView, top: UIView, bottom: UIView) -> UIScrollView { - let scrollView = UIScrollView() - view.addSubview(scrollView) - - scrollView.snp.makeConstraints { (make) in - make.left.right.equalToSuperview() - make.bottom.equalTo(bottom.snp.top) - make.top.equalTo(top.snp.bottom) - } - - return scrollView - } - - func makeScrollContentView(on view: UIView, baseView: UIView) -> UIView { - let contentView = UIView() - view.addSubview(contentView) - - contentView.backgroundColor = UIColor.nynja.clear - - contentView.snp.makeConstraints { (make) in - make.right.left.top.bottom.equalToSuperview() - make.width.equalTo(baseView.snp.width) - make.height.equalTo(baseView.snp.height) - } - - return contentView - } - - func makeEmailLoginView(on view: UIView, presenter: AuthPresenterProtocol) -> EmailLoginView { - let loginView = EmailLoginView() - view.addSubview(loginView) - - loginView.configure(config: EmailLoginView.Config(nextAction: { - presenter.loginViaEmail($0) { (result) in - print(#function) - } - })) - - loginView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - - return loginView - } - - func makePhoneNumberLoginView(on view: UIView, presenter: AuthPresenterProtocol, country: Country) -> PhoneNumberLoginView { - let loginView = PhoneNumberLoginView() - view.addSubview(loginView) - - loginView.configure(config: PhoneNumberLoginView.Config( - country: country, - countrySelectorAction: { - presenter.selectCountry { (result) in - result.onSuccess { loginView.updateCountry($0) } - } - }, - nextAction: { - presenter.loginViaPhoneNumber($0) { (result) in - print(#function) - } - })) - - loginView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - - return loginView - } - - func makeBottomView(on view: UIView, presenter: AuthPresenterProtocol) -> LoginOptionsView { - let bottom = LoginOptionsView() - - bottom.configure(config: LoginOptionsView.Config( - loginOption: presenter.loginOption, - switchLoginAction: { [weak self] () -> LoginOption in - presenter.switchLoginOption() - let loginOption = presenter.loginOption - - switch loginOption { - case .email: self?.showEmailLogin(animated: true) - case .phoneNumber: self?.showPhoneNumberLogin(animated: true) - } - - return loginOption - }, - facebookLoginAction: { - presenter.loginViaFacebook { (result) in - print(#function) - } - }, - googleLoginAction: { - presenter.loginViaGoogle { (result) in - print(#function) - } - })) - - view.addSubview(bottom) - bottom.snp.makeConstraints { (make) in - make.bottom.left.right.equalToSuperview() - } - - return bottom + let viewsFactory: AuthViewsFactoryProtocol } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift index a39f0aa29..d47e3bb5e 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift @@ -9,8 +9,20 @@ import Foundation final class AuthHeaderView: UIView, Configurable { - private lazy var welcomeLabel: UILabel = makeWelcomeLabel(on: self) - private lazy var logoImageView: UIImageView = makeLogoImageView(on: self, top: welcomeLabel) + private lazy var welcomeLabel: UILabel = viewsFactory.makeWelcomeLabel(on: self) + private lazy var logoImageView: UIImageView = viewsFactory.makeLogoImageView(on: self, top: welcomeLabel) + + private let viewsFactory: AuthViewsFactoryProtocol + + init(viewsFactory: AuthViewsFactoryProtocol) { + self.viewsFactory = viewsFactory + + super.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } // MARK: - Configurable @@ -23,42 +35,3 @@ extension AuthHeaderView { _ = [welcomeLabel, logoImageView] } } - -// MARK: - UI fabric methods - -private extension AuthHeaderView { - func makeWelcomeLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white - - label.text = "Welcome to".localized - - label.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(70) - make.centerX.equalToSuperview() - } - - return label - } - - func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { - let imageView = UIImageView() - view.addSubview(imageView) - - imageView.contentMode = .scaleAspectFill - imageView.image = UIImage.logoImage - - imageView.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom).offset(16) - make.bottom.equalToSuperview().offset(-16) - make.centerX.equalToSuperview() - make.width.equalToSuperview().multipliedBy(9/20) - } - - return imageView - } -} - diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index 47b61f39b..e8fefa32d 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -10,13 +10,25 @@ import Foundation final class EmailLoginView: UIView, Configurable { - private lazy var inputFieldContainer: UIView = makeInputFieldContainer(on: self) - private lazy var inputField: UITextField = makeInputField(on: inputFieldContainer) - private lazy var detailsLabel: UILabel = makeDetailsLabel(on: self, top: inputFieldContainer) - private lazy var nextButton: UIButton = makeNextButton(on: self, top: detailsLabel) + private lazy var inputFieldContainer: UIView = viewsFactory.makeInputFieldContainer(on: self) + private lazy var inputField: UITextField = viewsFactory.makeInputField(on: inputFieldContainer) + private lazy var detailsLabel: UILabel = viewsFactory.makeDetailsLabel(on: self, top: inputFieldContainer) + private lazy var nextButton: UIButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) private var textFieldController: TextFieldController? private var nextAction: ((String) -> Void)? + + private let viewsFactory: AuthViewsFactoryProtocol + + init(viewsFactory: AuthViewsFactoryProtocol) { + self.viewsFactory = viewsFactory + + super.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } // MARK: - Configurable @@ -38,6 +50,7 @@ extension EmailLoginView { } nextAction = config.nextAction + inputField.delegate = textFieldController _ = [inputFieldContainer, inputField, detailsLabel, nextButton] } @@ -51,88 +64,6 @@ private extension EmailLoginView { } } -// MARK: - UI fabric methdos - -private extension EmailLoginView { - func makeInputFieldContainer(on view: UIView) -> UIView { - let container = UIView() - view.addSubview(container) - - container.backgroundColor = UIColor.nynja.clear - - container.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(16) - make.height.equalTo(44) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - - return container - } - - func makeInputField(on view: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.attributedPlaceholder = NSAttributedString( - string: "Email".localized, - attributes: [NSAttributedStringKey.foregroundColor : UIColor.nynja.dustyGray]) - textField.textColor = UIColor.nynja.white - textField.font = FontFamily.NotoSans.medium.font(size: 16) - - textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) - - textField.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - return textField - } - - func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.text = "Enter your email adsress to receive the login code.".localized - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - - label.snp.makeConstraints { (make) in - make.height.equalTo(40) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.top.equalTo(top.snp.bottom) - } - - return label - } - - func makeNextButton(on view: UIView, top: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.layer.cornerRadius = 22 - button.setTitle("next".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.backgroundColor = UIColor.nynja.darkRed - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - - button.isEnabled = false - - button.snp.makeConstraints { (make) in - make.height.equalTo(44) - make.bottom.equalToSuperview().offset(-16) - make.top.equalTo(top.snp.bottom).offset(88) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - - return button - } -} - // MARK: - Validator private extension EmailLoginView { diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift index 4f1c500e4..55c53abd6 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift @@ -10,13 +10,33 @@ import Foundation final class LoginOptionsView: UIView, Configurable { - private lazy var switchLoginButton: UIButton = makeSwitchLoginButton(on: self, bottom: loginWithFacebook) - private lazy var loginWithFacebook: UIButton = makeLoginWithFacebookButton(on: self, bottom: loginWithGoogle) - private lazy var loginWithGoogle: UIButton = makeLoginWithGoogleButton(on: self) + private let viewsFactory: AuthViewsFactoryProtocol + + private lazy var switchLoginButton: UIButton = viewsFactory.makeSwitchLoginButton(on: self, + bottom: loginWithFacebook, + target: self, + selector: #selector(switchLogin(sender:))) + private lazy var loginWithFacebook: UIButton = viewsFactory.makeLoginWithFacebookButton(on: self, + bottom: loginWithGoogle, + target: self, + selector: #selector(loginWithFacebook(sender:))) + private lazy var loginWithGoogle: UIButton = viewsFactory.makeLoginWithGoogleButton(on: self, + target: self, + selector: #selector(loginWithGoogle(sender:))) private var switchLoginAction: (() -> LoginOption)? private var facebookLoginAction: (() -> Void)? private var googleLoginAction: (() -> Void)? + + init(viewsFactory: AuthViewsFactoryProtocol) { + self.viewsFactory = viewsFactory + + super.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } // MARK: - Configurable @@ -69,85 +89,11 @@ private extension LoginOptionsView { switch loginOption { case .email: switchLoginButton.setTitle("Log in with phone number".localized.uppercased(), for: .normal) - switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) + switchLoginButton.setImage(UIImage(named: "icons_general_ic_accept_call"), for: .normal) case .phoneNumber: switchLoginButton.setTitle("Log in with email".localized.uppercased(), for: .normal) - switchLoginButton.setImage(UIImage.backButtonImage, for: .normal) + switchLoginButton.setImage(UIImage(named: "icons_general_ic_email"), for: .normal) + default: break } } } - -// MARK: - UI fabric methods - -private extension LoginOptionsView { - func makeLoginWithGoogleButton(on view: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.backgroundColor = UIColor.nynja.white - button.setTitle("Log in with Google".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.subtitleGray, for: .normal) - button.setImage(UIImage.backButtonImage, for: .normal) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) - - button.layer.cornerRadius = 22 - - button.addTarget(self, action: #selector(loginWithGoogle(sender:)), for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.bottom.equalToSuperview().offset(-30) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - } - - return button - } - - func makeLoginWithFacebookButton(on view: UIView, bottom: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.backgroundColor = UIColor.nynja.dodgerBlue - button.setTitle("Log in with Facebook".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.setImage(UIImage.backButtonImage, for: .normal) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) - - button.layer.cornerRadius = 22 - - button.addTarget(self, action: #selector(loginWithFacebook(sender:)), for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.bottom.equalTo(bottom.snp.top).offset(-16) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - } - - return button - } - - func makeSwitchLoginButton(on view: UIView, bottom: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.backgroundColor = UIColor.nynja.mainRed - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) - - button.layer.cornerRadius = 22 - - button.addTarget(self, action: #selector(switchLogin(sender:)), for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(30) - make.bottom.equalTo(bottom.snp.top).offset(-16) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - } - - return button - } -} diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index bc9364173..2e7283dbf 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -10,22 +10,34 @@ import Foundation final class PhoneNumberLoginView: UIView, Configurable { - private lazy var countrySelector: UIButton = makeCountrySelector(on: self) + private lazy var countrySelector: UIButton = viewsFactory.makeCountrySelector(on: self, target: self, selector: #selector(changeCountry(sender:))) - private lazy var countryCodeContainer: UIView = makeCountryCodeContainer(on: self, top: countrySelector) - private lazy var countryCodeLabel: UILabel = makeCountryCodeLabel(on: countryCodeContainer) + private lazy var countryCodeContainer: UIView = viewsFactory.makeCountryCodeContainer(on: self, top: countrySelector) + private lazy var countryCodeLabel: UILabel = viewsFactory.makeCountryCodeLabel(on: countryCodeContainer) private var phoneNumberTextFieldController: TextFieldController? - private lazy var phoneNumberContainer: UIView = makePhoneNumberContainer(on: self, left: countryCodeLabel) - private lazy var phoneNumberTextField: UITextField = makePhoneNumberTextField(on: phoneNumberContainer) + private lazy var phoneNumberContainer: UIView = viewsFactory.makePhoneNumberContainer(on: self, left: countryCodeLabel) + private lazy var phoneNumberTextField: UITextField = viewsFactory.makePhoneNumberTextField(on: phoneNumberContainer) - private lazy var detailsLabel: UILabel = makeDetailsLabel(on: self, top: countryCodeContainer) - private lazy var nextButton: UIButton = makeNextButton(on: self, top: detailsLabel) + private lazy var detailsLabel: UILabel = viewsFactory.makeDetailsNumberLabel(on: self, top: countryCodeContainer) + private lazy var nextButton: UIButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) private var country: Country? private var countrySelectorAction: (() -> Void)? private var nextAction: ((String) -> Void)? + + private let viewsFactory: AuthViewsFactoryProtocol + + init(viewsFactory: AuthViewsFactoryProtocol) { + self.viewsFactory = viewsFactory + + super.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } // MARK: - Configurable @@ -40,9 +52,8 @@ extension PhoneNumberLoginView { func configure(config: PhoneNumberLoginView.Config) { country = config.country countrySelectorAction = config.countrySelectorAction - - phoneNumberTextFieldController = TextFieldController() - phoneNumberTextFieldController?.isFullFilelledAction = { [weak self] result in + nextAction = config.nextAction + phoneNumberTextFieldController = TextFieldController(template: config.country.numberTemplate) { [weak self] result in if result { self?.nextButton.backgroundColor = UIColor.nynja.mainRed } else { @@ -87,157 +98,19 @@ private extension PhoneNumberLoginView { } } -// MARK: - UI fabric methods - -extension PhoneNumberLoginView { - func makeCountrySelector(on view: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - button.setTitleColor(UIColor.nynja.white, for: .normal) - - button.addTarget(self, action: #selector(changeCountry(sender:)), for: .touchUpInside) - - button.titleLabel?.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) - - button.titleLabel?.snp.makeConstraints { (make) in - make.width.equalToSuperview() - } - - button.snp.makeConstraints { (make) in - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - make.top.equalToSuperview().offset(10) - } - - return button - } - - func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView { - let container = UIView() - view.addSubview(container) - - container.backgroundColor = UIColor.nynja.clear - - container.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom) - make.left.equalToSuperview().offset(16) - make.width.equalTo(100) - make.height.equalTo(64) - } - - return container - } - - func makeCountryCodeLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white - - label.snp.makeConstraints { (make) in - make.left.equalToSuperview() - make.right.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - - return label - } - - func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView { - let container = UIView() - view.addSubview(container) - - container.backgroundColor = UIColor.nynja.clear - - container.snp.makeConstraints { (make) in - make.height.equalTo(left.snp.height) - make.right.equalToSuperview().offset(-16) - make.left.equalTo(left.snp.right) - make.centerY.equalTo(left.snp.centerY) - } - - return container - } - - func makePhoneNumberTextField(on view: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) - - textField.font = FontFamily.NotoSans.medium.font(size: 16) - textField.textColor = UIColor.nynja.white - textField.keyboardType = .numberPad - - textField.snp.makeConstraints { (make) in - make.centerY.right.equalToSuperview() - make.left.equalToSuperview().offset(16) - } - - return textField - } - - func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.text = "Please choose your country code and enter your phone number.".localized - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - label.numberOfLines = 0 - - label.snp.makeConstraints { (make) in - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.top.equalTo(top.snp.bottom) - } - - return label - } - - func makeNextButton(on view: UIView, top: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.layer.cornerRadius = 22 - button.setTitle("next".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.backgroundColor = UIColor.nynja.darkRed - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - - button.isEnabled = false - - button.addTarget(self, action: #selector(next(sender:)), for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.height.equalTo(44) - make.bottom.equalToSuperview().offset(-16) - make.top.equalTo(top.snp.bottom).offset(24) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - - return button - } -} - // MARK: - Text field controller -extension PhoneNumberLoginView { +private extension PhoneNumberLoginView { final class TextFieldController: NSObject, UITextFieldDelegate { - var template: String? + var template: String var isFullFilelledAction: ((Bool) -> Void)? + init(template: String, isFullFilelledAction: ((Bool) -> Void)?) { + self.template = template + self.isFullFilelledAction = isFullFilelledAction + } + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - guard let template = template else { - return false - } - textField.text = textAfterUpdate(textField: textField, range: range, replacementString: string) .updateWithMask(placeHolder: template) diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift new file mode 100644 index 000000000..162b477b4 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -0,0 +1,501 @@ +// +// AuthViewsFactory.swift +// Nynja +// +// Created by Ash on 10/10/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AuthViewsFactoryProtocol { + + // MARK: - Auth View Controller + + func makeHeaderView(on view: UIView) -> AuthHeaderView + func makeScrollView(on view: UIView, top: UIView, bottom: UIView) -> UIScrollView + func makeScrollContentView(on view: UIView, baseView: UIView) -> UIView + func makeEmailLoginView(on view: UIView, presenter: AuthPresenterProtocol) -> EmailLoginView + func makePhoneNumberLoginView(on view: UIView, presenter: AuthPresenterProtocol, country: Country) -> PhoneNumberLoginView + func makeBottomView(on view: UIView, presenter: AuthPresenterProtocol, showEmailLoginAction: @escaping (Bool) -> Void, + showPhoneNumberLoginAction: @escaping (Bool) -> Void) -> LoginOptionsView + + // MARK: - Login Options View + + func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeLoginWithFacebookButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeSwitchLoginButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton + + // MARK: - AuthHeaderView + + func makeWelcomeLabel(on view: UIView) -> UILabel + func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView + + // MARK: - Email Login View + + func makeInputFieldContainer(on view: UIView) -> UIView + func makeInputField(on view: UIView) -> UITextField + func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel + func makeEmailNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton + + // MARK: - Phone Number Login View + + func makeCountrySelector(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView + func makeCountryCodeLabel(on view: UIView) -> UILabel + func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView + func makePhoneNumberTextField(on view: UIView) -> UITextField + func makeDetailsNumberLabel(on view: UIView, top: UIView) -> UILabel + func makeNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton +} + +final class AuthViewsFactory: AuthViewsFactoryProtocol { + + // MARK: - Auth View Controller + + func makeHeaderView(on view: UIView) -> AuthHeaderView { + let header = AuthHeaderView(viewsFactory: self) + header.configure(config: NSNull()) + + view.addSubview(header) + header.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + } + + return header + } + + func makeScrollView(on view: UIView, top: UIView, bottom: UIView) -> UIScrollView { + let scrollView = UIScrollView() + view.addSubview(scrollView) + + scrollView.snp.makeConstraints { (make) in + make.left.right.equalToSuperview() + make.bottom.equalTo(bottom.snp.top) + make.top.equalTo(top.snp.bottom) + } + + return scrollView + } + + func makeScrollContentView(on view: UIView, baseView: UIView) -> UIView { + let contentView = UIView() + view.addSubview(contentView) + + contentView.backgroundColor = UIColor.nynja.clear + + contentView.snp.makeConstraints { (make) in + make.right.left.top.bottom.equalToSuperview() + make.width.equalTo(baseView.snp.width) + make.height.equalTo(baseView.snp.height) + } + + return contentView + } + + func makeEmailLoginView(on view: UIView, presenter: AuthPresenterProtocol) -> EmailLoginView { + let loginView = EmailLoginView(viewsFactory: self) + view.addSubview(loginView) + + loginView.configure(config: EmailLoginView.Config(nextAction: { + presenter.loginViaEmail($0) { (result) in + print(#function) + } + })) + + loginView.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + } + + return loginView + } + + func makePhoneNumberLoginView(on view: UIView, presenter: AuthPresenterProtocol, country: Country) -> PhoneNumberLoginView { + let loginView = PhoneNumberLoginView(viewsFactory: self) + view.addSubview(loginView) + + loginView.configure(config: PhoneNumberLoginView.Config( + country: country, + countrySelectorAction: { + presenter.selectCountry { (result) in + result.onSuccess { loginView.updateCountry($0) } + } + }, + nextAction: { + presenter.loginViaPhoneNumber($0) { (result) in + print(#function) + } + })) + + loginView.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + } + + return loginView + } + + func makeBottomView(on view: UIView, presenter: AuthPresenterProtocol, showEmailLoginAction: @escaping (Bool) -> Void, + showPhoneNumberLoginAction: @escaping (Bool) -> Void) -> LoginOptionsView { + let bottom = LoginOptionsView(viewsFactory: self) + + bottom.configure(config: LoginOptionsView.Config( + loginOption: presenter.loginOption, + switchLoginAction: { [weak self] () -> LoginOption in + presenter.switchLoginOption() + let loginOption = presenter.loginOption + + switch loginOption { + case .email: showEmailLoginAction(true) + case .phoneNumber: showPhoneNumberLoginAction(true) + default: break + } + + return loginOption + }, + facebookLoginAction: { + presenter.loginViaFacebook { (result) in + print(#function) + } + }, + googleLoginAction: { + presenter.loginViaGoogle { (result) in + print(#function) + } + })) + + view.addSubview(bottom) + bottom.snp.makeConstraints { (make) in + make.bottom.left.right.equalToSuperview() + } + + return bottom + } + + // MARK: - Login Options View + + func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.backgroundColor = UIColor.nynja.white + button.setTitle("Log in with Google".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.subtitleGray, for: .normal) + button.setImage(UIImage(named: "icons_general_ic_google"), for: .normal) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + + button.layer.cornerRadius = 22 + + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.bottom.equalToSuperview().offset(-30) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + } + + return button + } + + func makeLoginWithFacebookButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.backgroundColor = UIColor.nynja.dodgerBlue + button.setTitle("Log in with Facebook".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.setImage(UIImage(named: "ic_facebook"), for: .normal) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + + button.layer.cornerRadius = 22 + + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.bottom.equalTo(bottom.snp.top).offset(-16) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + } + + return button + } + + func makeSwitchLoginButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.backgroundColor = UIColor.nynja.mainRed + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + + button.layer.cornerRadius = 22 + + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(30) + make.bottom.equalTo(bottom.snp.top).offset(-16) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + } + + return button + } + + // MARK: - AuthHeaderView + + func makeWelcomeLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.text = "Welcome to".localized + + label.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(70) + make.centerX.equalToSuperview() + } + + return label + } + + func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { + let imageView = UIImageView() + view.addSubview(imageView) + + imageView.contentMode = .scaleAspectFill + imageView.image = UIImage.logoImage + + imageView.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom).offset(16) + make.bottom.equalToSuperview().offset(-16) + make.centerX.equalToSuperview() + make.width.equalToSuperview().multipliedBy(9/20) + } + + return imageView + } + + // MARK: - Email Login View + + func makeInputFieldContainer(on view: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + + container.backgroundColor = UIColor.nynja.clear + + container.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(16) + make.height.equalTo(44) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + + return container + } + + func makeInputField(on view: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.attributedPlaceholder = NSAttributedString( + string: "Email".localized, + attributes: [NSAttributedStringKey.foregroundColor : UIColor.nynja.dustyGray]) + textField.textColor = UIColor.nynja.white + textField.font = FontFamily.NotoSans.medium.font(size: 16) + + textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + + textField.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + } + + return textField + } + + func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.text = "Enter your email adsress to receive the login code.".localized + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + + label.snp.makeConstraints { (make) in + make.height.equalTo(40) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom) + } + + return label + } + + func makeEmailNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.layer.cornerRadius = 22 + button.setTitle("next".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.backgroundColor = UIColor.nynja.darkRed + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + + button.isEnabled = false + + button.snp.makeConstraints { (make) in + make.height.equalTo(44) + make.bottom.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom).offset(88) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + + return button + } + + // MARK: - Phone Number Login View + + func makeCountrySelector(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.setTitleColor(UIColor.nynja.white, for: .normal) + + button.addTarget(target, action: selector, for: .touchUpInside) + + button.titleLabel?.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + + button.titleLabel?.snp.makeConstraints { (make) in + make.width.equalToSuperview() + } + + button.snp.makeConstraints { (make) in + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(44) + make.top.equalToSuperview().offset(10) + } + + return button + } + + func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + + container.backgroundColor = UIColor.nynja.clear + + container.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom) + make.left.equalToSuperview().offset(16) + make.width.equalTo(100) + make.height.equalTo(64) + } + + return container + } + + func makeCountryCodeLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.snp.makeConstraints { (make) in + make.left.equalToSuperview() + make.right.equalToSuperview().offset(-16) + make.centerY.equalToSuperview() + } + + return label + } + + func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + + container.backgroundColor = UIColor.nynja.clear + + container.snp.makeConstraints { (make) in + make.height.equalTo(left.snp.height) + make.right.equalToSuperview().offset(-16) + make.left.equalTo(left.snp.right) + make.centerY.equalTo(left.snp.centerY) + } + + return container + } + + func makePhoneNumberTextField(on view: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + + textField.font = FontFamily.NotoSans.medium.font(size: 16) + textField.textColor = UIColor.nynja.white + textField.keyboardType = .numberPad + + textField.snp.makeConstraints { (make) in + make.centerY.right.equalToSuperview() + make.left.equalToSuperview().offset(16) + } + + return textField + } + + func makeDetailsNumberLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.text = "Please choose your country code and enter your phone number.".localized + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + label.numberOfLines = 0 + + label.snp.makeConstraints { (make) in + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom) + } + + return label + } + + func makeNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.layer.cornerRadius = 22 + button.setTitle("next".localized.uppercased(), for: .normal) + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.backgroundColor = UIColor.nynja.darkRed + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + + button.isEnabled = false + + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.height.equalTo(44) + make.bottom.equalToSuperview().offset(-16) + make.top.equalTo(top.snp.bottom).offset(32) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + } + + return button + } +} diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index ce0861f5e..0dd258504 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -29,15 +29,13 @@ final class AuthWireframe: AuthWireframeProtocol { } func prepareModule(parameters: NSNull, dependencies: AuthWireframe.Dependencies) -> UIViewController { - let view = AuthViewController() let presenter = AuthPresenter() + let view = AuthViewController(dependencies: AuthViewController.Dependencies(presenter: presenter, viewsFactory: AuthViewsFactory())) let interactor = AuthInteractor() - let viewDep = AuthViewController.Dependencies(presenter: presenter) let presenterDep = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) let interactorDep = AuthInteractor.Dependencies(presenter: presenter) - view.inject(dependencies: viewDep) presenter.inject(dependencies: presenterDep) interactor.inject(dependencies: interactorDep) diff --git a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift index a2298304f..96f9d2cd0 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift @@ -9,7 +9,7 @@ import Foundation protocol CodeConfirmationWireframeProtocol: WireframeProtocol { - func codeValid() + func codeValid(with type: AuthenticationType) func codeInvalid() func back() } @@ -36,7 +36,7 @@ protocol CodeConfirmationInputInteractorProtocol { var address: String { get } var authProviderType: AuthProviderType { get } - func sendConfirmationCode(code: String, completion: (Result) -> Void) + func sendConfirmationCode(code: String, completion: (Result) -> Void) func resendCode() func askForCall() diff --git a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift new file mode 100644 index 000000000..e0513a49b --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift @@ -0,0 +1,15 @@ +// +// AuthType.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +enum AuthenticationType { + case register + case login +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index b10a83e5c..eaa401956 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -20,12 +20,8 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, self.authProviderType = authProviderType } - func sendConfirmationCode(code: String, completion: (Result) -> Void) { - completion(.success(())) - -// struct InnerError: Error {} -// -// completion(.failure(InnerError())) + func sendConfirmationCode(code: String, completion: (Result) -> Void) { + completion(.success(.register)) } func resendCode() { diff --git a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index 5aa9d1db9..8d8b43f8d 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -13,9 +13,13 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo private var view: CodeConfirmationViewProtocol? private var interactor: CodeConfirmationInputInteractorProtocol? private var wireframe: CodeConfirmationWireframe? - private var timerValue = 61 { + private var timerValue = 0 { didSet { - view?.updateTimerLabel(text: "You should receive it within \(timerValue) seconds.") + if timerValue > 60 { + view?.updateTimerLabel(text: "You should receive it within \((timerValue / 60) + 1) minutes.") + } else { + view?.updateTimerLabel(text: "You should receive it within \(timerValue) seconds.") + } if timerValue == 0 { view?.showButtons() @@ -52,21 +56,24 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } func viewDidLoad() { + switch interactor!.authProviderType { + case .email: timerValue = 15 * 60 + case .phoneNumber: timerValue = 60 + } + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.timerValue = self.timerValue - 1 } } func sendConfirmationCode(code: String, completion: (Result) -> Void) { - interactor?.sendConfirmationCode(code: code) { (result) in - result - .onSuccess { - wireframe?.codeValid() - completion(.success(())) - } - .onFailure { (error) in + interactor?.sendConfirmationCode(code: code) { + $0.onSuccess { + completion(.success(())) + wireframe?.codeValid(with: $0) + }.onFailure { + completion(.failure($0)) wireframe?.codeInvalid() - completion(.failure(error)) } } } diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift index 0b5ace9b9..2196824ba 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -9,167 +9,104 @@ import Foundation -final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, SetInjectable { - private final class TextFieldsController: NSObject, UITextFieldDelegate { - private var textFields: [UITextField] = [] - var allFieldsFilledAction: ((_ code: String) -> Void)? - - func add(textFields: [UITextField]) { - self.textFields.forEach { $0.delegate = nil } - self.textFields = textFields - self.textFields.forEach { $0.delegate = self } - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if string == "" { - textFields.previous(before: textField)?.becomeFirstResponder() - textField.text = string - return false - } - - var isNew = false - - if range.location > 0 || range.length > 0 { - if let newTextField = textFields.next(after: textField) { - newTextField.becomeFirstResponder() - newTextField.text = string - isNew = true - } else { - if isAllFieldsFilled() { - allFieldsFilledAction?(allValues()) - } - - textField.text = String(string.last ?? Character("")) - - return false - } - } - - if isAllFieldsFilled() { - allFieldsFilledAction?(allValues()) - } - - if string.count == 1 && !isNew { - textField.text = string - } - - return false - } - - func textFieldDidEndEditing(_ textField: UITextField) { - print("end") - } - - private func isAllFieldsFilled() -> Bool { - return textFields - .filter { ($0.text ?? "").count == 0 } - .count < 1 - } - - private func allValues() -> String { - return textFields - .map { $0.text } - .compactMap { $0 } - .joined() - } - } +final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, InitializeInjectable { + private let viewsFactory: CodeConfirmationViewsFactoryProtocol + private let presenter: CodeConfirmationPresenterProtocol - private var presenter: CodeConfirmationPresenterProtocol? - private weak var backButton: UIButton! - private weak var welcomeLabel: UILabel! - private weak var logoImageView: UIImageView! - private weak var addressLabel: UILabel! + private lazy var backButton: UIButton = viewsFactory.makeBackButton(on: view, target: self, selector: #selector(back(sender:))) + private lazy var welcomeLabel: UILabel = viewsFactory.makeWelcomeLabel(on: view) + private lazy var logoImageView: UIImageView = viewsFactory.makeLogoImageView(on: view, top: welcomeLabel) + private lazy var addressLabel: UILabel = viewsFactory.makeAddressLabel(on: view, top: logoImageView) - private weak var textFieldsContainer: UIView! - private var textFieldsController: TextFieldsController = TextFieldsController() + private lazy var textFieldsContainer: UIView = viewsFactory.makeTextFieldsContainer(on: view, top: addressLabel) + private let textFieldsController: TextFieldsController - private weak var textField1: UITextField! - private weak var textField2: UITextField! - private weak var textField3: UITextField! - private weak var textField4: UITextField! - private weak var textField5: UITextField! - private weak var textField6: UITextField! + private lazy var textField1: UITextField = viewsFactory.makeFirstTextField(on: textFieldsContainer) + private lazy var textField2: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField1) + private lazy var textField3: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField2) + private lazy var textField4: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField3) + private lazy var textField5: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField4) + private lazy var textField6: UITextField = viewsFactory.makeLastTextField(on: textFieldsContainer, left: textField5) - private weak var descriptionLabel: UILabel! + private lazy var descriptionLabel: UILabel = viewsFactory.makeDescriptionLabel(on: view, top: textFieldsContainer) - private weak var timerLabel: UILabel? + private lazy var timerLabel: UILabel = viewsFactory.makeTimerLabel(on: view) - private weak var resendCodeButton: UIButton! + private weak var resendCodeButton: UIButton? private weak var callMeButton: UIButton? + init(dependencies: Dependencies) { + presenter = dependencies.presenter + viewsFactory = dependencies.viewsFactory + textFieldsController = TextFieldsController() + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() + _ = [backButton, welcomeLabel, logoImageView, addressLabel, textFieldsContainer, textField1, textField2, textField3, textField4, textField5, textField6, descriptionLabel, timerLabel, resendCodeButton, callMeButton] view.backgroundColor = UIColor.nynja.backgroundColor - backButton = makeBackButton(on: view) - welcomeLabel = makeWelcomeLabel(on: view) - logoImageView = makeLogoImageView(on: view, top: welcomeLabel) - addressLabel = makeAddressLabel(on: view, top: logoImageView) - textFieldsContainer = makeTextFieldsContainer(on: view, top: addressLabel) - - textField1 = makeFirstTextField(on: textFieldsContainer) - textField2 = makeMiddleTextField(on: textFieldsContainer, left: textField1) - textField3 = makeMiddleTextField(on: textFieldsContainer, left: textField2) - textField4 = makeMiddleTextField(on: textFieldsContainer, left: textField3) - textField5 = makeMiddleTextField(on: textFieldsContainer, left: textField4) - textField6 = makeLastTextField(on: textFieldsContainer, left: textField5) - - descriptionLabel = makeDescriptionLabel(on: view, top: textFieldsContainer) - - timerLabel = makeTimerLabel(on: view) - - addressLabel.text = presenter?.address - descriptionLabel.text = presenter?.descriptionText + addressLabel.text = presenter.address + descriptionLabel.text = presenter.descriptionText textField1.becomeFirstResponder() view.layoutIfNeeded() - appendBottomBorder(to: [textField1, textField2, textField3, textField4, textField5, textField6]) + [textField1, textField2, textField3, textField4, textField5, textField6] + .forEach { $0.appendBottomBorder(color: UIColor.nynja.mainRed, width: 2) } textFieldsController.add(textFields: [textField1, textField2, textField3, textField4, textField5, textField6]) textFieldsController.allFieldsFilledAction = { [weak self] code in self?.showHUD() - self?.presenter?.sendConfirmationCode(code: code) { _ in self?.hideHUD()} + self?.presenter.sendConfirmationCode(code: code) { _ in self?.hideHUD()} } - presenter?.viewDidLoad() + presenter.viewDidLoad() } - func showHUD() { - - } - - func hideHUD() { - + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent } - +} + +// MARK: - CodeConfirmationViewProtocol + +extension CodeConfirmationViewController { func updateTimerLabel(text: String) { - timerLabel?.text = text + timerLabel.text = text } func showButtons() { - timerLabel?.isHidden = true - resendCodeButton = makeResendCodeButton(on: view) + timerLabel.isHidden = true + resendCodeButton = viewsFactory.makeResendCodeButton(on: view, target: self, selector: #selector(resendCode(sender:))) - if presenter?.isCanAskForCall ?? false { - callMeButton = makeCallMeButton(on: view, top: resendCodeButton) + if presenter.isCanAskForCall { + callMeButton = viewsFactory.makeCallMeButton(on: view, top: resendCodeButton!, target: self, selector: #selector(callMe(sender:))) } } } +// MARK: - Actions + extension CodeConfirmationViewController { @objc func back(sender: UIButton) { - presenter?.back() + presenter.back() } @objc func resendCode(sender: UIButton) { - presenter?.resendCode() + presenter.resendCode() } @objc func callMe(sender: UIButton) { - presenter?.askForCall() + presenter.askForCall() } } @@ -178,222 +115,82 @@ extension CodeConfirmationViewController { extension CodeConfirmationViewController { struct Dependencies { let presenter: CodeConfirmationPresenterProtocol - } - - func inject(dependencies: CodeConfirmationViewController.Dependencies) { - presenter = dependencies.presenter + let viewsFactory: CodeConfirmationViewsFactoryProtocol } } -// MARK: - UI fabric methods +// MARK: - Private private extension CodeConfirmationViewController { - func appendBottomBorder(to textFields: [UITextField]) { - textFields.forEach { $0.appendBottomBorder(color: UIColor.nynja.mainRed, width: 2) } - } - - func makeBackButton(on view: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setImage(UIImage.backButtonImage, for: .normal) - button.addTarget(self, action: #selector(back(sender:)), for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.left.equalToSuperview().offset(8) - make.top.equalToSuperview().offset(37) - make.width.height.equalTo(24) - } - - return button - } - - func makeWelcomeLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white - - label.text = "Welcome to".localized - - label.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(70) - make.centerX.equalToSuperview() - } - - return label - } - - func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { - let imageView = UIImageView() - view.addSubview(imageView) - - imageView.contentMode = .scaleAspectFill - imageView.image = UIImage.logoImage - - imageView.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom).offset(16) - make.centerX.equalToSuperview() - make.width.equalToSuperview().multipliedBy(9/20) - } - - return imageView - } - - func makeAddressLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white - - label.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom).offset(42) - make.centerX.equalToSuperview() - } - - return label - } - - func makeTextFieldsContainer(on view: UIView, top: UIView) -> UIView { - let container = UIView() - view.addSubview(container) - container.snp.makeConstraints { (make) in - make.height.equalTo(64) - make.centerX.equalToSuperview() - make.top.equalTo(top.snp.bottom).offset(16) - } - - return container - } - - func makeFirstTextField(on view: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.keyboardType = .numberPad - textField.tintColor = UIColor.nynja.mainRed - textField.textAlignment = .center - textField.textColor = UIColor.nynja.white - - textField.snp.makeConstraints { (make) in - make.left.equalToSuperview() - make.centerY.equalToSuperview() - make.width.equalTo(36) - } - - return textField - } - - func makeMiddleTextField(on view: UIView, left: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.keyboardType = .numberPad - textField.tintColor = UIColor.nynja.mainRed - textField.textAlignment = .center - textField.textColor = UIColor.nynja.white - - textField.snp.makeConstraints { (make) in - make.left.equalTo(left.snp.right).offset(5) - make.centerY.equalToSuperview() - make.width.equalTo(36) - } + func showHUD() { - return textField } - func makeLastTextField(on view: UIView, left: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.keyboardType = .numberPad - textField.tintColor = UIColor.nynja.mainRed - textField.textAlignment = .center - textField.textColor = UIColor.nynja.white - - textField.snp.makeConstraints { (make) in - make.left.equalTo(left.snp.right).offset(5) - make.centerY.equalToSuperview() - make.width.equalTo(36) - make.right.equalToSuperview() - } + func hideHUD() { - return textField } - - func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.manatee +} + +// MARK: - Text field controller + +private extension CodeConfirmationViewController { + final class TextFieldsController: NSObject, UITextFieldDelegate { + private var textFields: [UITextField] = [] + var allFieldsFilledAction: ((_ code: String) -> Void)? - label.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom) - make.centerX.equalToSuperview() + func add(textFields: [UITextField]) { + self.textFields.forEach { $0.delegate = nil } + self.textFields = textFields + self.textFields.forEach { $0.delegate = self } } - return label - } - - func makeTimerLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - label.textAlignment = .center - label.textColor = UIColor.nynja.white - label.font = FontFamily.NotoSans.regular.font(size: 16) - - label.snp.makeConstraints { (make) in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-300) - make.width.lessThanOrEqualToSuperview() + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string == "" { + textFields.previous(before: textField)?.becomeFirstResponder() + textField.text = string + return false + } + + var isNew = false + + if range.location > 0 || range.length > 0 { + if let newTextField = textFields.next(after: textField) { + newTextField.becomeFirstResponder() + newTextField.text = string + isNew = true + } else { + if isAllFieldsFilled() { + allFieldsFilledAction?(allValues()) + } + + textField.text = String(string.last ?? Character("")) + + return false + } + } + + if isAllFieldsFilled() { + allFieldsFilledAction?(allValues()) + } + + if string.count == 1 && !isNew { + textField.text = string + } + + return false } - return label - } - - func makeResendCodeButton(on view: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setTitle("Resend code".localized, for: .normal) - button.setTitleColor(UIColor.nynja.mainRed, for: .normal) - button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - button.addTarget(self, action: #selector(resendCode(sender:)), for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-300) + private func isAllFieldsFilled() -> Bool { + return textFields + .filter { ($0.text ?? "").count == 0 } + .count < 1 } - return button - } - - func makeCallMeButton(on view: UIView, top: UIView) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setTitle("Call me".localized, for: .normal) - button.setTitleColor(UIColor.nynja.mainRed, for: .normal) - button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - button.addTarget(self, action: #selector(callMe(sender:)), for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.centerX.equalToSuperview() - make.top.equalTo(top.snp.bottom).offset(10) - + private func allValues() -> String { + return textFields + .map { $0.text } + .compactMap { $0 } + .joined() } - - return button } } - -// MARK: - Layout - -extension CodeConfirmationViewController { - -} diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift b/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift new file mode 100644 index 000000000..987d18780 --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift @@ -0,0 +1,225 @@ +// +// CodeConfirmationViewsFactory.swift +// Nynja +// +// Created by Ash on 10/10/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol CodeConfirmationViewsFactoryProtocol { + func makeBackButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeWelcomeLabel(on view: UIView) -> UILabel + func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView + func makeAddressLabel(on view: UIView, top: UIView) -> UILabel + func makeTextFieldsContainer(on view: UIView, top: UIView) -> UIView + func makeFirstTextField(on view: UIView) -> UITextField + func makeMiddleTextField(on view: UIView, left: UIView) -> UITextField + func makeLastTextField(on view: UIView, left: UIView) -> UITextField + func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel + func makeTimerLabel(on view: UIView) -> UILabel + func makeResendCodeButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeCallMeButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton +} + +final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { + func makeBackButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setImage(UIImage.backButtonImage, for: .normal) + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.left.equalToSuperview().offset(8) + make.top.equalToSuperview().offset(37) + make.width.height.equalTo(24) + } + + return button + } + + func makeWelcomeLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.text = "Welcome to".localized + + label.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(70) + make.centerX.equalToSuperview() + } + + return label + } + + func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { + let imageView = UIImageView() + view.addSubview(imageView) + + imageView.contentMode = .scaleAspectFill + imageView.image = UIImage.logoImage + + imageView.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom).offset(16) + make.centerX.equalToSuperview() + make.width.equalToSuperview().multipliedBy(9/20) + } + + return imageView + } + + func makeAddressLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.medium.font(size: 16) + label.textColor = UIColor.nynja.white + + label.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom).offset(42) + make.centerX.equalToSuperview() + } + + return label + } + + func makeTextFieldsContainer(on view: UIView, top: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + container.snp.makeConstraints { (make) in + make.height.equalTo(64) + make.centerX.equalToSuperview() + make.top.equalTo(top.snp.bottom).offset(16) + } + + return container + } + + func makeFirstTextField(on view: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.keyboardType = .numberPad + textField.tintColor = UIColor.nynja.mainRed + textField.textAlignment = .center + textField.textColor = UIColor.nynja.white + + textField.snp.makeConstraints { (make) in + make.left.equalToSuperview() + make.centerY.equalToSuperview() + make.width.equalTo(36) + } + + return textField + } + + func makeMiddleTextField(on view: UIView, left: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.keyboardType = .numberPad + textField.tintColor = UIColor.nynja.mainRed + textField.textAlignment = .center + textField.textColor = UIColor.nynja.white + + textField.snp.makeConstraints { (make) in + make.left.equalTo(left.snp.right).offset(5) + make.centerY.equalToSuperview() + make.width.equalTo(36) + } + + return textField + } + + func makeLastTextField(on view: UIView, left: UIView) -> UITextField { + let textField = UITextField() + view.addSubview(textField) + + textField.keyboardType = .numberPad + textField.tintColor = UIColor.nynja.mainRed + textField.textAlignment = .center + textField.textColor = UIColor.nynja.white + + textField.snp.makeConstraints { (make) in + make.left.equalTo(left.snp.right).offset(5) + make.centerY.equalToSuperview() + make.width.equalTo(36) + make.right.equalToSuperview() + } + + return textField + } + + func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.manatee + + label.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom) + make.centerX.equalToSuperview() + } + return label + } + + func makeTimerLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.textAlignment = .center + label.textColor = UIColor.nynja.white + label.font = FontFamily.NotoSans.regular.font(size: 16) + + label.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-300) + make.width.lessThanOrEqualToSuperview() + } + + return label + } + + func makeResendCodeButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setTitle("Resend code".localized, for: .normal) + button.setTitleColor(UIColor.nynja.mainRed, for: .normal) + button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-300) + } + + return button + } + + func makeCallMeButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setTitle("Call me".localized, for: .normal) + button.setTitleColor(UIColor.nynja.mainRed, for: .normal) + button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.top.equalTo(top.snp.bottom).offset(10) + + } + + return button + } +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index b5b3c1d41..b4adfec5a 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -34,29 +34,28 @@ final class CodeConfirmationWireframe: CodeConfirmationWireframeProtocol { } enum State { - case validCode + case validCode(type: AuthenticationType) case invalidCode case back } func prepareModule(parameters: CodeConfirmationWireframe.Parameters, dependencies: CodeConfirmationWireframe.Dependencies) -> UIViewController { - let view = CodeConfirmationViewController() let presenter = CodeConfirmationPresenter() + let viewDep = CodeConfirmationViewController.Dependencies(presenter: presenter, viewsFactory: CodeConfirmationViewsFactory()) + let view = CodeConfirmationViewController(dependencies: viewDep) let interactor = CodeConfirmationInteractor(address: parameters.address, authProviderType: parameters.authType) - let viewDep = CodeConfirmationViewController.Dependencies(presenter: presenter) let presenterDep = CodeConfirmationPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) let interactorDep = CodeConfirmationInteractor.Dependencies(presenter: presenter) - view.inject(dependencies: viewDep) presenter.inject(dependencies: presenterDep) interactor.inject(dependencies: interactorDep) return view } - func codeValid() { - coordinator.wireframe(self, didEndWith: .validCode) + func codeValid(with type: AuthenticationType) { + coordinator.wireframe(self, didEndWith: .validCode(type: type)) } func codeInvalid() { diff --git a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift index a254c6018..e1759e46b 100644 --- a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift +++ b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift @@ -9,26 +9,50 @@ import Foundation -final class CountrySelectorViewController: UIViewController, CountrySelectorViewProtocol, SetInjectable, UITableViewDelegate, UITableViewDataSource { - private var presenter: CountrySelectorPresenterProtocol? +final class CountrySelectorViewController: UIViewController, CountrySelectorViewProtocol, InitializeInjectable, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate, KeyboardInteractive { + private let presenter: CountrySelectorPresenterProtocol + private let viewsFactory: CountrySelectorViewsFactoryProtocol - private var headerView: NavigationView? - private var tableView: UITableView? - private var controlContainerView: NynjaControlContainerView? + private lazy var topHeaderLayoutGuide: UILayoutGuide = viewsFactory.makeTopLayoutGuide(on: view) + private lazy var headerView: NavigationView = viewsFactory.makeHeaderView(on: view, layoutGuide: topHeaderLayoutGuide, presenter: presenter) + private lazy var tableView: UITableView = viewsFactory.makeTableView(on: view, top: headerView, bottom: controlContainerView, delegate: self) + private lazy var controlContainerView: NynjaControlContainerView = viewsFactory.makeControlContainerView(on: view, searchField: searchField, verticalInsetCalculationAction: adjustVerticalInset) - private var searchField: NynjaSearchField? + private lazy var searchField: NynjaSearchField = viewsFactory.makeSearchField(presenter: presenter) + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + viewsFactory = dependencies.viewsFactory + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func viewDidLoad() { super.viewDidLoad() - + view.backgroundColor = UIColor.nynja.darkLight + + _ = [topHeaderLayoutGuide, headerView, tableView, controlContainerView, searchField] - headerView = makeHeaderView(on: view) - searchField = makeSearchField() - controlContainerView = makeControlContainerView(on: view, searchField: searchField!) - tableView = makeTableView(on: view, top: headerView!, bottom: controlContainerView!) - - presenter?.viewDidLoad() + presenter.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unregisterForKeyboardNotifications() + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent } } @@ -37,10 +61,7 @@ final class CountrySelectorViewController: UIViewController, CountrySelectorView extension CountrySelectorViewController { struct Dependencies { let presenter: CountrySelectorPresenterProtocol - } - - func inject(dependencies: CountrySelectorViewController.Dependencies) { - presenter = dependencies.presenter + let viewsFactory: CountrySelectorViewsFactoryProtocol } } @@ -48,7 +69,7 @@ extension CountrySelectorViewController { extension CountrySelectorViewController { func reloadData() { - tableView?.reloadData() + tableView.reloadData() } } @@ -57,10 +78,9 @@ extension CountrySelectorViewController { extension CountrySelectorViewController { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let view = CountryHeaderTVView() + let section = presenter.sections[section] - if let section = presenter?.sections[section] { - view.configure(config: CountryHeaderTVView.Config(symbol: section.symbol)) - } + view.configure(config: CountryHeaderTVView.Config(symbol: section.symbol)) return view } @@ -74,9 +94,7 @@ extension CountrySelectorViewController { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let country = countryWithIndexPath(indexPath) { - presenter?.selectCountry(country) - } + presenter.selectCountry(countryWithIndexPath(indexPath)) } } @@ -84,14 +102,11 @@ extension CountrySelectorViewController { extension CountrySelectorViewController { func numberOfSections(in tableView: UITableView) -> Int { - return presenter?.sections.count ?? 0 + return presenter.sections.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - guard let sections = presenter?.sections else { - return 0 - } - + let sections = presenter.sections let currentSection = sections[section] let countriesInSection = currentSection.countries @@ -103,95 +118,43 @@ extension CountrySelectorViewController { fatalError() } - if let country = countryWithIndexPath(indexPath) { - cell.configure(config: CountryTVCell.Config(name: country.name, code: country.code)) - } + let country = countryWithIndexPath(indexPath) + cell.configure(config: CountryTVCell.Config(name: country.name, code: country.code)) return cell } } -private extension CountrySelectorViewController { - func sectionWithIndexPath(_ indexPath: IndexPath) -> CountriesSection? { - return presenter?.sections[indexPath.section] - } - - func countryWithIndexPath(_ indexPath: IndexPath) -> Country? { - return sectionWithIndexPath(indexPath)?.countries[indexPath.row] +// MARK: - UIScrollViewDelegate + +extension CountrySelectorViewController { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let verticalIndicator = scrollView.subviews[scrollView.subviews.count - 1] + + verticalIndicator.backgroundColor = UIColor.nynja.mainRed } } -// MARK: - UI factory methods +// MARK: - KeyboardInteractive -private extension CountrySelectorViewController { - func makeHeaderView(on view: UIView) -> NavigationView { - let config = NavigationView.Config( - isVisibleSeparator: true, - isVisibleBackButton: true, - title: "select country".uppercased(), - navigationHandler: presenter, - backButtonImage: UIImage.backButtonImage) - - return UIView.makeHeaderView(on: view, top: nil, config: config) - } - - func makeTableView(on view: UIView, top: UIView, bottom: UIView) -> UITableView { - let tableView = UITableView() - view.addSubview(tableView) - - tableView.keyboardDismissMode = .interactive - tableView.backgroundColor = UIColor.nynja.clear - tableView.separatorStyle = .singleLine - tableView.tableFooterView = UIView() - tableView.showsHorizontalScrollIndicator = false - tableView.rowHeight = 52 - tableView.estimatedRowHeight = tableView.rowHeight - - tableView.delegate = self - tableView.dataSource = self - - tableView.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom) - make.bottom.equalTo(bottom.snp.top) - make.left.right.equalToSuperview() - } - - tableView.register(CountryTVCell.self, - forCellReuseIdentifier: CountryTVCell.identifier) - - return tableView - } - - func makeControlContainerView(on view: UIView, searchField: NynjaSearchField) -> NynjaControlContainerView { - let containerView = NynjaControlContainerView(contentView: searchField) - view.addSubview(containerView) - - containerView.snp.makeConstraints { make in - make.left.right.equalToSuperview() - adjustVerticalInset(.bottom, make: make, offset: -28) - } - - containerView.addGradientView() - - return containerView - } - - func makeSearchField() -> NynjaSearchField { - let searchField = NynjaSearchField() - - searchField.searchTextChangeHandler = { [weak self] searchQuery in - let filter = searchQuery ?? "" - self?.presenter?.applyFilter(filter) +extension CountrySelectorViewController { + func keyboardNotified(endFrame: CGRect) { + if endFrame.origin.y >= UIScreen.main.bounds.size.height { + updateToHide(view: controlContainerView, offset: -28) + } else { + updateToShow(view: controlContainerView, offset: -28 - endFrame.height) } - - return searchField } } -// MARK: - Layout +// MARK: - Private private extension CountrySelectorViewController { - enum ControlContainerLayout { - + func sectionWithIndexPath(_ indexPath: IndexPath) -> CountriesSection { + return presenter.sections[indexPath.section] + } + + func countryWithIndexPath(_ indexPath: IndexPath) -> Country { + return sectionWithIndexPath(indexPath).countries[indexPath.row] } } diff --git a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift new file mode 100644 index 000000000..7275222e8 --- /dev/null +++ b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift @@ -0,0 +1,96 @@ +// +// CountrySelectorViewsFactory.swift +// Nynja +// +// Created by Ash on 10/10/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import SnapKit + +typealias VerticalInsetCalculationAction = (_ inset: AdjustedInset, _ make: ConstraintMaker, _ offset: Int) -> Void + +protocol CountrySelectorViewsFactoryProtocol { + func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide + func makeHeaderView(on view: UIView, layoutGuide: UILayoutGuide, presenter: CountrySelectorPresenterProtocol) -> NavigationView + func makeTableView(on view: UIView, top: UIView, bottom: UIView, delegate: UITableViewDelegate & UITableViewDataSource) -> UITableView + func makeControlContainerView(on view: UIView, searchField: NynjaSearchField, verticalInsetCalculationAction: VerticalInsetCalculationAction) -> NynjaControlContainerView + func makeSearchField(presenter: CountrySelectorPresenterProtocol) -> NynjaSearchField +} + +final class CountrySelectorViewsFactory: CountrySelectorViewsFactoryProtocol { + func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide { + let layoutGuide = UILayoutGuide() + view.addLayoutGuide(layoutGuide) + + layoutGuide.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.height.equalTo(20 + UIWindow.safeAreaTopPadding()) + } + + return layoutGuide + } + + func makeHeaderView(on view: UIView, layoutGuide: UILayoutGuide, presenter: CountrySelectorPresenterProtocol) -> NavigationView { + let config = NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: "select country".uppercased(), + navigationHandler: presenter, + backButtonImage: UIImage.backButtonImage) + + return UIView.makeHeaderView(on: view, top: layoutGuide, config: config) + } + + func makeTableView(on view: UIView, top: UIView, bottom: UIView, delegate: UITableViewDelegate & UITableViewDataSource) -> UITableView { + let tableView = UITableView() + view.addSubview(tableView) + + tableView.keyboardDismissMode = .interactive + tableView.backgroundColor = UIColor.nynja.clear + tableView.separatorStyle = .singleLine + tableView.tableFooterView = UIView() + tableView.showsHorizontalScrollIndicator = false + tableView.rowHeight = 52 + tableView.estimatedRowHeight = tableView.rowHeight + + tableView.delegate = delegate + tableView.dataSource = delegate + + tableView.snp.makeConstraints { (make) in + make.top.equalTo(top.snp.bottom) + make.bottom.equalTo(bottom.snp.top) + make.left.right.equalToSuperview() + } + + tableView.register(CountryTVCell.self, + forCellReuseIdentifier: CountryTVCell.identifier) + + return tableView + } + + func makeControlContainerView(on view: UIView, searchField: NynjaSearchField, verticalInsetCalculationAction: VerticalInsetCalculationAction) -> NynjaControlContainerView { + let containerView = NynjaControlContainerView(contentView: searchField) + view.addSubview(containerView) + + containerView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + verticalInsetCalculationAction(.bottom, make, -28) + } + + containerView.addGradientView() + + return containerView + } + + func makeSearchField(presenter: CountrySelectorPresenterProtocol) -> NynjaSearchField { + let searchField = NynjaSearchField() + + searchField.searchTextChangeHandler = { [weak self] searchQuery in + presenter.applyFilter(searchQuery ?? "") + } + + return searchField + } +} diff --git a/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift b/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift index f742d2a08..222c5a2dd 100644 --- a/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift +++ b/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift @@ -31,15 +31,14 @@ final class CountrySelectorWireframe: CountrySelectorWireframeProtocol { } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { - let view = CountrySelectorViewController() let presenter = CountrySelectorPresenter() + let viewDep = CountrySelectorViewController.Dependencies(presenter: presenter, viewsFactory: CountrySelectorViewsFactory()) + let view = CountrySelectorViewController(dependencies: viewDep) let interactor = CountrySelectorInteractor() - let viewDep = CountrySelectorViewController.Dependencies(presenter: presenter) let presenterDep = CountrySelectorPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) let interactorDep = CountrySelectorInteractor.Dependencies(presenter: presenter, storageService: dependencies.storageService) - view.inject(dependencies: viewDep) presenter.inject(dependencies: presenterDep) interactor.inject(dependencies: interactorDep) diff --git a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift new file mode 100644 index 000000000..3b767f395 --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift @@ -0,0 +1,64 @@ +// +// CreateProfileProtocols.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum ProfileField { + case firstName + case lastName + case accountName + case userName + + var isRequired: Bool { + return self == .firstName + } + + var validationRule: String { + return "^([a-zA-Z]|[0-9]|_){2,}$" + } + + var placeholder: String { + switch self { + case .firstName: return "First Name" + case .lastName: return "Last Name" + case .accountName: return "Account Name" + case .userName: return "Username" + } + } +} + +protocol CreateProfileWireframeProtocol: WireframeProtocol { + func back() + func end() + func chooseAvatar(completion: @escaping (UIImage?) -> Void) +} + +protocol CreateProfileViewProtocol: class where Self: UIViewController { + func updateProfileField(_ field: ProfileField, value: String) + func setCreateEnabled(_ enabled: Bool) +} + +protocol CreateProfilePresenterProtocol: NavigationProtocol { + func createAccount() + func isValidValue(_ value: String, for field: ProfileField) -> Result + func setProfileField(value: String, field: ProfileField) + func chooseAvatar(completion: @escaping (UIImage?) -> Void) + func checkTermsOfUse() -> Bool +} + +protocol CreateProfileInputInteractorProtocol { + func setProfileField(_ field: ProfileField, value: String) + func checkTermsOfUse() -> Bool + func setAvatar(image: UIImage?) + func isValidValue(_ value: String, for field: ProfileField) -> Result +} + +protocol CreateProfileOutputInteractorProtocol { + func minimalRequirementsAreSatisfied(_ satisfied: Bool) + func profileFieldUpdated(_ profileField: ProfileField, value: String) +} diff --git a/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift new file mode 100644 index 000000000..c91a40722 --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -0,0 +1,110 @@ +// +// CreateProfileInteractor.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, SetInjectable { + private var presenter: CreateProfileOutputInteractorProtocol? + + private var checkTermsOfUsage: Bool = false { + didSet { + presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) + } + } + + private var avatar: UIImage? = nil + + private var firstName: String = "" { + didSet { + if oldValue == userName { + userName = firstName + } + + presenter?.profileFieldUpdated(.firstName, value: firstName) + presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) + } + } + private var lastName: String = "" { + didSet { + presenter?.profileFieldUpdated(.lastName, value: lastName) + presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) + } + } + + private var accountName: String = "" { + didSet { + presenter?.profileFieldUpdated(.accountName, value: accountName) + presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) + } + } + private var userName: String = "" { + didSet { + presenter?.profileFieldUpdated(.userName, value: userName) + presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) + } + } +} + +// MARK: - CreateProfileInputInteractorProtocol + +extension CreateProfileInteractor { + func checkTermsOfUse() -> Bool { + checkTermsOfUsage = !checkTermsOfUsage + + return checkTermsOfUsage + } + + func setAvatar(image: UIImage?) { + avatar = image + } + + func setProfileField(_ field: ProfileField, value: String) { + switch field { + case .firstName: firstName = value + case .lastName: lastName = value + case .accountName: accountName = value + case .userName: userName = value + } + } + + func isValidValue(_ value: String, for field: ProfileField) -> Result { + enum ValidationError: Error { + case somethingWentWrong + } + + let predicate = NSPredicate(format:"SELF MATCHES %@", field.validationRule) + + return predicate.evaluate(with: value) + ? .success(()) + : .failure(ValidationError.somethingWentWrong) + } +} + +// MARK: - SetInjectable + +extension CreateProfileInteractor { + struct Dependencies { + let presenter: CreateProfileOutputInteractorProtocol + } + + func inject(dependencies: CreateProfileInteractor.Dependencies) { + presenter = dependencies.presenter + } +} + +// MARK: - Private + +private extension CreateProfileInteractor { + func isAllRequirementsAreSatisfied() -> Bool { + return checkTermsOfUsage + && isValidValue(firstName, for: .firstName).isSuccess + && firstName.count >= 2 + && isValidValue(userName, for: .userName).isSuccess + && userName.count >= 2 + } +} diff --git a/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift new file mode 100644 index 000000000..492cb6ac0 --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -0,0 +1,71 @@ +// +// CreateProfilePresenter.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, CreateProfilePresenterProtocol, SetInjectable { + private var wireframe: CreateProfileWireframe? + private var interactor: CreateProfileInputInteractorProtocol? + private weak var view: CreateProfileViewProtocol? +} + +// MARK: - CreateProfileOutputInteractorProtocol & CreateProfilePresenterProtocol + +extension CreateProfilePresenter { + func isValidValue(_ value: String, for field: ProfileField) -> Result { + return interactor?.isValidValue(value, for: field) ?? .success(()) + } + + func profileFieldUpdated(_ profileField: ProfileField, value: String) { + view?.updateProfileField(profileField, value: value) + } + + func createAccount() { + wireframe?.end() + } + + func setProfileField(value: String, field: ProfileField) { + interactor?.setProfileField(field, value: value) + } + + func chooseAvatar(completion: @escaping (UIImage?) -> Void) { + wireframe?.chooseAvatar(completion: { (image) in + completion(image) + self.interactor?.setAvatar(image: image) + }) + } + + func checkTermsOfUse() -> Bool { + return interactor?.checkTermsOfUse() ?? false + } + + func minimalRequirementsAreSatisfied(_ satisfied: Bool) { + view?.setCreateEnabled(satisfied) + } + + func back() { + wireframe?.back() + } +} + +// MARK: - SetInjectable + +extension CreateProfilePresenter { + struct Dependencies { + let wireframe: CreateProfileWireframe + let interactor: CreateProfileInputInteractorProtocol + let view: CreateProfileViewProtocol + } + + func inject(dependencies: CreateProfilePresenter.Dependencies) { + wireframe = dependencies.wireframe + interactor = dependencies.interactor + view = dependencies.view + } +} diff --git a/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift new file mode 100644 index 000000000..96c9fcc7a --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift @@ -0,0 +1,104 @@ +// +// CreateProfileViewController.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class CreateProfileViewController: UIViewController, CreateProfileViewProtocol, InitializeInjectable, KeyboardInteractive { + private let presenter: CreateProfilePresenterProtocol + private let viewsFactory: CreateProfileViewsFactoryProtocol + + private lazy var topHeaderLayoutGuide: UILayoutGuide = viewsFactory.makeTopLayoutGuide(on: view) + private lazy var headerView: NavigationView = viewsFactory.makeHeaderView(on: view, topLayoutGuide: topHeaderLayoutGuide, navigationHandler: presenter) + private lazy var createButton: UIButton = viewsFactory.makeCreateButton(on: view, target: self, selector: #selector(createAccount(sender:))) + private lazy var container: UIView = viewsFactory.makeContainer(on: view, headerView: headerView, footerView: createButton) + private lazy var scrollView: UIScrollView = viewsFactory.makeScrollView(on: container) + private lazy var contentContainer: CreateProfileContentView = viewsFactory.makeContentContainer(on: scrollView, widthView: view, presenter: presenter) + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + viewsFactory = dependencies.viewsFactory + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.nynja.darkLight + + _ = [topHeaderLayoutGuide, headerView, createButton, container, scrollView, contentContainer] + + enableKeyboardHidingWhenTappedAround() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unregisterForKeyboardNotifications() + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } +} + +// MARK: - CreateProfileViewProtocol + +extension CreateProfileViewController { + func updateProfileField(_ field: ProfileField, value: String) { + contentContainer.textFieldValueDidChange(value: value, field: field) + } + + func setCreateEnabled(_ enabled: Bool) { + createButton.isEnabled = enabled + } +} + +// MARK: - InitializeInjectable + +extension CreateProfileViewController { + struct Dependencies { + let presenter: CreateProfilePresenterProtocol + let viewsFactory: CreateProfileViewsFactoryProtocol + } +} + +// MARK: - KeyboardInteractive + +extension CreateProfileViewController { + func keyboardNotified(endFrame: CGRect) { + var bottomInset: CGFloat = 28 + + if endFrame.origin.y < UIScreen.main.bounds.size.height { + bottomInset += endFrame.height + } else { + bottomInset += UIWindow.safeAreaBottomPadding() + } + + createButton.snp.updateConstraints { (make) in + make.bottom.equalToSuperview().inset(bottomInset) + } + } +} + +//MARK: - Actions + +extension CreateProfileViewController { + @objc func createAccount(sender: UIButton) { + presenter.createAccount() + } +} diff --git a/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift b/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift new file mode 100644 index 000000000..e23958853 --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift @@ -0,0 +1,144 @@ +// +// CreateProfileContentView.swift +// Nynja +// +// Created by Ash on 10/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class CreateProfileContentView: UIView, Configurable { + private let viewsFactory: CreateProfileViewsFactoryProtocol + + private var chooseAvatarAction: (() -> Void)? + private var markCheckAction: (() -> Bool)? + private var textChangedAction: ((String, ProfileField) -> Void)? + private var isValidAction: ((String, ProfileField) -> Result)? + + init(viewsFactory: CreateProfileViewsFactoryProtocol) { + self.viewsFactory = viewsFactory + + super.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private lazy var avatarbutton: UIButton = viewsFactory.makeAvatarButton(on: self, target: self, selector: #selector(chooseAvatar(sender:))) + + private lazy var firstNameTextField: MaterialTextField = viewsFactory.makeFirstNameTextField( + on: self, + top: avatarbutton, + textChangedHandler: textChangedHandler, + shouldChangeTextHandler: shouldChangeTextHandler) + + private lazy var lastNameTextField: MaterialTextField = viewsFactory.makeLastNameTextField( + on: self, + top: firstNameTextField, + textChangedHandler: textChangedHandler, + shouldChangeTextHandler: shouldChangeTextHandler) + + private lazy var accountNameTextField: MaterialTextField = viewsFactory.makeAccountNameTextField( + on: self, + top: lastNameTextField, + textChangedHandler: textChangedHandler, + shouldChangeTextHandler: shouldChangeTextHandler) + + private lazy var usernameTextField: MaterialTextField = viewsFactory.makeUsernameTextField( + on: self, + top: accountNameTextField, + textChangedHandler: textChangedHandler, + shouldChangeTextHandler: shouldChangeTextHandler) + + private lazy var descriptionLabel: UILabel = viewsFactory.makeDescriptionLabel(on: self, top: usernameTextField) + + private lazy var checkBoxContainerView: UIView = viewsFactory.makeCheckBoxContainerView(on: self, top: descriptionLabel) + private lazy var checkButton: UIButton = viewsFactory.makeCheckButton(on: checkBoxContainerView, target: self, selector: #selector(markCheck(sender:))) + private lazy var termsOfUseTextView: UITextView = viewsFactory.makeTermsOfUseTextView(on: checkBoxContainerView, left: checkButton) +} + +// MARK: - Configurable + +extension CreateProfileContentView { + struct Config { + let chooseAvatarAction: () -> Void + let markCheckAction: () -> Bool + let textChangedAction: (String, ProfileField) -> Void + let isValidAction: (String, ProfileField) -> Result + } + + func configure(config: CreateProfileContentView.Config) { + chooseAvatarAction = config.chooseAvatarAction + markCheckAction = config.markCheckAction + isValidAction = config.isValidAction + textChangedAction = config.textChangedAction + + _ = [avatarbutton, firstNameTextField, lastNameTextField, accountNameTextField, usernameTextField, descriptionLabel, checkBoxContainerView, checkButton, termsOfUseTextView] + } +} + +// MARK: - Public + +extension CreateProfileContentView { + func textFieldValueDidChange(value: String, field: ProfileField) { + switch field { + case .firstName: firstNameTextField.text = value + case .lastName: lastNameTextField.text = value + case .accountName: accountNameTextField.text = value + case .userName: usernameTextField.text = value + } + } + + func updateAvatar(with image: UIImage?) { + let avatar = image ?? UIImage(named: "ic_empty_avatar") + + avatarbutton.setImage(avatar, for: .normal) + } +} + +// MARK: - Actions + +extension CreateProfileContentView { + @objc func chooseAvatar(sender: UIButton) { + chooseAvatarAction?() + } + + @objc func markCheck(sender: UIButton) { + guard let result = markCheckAction?() else { + return + } + + let imageForSender = result + ? UIImage(named: "table_overrides_right_overrides_checkbox_ic_unchecked") + : nil + + sender.setImage(imageForSender, for: .normal) + } +} + +// MARK: - Private + +private extension CreateProfileContentView { + func textChangedHandler(field: ProfileField) -> MTITextChangedHandler { + return { [weak self] input in + self?.textChangedAction?(input.text, field) + } + } + + func shouldChangeTextHandler(field: ProfileField) -> MTIShouldChangeTextHandler { + return { [weak self] input, range, string in + guard let isValid = self?.isValidAction?((input.text as NSString).replacingCharacters(in: range, with: string), field) else { + return true + } + + isValid + .onSuccess { input.info = nil } + .onFailure { input.info = InputInfo(text: $0.localizedDescription, kind: .warning) } + + return true + } + } +} diff --git a/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift new file mode 100644 index 000000000..311f019d1 --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift @@ -0,0 +1,299 @@ +// +// CreateProfileViewsFactory.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol CreateProfileViewsFactoryProtocol { + typealias ChangeTextHandler = (ProfileField) -> MTITextChangedHandler + typealias ShouldChangeTextHandler = (ProfileField) -> MTIShouldChangeTextHandler + + // MARK: - CreateProfileViewController + + func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide + func makeHeaderView(on view: UIView, topLayoutGuide: UILayoutGuide, navigationHandler: NavigationProtocol) -> NavigationView + func makeCreateButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeContainer(on view: UIView, headerView: UIView, footerView: UIView) -> UIView + func makeScrollView(on view: UIView) -> UIScrollView + func makeContentContainer(on view: UIView, widthView: UIView, presenter: CreateProfilePresenterProtocol) -> CreateProfileContentView + + // MARK: - CreateProfileContentView + + func makeAvatarButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeFirstNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField + func makeLastNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField + func makeAccountNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField + func makeUsernameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField + func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel + func makeCheckBoxContainerView(on view: UIView, top: UIView) -> UIView + func makeCheckButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeTermsOfUseTextView(on view: UIView, left: UIView) -> UITextView +} + +final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { + + // MARK: - CreateProfileViewController + + func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide { + let layoutGuide = UILayoutGuide() + view.addLayoutGuide(layoutGuide) + + layoutGuide.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.height.equalTo(20 + UIWindow.safeAreaTopPadding()) + } + + return layoutGuide + } + + func makeHeaderView(on view: UIView, topLayoutGuide: UILayoutGuide, navigationHandler: NavigationProtocol) -> NavigationView { + let navigationView = UIView.makeHeaderView( + on: view, + top: topLayoutGuide, + config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: "Create profile".localized.uppercased(), + navigationHandler: navigationHandler, + backButtonImage: UIImage.backButtonImage)) + + return navigationView + } + + func makeCreateButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.mainRed), for: .normal) + button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.darkRed), for: .disabled) + + button.setTitle("create".localized.uppercased(), for: .normal) + + button.setTitleColor(UIColor.nynja.white, for: .normal) + button.setTitleColor(UIColor.nynja.gray, for: .disabled) + + button.layer.cornerRadius = 22 + button.clipsToBounds = true + + button.isEnabled = false + + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.left.right.equalToSuperview().inset(16) + make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottomPadding() - 28) + make.height.equalTo(44) + } + + return button + } + + func makeContainer(on view: UIView, headerView: UIView, footerView: UIView) -> UIView { + let container = UIView() + view.addSubview(container) + + container.clipsToBounds = true + + container.backgroundColor = UIColor.nynja.darkLight + + container.snp.makeConstraints { (make) in + make.top.equalTo(headerView.snp.bottom) + make.left.right.equalToSuperview() + make.bottom.equalTo(footerView.snp.top) + } + + return container + } + + func makeScrollView(on view: UIView) -> UIScrollView { + let scrollView = UIScrollView() + view.addSubview(scrollView) + + scrollView.backgroundColor = UIColor.nynja.clear + + scrollView.snp.makeConstraints { (make) in + make.edges.equalToSuperview() + } + + return scrollView + } + + func makeContentContainer(on view: UIView, widthView: UIView, presenter: CreateProfilePresenterProtocol) -> CreateProfileContentView { + let contentContainer = CreateProfileContentView(viewsFactory: self) + view.addSubview(contentContainer) + + let config = CreateProfileContentView.Config( + chooseAvatarAction: { + presenter.chooseAvatar(completion: contentContainer.updateAvatar) + }, + markCheckAction: presenter.checkTermsOfUse, + textChangedAction: presenter.setProfileField, + isValidAction: presenter.isValidValue) + + contentContainer.configure(config: config) + + contentContainer.snp.makeConstraints { (make) in + make.top.equalToSuperview().offset(UIWindow.safeAreaTopPadding()) + make.bottom.equalToSuperview().offset(-16) + make.left.right.equalTo(widthView) + } + + return contentContainer + } + + // MARK: - CreateProfileContentView + + func makeAvatarButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setImage(UIImage(named: "ic_empty_avatar"), for: .normal) + button.addTarget(target, action: selector, for: .touchUpInside) + + button.layer.cornerRadius = 47 + button.clipsToBounds = true + + button.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.top.equalTo(32) + make.height.width.equalTo(94) + } + + return button + } + + func makeFirstNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { + return makeTextField(fieldType: .firstName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) + } + + func makeLastNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { + return makeTextField(fieldType: .lastName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) + } + + func makeAccountNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { + return makeTextField(fieldType: .accountName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) + } + + func makeUsernameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { + return makeTextField(fieldType: .userName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) + } + + func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.text = + "You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.".localized + + "\n" + + "You can use a-z, 0-9 and underscores. Minimum lenght is 2 characters.".localized + + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + + label.numberOfLines = 0 + + label.snp.makeConstraints { (make) in + make.left.right.equalToSuperview().inset(16) + make.top.equalTo(top.snp.bottom) + } + + return label + } + + func makeCheckBoxContainerView(on view: UIView, top: UIView) -> UIView { + let containerView = UIView() + view.addSubview(containerView) + containerView.backgroundColor = UIColor.nynja.clear + + containerView.snp.makeConstraints { (make) in + make.left.right.equalToSuperview().inset(16) + make.top.equalTo(top.snp.bottom) + make.height.equalTo(56) + make.bottom.equalToSuperview().offset(16) + } + + return containerView + } + + func makeCheckButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.layer.cornerRadius = 12 + button.layer.borderColor = UIColor.nynja.dustyGray.cgColor + button.layer.borderWidth = 1 + + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.height.width.equalTo(24) + make.left.equalToSuperview() + make.centerY.equalToSuperview() + } + + return button + } + + func makeTermsOfUseTextView(on view: UIView, left: UIView) -> UITextView { + let textView = UITextView() + view.addSubview(textView) + + textView.backgroundColor = UIColor.nynja.clear + textView.isScrollEnabled = false + + textView.dataDetectorTypes = .link + textView.isEditable = false + + let beginOfStr = NSMutableAttributedString(string: "I agree at".localized) + beginOfStr.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.nynja.dustyGray, + NSAttributedStringKey.font: FontFamily.NotoSans.regular.font(size: 14)], range: NSMakeRange(0, beginOfStr.length)) + + let attributes = [NSAttributedStringKey.link : "http://www.google.com", + NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue, + NSAttributedStringKey.underlineColor: UIColor.nynja.blue, + NSAttributedStringKey.foregroundColor: UIColor.nynja.blue, + NSAttributedStringKey.font: FontFamily.NotoSans.regular.font(size: 14)] as [NSAttributedStringKey : Any] + + let termsOfUseStr = NSMutableAttributedString(string: "terms of use".localized) + termsOfUseStr.addAttributes(attributes, range: NSMakeRange(0, termsOfUseStr.length)) + + beginOfStr.append(NSAttributedString(string: " ")) + beginOfStr.append(termsOfUseStr) + + textView.attributedText = beginOfStr + + textView.snp.makeConstraints { (make) in + make.left.equalTo(left.snp.right) + make.centerY.equalTo(left.snp.centerY) + make.right.equalToSuperview() + } + + textView.sizeToFit() + + return textView + } +} + +private extension CreateProfileViewsFactory { + func makeTextField(fieldType: ProfileField, on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { + let textField = MaterialTextField() + view.addSubview(textField) + + textField.placeholder = fieldType.placeholder.localized + (fieldType.isRequired ? "*" : "") + + textField.snp.makeConstraints { (make) in + make.left.right.equalToSuperview().inset(16) + make.top.equalTo(top.snp.bottom).offset(22) + make.height.equalTo(65) + } + + textField.textChanged = textChangedHandler(fieldType) + textField.shouldTextChanged = shouldChangeTextHandler(fieldType) + + return textField + } +} diff --git a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift new file mode 100644 index 000000000..f7e25b651 --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -0,0 +1,57 @@ +// +// CreateProfileWireframe.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol CreateProfileCoordinatorProtocol { + func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) +} + +final class CreateProfileWireframe: CreateProfileWireframeProtocol { + struct Parameters {} + + struct Dependencies {} + + enum State { + case back + case next + case chooseAvatar(completion: (UIImage?) -> Void) + } + + private let coordinator: CreateProfileCoordinatorProtocol + + init(coordinator: CreateProfileCoordinatorProtocol) { + self.coordinator = coordinator + } + + func prepareModule(parameters: CreateProfileWireframe.Parameters, dependencies: CreateProfileWireframe.Dependencies) -> UIViewController { + let presenter = CreateProfilePresenter() + let view = CreateProfileViewController(dependencies: CreateProfileViewController.Dependencies(presenter: presenter, viewsFactory: CreateProfileViewsFactory())) + let interactor = CreateProfileInteractor() + + let presenterDep = CreateProfilePresenter.Dependencies(wireframe: self, interactor: interactor, view: view) + let interactorDep = CreateProfileInteractor.Dependencies(presenter: presenter) + + presenter.inject(dependencies: presenterDep) + interactor.inject(dependencies: interactorDep) + + return view + } + + func back() { + coordinator.wireframe(self, didEndWithState: .back) + } + + func end() { + coordinator.wireframe(self, didEndWithState: .next) + } + + func chooseAvatar(completion: @escaping (UIImage?) -> Void) { + coordinator.wireframe(self, didEndWithState: .chooseAvatar(completion: completion)) + } +} diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@2x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@2x.png index 8dbfc42202cde8d345ffce861e4fea6bde4afd33..fa2948118838a122b3e275fbe97aeba2fe1f83f9 100644 GIT binary patch delta 899 zcmV-}1AP392agAk8Gi-<00374`G)`i00DDSM?wIu&K&6g00T)$L_t(&1@+OlgZ(1cH}?Zz;m#htC%3^rP%T zT%G0inL-qS*wx^drRZ9>{EI-*-da`4{nNRQ1Y)l$1)JQD=T-!2UYHaq`(z5P%cF=g zNtb#^+(?Wr4}WpgA*6ssYL=O>*xVhqR|=TtDa8{{aa3WXfG;YB+4(Wu_soaF_DTVx zjwxQk-(4z|Dvoh7(WQWQnV40Qi0?rBSBDr*R4HIEV}#trPwD3R-LAami~C2gB4goR zX=R50H4)z^qpre#iINktrQYwysGOn--EU;=2ZGqg7A_$be^s2hPd z-#ST1M})|8!8p)baUS+jenP7r|39D@Qwbr>;4RSFX9%q!XNYxdglq+q+6wT04i;J` z3h9WDQyB^nNN;2)hPxonW=l0~vKms>c< z1;SQ*6wkc~BFQ&|8&O1u_6J-U5v?6@8GtD+8Gq#-aZ{Vwqkp%LB4i{kdlaP&UUY-c14 zwL|hbU?;c>H1DDAw$002ovPDHLkV1h|4oCg2^ literal 908 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sjKx9jP7LeL$-D%z-2;3=T!DiB zM*+nlV3k=N1q{lZk|4ie21X`k7FITP4o+?!VG&U=NhvvHH9cc9a~oTGXID3`fZ&Me zgtW};yuy;wvhs?`>YBR7=C<~Z&aR#bQ)bMZyeck%Lkm3L1~t?NVWk40?s zUH?7thy8mMo8Mb1&OcV4n|qwGK>O6ajf=GcCh3bs7^W5;70_mzU-{r#=DIwK10{3r zx?B~NIJfvl^?SBwF>TlFuPy)rT%#ODg7I(j!hJPGk5B= p%8yF(w-seg(fzD+E%^5X_CKN7qA&Bx-vFbZ!PC{xWt~$(69DhuONIad diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@3x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@3x.png index 362d8469bfaf36974cd0f9011158ccc9f0432020..f893d7451f8ee515e6cff2f55cb5d9485a469d78 100644 GIT binary patch delta 1371 zcmV-h1*H0i3gZfp8Gi-<0033(vqt~`00DDSM?wIu&K&6g00kXML_t(|0p*)rh!tfV z#y#e0q^V(&9|Yz{MwwA$`GF;fC>T-Ug%HYWYS$^WUW6Ay5FrRc_>EshN}y(UZMAwK z(vpZ5vgjg0q+(ft$daa>evh4_XU8);=i~pr^UnB(XJ_V}nSba1f1Wcl`_9Ze{rV+D zu~>Wqd=1WnzrZo@F&L1rYra=wz(BAO+yT|%6pfSf?Lv$?gtvfitGNR&n?Rx`Ei_>FxCGq<i`W5a!J+wns3ijYZtPG#%qjl_n6ux| zprdeQ|9{9SnWJlvqW%Ag$=_FD8EwK83JJgXFmT=QDB2Z@HnTb~XS1O}x8Zulthf+} z5H!WmAWO_i0Y6s!&qioVA^CDv(V5SQT+)u_lD00h{3sg^evK+LSkBxF#8^`?L*t}%>tGrBFZp(C(yXmO%%_&_l(>4B zF3g%2RqI8_0PuZDnB_#H+FGk7%~~TubUo^_bfS|u6M~K zS{_q`MYEm&ywy8n@)G;3sUi6}w$WfN_d%ldH5An7Ci=oi_k%-gQNnh$m{YDrCTZ-X z`ykQAa#lyv-{5_xFD0yB-9Tub9}*PWv-eGzauT3`9iYP7xXWyo)^lWss|Qr1Er-gHzulFST}BbcONcJG1xT7S-1 z=!?k*Ofoe{hG3Fz72XFswVbgin{=jBoii`+q z0Smw;&<*MhjaK;)%ukZx?OrUYkY?5+RL^f_fR};p^_~HGK{y;}*q{1Y-QYAh4i3`~ dyTUXv{sW$upX>mjHaY+R002ovPDHLkV1i$%m(2hG literal 1287 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifV{wqX6T`Z5GB1JbJpn!;u0X;6 zqhM%;z^WTBrvT$`a!HV1FaskKGYcylI|mmJAHRU0u!yL*grt~P2`u*qcKg%}NWef~VQJyZ2ArXhy-gp@p@#M*mWn9No$BGp@+(fMfoo%n^hrSv-WzB4YX96}=aVJqU~QLp$+a3?HZ93KocnQY z!4kp4ymnKcc-S?ESRHL>^Zlv0{wvuJvGrcip2`KO(%UtHXs%H!mO z_bv?Gy8rf-8~t;p*)I{jy49}lNc|FV_3CwR{+a&~junmkK27RWJ}?t7c)I$ztaD0e F0su1TIN<;Q diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Contents.json new file mode 100644 index 000000000..599c82dcf --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "info" : { + "author" : "xcode", + "version" : "1" + }, + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "Icons_General_ic_accept_call.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Icons_General_ic_accept_call@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "Icons_General_ic_accept_call@3x.png" + } + ] +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call.png new file mode 100644 index 0000000000000000000000000000000000000000..fe2af6690bcfb281bacf7fdccf3cabe46d038f9e GIT binary patch literal 377 zcmV-<0fzpGP)Px$Gf6~2R7ef&l)Xv=K@^2|EhJl61WOAaAT|m@kaQ`12z>+#3&98QC+}dPVDD4d zB~7AMfwbBy{;c1DVYxGLj62&&E_}?LnS0LNy|9~Q%lP*b$n6`JW!Yzc1cg1Xkv~&= zk>~mSj~W^bp=mr=^A)=xUk~;v+m;oD4UV}iZ9&ak+LK>3%;x}J6Xx`W=5Ueu4MhcX zU<7{+)oy{b*U=`uejw;6dT-XNTmmHs8mdy~}Iq^R0hBdM| zUvlMJB`VOP*uvD@HV8kok5p5m^E|=^x27;P^*p>0$&o4Tva}Q3U!s4~M8l=5`2@ZJ XE6eb-Xon`W00000NkvXXu0mjfzh9t; literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@2x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b35bf06e5963a049ab72da5b7bf83a16df7ff57b GIT binary patch literal 777 zcmV+k1NQuhP)Px%!%0LzRA>e5m`iIEK@i6?F?!OdcrnI+@sW7&=rIS;Tdo1oPY^;N`1lC=4e|{{ z^eiZf2Ne|$>OsgYhpdR82OnT0!3VCt(y7_rp}VIiGdm;F1^;wab^WVqs&{*LGc*(> zP9RPoP9RPoP9RR8y$K|~(%o)%AN#Xl5)Aud2e9{xoNM4#k|gV{{LTlU`J<2?f`cw{ zL;ly)In6_KZ1sFoy$Yf2=B4LDU|AHK_)g#*g#9*B3%)O$z;0Vz-wABD5nA$ncGdMg zzylYl1^**gP2Wps3epPL;UYKW|3RG-yo6S5^_(Y=66S5BhJA~!=7aj+I$Z=mLDM2N zmx7wK1wqbE2mKUy1DcYz6gs1Qp>I{0^y9&jBDT5eKcuTgg=_HDI;*R;zO z)@hT3eHMI7_v>6fk?dTq)-imqq=@Mc(uI`DChKKjzN8Ld-%I!FSniW#TpL3l1{c8w z_#?U(yeiRHpP^@`ft~<)gJ(t1%ZhN9Kz5y@Pl2bPPl6lQz&>`*kb;+hc^cE$FM1)t z+Djsj8}(%B7l3sj8W+44wDE-fBbp3-1Y7|j*R8}zZ1zd2=m3{=*qF(2=JBn&2 zgGu0xRZfkoT;uZZ#2eh8?`zB(Q^g6y3B(D+3B(EfI}`W~37KSa#JAkb00000NkvXX Hu0mjf5%X(u literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@3x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a77524e9ec080d0ac9ec0dbad837f04b4870b35b GIT binary patch literal 1142 zcmV-+1d02JP)Px(FiAu~RCodHnq7!ZQ545r#xRY>*BDO|g_Mt>lw@iiBuXioNGU}^JVR{h)OtiASst@WRC z&fVwE9W~0#6fgx$0aL&fFa=BjQ@|831xx``z!WeAOo6geAQQElzP`S(teFQUM_oI@ z@!jNqlF4L#7o`q_sRpM(4@j&1r0`>)N-L7;3W9J9g!jM_Epds*-J!lFuBKjJouhpL z!Tc7+auG*bS`H4#fzP4{V*c zi+QT4S1Zs(4Oz-!&S>h5@-g25;X(~*3Su5pV*?-aeZE>%S5UBD*a=yWk1z%Ezo>DS zMjYuyC6EL{*v_yOcvYA961Ls+a=U2tv8&z#{(!VuF9q){nlRbKBw7u+Q>4&0YF(4v zL!!r!SB44k)q`uG)a^P2&T!P9(^EJ`eNKX#pf!c9pmIVJ?@JtGgEl7BwkOe09@}}^-oa3(qM}RyGN6I93 zD{$UE_?tl&_;0R0V)^H-Z;gvBP!vibPF$YN}=Ke7F!-Y%E`xI5_{p znD1(uz}$&E>x$t%32BY1Is6;ICy-;V_*lLi=An*4SJPTTKMoB~^z1Wi>s?LZp9U@z z%H~=Z%U38#Feim>bdlq)1DAuiB!?@IP0y24o;A*^Cc6OF+E~79QqQs6=Q-N}Ns50v z`0iP+nf^i04p$@i<>ULDhD2gq9I<@bgh8C#QtMoa@XrG6L0p_c*3QnuEw}TBD5Nf) zSiUGpvtmziJTaUQUj;Y>qP)*ZtgNYU@YUdN1CcGv75hAL=D`JTIb7*Vj9<0`&j&;E z9!X4N9*z~vy~(@MGvVTl<@4n6P5&i8{#iFZoE%>jI1J>u5;pP4T+ToF@QnjUfpEvR za6PVb?Kg@^JkaD@yv=+U^C(;h9Ztcw4J-}j?lZO#+ZCJ-48%5YauzHYaK=1g8CNGw< z({(<8r6`#TVv6zt>}{L?)jdd&Nj!rJ)$@+YtKfAIG?Q@^s1MGIGH9Mbr6N#K1n`k+ z1d9By^d&8~c@xO<7d3%QAddo`X}f|F5=ed_T5hK5NqYr7EH`!NN7a*7pjHTry;&U2 zzk&23J$X$f5F0Al<+cFt&t|fGJ=Km;$DNDG*tKe=DbJG`K)xdjJ3c07*qo IM6N<$f~SlO>Hq)$ literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/Contents.json new file mode 100644 index 000000000..abc5db876 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "info" : { + "author" : "xcode", + "version" : "1" + }, + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "ic_empty_avatar.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "ic_empty_avatar@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "ic_empty_avatar@3x.png" + } + ] +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar.png b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..35b90503c59c956096fdd7ec9757bfcf53f0ccb1 GIT binary patch literal 6390 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 Y0d2Y&&tNVj1^@s607*qoM6N<$f&vNFB>(^b literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@3x.png b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ce127723957eb0f2226481d4152790025a9b58f2 GIT binary patch literal 2368 zcmV-G3BUGPx-`bk7VRCodHTzhO(RT}@@JEcfpP!#*5P!n0Qghg7>_z0q~t43IqhmUB8X2Wh( z1ht5Qh`cNsEGiP=7_D`BMt0ITDafdH!(f zR_OysU6gO4PlF7JKJx#|u**H;svefTu5LnAyDSfRSu^}EcZ!yg}~ZJ*Vo5k^zKr8 z+aC*`P;E>Fs1D<@df}{~U$;_)njcEA+4m$!E##%unXv7eg4Q0h_?rTdidkug@YQk& z${O;P3(Zn47CvN9n+mz=3jAifgby9D@bGS97@)(*y_QDIo#Zi7y%{GuPabuI`t<{s zqy1wey2K1n9j2t8Mf39n%rf$tnE4n{K3rQ;N+qCnWT0^66x7+n)em$(Cd0l!-X#hK z{y_}O12IiPT8)g6x6i<-6Vd7xH9$4E@CGN8f0E~nj#gMA>ysyS+8{3|N85j+(IcwI z??H@R?1Xbqs)56Ru+FSI+h7SmhcI?2J*D2H@g_YQ0m)RCb!QvcmPqxAsEaY`Zg8UV zxJZ?VcReziCYbV_SK`cD;beyeNE>swTcxh&1OkB!x7%R5Ioy~O5+HtBsj%0k;+PW% z6ki$-+!E5uLOtDrb{U@M@S}XFg2Q03ZEPdk>ImV!4*PZ4KBq zU>DPt59)zrogoLb!~l^%P-(wuW}sz>p9k^6Q(Idl=kmb-4ndo;BWO0yO7pGc2N18y zsc$CL!qZo2E6Qv$+wRL-=wjM_&fvF%XhOFve3gt>nZRhv#E6OB`$>Wa@n+X>1&4nG zwq4usJNJ`#JjlIG+C;7v*PXy~su;`3*;$ZBro(aij3GueH)^5n+fse|*5g6i9&&1e zb8*FeC~G~A=|3Mw(ZU5%M=?bWQ#@_xg7TUW!4=B=XK~jecH$@7~9Eg75>ODb^Y zjoku0_JW3q%&&v#0XH*tM)7S~@c0UpY~3UXRUHm|bN5oT{rzo$Zls{S1zs;pHMppp zy4YbO%;MFyVD%#yU$ssU$KV{l>knvu`#phfMj;Jpm(#jSnPvtvt9cYYS&8D!YXvdN z;2^%a<36;%^S(egtH9V0K(133X7RLyPvKLKp?KpOfwy9}e-#t|IXfrD(GJ(~tl$+l-u*3yW}nvY`4+#6#8sd&pe6t1Zh z+B$oC(Rj=4=-PikpvR$*Egiy`0h(F|-PMm&i4g^PnBH9Psm=pwK7mX+D|8}d)75NDdcth){Xw) zba$iSrbP(>$E|EPEj7#m$l2M2@0N>C9C_3zO#0WG$hvAqXmF}GqhRGjLLFyU7aDF{ zh@MXl3iNmu7#s2+*HK$j9*xO+-@&M6?N7)kp?%o-W~Xjbb*e?Y?8Iu2Xt7a%#Tk$i{`58pzH^KZbkj|^U!zX>!5WKaf9YS9gGeAz8n}L z9k5b~AD-NdpSEuI6Jc*Tg`>0PqOb1U1kxUQnwm7cEUPA1~?Na z3T?NJ=SU(KK+NkPa{oh~^g9E5BW}toe#fcSi0j9{qO|u)w7XW@9*-fUUXPa|$qkNc zo5O>J2Oy5O;urVv0x4PLxDPMv*7frMq@8AvfS}GA zAC8G_XAHb;7XaduRUC}wU12!o*>>;KWo)|u5EHaWJt=t>M66UC5{*4a*mhr0egJV8 zBM~1UccW?1@JMW{X0v|?ryoEJ37;1xM0X4ic=)j{?Fe*6n(Yq=5Z~Z1M!M8+eu$?U zx2*xqB<~8I&IhxE@F0ebqhL6Ukx;Y;hcPwpuuW`Rs7N2SENBo(3~5_r zXe73UZ3-7F#@2=fh%0H4d2+>207ktcU(=3nXgpB{Ou_OfF&i}!ZHyR=G=l`NB1USC8+^>`t(TsxPLDZQvq8t8`H{wMR+3LQO{gXFe mko2;O#kN{}a9Jw-P2ewsGyUYWl4-R70000 Date: Tue, 30 Oct 2018 12:18:07 +0200 Subject: [PATCH 032/138] Remove unused files from project. Fixed generated assets. --- Nynja.xcodeproj/project.pbxproj | 14 - Nynja/Generated/AssetsConstants.swift | 1097 +++++------------ Nynja/Generated/ColorsConstants.swift | 76 +- Nynja/Generated/FontsConstants.swift | 65 +- Nynja/Generated/LocalizableConstants.swift | 21 +- .../next-bttn.imageset/Contents.json | 23 - .../next-bttn.imageset/next-bttn.png | Bin 6428 -> 0 bytes .../next-bttn.imageset/next-bttn@2x.png | Bin 10317 -> 0 bytes .../next-bttn.imageset/next-bttn@3x.png | Bin 11261 -> 0 bytes .../New Folder/qr-code.imageset/Contents.json | 23 - .../New Folder/qr-code.imageset/qr-code.png | Bin 1672 -> 0 bytes .../qr-code.imageset/qr-code@2x.png | Bin 2505 -> 0 bytes .../qr-code.imageset/qr-code@3x.png | Bin 4980 -> 0 bytes .../wheel_left_image.imageset/Contents.json | 12 - .../wheel_left_image.imageset/left_image.pdf | Bin 253015 -> 0 bytes .../wheel_right_image.imageset/Contents.json | 12 - .../right_image.pdf | Bin 260993 -> 0 bytes .../ic_camera_frame.imageset/Contents.json | 35 - .../ic_camera_frame.pdf | Bin 3953 -> 0 bytes .../ic_new_group.imageset/Contents.json | 15 - .../ic_new_group.imageset/ic_new_group.pdf | Bin 7638 -> 0 bytes .../ic_search.imageset/Contents.json | 21 - .../ic_search.imageset/ic_search.pdf | Bin 4243 -> 0 bytes 23 files changed, 420 insertions(+), 994 deletions(-) delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn.png delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@2x.png delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@3x.png delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code.png delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@2x.png delete mode 100644 Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@3x.png delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/right_image.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/ic_camera_frame.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/ic_new_group.imageset/ic_new_group.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/ic_search.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/ic_search.imageset/ic_search.pdf diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index d3f83f3b4..336de409a 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -670,7 +670,6 @@ 5E7E9FC2215BA681004D306B /* CountryTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */; }; 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */; }; 5EB13FDBA6153EE67366115F /* ScheduleMessageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */; }; - 5EC8C841216648B6003D4731 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EC8C840216648B6003D4731 /* ViewController.swift */; }; 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD49CF323041B47A752603E /* MapSearchInteractor.swift */; }; 5EEB73A4215D00E300D8ECE6 /* CountrySelectorInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */; }; 5EEB73A6215D00F100D8ECE6 /* CountrySelectorWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */; }; @@ -2822,13 +2821,11 @@ 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileWireframe.swift; sourceTree = ""; }; 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileContentView.swift; sourceTree = ""; }; 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; - 5E7E9FB2215BA059004D306B /* Nynja copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Nynja copy-Info.plist"; path = "/Users/ash/Projects/NynjaIOSWallet/Nynja copy-Info.plist"; sourceTree = ""; }; 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorProtocols.swift; sourceTree = ""; }; 5E7E9FBB215BA19B004D306B /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewController.swift; sourceTree = ""; }; 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVCell.swift; sourceTree = ""; }; 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVHeader.swift; sourceTree = ""; }; - 5EC8C840216648B6003D4731 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 5EEA3D18EFB98D7959F993E4 /* AddParticipantsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsProtocols.swift; sourceTree = ""; }; 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorInteractor.swift; sourceTree = ""; }; 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorWireframe.swift; sourceTree = ""; }; @@ -5907,7 +5904,6 @@ 767274D7745E7E490BE6C79C /* Pods */, E853D758816E611EE4809ED3 /* Frameworks */, 851EBD7D20B403B90065C644 /* Recovered References */, - 5E7E9FB2215BA059004D306B /* Nynja copy-Info.plist */, ); sourceTree = ""; }; @@ -5930,7 +5926,6 @@ 3A768DE41ECB3E7600108F7C /* Library */, 3ABCE9021EC9357900A80B15 /* Resources */, 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */, - 5EC8C840216648B6003D4731 /* ViewController.swift */, 3AC07E2E1F05572400ADBE26 /* Nynja-Bridging-Header.h */, F11786B320A8A5EB007A9A1B /* Coordinators */, 49E75E252CE2F3C96A626230 /* Modules */, @@ -6862,7 +6857,6 @@ 5E07BC47216F64DB000E4558 /* Wireframe */, 5E07BC48216F64DB000E4558 /* View */, 5E07BC4A216F64DB000E4558 /* Interactor */, - 5E07BC4B216F64DB000E4558 /* Entities */, 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */, ); path = CreateProfile; @@ -6910,13 +6904,6 @@ path = Interactor; sourceTree = ""; }; - 5E07BC4B216F64DB000E4558 /* Entities */ = { - isa = PBXGroup; - children = ( - ); - path = Entities; - sourceTree = ""; - }; 5E0B9FF02170BCD400A95467 /* Subviews */ = { isa = PBXGroup; children = ( @@ -15675,7 +15662,6 @@ 6547BE911E492D790E0D4390 /* EditGroupNameInteractor.swift in Sources */, 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */, 263A60AE1FB51C22006F9D52 /* MemberExtension.swift in Sources */, - 5EC8C841216648B6003D4731 /* ViewController.swift in Sources */, E709383F1FBEE41D006CCDC6 /* Describable.swift in Sources */, A42D52DA206A53AB00EEB952 /* Desc_Spec.swift in Sources */, A433D9A120A5C18C00C946F9 /* ContactsProvider.swift in Sources */, diff --git a/Nynja/Generated/AssetsConstants.swift b/Nynja/Generated/AssetsConstants.swift index f2fd83e47..9db22d105 100644 --- a/Nynja/Generated/AssetsConstants.swift +++ b/Nynja/Generated/AssetsConstants.swift @@ -1,379 +1,276 @@ +// swiftlint:disable all // Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen #if os(OSX) import AppKit.NSImage internal typealias AssetColorTypeAlias = NSColor - internal typealias Image = NSImage + internal typealias AssetImageTypeAlias = NSImage #elseif os(iOS) || os(tvOS) || os(watchOS) import UIKit.UIImage internal typealias AssetColorTypeAlias = UIColor - internal typealias Image = UIImage + internal typealias AssetImageTypeAlias = UIImage #endif // swiftlint:disable superfluous_disable_command // swiftlint:disable file_length -@available(*, deprecated, renamed: "ImageAsset") -internal typealias AssetType = ImageAsset - -internal struct ImageAsset { - internal fileprivate(set) var name: String - - internal var image: Image { - let bundle = Bundle(for: BundleToken.self) - #if os(iOS) || os(tvOS) - let image = Image(named: name, in: bundle, compatibleWith: nil) - #elseif os(OSX) - let image = bundle.image(forResource: NSImage.Name(name)) - #elseif os(watchOS) - let image = Image(named: name) - #endif - guard let result = image else { fatalError("Unable to load image named \(name).") } - return result - } -} - -internal struct ColorAsset { - internal fileprivate(set) var name: String - - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) - internal var color: AssetColorTypeAlias { - return AssetColorTypeAlias(asset: self) - } -} +// MARK: - Asset Catalogs // swiftlint:disable identifier_name line_length nesting type_body_length type_name internal enum Asset { internal static let appIcon = ImageAsset(name: "App_Icon") - internal enum Background { - internal static let background = ImageAsset(name: "background") - internal static let backgroundLight = ImageAsset(name: "background_light") - internal static let bgSecurityPlaceholderDark = ImageAsset(name: "bg_security_placeholder_dark") - internal static let bgSecurityPlaceholderLight = ImageAsset(name: "bg_security_placeholder_light") - } - internal enum Buttons { - internal enum Checkable { - internal static let icChecked = ImageAsset(name: "ic_checked") - internal static let icPartialySelected = ImageAsset(name: "ic_partialy_selected") - internal static let icUnchecked = ImageAsset(name: "ic_unchecked") - } - } - internal enum CallItems { - internal static let icAcceptCall = ImageAsset(name: "ic_accept_call") - internal static let icAcceptCallBig = ImageAsset(name: "ic_accept_call_big") - internal static let icDeclineCall = ImageAsset(name: "ic_decline_call") - internal static let icGoToChat = ImageAsset(name: "ic_go_to_chat") - internal static let icMoreVoiceCall = ImageAsset(name: "ic_more_voice_call") - internal static let icMuteVoiceCall = ImageAsset(name: "ic_mute_voice_call") - internal static let icOutgoingCall = ImageAsset(name: "ic_outgoing_call") - internal static let icPortOut1 = ImageAsset(name: "ic_port_out-1") - internal static let icPortOut = ImageAsset(name: "ic_port_out") - internal static let icSpeakerOff = ImageAsset(name: "ic_speaker_off") - internal static let icSpeakerOn = ImageAsset(name: "ic_speaker_on") - internal static let icUnmuteVoiceCall = ImageAsset(name: "ic_unmute_voice_call") - internal static let icVideoOffVoiceCall = ImageAsset(name: "ic_video_off_voice_call") - internal static let icVideoOnVoiceCall = ImageAsset(name: "ic_video_on_voice_call") - } - internal enum CameraItems { - internal static let icCameraFrame = ImageAsset(name: "ic_camera_frame") - internal static let icFlashlightActive = ImageAsset(name: "ic_flashlight_active") - internal static let icFlashlightInactive = ImageAsset(name: "ic_flashlight_inactive") - } + internal static let background = ImageAsset(name: "background") + internal static let backgroundLight = ImageAsset(name: "background_light") + internal static let bgSecurityPlaceholderDark = ImageAsset(name: "bg_security_placeholder_dark") + internal static let bgSecurityPlaceholderLight = ImageAsset(name: "bg_security_placeholder_light") + internal static let icChecked = ImageAsset(name: "ic_checked") + internal static let icPartialySelected = ImageAsset(name: "ic_partialy_selected") + internal static let icUnchecked = ImageAsset(name: "ic_unchecked") + internal static let icAcceptCall = ImageAsset(name: "ic_accept_call") + internal static let icAcceptCallBig = ImageAsset(name: "ic_accept_call_big") + internal static let icDeclineCall = ImageAsset(name: "ic_decline_call") + internal static let icGoToChat = ImageAsset(name: "ic_go_to_chat") + internal static let icMoreVoiceCall = ImageAsset(name: "ic_more_voice_call") + internal static let icMuteVoiceCall = ImageAsset(name: "ic_mute_voice_call") + internal static let icOutgoingCall = ImageAsset(name: "ic_outgoing_call") + internal static let icPortOut1 = ImageAsset(name: "ic_port_out-1") + internal static let icPortOut = ImageAsset(name: "ic_port_out") + internal static let icSpeakerOff = ImageAsset(name: "ic_speaker_off") + internal static let icSpeakerOn = ImageAsset(name: "ic_speaker_on") + internal static let icUnmuteVoiceCall = ImageAsset(name: "ic_unmute_voice_call") + internal static let icVideoOffVoiceCall = ImageAsset(name: "ic_video_off_voice_call") + internal static let icVideoOnVoiceCall = ImageAsset(name: "ic_video_on_voice_call") + internal static let icCameraFrame = ImageAsset(name: "ic_camera_frame") + internal static let icFlashlightActive = ImageAsset(name: "ic_flashlight_active") + internal static let icFlashlightInactive = ImageAsset(name: "ic_flashlight_inactive") internal static let combinedShape1960 = ImageAsset(name: "Combined_shape_1960") - internal enum Contacts { - internal static let avaPlaceholder = ImageAsset(name: "ava_placeholder") - internal static let circleRed = ImageAsset(name: "circle_red") - internal static let contactSeparatop = ImageAsset(name: "contact_separatop") - } - internal enum Forward { - internal static let icForwardContacts = ImageAsset(name: "ic_forward_contacts") - internal static let icForwardContactsSelected = ImageAsset(name: "ic_forward_contacts_selected") - internal static let icForwardGroups = ImageAsset(name: "ic_forward_groups") - internal static let icForwardGroupsSelected = ImageAsset(name: "ic_forward_groups_selected") - internal static let icForwardSendIcon = ImageAsset(name: "ic_forward_send_icon") - } - internal enum GroupStorage { - internal static let icAudioFormat = ImageAsset(name: "ic_audio_format") - internal static let icUnknownFormat = ImageAsset(name: "ic_unknown_format") - } + internal static let avaPlaceholder = ImageAsset(name: "ava_placeholder") + internal static let circleRed = ImageAsset(name: "circle_red") + internal static let contactSeparatop = ImageAsset(name: "contact_separatop") + internal static let icForwardContacts = ImageAsset(name: "ic_forward_contacts") + internal static let icForwardContactsSelected = ImageAsset(name: "ic_forward_contacts_selected") + internal static let icForwardGroups = ImageAsset(name: "ic_forward_groups") + internal static let icForwardGroupsSelected = ImageAsset(name: "ic_forward_groups_selected") + internal static let icForwardSendIcon = ImageAsset(name: "ic_forward_send_icon") + internal static let icAudioFormat = ImageAsset(name: "ic_audio_format") + internal static let icUnknownFormat = ImageAsset(name: "ic_unknown_format") internal static let group1 = ImageAsset(name: "Group_1") internal static let group2 = ImageAsset(name: "Group_2") + internal static let iconsGeneralIcAcceptCall = ImageAsset(name: "Icons_General_ic_accept_call") internal static let iconsGeneralIcClose = ImageAsset(name: "Icons_General_ic_close") + internal static let iconsGeneralIcEmail = ImageAsset(name: "Icons_General_ic_email") internal static let iconsGeneralIcEye = ImageAsset(name: "Icons_General_ic_eye") + internal static let iconsGeneralIcGoogle = ImageAsset(name: "Icons_General_ic_google") internal static let iconsGeneralIcGreyClose = ImageAsset(name: "Icons_General_ic_grey_close") internal static let iconsGeneralIcTranslate = ImageAsset(name: "Icons_General_ic_translate") - internal enum LastMessageType { - internal static let icLastContact = ImageAsset(name: "ic_last_contact") - internal static let icLastEmoji = ImageAsset(name: "ic_last_emoji") - internal static let icLastFile = ImageAsset(name: "ic_last_file") - internal static let icLastLocation = ImageAsset(name: "ic_last_location") - internal static let icLastPhoto = ImageAsset(name: "ic_last_photo") - internal static let icLastVideo = ImageAsset(name: "ic_last_video") - internal static let icLastVideoCall = ImageAsset(name: "ic_last_video_call") - internal static let icLastVoiceMsg = ImageAsset(name: "ic_last_voice_msg") - } - internal enum LinkField { - internal static let icRefresh = ImageAsset(name: "ic_refresh") - } - internal enum Logo { - internal static let authLightLogo = ImageAsset(name: "auth_light_logo") - internal static let darkLogo = ImageAsset(name: "dark_logo") - internal static let icLogo = ImageAsset(name: "ic_logo") - internal static let lightLogo = ImageAsset(name: "light_logo") - internal static let logo1 = ImageAsset(name: "logo-1") - internal static let logo = ImageAsset(name: "logo") - } - internal enum MainWheel { - internal enum Chat { - internal static let disableFamily = ImageAsset(name: "disable_family") - internal static let disableFavorites = ImageAsset(name: "disable_favorites") - internal static let disableWork = ImageAsset(name: "disable_work") - internal static let family = ImageAsset(name: "family") - internal static let favorites = ImageAsset(name: "favorites") - internal static let list = ImageAsset(name: "list") - internal static let work = ImageAsset(name: "work") - } - internal enum NewContact { - internal static let history = ImageAsset(name: "History") - internal static let qrCode = ImageAsset(name: "QR code") - internal static let username = ImageAsset(name: "Username") - internal static let byContacts = ImageAsset(name: "by_contacts") - internal static let code = ImageAsset(name: "code") - internal static let number = ImageAsset(name: "number") - } - internal enum Actions { - internal static let file = ImageAsset(name: "File") - internal static let contact = ImageAsset(name: "contact") - internal static let event = ImageAsset(name: "event") - internal static let galery = ImageAsset(name: "galery") - internal static let location = ImageAsset(name: "location") - internal static let photos = ImageAsset(name: "photos") - internal static let videoCall = ImageAsset(name: "video_call") - internal static let voiceCall = ImageAsset(name: "voice_call") - } - internal static let actions = ImageAsset(name: "actions") - internal static let chats = ImageAsset(name: "chats") - internal static let contacts = ImageAsset(name: "contacts") - internal static let disableGroups = ImageAsset(name: "disable_groups") - internal static let disableNewGroup = ImageAsset(name: "disable_new group") - internal static let disableSearch = ImageAsset(name: "disable_search") - internal static let fav = ImageAsset(name: "fav") - internal static let groups = ImageAsset(name: "groups") - internal static let icCloseWheel = ImageAsset(name: "ic_close_wheel") - internal static let myProfile = ImageAsset(name: "my_profile") - internal enum NewGroup { - internal static let done = ImageAsset(name: "done") - } - internal static let newContact = ImageAsset(name: "new_contact") - internal static let newGroup = ImageAsset(name: "new_group") - internal static let nextBttn = ImageAsset(name: "next_bttn") - internal static let search = ImageAsset(name: "search") - internal static let send = ImageAsset(name: "send") - internal static let settings = ImageAsset(name: "settings") - internal static let wheelInactive = ImageAsset(name: "wheel_inactive") - } - internal enum Marketplace { - internal static let marketplaceSwapButton = ImageAsset(name: "marketplace_swap_button") - internal enum Menu { - internal enum Access { - internal static let marketplaceApps = ImageAsset(name: "marketplace_apps") - internal static let marketplaceBots = ImageAsset(name: "marketplace_bots") - internal static let marketplaceGroupsChannels = ImageAsset(name: "marketplace_groups_channels") - } - internal enum Freelance { - internal static let marketplaceDesign = ImageAsset(name: "marketplace_design") - internal static let marketplaceInterpretation = ImageAsset(name: "marketplace_interpretation") - internal static let marketplaceSupport = ImageAsset(name: "marketplace_support") - } - internal enum Main { - internal static let marketplaceAccess = ImageAsset(name: "marketplace_access") - internal static let marketplaceFreelance = ImageAsset(name: "marketplace_freelance") - internal static let marketplaceVirtualGoods = ImageAsset(name: "marketplace_virtual_goods") - } - internal enum VirtualGoods { - internal static let marketplaceMediaContent = ImageAsset(name: "marketplace_media_content") - internal static let marketplaceSticker = ImageAsset(name: "marketplace_sticker") - } - } - } - internal enum MentionIndicator { - internal static let icChatMentionIndicator = ImageAsset(name: "ic_chat_mention_indicator") - internal static let icHomeMentionIndicator = ImageAsset(name: "ic_home_mention_indicator") - } - internal enum Messages { - internal enum ContextMenu { - internal static let contextMenuNext = ImageAsset(name: "contextMenuNext") - internal static let contextMenuPrevious = ImageAsset(name: "contextMenuPrevious") - internal static let icAnotherLanguageTranscribeContextMenu = ImageAsset(name: "ic_another_language_transcribe_context_menu") - internal static let icAnotherLanguageTranslateContextMenu = ImageAsset(name: "ic_another_language_translate_context_menu") - internal static let icCopyContextMenu = ImageAsset(name: "ic_copy_context_menu") - internal static let icDeleteContextMenu = ImageAsset(name: "ic_delete_context_menu") - internal static let icEditContextMenu = ImageAsset(name: "ic_edit_context_menu") - internal static let icForwardContextMenu = ImageAsset(name: "ic_forward_context_menu") - internal static let icReplyContextMenu = ImageAsset(name: "ic_reply_context_menu") - internal static let icSaveToDownloadsContextMenu = ImageAsset(name: "ic_save_to_downloads_context_menu") - internal static let icSaveToGalleryContextMenu = ImageAsset(name: "ic_save_to_gallery_context_menu") - internal static let icShareContextMenu = ImageAsset(name: "ic_share_context_menu") - internal static let icStarContextMenu = ImageAsset(name: "ic_star_context_menu") - internal static let icTranscribeContextMenu = ImageAsset(name: "ic_transcribe_context_menu") - internal static let icTranslateContextMenu = ImageAsset(name: "ic_translate_context_menu") - } - internal enum Counter { - internal static let icEyeBubleGray = ImageAsset(name: "ic_eye_buble_gray") - internal static let icReplyBubbleGray = ImageAsset(name: "ic_reply_bubble_gray") - internal static let icShareBubbleGray = ImageAsset(name: "ic_share_bubble_gray") - } - internal enum Delivery { - internal static let iicMessageUnsent = ImageAsset(name: "Iic_message_unsent") - internal static let icMessageRead = ImageAsset(name: "ic_message_read") - internal static let icMessageSent = ImageAsset(name: "ic_message_sent") - } - internal enum Loading { - internal static let icBtnStartDownload = ImageAsset(name: "ic_btn_start_download") - internal static let icBtnStopDownload = ImageAsset(name: "ic_btn_stop_download") - internal static let startDownload = ImageAsset(name: "start_download") - internal static let stopDownload = ImageAsset(name: "stop_download") - } - internal enum Media { - internal static let icBtnPlay = ImageAsset(name: "ic_btn_play") - internal static let icPauseBubble = ImageAsset(name: "ic_pause_bubble") - internal static let icPlayBubble = ImageAsset(name: "ic_play_bubble") - } - } - internal enum NewFolder { - internal static let delayMessage = ImageAsset(name: "Delay-Message") - internal static let emoji = ImageAsset(name: "Emoji") - internal static let acceptAudioBttn = ImageAsset(name: "accept-audio-bttn") - internal static let acceptBttn = ImageAsset(name: "accept-bttn") - internal static let activeMap = ImageAsset(name: "active-map") - internal static let cancelCall = ImageAsset(name: "cancel-call") - internal static let cancelVideoCall = ImageAsset(name: "cancel_video_call") - internal static let centerIcon = ImageAsset(name: "center-icon") - internal static let changePhoto = ImageAsset(name: "change-photo") - internal static let country = ImageAsset(name: "country") - internal static let delay = ImageAsset(name: "delay") - internal static let detailsMap = ImageAsset(name: "details_map") - internal static let editIcon1 = ImageAsset(name: "edit-icon-1") - internal static let evntsInactive = ImageAsset(name: "evnts-inactive") - internal static let familyInactive = ImageAsset(name: "family-inactive") - internal static let favoritesActive = ImageAsset(name: "favorites-active") - internal static let favoritesInactive = ImageAsset(name: "favorites-inactive") - internal static let icIncomingCallWhite = ImageAsset(name: "ic_incoming_call_white") - internal static let icOutgoingCallWhite = ImageAsset(name: "ic_outgoing_call_white") - internal static let iconNinja = ImageAsset(name: "icon_ninja") - internal static let incomingDark = ImageAsset(name: "incoming_dark") - internal static let incomingLight = ImageAsset(name: "incoming_light") - internal static let incomingVideoDark = ImageAsset(name: "incoming_video_dark") - internal static let incomingVideoLight = ImageAsset(name: "incoming_video_light") - internal static let lastVideoCall = ImageAsset(name: "last-Video-Call") - internal static let lastLocation = ImageAsset(name: "last-location") - internal static let lastPhoto = ImageAsset(name: "last-photo") - internal static let lastVideo = ImageAsset(name: "last-video") - internal static let lastVoiceMsg = ImageAsset(name: "last-voice-msg") - internal static let messageBttn = ImageAsset(name: "message-bttn") - internal static let microphoneBttn = ImageAsset(name: "microphone-bttn") - internal static let nextBttn = ImageAsset(name: "next-bttn") - internal static let outgoingDark = ImageAsset(name: "outgoing_dark") - internal static let outgoingLight = ImageAsset(name: "outgoing_light") - internal static let outgoingVideoDark = ImageAsset(name: "outgoing_video_dark") - internal static let outgoingVideoLight = ImageAsset(name: "outgoing_video_light") - internal static let phoneNumber = ImageAsset(name: "phone-number") - internal static let qrCode = ImageAsset(name: "qr-code") - internal static let recBar = ImageAsset(name: "rec-bar") - internal static let recProcess = ImageAsset(name: "rec-process") - internal static let recLight = ImageAsset(name: "rec_light") - internal static let schedule = ImageAsset(name: "schedule") - internal static let searchInactive = ImageAsset(name: "search-inactive") - internal static let sendBttn = ImageAsset(name: "send-bttn") - internal static let separators = ImageAsset(name: "separators") - internal static let speakerBttn = ImageAsset(name: "speaker-bttn") - internal static let switchCameraBttn = ImageAsset(name: "switch-camera-bttn") - internal static let textBar = ImageAsset(name: "text-bar") - internal static let workInactive = ImageAsset(name: "work-inactive") - } + internal static let icLastContact = ImageAsset(name: "ic_last_contact") + internal static let icLastEmoji = ImageAsset(name: "ic_last_emoji") + internal static let icLastFile = ImageAsset(name: "ic_last_file") + internal static let icLastLocation = ImageAsset(name: "ic_last_location") + internal static let icLastPhoto = ImageAsset(name: "ic_last_photo") + internal static let icLastVideo = ImageAsset(name: "ic_last_video") + internal static let icLastVideoCall = ImageAsset(name: "ic_last_video_call") + internal static let icLastVoiceMsg = ImageAsset(name: "ic_last_voice_msg") + internal static let icRefresh = ImageAsset(name: "ic_refresh") + internal static let authLightLogo = ImageAsset(name: "auth_light_logo") + internal static let darkLogo = ImageAsset(name: "dark_logo") + internal static let icLogo = ImageAsset(name: "ic_logo") + internal static let lightLogo = ImageAsset(name: "light_logo") + internal static let logo1 = ImageAsset(name: "logo-1") + internal static let logo = ImageAsset(name: "logo") + internal static let disableFamily = ImageAsset(name: "disable_family") + internal static let disableFavorites = ImageAsset(name: "disable_favorites") + internal static let disableWork = ImageAsset(name: "disable_work") + internal static let family = ImageAsset(name: "family") + internal static let favorites = ImageAsset(name: "favorites") + internal static let list = ImageAsset(name: "list") + internal static let work = ImageAsset(name: "work") + internal static let history = ImageAsset(name: "History") + internal static let qrCode = ImageAsset(name: "QR code") + internal static let username = ImageAsset(name: "Username") + internal static let byContacts = ImageAsset(name: "by_contacts") + internal static let code = ImageAsset(name: "code") + internal static let number = ImageAsset(name: "number") + internal static let file = ImageAsset(name: "File") + internal static let contact = ImageAsset(name: "contact") + internal static let event = ImageAsset(name: "event") + internal static let galery = ImageAsset(name: "galery") + internal static let location = ImageAsset(name: "location") + internal static let photos = ImageAsset(name: "photos") + internal static let videoCall = ImageAsset(name: "video_call") + internal static let voiceCall = ImageAsset(name: "voice_call") + internal static let actions = ImageAsset(name: "actions") + internal static let chats = ImageAsset(name: "chats") + internal static let contacts = ImageAsset(name: "contacts") + internal static let disableGroups = ImageAsset(name: "disable_groups") + internal static let disableNewGroup = ImageAsset(name: "disable_new group") + internal static let disableSearch = ImageAsset(name: "disable_search") + internal static let fav = ImageAsset(name: "fav") + internal static let groups = ImageAsset(name: "groups") + internal static let icCloseWheel = ImageAsset(name: "ic_close_wheel") + internal static let myProfile = ImageAsset(name: "my_profile") + internal static let done = ImageAsset(name: "done") + internal static let newContact = ImageAsset(name: "new_contact") + internal static let newGroup = ImageAsset(name: "new_group") + internal static let nextBttn = ImageAsset(name: "next_bttn") + internal static let search = ImageAsset(name: "search") + internal static let send = ImageAsset(name: "send") + internal static let settings = ImageAsset(name: "settings") + internal static let wheelInactive = ImageAsset(name: "wheel_inactive") + internal static let marketplaceSwapButton = ImageAsset(name: "marketplace_swap_button") + internal static let marketplaceApps = ImageAsset(name: "marketplace_apps") + internal static let marketplaceBots = ImageAsset(name: "marketplace_bots") + internal static let marketplaceGroupsChannels = ImageAsset(name: "marketplace_groups_channels") + internal static let marketplaceDesign = ImageAsset(name: "marketplace_design") + internal static let marketplaceInterpretation = ImageAsset(name: "marketplace_interpretation") + internal static let marketplaceSupport = ImageAsset(name: "marketplace_support") + internal static let marketplaceAccess = ImageAsset(name: "marketplace_access") + internal static let marketplaceFreelance = ImageAsset(name: "marketplace_freelance") + internal static let marketplaceVirtualGoods = ImageAsset(name: "marketplace_virtual_goods") + internal static let marketplaceMediaContent = ImageAsset(name: "marketplace_media_content") + internal static let marketplaceSticker = ImageAsset(name: "marketplace_sticker") + internal static let icChatMentionIndicator = ImageAsset(name: "ic_chat_mention_indicator") + internal static let icHomeMentionIndicator = ImageAsset(name: "ic_home_mention_indicator") + internal static let contextMenuNext = ImageAsset(name: "contextMenuNext") + internal static let contextMenuPrevious = ImageAsset(name: "contextMenuPrevious") + internal static let icAnotherLanguageTranscribeContextMenu = ImageAsset(name: "ic_another_language_transcribe_context_menu") + internal static let icAnotherLanguageTranslateContextMenu = ImageAsset(name: "ic_another_language_translate_context_menu") + internal static let icCopyContextMenu = ImageAsset(name: "ic_copy_context_menu") + internal static let icDeleteContextMenu = ImageAsset(name: "ic_delete_context_menu") + internal static let icEditContextMenu = ImageAsset(name: "ic_edit_context_menu") + internal static let icForwardContextMenu = ImageAsset(name: "ic_forward_context_menu") + internal static let icReplyContextMenu = ImageAsset(name: "ic_reply_context_menu") + internal static let icSaveToDownloadsContextMenu = ImageAsset(name: "ic_save_to_downloads_context_menu") + internal static let icSaveToGalleryContextMenu = ImageAsset(name: "ic_save_to_gallery_context_menu") + internal static let icShareContextMenu = ImageAsset(name: "ic_share_context_menu") + internal static let icStarContextMenu = ImageAsset(name: "ic_star_context_menu") + internal static let icTranscribeContextMenu = ImageAsset(name: "ic_transcribe_context_menu") + internal static let icTranslateContextMenu = ImageAsset(name: "ic_translate_context_menu") + internal static let icEyeBubleGray = ImageAsset(name: "ic_eye_buble_gray") + internal static let icReplyBubbleGray = ImageAsset(name: "ic_reply_bubble_gray") + internal static let icShareBubbleGray = ImageAsset(name: "ic_share_bubble_gray") + internal static let iicMessageUnsent = ImageAsset(name: "Iic_message_unsent") + internal static let icMessageRead = ImageAsset(name: "ic_message_read") + internal static let icMessageSent = ImageAsset(name: "ic_message_sent") + internal static let icBtnStartDownload = ImageAsset(name: "ic_btn_start_download") + internal static let icBtnStopDownload = ImageAsset(name: "ic_btn_stop_download") + internal static let startDownload = ImageAsset(name: "start_download") + internal static let stopDownload = ImageAsset(name: "stop_download") + internal static let icBtnPlay = ImageAsset(name: "ic_btn_play") + internal static let icPauseBubble = ImageAsset(name: "ic_pause_bubble") + internal static let icPlayBubble = ImageAsset(name: "ic_play_bubble") + internal static let delayMessage = ImageAsset(name: "Delay-Message") + internal static let emoji = ImageAsset(name: "Emoji") + internal static let acceptAudioBttn = ImageAsset(name: "accept-audio-bttn") + internal static let acceptBttn = ImageAsset(name: "accept-bttn") + internal static let activeMap = ImageAsset(name: "active-map") + internal static let cancelCall = ImageAsset(name: "cancel-call") + internal static let cancelVideoCall = ImageAsset(name: "cancel_video_call") + internal static let centerIcon = ImageAsset(name: "center-icon") + internal static let changePhoto = ImageAsset(name: "change-photo") + internal static let country = ImageAsset(name: "country") + internal static let delay = ImageAsset(name: "delay") + internal static let detailsMap = ImageAsset(name: "details_map") + internal static let editIcon1 = ImageAsset(name: "edit-icon-1") + internal static let evntsInactive = ImageAsset(name: "evnts-inactive") + internal static let familyInactive = ImageAsset(name: "family-inactive") + internal static let favoritesActive = ImageAsset(name: "favorites-active") + internal static let favoritesInactive = ImageAsset(name: "favorites-inactive") + internal static let icIncomingCallWhite = ImageAsset(name: "ic_incoming_call_white") + internal static let icOutgoingCallWhite = ImageAsset(name: "ic_outgoing_call_white") + internal static let iconNinja = ImageAsset(name: "icon_ninja") + internal static let incomingDark = ImageAsset(name: "incoming_dark") + internal static let incomingLight = ImageAsset(name: "incoming_light") + internal static let incomingVideoDark = ImageAsset(name: "incoming_video_dark") + internal static let incomingVideoLight = ImageAsset(name: "incoming_video_light") + internal static let lastVideoCall = ImageAsset(name: "last-Video-Call") + internal static let lastLocation = ImageAsset(name: "last-location") + internal static let lastPhoto = ImageAsset(name: "last-photo") + internal static let lastVideo = ImageAsset(name: "last-video") + internal static let lastVoiceMsg = ImageAsset(name: "last-voice-msg") + internal static let messageBttn = ImageAsset(name: "message-bttn") + internal static let microphoneBttn = ImageAsset(name: "microphone-bttn") + internal static let outgoingDark = ImageAsset(name: "outgoing_dark") + internal static let outgoingLight = ImageAsset(name: "outgoing_light") + internal static let outgoingVideoDark = ImageAsset(name: "outgoing_video_dark") + internal static let outgoingVideoLight = ImageAsset(name: "outgoing_video_light") + internal static let phoneNumber = ImageAsset(name: "phone-number") + internal static let recBar = ImageAsset(name: "rec-bar") + internal static let recProcess = ImageAsset(name: "rec-process") + internal static let recLight = ImageAsset(name: "rec_light") + internal static let schedule = ImageAsset(name: "schedule") + internal static let searchInactive = ImageAsset(name: "search-inactive") + internal static let sendBttn = ImageAsset(name: "send-bttn") + internal static let separators = ImageAsset(name: "separators") + internal static let speakerBttn = ImageAsset(name: "speaker-bttn") + internal static let switchCameraBttn = ImageAsset(name: "switch-camera-bttn") + internal static let textBar = ImageAsset(name: "text-bar") + internal static let workInactive = ImageAsset(name: "work-inactive") internal static let oval14 = ImageAsset(name: "Oval_14") internal static let oval17 = ImageAsset(name: "Oval_17") internal static let oval6 = ImageAsset(name: "Oval_6") internal static let sendAsFile = ImageAsset(name: "SendAsFile") - internal enum Stickers { - internal static let stickerStub = ImageAsset(name: "sticker_stub") - internal static let stickersIcAdd = ImageAsset(name: "stickers_ic_add") - internal static let stickersIcDefaultPack = ImageAsset(name: "stickers_ic_default_pack") - internal static let stickersIcRecent = ImageAsset(name: "stickers_ic_recent") - internal static let stickersIcSearch = ImageAsset(name: "stickers_ic_search") - } - internal enum Wallet { - internal static let icArrowTransferDown = ImageAsset(name: "ic_arrow_transfer_down") - internal static let icArrowTransferUp = ImageAsset(name: "ic_arrow_transfer_up") - internal static let icBtc = ImageAsset(name: "ic_btc") - internal static let icEos = ImageAsset(name: "ic_eos") - internal static let icNyn = ImageAsset(name: "ic_nyn") - internal static let icPay = ImageAsset(name: "ic_pay") - internal static let icWallet = ImageAsset(name: "ic_wallet") - } - internal enum Wheel { - internal enum Button { - internal static let btnWheelInactive = ImageAsset(name: "btn_wheel_inactive") - internal static let btnWheelInactiveLight = ImageAsset(name: "btn_wheel_inactive_light") - internal static let wheel = ImageAsset(name: "wheel") - } - internal enum Placeholders { - internal static let icBrokenImagePlaceholder = ImageAsset(name: "ic_broken_image_placeholder") - internal static let icEmptyImagePlaceholder = ImageAsset(name: "ic_empty_image_placeholder") - internal static let icEmptyLocationPlaceholder = ImageAsset(name: "ic_empty_location_placeholder") - } - internal enum WheelItems { - internal static let icActions = ImageAsset(name: "ic_actions") - internal static let icByContacts = ImageAsset(name: "ic_by_contacts") - internal static let icByNumber = ImageAsset(name: "ic_by_number") - internal static let icByPassword = ImageAsset(name: "ic_by_password") - internal static let icByQrCode = ImageAsset(name: "ic_by_qr_code") - internal static let icByUsername = ImageAsset(name: "ic_by_username") - internal static let icCalls = ImageAsset(name: "ic_calls") - internal static let icCamera = ImageAsset(name: "ic_camera") - internal static let icChannelInactive = ImageAsset(name: "ic_channel_inactive") - internal static let icChannelNew = ImageAsset(name: "ic_channel_new") - internal static let icChats = ImageAsset(name: "ic_chats") - internal static let icContacts = ImageAsset(name: "ic_contacts") - internal static let icEditProfile = ImageAsset(name: "ic_edit_profile") - internal static let icFamily = ImageAsset(name: "ic_family") - internal static let icFile = ImageAsset(name: "ic_file") - internal static let icFriends = ImageAsset(name: "ic_friends") - internal static let icGallery = ImageAsset(name: "ic_gallery") - internal static let icGroupCall = ImageAsset(name: "ic_group_call") - internal static let icGroupCalls = ImageAsset(name: "ic_group_calls") - internal static let icGroupSettings = ImageAsset(name: "ic_group_settings") - internal static let icGroupVideo = ImageAsset(name: "ic_group_video") - internal static let icGroups = ImageAsset(name: "ic_groups") - internal static let icHistory = ImageAsset(name: "ic_history") - internal static let icHome = ImageAsset(name: "ic_home") - internal static let icList = ImageAsset(name: "ic_list") - internal static let icLocation = ImageAsset(name: "ic_location") - internal static let icMyself = ImageAsset(name: "ic_myself") - internal static let icNew = ImageAsset(name: "ic_new") - internal static let icNewAudioCall = ImageAsset(name: "ic_new_audio_call") - internal static let icNewChat = ImageAsset(name: "ic_new_chat") - internal static let icNewContact = ImageAsset(name: "ic_new_contact") - internal static let icNewGroup = ImageAsset(name: "ic_new_group") - internal static let icNewPhoneCall = ImageAsset(name: "ic_new_phone_call") - internal static let icNewVideoCall = ImageAsset(name: "ic_new_video_call") - internal static let icOptions = ImageAsset(name: "ic_options") - internal static let icRecents = ImageAsset(name: "ic_recents") - internal static let icSearch = ImageAsset(name: "ic_search") - internal static let icStarred = ImageAsset(name: "ic_starred") - internal static let icVideo = ImageAsset(name: "ic_video") - internal static let icVideoIndicator = ImageAsset(name: "ic_video_indicator") - internal static let icWork = ImageAsset(name: "ic_work") - } - internal enum WheelPosition { - internal static let wheelLeftImage = ImageAsset(name: "wheel_left_image") - internal static let wheelRightImage = ImageAsset(name: "wheel_right_image") - } - } - internal enum WheelPosition { - internal static let wheelLeftImage = ImageAsset(name: "wheel_left_image") - internal static let wheelRightImage = ImageAsset(name: "wheel_right_image") - } + internal static let stickerStub = ImageAsset(name: "sticker_stub") + internal static let stickersIcAdd = ImageAsset(name: "stickers_ic_add") + internal static let stickersIcDefaultPack = ImageAsset(name: "stickers_ic_default_pack") + internal static let stickersIcRecent = ImageAsset(name: "stickers_ic_recent") + internal static let stickersIcSearch = ImageAsset(name: "stickers_ic_search") + internal static let icArrowTransferDown = ImageAsset(name: "ic_arrow_transfer_down") + internal static let icArrowTransferUp = ImageAsset(name: "ic_arrow_transfer_up") + internal static let icBtc = ImageAsset(name: "ic_btc") + internal static let icEos = ImageAsset(name: "ic_eos") + internal static let icNyn = ImageAsset(name: "ic_nyn") + internal static let icPay = ImageAsset(name: "ic_pay") + internal static let icWallet = ImageAsset(name: "ic_wallet") + internal static let btnWheelInactive = ImageAsset(name: "btn_wheel_inactive") + internal static let btnWheelInactiveLight = ImageAsset(name: "btn_wheel_inactive_light") + internal static let wheel = ImageAsset(name: "wheel") + internal static let icBrokenImagePlaceholder = ImageAsset(name: "ic_broken_image_placeholder") + internal static let icEmptyImagePlaceholder = ImageAsset(name: "ic_empty_image_placeholder") + internal static let icEmptyLocationPlaceholder = ImageAsset(name: "ic_empty_location_placeholder") + internal static let icActions = ImageAsset(name: "ic_actions") + internal static let icByContacts = ImageAsset(name: "ic_by_contacts") + internal static let icByNumber = ImageAsset(name: "ic_by_number") + internal static let icByPassword = ImageAsset(name: "ic_by_password") + internal static let icByQrCode = ImageAsset(name: "ic_by_qr_code") + internal static let icByUsername = ImageAsset(name: "ic_by_username") + internal static let icCalls = ImageAsset(name: "ic_calls") + internal static let icCamera = ImageAsset(name: "ic_camera") + internal static let icChannelInactive = ImageAsset(name: "ic_channel_inactive") + internal static let icChannelNew = ImageAsset(name: "ic_channel_new") + internal static let icChats = ImageAsset(name: "ic_chats") + internal static let icContacts = ImageAsset(name: "ic_contacts") + internal static let icEditProfile = ImageAsset(name: "ic_edit_profile") + internal static let icFamily = ImageAsset(name: "ic_family") + internal static let icFile = ImageAsset(name: "ic_file") + internal static let icFriends = ImageAsset(name: "ic_friends") + internal static let icGallery = ImageAsset(name: "ic_gallery") + internal static let icGroupCall = ImageAsset(name: "ic_group_call") + internal static let icGroupCalls = ImageAsset(name: "ic_group_calls") + internal static let icGroupSettings = ImageAsset(name: "ic_group_settings") + internal static let icGroupVideo = ImageAsset(name: "ic_group_video") + internal static let icGroups = ImageAsset(name: "ic_groups") + internal static let icHistory = ImageAsset(name: "ic_history") + internal static let icHome = ImageAsset(name: "ic_home") + internal static let icList = ImageAsset(name: "ic_list") + internal static let icLocation = ImageAsset(name: "ic_location") + internal static let icMyself = ImageAsset(name: "ic_myself") + internal static let icNew = ImageAsset(name: "ic_new") + internal static let icNewAudioCall = ImageAsset(name: "ic_new_audio_call") + internal static let icNewChat = ImageAsset(name: "ic_new_chat") + internal static let icNewContact = ImageAsset(name: "ic_new_contact") + internal static let icNewGroup = ImageAsset(name: "ic_new_group") + internal static let icNewPhoneCall = ImageAsset(name: "ic_new_phone_call") + internal static let icNewVideoCall = ImageAsset(name: "ic_new_video_call") + internal static let icOptions = ImageAsset(name: "ic_options") + internal static let icRecents = ImageAsset(name: "ic_recents") + internal static let icSearch = ImageAsset(name: "ic_search") + internal static let icStarred = ImageAsset(name: "ic_starred") + internal static let icVideo = ImageAsset(name: "ic_video") + internal static let icVideoIndicator = ImageAsset(name: "ic_video_indicator") + internal static let icWork = ImageAsset(name: "ic_work") + internal static let wheelLeftImage = ImageAsset(name: "wheel_left_image") + internal static let wheelRightImage = ImageAsset(name: "wheel_right_image") internal static let arrowCollapse = ImageAsset(name: "arrow_collapse") internal static let arrowExpand = ImageAsset(name: "arrow_expand") internal static let barsTabBarOverridesCenterButtonBtnPlayVideo = ImageAsset(name: "bars_tab_bar_overrides_center_button_btn_play_video") @@ -402,7 +299,6 @@ internal enum Asset { internal static let icBack = ImageAsset(name: "ic_back") internal static let icBackNavigation = ImageAsset(name: "ic_back_navigation") internal static let icBottomArrow = ImageAsset(name: "ic_bottom_arrow") - internal static let icCameraFrame = ImageAsset(name: "ic_camera_frame") internal static let icChangeCameraIos = ImageAsset(name: "ic_change_camera_ios") internal static let icCheckmarkRed = ImageAsset(name: "ic_checkmark_red") internal static let icClock = ImageAsset(name: "ic_clock") @@ -413,6 +309,8 @@ internal enum Asset { internal static let icDeleteRedCircle = ImageAsset(name: "ic_delete_red_circle") internal static let icEditDone = ImageAsset(name: "ic_edit_done") internal static let icEmailStorage = ImageAsset(name: "ic_email_storage") + internal static let icEmptyAvatar = ImageAsset(name: "ic_empty_avatar") + internal static let icFacebook = ImageAsset(name: "ic_facebook") internal static let icFlashlightAuto = ImageAsset(name: "ic_flashlight_auto") internal static let icFlashlightOff = ImageAsset(name: "ic_flashlight_off") internal static let icFlashlightOn = ImageAsset(name: "ic_flashlight_on") @@ -427,14 +325,12 @@ internal enum Asset { internal static let icMarketplaceWheelContextMenu = ImageAsset(name: "ic_marketplace_wheel_context_menu") internal static let icMic = ImageAsset(name: "ic_mic") internal static let icMicDarkGray = ImageAsset(name: "ic_mic_dark_gray") - internal static let icNewGroup = ImageAsset(name: "ic_new_group") internal static let icParticipantsSearch = ImageAsset(name: "ic_participants_search") internal static let icPhoneStorage = ImageAsset(name: "ic_phone_storage") internal static let icPhotoPlaceholder = ImageAsset(name: "ic_photo_placeholder") internal static let icPlaceStar = ImageAsset(name: "ic_place_star") internal static let icQrCode = ImageAsset(name: "ic_qr_code") internal static let icScheduledMsgCheck = ImageAsset(name: "ic_scheduled_msg_check") - internal static let icSearch = ImageAsset(name: "ic_search") internal static let icSearchEmpty = ImageAsset(name: "ic_search_empty") internal static let icSendAsFile = ImageAsset(name: "ic_send_as_file") internal static let icSoundOff = ImageAsset(name: "ic_sound_off") @@ -458,30 +354,29 @@ internal enum Asset { internal static let imgEmptyStatesP2p = ImageAsset(name: "img_empty_states_p2p") internal static let imgEmptyStatesScheduled = ImageAsset(name: "img_empty_states_scheduled") internal static let imgEmptyStatesStarred = ImageAsset(name: "img_empty_states_starred") - internal enum Input { - internal static let arrowUp = ImageAsset(name: "arrow_up") - internal static let delayRed = ImageAsset(name: "delay_red") - internal static let editMsgButton = ImageAsset(name: "edit_msg_button") - internal static let icDelete = ImageAsset(name: "ic_delete") - internal static let icPause = ImageAsset(name: "ic_pause") - internal static let icPlay = ImageAsset(name: "ic_play") - internal static let icSend = ImageAsset(name: "ic_send") - internal static let icVoice = ImageAsset(name: "ic_voice") - internal static let inputKeyboard = ImageAsset(name: "input_keyboard") - internal static let inputLeftEmojiButton = ImageAsset(name: "input_left_emoji_button") - internal static let inputLeftKeyboardButton = ImageAsset(name: "input_left_keyboard_button") - internal static let inputMicrophone = ImageAsset(name: "input_microphone") - internal static let pause = ImageAsset(name: "pause") - internal static let playBtn = ImageAsset(name: "play_btn") - internal static let recBackground = ImageAsset(name: "rec_background") - internal static let roundedRedBig = ImageAsset(name: "rounded_red_big") - internal static let sendButton = ImageAsset(name: "send_button") - internal static let trash = ImageAsset(name: "trash") - internal static let voiceButton = ImageAsset(name: "voice_button") - } + internal static let arrowUp = ImageAsset(name: "arrow_up") + internal static let delayRed = ImageAsset(name: "delay_red") + internal static let editMsgButton = ImageAsset(name: "edit_msg_button") + internal static let icDelete = ImageAsset(name: "ic_delete") + internal static let icPause = ImageAsset(name: "ic_pause") + internal static let icPlay = ImageAsset(name: "ic_play") + internal static let icSend = ImageAsset(name: "ic_send") + internal static let icVoice = ImageAsset(name: "ic_voice") + internal static let inputKeyboard = ImageAsset(name: "input_keyboard") + internal static let inputLeftEmojiButton = ImageAsset(name: "input_left_emoji_button") + internal static let inputLeftKeyboardButton = ImageAsset(name: "input_left_keyboard_button") + internal static let inputMicrophone = ImageAsset(name: "input_microphone") + internal static let pause = ImageAsset(name: "pause") + internal static let playBtn = ImageAsset(name: "play_btn") + internal static let recBackground = ImageAsset(name: "rec_background") + internal static let roundedRedBig = ImageAsset(name: "rounded_red_big") + internal static let sendButton = ImageAsset(name: "send_button") + internal static let trash = ImageAsset(name: "trash") + internal static let voiceButton = ImageAsset(name: "voice_button") internal static let inputLeftButton = ImageAsset(name: "input_left_button") internal static let leftButton = ImageAsset(name: "left_button") internal static let lock = ImageAsset(name: "lock") + internal static let logo2 = ImageAsset(name: "logo-2") internal static let maximaze = ImageAsset(name: "maximaze") internal static let minimaze = ImageAsset(name: "minimaze") internal static let myLocation = ImageAsset(name: "my_location") @@ -496,416 +391,86 @@ internal enum Asset { internal static let transcribed = ImageAsset(name: "transcribed") internal static let translated = ImageAsset(name: "translated") internal static let world = ImageAsset(name: "world") - - // swiftlint:disable trailing_comma - internal static let allColors: [ColorAsset] = [ - ] - internal static let allImages: [ImageAsset] = [ - appIcon, - Background.background, - Background.backgroundLight, - Background.bgSecurityPlaceholderDark, - Background.bgSecurityPlaceholderLight, - Buttons.Checkable.icChecked, - Buttons.Checkable.icPartialySelected, - Buttons.Checkable.icUnchecked, - CallItems.icAcceptCall, - CallItems.icAcceptCallBig, - CallItems.icDeclineCall, - CallItems.icGoToChat, - CallItems.icMoreVoiceCall, - CallItems.icMuteVoiceCall, - CallItems.icOutgoingCall, - CallItems.icPortOut1, - CallItems.icPortOut, - CallItems.icSpeakerOff, - CallItems.icSpeakerOn, - CallItems.icUnmuteVoiceCall, - CallItems.icVideoOffVoiceCall, - CallItems.icVideoOnVoiceCall, - CameraItems.icCameraFrame, - CameraItems.icFlashlightActive, - CameraItems.icFlashlightInactive, - combinedShape1960, - Contacts.avaPlaceholder, - Contacts.circleRed, - Contacts.contactSeparatop, - Forward.icForwardContacts, - Forward.icForwardContactsSelected, - Forward.icForwardGroups, - Forward.icForwardGroupsSelected, - Forward.icForwardSendIcon, - GroupStorage.icAudioFormat, - GroupStorage.icUnknownFormat, - group1, - group2, - iconsGeneralIcClose, - iconsGeneralIcEye, - iconsGeneralIcGreyClose, - iconsGeneralIcTranslate, - LastMessageType.icLastContact, - LastMessageType.icLastEmoji, - LastMessageType.icLastFile, - LastMessageType.icLastLocation, - LastMessageType.icLastPhoto, - LastMessageType.icLastVideo, - LastMessageType.icLastVideoCall, - LastMessageType.icLastVoiceMsg, - LinkField.icRefresh, - Logo.authLightLogo, - Logo.darkLogo, - Logo.icLogo, - Logo.lightLogo, - Logo.logo1, - Logo.logo, - MainWheel.Chat.disableFamily, - MainWheel.Chat.disableFavorites, - MainWheel.Chat.disableWork, - MainWheel.Chat.family, - MainWheel.Chat.favorites, - MainWheel.Chat.list, - MainWheel.Chat.work, - MainWheel.NewContact.history, - MainWheel.NewContact.qrCode, - MainWheel.NewContact.username, - MainWheel.NewContact.byContacts, - MainWheel.NewContact.code, - MainWheel.NewContact.number, - MainWheel.Actions.file, - MainWheel.Actions.contact, - MainWheel.Actions.event, - MainWheel.Actions.galery, - MainWheel.Actions.location, - MainWheel.Actions.photos, - MainWheel.Actions.videoCall, - MainWheel.Actions.voiceCall, - MainWheel.actions, - MainWheel.chats, - MainWheel.contacts, - MainWheel.disableGroups, - MainWheel.disableNewGroup, - MainWheel.disableSearch, - MainWheel.fav, - MainWheel.groups, - MainWheel.icCloseWheel, - MainWheel.myProfile, - MainWheel.NewGroup.done, - MainWheel.newContact, - MainWheel.newGroup, - MainWheel.nextBttn, - MainWheel.search, - MainWheel.send, - MainWheel.settings, - MainWheel.wheelInactive, - Marketplace.marketplaceSwapButton, - Marketplace.Menu.Access.marketplaceApps, - Marketplace.Menu.Access.marketplaceBots, - Marketplace.Menu.Access.marketplaceGroupsChannels, - Marketplace.Menu.Freelance.marketplaceDesign, - Marketplace.Menu.Freelance.marketplaceInterpretation, - Marketplace.Menu.Freelance.marketplaceSupport, - Marketplace.Menu.Main.marketplaceAccess, - Marketplace.Menu.Main.marketplaceFreelance, - Marketplace.Menu.Main.marketplaceVirtualGoods, - Marketplace.Menu.VirtualGoods.marketplaceMediaContent, - Marketplace.Menu.VirtualGoods.marketplaceSticker, - MentionIndicator.icChatMentionIndicator, - MentionIndicator.icHomeMentionIndicator, - Messages.ContextMenu.contextMenuNext, - Messages.ContextMenu.contextMenuPrevious, - Messages.ContextMenu.icAnotherLanguageTranscribeContextMenu, - Messages.ContextMenu.icAnotherLanguageTranslateContextMenu, - Messages.ContextMenu.icCopyContextMenu, - Messages.ContextMenu.icDeleteContextMenu, - Messages.ContextMenu.icEditContextMenu, - Messages.ContextMenu.icForwardContextMenu, - Messages.ContextMenu.icReplyContextMenu, - Messages.ContextMenu.icSaveToDownloadsContextMenu, - Messages.ContextMenu.icSaveToGalleryContextMenu, - Messages.ContextMenu.icShareContextMenu, - Messages.ContextMenu.icStarContextMenu, - Messages.ContextMenu.icTranscribeContextMenu, - Messages.ContextMenu.icTranslateContextMenu, - Messages.Counter.icEyeBubleGray, - Messages.Counter.icReplyBubbleGray, - Messages.Counter.icShareBubbleGray, - Messages.Delivery.iicMessageUnsent, - Messages.Delivery.icMessageRead, - Messages.Delivery.icMessageSent, - Messages.Loading.icBtnStartDownload, - Messages.Loading.icBtnStopDownload, - Messages.Loading.startDownload, - Messages.Loading.stopDownload, - Messages.Media.icBtnPlay, - Messages.Media.icPauseBubble, - Messages.Media.icPlayBubble, - NewFolder.delayMessage, - NewFolder.emoji, - NewFolder.acceptAudioBttn, - NewFolder.acceptBttn, - NewFolder.activeMap, - NewFolder.cancelCall, - NewFolder.cancelVideoCall, - NewFolder.centerIcon, - NewFolder.changePhoto, - NewFolder.country, - NewFolder.delay, - NewFolder.detailsMap, - NewFolder.editIcon1, - NewFolder.evntsInactive, - NewFolder.familyInactive, - NewFolder.favoritesActive, - NewFolder.favoritesInactive, - NewFolder.icIncomingCallWhite, - NewFolder.icOutgoingCallWhite, - NewFolder.iconNinja, - NewFolder.incomingDark, - NewFolder.incomingLight, - NewFolder.incomingVideoDark, - NewFolder.incomingVideoLight, - NewFolder.lastVideoCall, - NewFolder.lastLocation, - NewFolder.lastPhoto, - NewFolder.lastVideo, - NewFolder.lastVoiceMsg, - NewFolder.messageBttn, - NewFolder.microphoneBttn, - NewFolder.nextBttn, - NewFolder.outgoingDark, - NewFolder.outgoingLight, - NewFolder.outgoingVideoDark, - NewFolder.outgoingVideoLight, - NewFolder.phoneNumber, - NewFolder.qrCode, - NewFolder.recBar, - NewFolder.recProcess, - NewFolder.recLight, - NewFolder.schedule, - NewFolder.searchInactive, - NewFolder.sendBttn, - NewFolder.separators, - NewFolder.speakerBttn, - NewFolder.switchCameraBttn, - NewFolder.textBar, - NewFolder.workInactive, - oval14, - oval17, - oval6, - sendAsFile, - Stickers.stickerStub, - Stickers.stickersIcAdd, - Stickers.stickersIcDefaultPack, - Stickers.stickersIcRecent, - Stickers.stickersIcSearch, - Wallet.icArrowTransferDown, - Wallet.icArrowTransferUp, - Wallet.icBtc, - Wallet.icEos, - Wallet.icNyn, - Wallet.icPay, - Wallet.icWallet, - Wheel.Button.btnWheelInactive, - Wheel.Button.btnWheelInactiveLight, - Wheel.Button.wheel, - Wheel.Placeholders.icBrokenImagePlaceholder, - Wheel.Placeholders.icEmptyImagePlaceholder, - Wheel.Placeholders.icEmptyLocationPlaceholder, - Wheel.WheelItems.icActions, - Wheel.WheelItems.icByContacts, - Wheel.WheelItems.icByNumber, - Wheel.WheelItems.icByPassword, - Wheel.WheelItems.icByQrCode, - Wheel.WheelItems.icByUsername, - Wheel.WheelItems.icCalls, - Wheel.WheelItems.icCamera, - Wheel.WheelItems.icChannelInactive, - Wheel.WheelItems.icChannelNew, - Wheel.WheelItems.icChats, - Wheel.WheelItems.icContacts, - Wheel.WheelItems.icEditProfile, - Wheel.WheelItems.icFamily, - Wheel.WheelItems.icFile, - Wheel.WheelItems.icFriends, - Wheel.WheelItems.icGallery, - Wheel.WheelItems.icGroupCall, - Wheel.WheelItems.icGroupCalls, - Wheel.WheelItems.icGroupSettings, - Wheel.WheelItems.icGroupVideo, - Wheel.WheelItems.icGroups, - Wheel.WheelItems.icHistory, - Wheel.WheelItems.icHome, - Wheel.WheelItems.icList, - Wheel.WheelItems.icLocation, - Wheel.WheelItems.icMyself, - Wheel.WheelItems.icNew, - Wheel.WheelItems.icNewAudioCall, - Wheel.WheelItems.icNewChat, - Wheel.WheelItems.icNewContact, - Wheel.WheelItems.icNewGroup, - Wheel.WheelItems.icNewPhoneCall, - Wheel.WheelItems.icNewVideoCall, - Wheel.WheelItems.icOptions, - Wheel.WheelItems.icRecents, - Wheel.WheelItems.icSearch, - Wheel.WheelItems.icStarred, - Wheel.WheelItems.icVideo, - Wheel.WheelItems.icVideoIndicator, - Wheel.WheelItems.icWork, - Wheel.WheelPosition.wheelLeftImage, - Wheel.WheelPosition.wheelRightImage, - WheelPosition.wheelLeftImage, - WheelPosition.wheelRightImage, - arrowCollapse, - arrowExpand, - barsTabBarOverridesCenterButtonBtnPlayVideo, - bgChannelsEmptyState, - btnRecordDefault, - btnRecordHightlighted, - btnRecording, - btnScheduleMessage, - btnStopPlayingVideo, - btnStopRecording, - btnTakePhotoDefault, - btnTakePhotoHighlighted, - btnWheelDone, - callKitImage, - camera, - cancel, - editIcon, - emojiWhite, - frame, - icAddParticipants, - icAddPhotoPlaceholder, - icArrowDown, - icArrowRight1, - icArrowRight, - icArrowUpRed, - icBack, - icBackNavigation, - icBottomArrow, - icCameraFrame, - icChangeCameraIos, - icCheckmarkRed, - icClock, - icClose, - icCloseRed, - icContactsEmpty, - icCurrentLocation, - icDeleteRedCircle, - icEditDone, - icEmailStorage, - icFlashlightAuto, - icFlashlightOff, - icFlashlightOn, - icIncomingCallDark, - icKeyboard, - icLinkStorage, - icMapCurrent, - icMapMode, - icMapPin, - icMapSearch, - icMarketplaceContextMenu, - icMarketplaceWheelContextMenu, - icMic, - icMicDarkGray, - icNewGroup, - icParticipantsSearch, - icPhoneStorage, - icPhotoPlaceholder, - icPlaceStar, - icQrCode, - icScheduledMsgCheck, - icSearch, - icSearchEmpty, - icSendAsFile, - icSoundOff, - icStar, - icStyle, - icUnmute, - icVoiceCallOutgoingDark, - iconRedCheck, - iconShare, - icon, - iconInvitation, - iconMe, - iconsCameraQrCodeIcFlashlightActive, - iconsGeneralIcAddMember, - iconsGeneralIcDone, - iconsGeneralIcEditDone, - iconsGeneralIcSoundOn, - iconsIcMic, - imgEmptyStatesContactRequests, - imgEmptyStatesGroups, - imgEmptyStatesP2p, - imgEmptyStatesScheduled, - imgEmptyStatesStarred, - Input.arrowUp, - Input.delayRed, - Input.editMsgButton, - Input.icDelete, - Input.icPause, - Input.icPlay, - Input.icSend, - Input.icVoice, - Input.inputKeyboard, - Input.inputLeftEmojiButton, - Input.inputLeftKeyboardButton, - Input.inputMicrophone, - Input.pause, - Input.playBtn, - Input.recBackground, - Input.roundedRedBig, - Input.sendButton, - Input.trash, - Input.voiceButton, - inputLeftButton, - leftButton, - lock, - maximaze, - minimaze, - myLocation, - phone, - profileBig, - star, - staticmap, - statusIcon, - switchCamera, - tableArrow, - tableOverridesRightOverridesCheckboxIcUnchecked, - transcribed, - translated, - world, - ] - // swiftlint:enable trailing_comma - @available(*, deprecated, renamed: "allImages") - internal static let allValues: [AssetType] = allImages } // swiftlint:enable identifier_name line_length nesting type_body_length type_name -internal extension Image { - @available(iOS 1.0, tvOS 1.0, watchOS 1.0, *) - @available(OSX, deprecated, - message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") - convenience init!(asset: ImageAsset) { - #if os(iOS) || os(tvOS) +// MARK: - Implementation Details + +internal struct ColorAsset { + internal fileprivate(set) var name: String + + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) + internal var color: AssetColorTypeAlias { + return AssetColorTypeAlias(asset: self) + } +} + +internal extension AssetColorTypeAlias { + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) + convenience init!(asset: ColorAsset) { let bundle = Bundle(for: BundleToken.self) + #if os(iOS) || os(tvOS) self.init(named: asset.name, in: bundle, compatibleWith: nil) #elseif os(OSX) - self.init(named: NSImage.Name(asset.name)) + self.init(named: NSColor.Name(asset.name), bundle: bundle) #elseif os(watchOS) self.init(named: asset.name) #endif } } -internal extension AssetColorTypeAlias { - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) - convenience init!(asset: ColorAsset) { +internal struct DataAsset { + internal fileprivate(set) var name: String + + #if os(iOS) || os(tvOS) || os(OSX) + @available(iOS 9.0, tvOS 9.0, OSX 10.11, *) + internal var data: NSDataAsset { + return NSDataAsset(asset: self) + } + #endif +} + +#if os(iOS) || os(tvOS) || os(OSX) +@available(iOS 9.0, tvOS 9.0, OSX 10.11, *) +internal extension NSDataAsset { + convenience init!(asset: DataAsset) { let bundle = Bundle(for: BundleToken.self) #if os(iOS) || os(tvOS) + self.init(name: asset.name, bundle: bundle) + #elseif os(OSX) + self.init(name: NSDataAsset.Name(asset.name), bundle: bundle) + #endif + } +} +#endif + +internal struct ImageAsset { + internal fileprivate(set) var name: String + + internal var image: AssetImageTypeAlias { + let bundle = Bundle(for: BundleToken.self) + #if os(iOS) || os(tvOS) + let image = AssetImageTypeAlias(named: name, in: bundle, compatibleWith: nil) + #elseif os(OSX) + let image = bundle.image(forResource: NSImage.Name(name)) + #elseif os(watchOS) + let image = AssetImageTypeAlias(named: name) + #endif + guard let result = image else { fatalError("Unable to load image named \(name).") } + return result + } +} + +internal extension AssetImageTypeAlias { + @available(iOS 1.0, tvOS 1.0, watchOS 1.0, *) + @available(OSX, deprecated, + message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") + convenience init!(asset: ImageAsset) { + #if os(iOS) || os(tvOS) + let bundle = Bundle(for: BundleToken.self) self.init(named: asset.name, in: bundle, compatibleWith: nil) #elseif os(OSX) - self.init(named: NSColor.Name(asset.name), bundle: bundle) + self.init(named: NSImage.Name(asset.name)) #elseif os(watchOS) self.init(named: asset.name) #endif diff --git a/Nynja/Generated/ColorsConstants.swift b/Nynja/Generated/ColorsConstants.swift index 61eef0faf..a9606800c 100644 --- a/Nynja/Generated/ColorsConstants.swift +++ b/Nynja/Generated/ColorsConstants.swift @@ -10,89 +10,89 @@ internal typealias SGColor = UIColor internal extension SGColor { enum nynja { /// 0x262626ff (r: 38, g: 38, b: 38, a: 255) - static let almostBlack = #colorLiteral(red: 0.14902, green: 0.14902, blue: 0.14902, alpha: 1.0) + static let almostBlack = #colorLiteral(red: 0.14901961, green: 0.14901961, blue: 0.14901961, alpha: 1.0) /// 0xe5e5e5ff (r: 229, g: 229, b: 229, a: 255) - static let almostWhite = #colorLiteral(red: 0.898039, green: 0.898039, blue: 0.898039, alpha: 1.0) + static let almostWhite = #colorLiteral(red: 0.8980392, green: 0.8980392, blue: 0.8980392, alpha: 1.0) /// 0x6dbee1ff (r: 109, g: 190, b: 225, a: 255) - static let aquaBlue = #colorLiteral(red: 0.427451, green: 0.745098, blue: 0.882353, alpha: 1.0) + static let aquaBlue = #colorLiteral(red: 0.42745098, green: 0.74509805, blue: 0.88235295, alpha: 1.0) /// 0x272a30ff (r: 39, g: 42, b: 48, a: 255) - static let backgroundColor = #colorLiteral(red: 0.152941, green: 0.164706, blue: 0.188235, alpha: 1.0) + static let backgroundColor = #colorLiteral(red: 0.15294118, green: 0.16470589, blue: 0.1882353, alpha: 1.0) /// 0xddddddff (r: 221, g: 221, b: 221, a: 255) - static let backgroundColorLight = #colorLiteral(red: 0.866667, green: 0.866667, blue: 0.866667, alpha: 1.0) + static let backgroundColorLight = #colorLiteral(red: 0.8666667, green: 0.8666667, blue: 0.8666667, alpha: 1.0) /// 0x3f3f3fff (r: 63, g: 63, b: 63, a: 255) - static let backgroundGray = #colorLiteral(red: 0.247059, green: 0.247059, blue: 0.247059, alpha: 1.0) + static let backgroundGray = #colorLiteral(red: 0.24705882, green: 0.24705882, blue: 0.24705882, alpha: 1.0) /// 0x000000ff (r: 0, g: 0, b: 0, a: 255) static let black = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) /// 0x45a5ffff (r: 69, g: 165, b: 255, a: 255) - static let blue = #colorLiteral(red: 0.270588, green: 0.647059, blue: 1.0, alpha: 1.0) + static let blue = #colorLiteral(red: 0.27058825, green: 0.64705884, blue: 1.0, alpha: 1.0) /// 0x957348ff (r: 149, g: 115, b: 72, a: 255) - static let brown = #colorLiteral(red: 0.584314, green: 0.45098, blue: 0.282353, alpha: 1.0) + static let brown = #colorLiteral(red: 0.58431375, green: 0.4509804, blue: 0.28235295, alpha: 1.0) /// 0x00e359ff (r: 0, g: 227, b: 89, a: 255) - static let callGreen = #colorLiteral(red: 0.0, green: 0.890196, blue: 0.34902, alpha: 1.0) + static let callGreen = #colorLiteral(red: 0.0, green: 0.8901961, blue: 0.34901962, alpha: 1.0) /// 0x00000000 (r: 0, g: 0, b: 0, a: 0) static let clear = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) /// 0x505255ff (r: 80, g: 82, b: 85, a: 255) - static let contentDisabledGray = #colorLiteral(red: 0.313726, green: 0.321569, blue: 0.333333, alpha: 1.0) + static let contentDisabledGray = #colorLiteral(red: 0.3137255, green: 0.32156864, blue: 0.33333334, alpha: 1.0) /// 0x35383eff (r: 53, g: 56, b: 62, a: 255) - static let contextMenuBackGray = #colorLiteral(red: 0.207843, green: 0.219608, blue: 0.243137, alpha: 1.0) + static let contextMenuBackGray = #colorLiteral(red: 0.20784314, green: 0.21960784, blue: 0.24313726, alpha: 1.0) /// 0x067655ff (r: 6, g: 118, b: 85, a: 255) - static let darkGreen = #colorLiteral(red: 0.0235294, green: 0.462745, blue: 0.333333, alpha: 1.0) + static let darkGreen = #colorLiteral(red: 0.023529412, green: 0.4627451, blue: 0.33333334, alpha: 1.0) /// 0x2c2e33ff (r: 44, g: 46, b: 51, a: 255) - static let darkLight = #colorLiteral(red: 0.172549, green: 0.180392, blue: 0.2, alpha: 1.0) + static let darkLight = #colorLiteral(red: 0.17254902, green: 0.18039216, blue: 0.2, alpha: 1.0) /// 0xa4000dff (r: 164, g: 0, b: 13, a: 255) - static let darkRed = #colorLiteral(red: 0.643137, green: 0.0, blue: 0.0509804, alpha: 1.0) + static let darkRed = #colorLiteral(red: 0.6431373, green: 0.0, blue: 0.050980393, alpha: 1.0) /// 0x3891ffff (r: 56, g: 145, b: 255, a: 255) - static let dodgerBlue = #colorLiteral(red: 0.219608, green: 0.568627, blue: 1.0, alpha: 1.0) + static let dodgerBlue = #colorLiteral(red: 0.21960784, green: 0.5686275, blue: 1.0, alpha: 1.0) /// 0x969696ff (r: 150, g: 150, b: 150, a: 255) - static let dustyGray = #colorLiteral(red: 0.588235, green: 0.588235, blue: 0.588235, alpha: 1.0) + static let dustyGray = #colorLiteral(red: 0.5882353, green: 0.5882353, blue: 0.5882353, alpha: 1.0) /// 0x666666ff (r: 102, g: 102, b: 102, a: 255) static let gray = #colorLiteral(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) /// 0x63666aff (r: 99, g: 102, b: 106, a: 255) - static let lightGray = #colorLiteral(red: 0.388235, green: 0.4, blue: 0.415686, alpha: 1.0) + static let lightGray = #colorLiteral(red: 0.3882353, green: 0.4, blue: 0.41568628, alpha: 1.0) /// 0xc90010ff (r: 201, g: 0, b: 16, a: 255) - static let mainRed = #colorLiteral(red: 0.788235, green: 0.0, blue: 0.0627451, alpha: 1.0) + static let mainRed = #colorLiteral(red: 0.7882353, green: 0.0, blue: 0.0627451, alpha: 1.0) /// 0x959699ff (r: 149, g: 150, b: 153, a: 255) - static let manatee = #colorLiteral(red: 0.584314, green: 0.588235, blue: 0.6, alpha: 1.0) + static let manatee = #colorLiteral(red: 0.58431375, green: 0.5882353, blue: 0.6, alpha: 1.0) /// 0x86bc5fff (r: 134, g: 188, b: 95, a: 255) - static let mantis = #colorLiteral(red: 0.52549, green: 0.737255, blue: 0.372549, alpha: 1.0) + static let mantis = #colorLiteral(red: 0.5254902, green: 0.7372549, blue: 0.37254903, alpha: 1.0) /// 0xe7e7e7ff (r: 231, g: 231, b: 231, a: 255) - static let mercury = #colorLiteral(red: 0.905882, green: 0.905882, blue: 0.905882, alpha: 1.0) + static let mercury = #colorLiteral(red: 0.90588236, green: 0.90588236, blue: 0.90588236, alpha: 1.0) /// 0xccccccff (r: 204, g: 204, b: 204, a: 255) static let middleGray = #colorLiteral(red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0) /// 0xf5b758ff (r: 245, g: 183, b: 88, a: 255) - static let orange = #colorLiteral(red: 0.960784, green: 0.717647, blue: 0.345098, alpha: 1.0) + static let orange = #colorLiteral(red: 0.9607843, green: 0.7176471, blue: 0.34509805, alpha: 1.0) /// 0x2f353bff (r: 47, g: 53, b: 59, a: 255) - static let outerSpace = #colorLiteral(red: 0.184314, green: 0.207843, blue: 0.231373, alpha: 1.0) + static let outerSpace = #colorLiteral(red: 0.18431373, green: 0.20784314, blue: 0.23137255, alpha: 1.0) /// 0xe16d9dff (r: 225, g: 109, b: 157, a: 255) - static let pink = #colorLiteral(red: 0.882353, green: 0.427451, blue: 0.615686, alpha: 1.0) + static let pink = #colorLiteral(red: 0.88235295, green: 0.42745098, blue: 0.6156863, alpha: 1.0) /// 0xe0e0e0ff (r: 224, g: 224, b: 224, a: 255) - static let profilePhoneNumberGray = #colorLiteral(red: 0.878431, green: 0.878431, blue: 0.878431, alpha: 1.0) + static let profilePhoneNumberGray = #colorLiteral(red: 0.8784314, green: 0.8784314, blue: 0.8784314, alpha: 1.0) /// 0x676bb9ff (r: 103, g: 107, b: 185, a: 255) - static let purple = #colorLiteral(red: 0.403922, green: 0.419608, blue: 0.72549, alpha: 1.0) + static let purple = #colorLiteral(red: 0.40392157, green: 0.41960785, blue: 0.7254902, alpha: 1.0) /// 0xe06356ff (r: 224, g: 99, b: 86, a: 255) - static let roman = #colorLiteral(red: 0.878431, green: 0.388235, blue: 0.337255, alpha: 1.0) + static let roman = #colorLiteral(red: 0.8784314, green: 0.3882353, blue: 0.3372549, alpha: 1.0) /// 0xd8d8d8ff (r: 216, g: 216, b: 216, a: 255) - static let selfBubleColor = #colorLiteral(red: 0.847059, green: 0.847059, blue: 0.847059, alpha: 1.0) + static let selfBubleColor = #colorLiteral(red: 0.84705883, green: 0.84705883, blue: 0.84705883, alpha: 1.0) /// 0x212226ff (r: 33, g: 34, b: 38, a: 255) - static let selfTextColor = #colorLiteral(red: 0.129412, green: 0.133333, blue: 0.14902, alpha: 1.0) + static let selfTextColor = #colorLiteral(red: 0.12941177, green: 0.13333334, blue: 0.14901961, alpha: 1.0) /// 0x1f1f1fff (r: 31, g: 31, b: 31, a: 255) - static let shadowBlack = #colorLiteral(red: 0.121569, green: 0.121569, blue: 0.121569, alpha: 1.0) + static let shadowBlack = #colorLiteral(red: 0.12156863, green: 0.12156863, blue: 0.12156863, alpha: 1.0) /// 0x2b2e32ff (r: 43, g: 46, b: 50, a: 255) - static let shark = #colorLiteral(red: 0.168627, green: 0.180392, blue: 0.196078, alpha: 1.0) + static let shark = #colorLiteral(red: 0.16862746, green: 0.18039216, blue: 0.19607843, alpha: 1.0) /// 0xbfbfbfff (r: 191, g: 191, b: 191, a: 255) - static let silver = #colorLiteral(red: 0.74902, green: 0.74902, blue: 0.74902, alpha: 1.0) + static let silver = #colorLiteral(red: 0.7490196, green: 0.7490196, blue: 0.7490196, alpha: 1.0) /// 0x696a6bff (r: 105, g: 106, b: 107, a: 255) - static let subtitleGray = #colorLiteral(red: 0.411765, green: 0.415686, blue: 0.419608, alpha: 1.0) + static let subtitleGray = #colorLiteral(red: 0.4117647, green: 0.41568628, blue: 0.41960785, alpha: 1.0) /// 0x4ecfb1ff (r: 78, g: 207, b: 177, a: 255) - static let turquoise = #colorLiteral(red: 0.305882, green: 0.811765, blue: 0.694118, alpha: 1.0) + static let turquoise = #colorLiteral(red: 0.30588236, green: 0.8117647, blue: 0.69411767, alpha: 1.0) /// 0xaaaaaaff (r: 170, g: 170, b: 170, a: 255) - static let tutorialGray = #colorLiteral(red: 0.666667, green: 0.666667, blue: 0.666667, alpha: 1.0) + static let tutorialGray = #colorLiteral(red: 0.6666667, green: 0.6666667, blue: 0.6666667, alpha: 1.0) /// 0x9f68a8ff (r: 159, g: 104, b: 168, a: 255) - static let violet = #colorLiteral(red: 0.623529, green: 0.407843, blue: 0.658824, alpha: 1.0) + static let violet = #colorLiteral(red: 0.62352943, green: 0.40784314, blue: 0.65882355, alpha: 1.0) /// 0x45484dff (r: 69, g: 72, b: 77, a: 255) - static let wheelBackHighlitedGray = #colorLiteral(red: 0.270588, green: 0.282353, blue: 0.301961, alpha: 1.0) + static let wheelBackHighlitedGray = #colorLiteral(red: 0.27058825, green: 0.28235295, blue: 0.3019608, alpha: 1.0) /// 0x32353bff (r: 50, g: 53, b: 59, a: 255) - static let wheelTopLevelSeparatorColor = #colorLiteral(red: 0.196078, green: 0.207843, blue: 0.231373, alpha: 1.0) + static let wheelTopLevelSeparatorColor = #colorLiteral(red: 0.19607843, green: 0.20784314, blue: 0.23137255, alpha: 1.0) /// 0xffffffff (r: 255, g: 255, b: 255, a: 255) static let white = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) } diff --git a/Nynja/Generated/FontsConstants.swift b/Nynja/Generated/FontsConstants.swift index 4d205bcb0..4ce35f59f 100644 --- a/Nynja/Generated/FontsConstants.swift +++ b/Nynja/Generated/FontsConstants.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen #if os(OSX) @@ -11,6 +12,41 @@ // swiftlint:disable superfluous_disable_command // swiftlint:disable file_length +// MARK: - Fonts + +// swiftlint:disable identifier_name line_length type_body_length +internal enum FontFamily { + internal enum Avenir { + internal static let black = FontConvertible(name: "Avenir-Black", family: "Avenir", path: "Avenir.ttc") + internal static let blackOblique = FontConvertible(name: "Avenir-BlackOblique", family: "Avenir", path: "Avenir.ttc") + internal static let book = FontConvertible(name: "Avenir-Book", family: "Avenir", path: "Avenir.ttc") + internal static let bookOblique = FontConvertible(name: "Avenir-BookOblique", family: "Avenir", path: "Avenir.ttc") + internal static let heavy = FontConvertible(name: "Avenir-Heavy", family: "Avenir", path: "Avenir.ttc") + internal static let heavyOblique = FontConvertible(name: "Avenir-HeavyOblique", family: "Avenir", path: "Avenir.ttc") + internal static let light = FontConvertible(name: "Avenir-Light", family: "Avenir", path: "Avenir.ttc") + internal static let lightOblique = FontConvertible(name: "Avenir-LightOblique", family: "Avenir", path: "Avenir.ttc") + internal static let medium = FontConvertible(name: "Avenir-Medium", family: "Avenir", path: "Avenir.ttc") + internal static let mediumOblique = FontConvertible(name: "Avenir-MediumOblique", family: "Avenir", path: "Avenir.ttc") + internal static let oblique = FontConvertible(name: "Avenir-Oblique", family: "Avenir", path: "Avenir.ttc") + internal static let roman = FontConvertible(name: "Avenir-Roman", family: "Avenir", path: "Avenir.ttc") + internal static let all: [FontConvertible] = [black, blackOblique, book, bookOblique, heavy, heavyOblique, light, lightOblique, medium, mediumOblique, oblique, roman] + } + internal enum NotoSans { + internal static let bold = FontConvertible(name: "NotoSans-Bold", family: "Noto Sans", path: "NotoSans-Bold.ttf") + internal static let italic = FontConvertible(name: "NotoSans-Italic", family: "Noto Sans", path: "NotoSans-Italic.ttf") + internal static let medium = FontConvertible(name: "NotoSans-Medium", family: "Noto Sans", path: "NotoSans-Medium.ttf") + internal static let regular = FontConvertible(name: "NotoSans-Regular", family: "Noto Sans", path: "NotoSans-Regular.ttf") + internal static let all: [FontConvertible] = [bold, italic, medium, regular] + } + internal static let allCustomFonts: [FontConvertible] = [Avenir.all, NotoSans.all].flatMap { $0 } + internal static func registerAllCustomFonts() { + allCustomFonts.forEach { $0.register() } + } +} +// swiftlint:enable identifier_name line_length type_body_length + +// MARK: - Implementation Details + internal struct FontConvertible { internal let name: String internal let family: String @@ -21,9 +57,9 @@ internal struct FontConvertible { } internal func register() { + // swiftlint:disable:next conditional_returns_on_newline guard let url = url else { return } - var errorRef: Unmanaged? - CTFontManagerRegisterFontsForURL(url as CFURL, .process, &errorRef) + CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) } fileprivate var url: URL? { @@ -48,29 +84,4 @@ internal extension Font { } } -// swiftlint:disable identifier_name line_length type_body_length -internal enum FontFamily { - internal enum Avenir { - internal static let black = FontConvertible(name: "Avenir-Black", family: "Avenir", path: "Avenir.ttc") - internal static let blackOblique = FontConvertible(name: "Avenir-BlackOblique", family: "Avenir", path: "Avenir.ttc") - internal static let book = FontConvertible(name: "Avenir-Book", family: "Avenir", path: "Avenir.ttc") - internal static let bookOblique = FontConvertible(name: "Avenir-BookOblique", family: "Avenir", path: "Avenir.ttc") - internal static let heavy = FontConvertible(name: "Avenir-Heavy", family: "Avenir", path: "Avenir.ttc") - internal static let heavyOblique = FontConvertible(name: "Avenir-HeavyOblique", family: "Avenir", path: "Avenir.ttc") - internal static let light = FontConvertible(name: "Avenir-Light", family: "Avenir", path: "Avenir.ttc") - internal static let lightOblique = FontConvertible(name: "Avenir-LightOblique", family: "Avenir", path: "Avenir.ttc") - internal static let medium = FontConvertible(name: "Avenir-Medium", family: "Avenir", path: "Avenir.ttc") - internal static let mediumOblique = FontConvertible(name: "Avenir-MediumOblique", family: "Avenir", path: "Avenir.ttc") - internal static let oblique = FontConvertible(name: "Avenir-Oblique", family: "Avenir", path: "Avenir.ttc") - internal static let roman = FontConvertible(name: "Avenir-Roman", family: "Avenir", path: "Avenir.ttc") - } - internal enum NotoSans { - internal static let bold = FontConvertible(name: "NotoSans-Bold", family: "Noto Sans", path: "NotoSans-Bold.ttf") - internal static let italic = FontConvertible(name: "NotoSans-Italic", family: "Noto Sans", path: "NotoSans-Italic.ttf") - internal static let medium = FontConvertible(name: "NotoSans-Medium", family: "Noto Sans", path: "NotoSans-Medium.ttf") - internal static let regular = FontConvertible(name: "NotoSans-Regular", family: "Noto Sans", path: "NotoSans-Regular.ttf") - } -} -// swiftlint:enable identifier_name line_length type_body_length - private final class BundleToken {} diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index b2a681105..af13205c6 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen import Foundation @@ -5,7 +6,9 @@ import Foundation // swiftlint:disable superfluous_disable_command // swiftlint:disable file_length -// swiftlint:disable identifier_name line_length type_body_length +// MARK: - Strings + +// swiftlint:disable function_parameter_count identifier_name line_length type_body_length internal enum Localizable { /// Accept Audio internal static let acceptAudio = Localizable.tr("Localizable", "accept_audio") @@ -267,6 +270,8 @@ internal enum Localizable { internal static let confirmContryShort = Localizable.tr("Localizable", "confirm_contry_short") /// connected internal static let connected = Localizable.tr("Localizable", "connected") + /// connecting... + internal static let connecting = Localizable.tr("Localizable", "connecting...") /// Can't connect to the server. Try again later. internal static let connectionToServerFailed = Localizable.tr("Localizable", "connection_to_server_failed") /// Contact Requests @@ -487,6 +492,8 @@ internal enum Localizable { internal static let invited = Localizable.tr("Localizable", "invited") /// Join internal static let join = Localizable.tr("Localizable", "join") + /// Keep your existing name. + internal static let keepYourExistingName = Localizable.tr("Localizable", "Keep_your_existing_name.") /// Keep your existing username. internal static let keepYourExistingUsername = Localizable.tr("Localizable", "Keep_your_existing_username") /// Language for interpretation @@ -1091,6 +1098,8 @@ internal enum Localizable { internal static let voiceMessage = Localizable.tr("Localizable", "voice_message") /// Voice Messages internal static let voiceMessages = Localizable.tr("Localizable", "voice_messages") + /// waiting for network... + internal static let waitingForNetwork = Localizable.tr("Localizable", "waiting for network...") /// Add\nWallet internal static let walletAddTitle = Localizable.tr("Localizable", "wallet_add_title") /// Your balance has not enough funds to transfer this amount @@ -1291,14 +1300,10 @@ internal enum Localizable { internal static let youWasRemoved = Localizable.tr("Localizable", "you_was_removed") /// Your device appears to be rooted. The security of your app can be compromised. internal static let yourDeviceIsRooted = Localizable.tr("Localizable", "your_device_is_rooted") - /// connecting... - internal static let connecting = Localizable.tr("Localizable", "connecting...") - /// Keep your existing name. - internal static let keepYourExistingName = Localizable.tr("Localizable", "Keep_your_existing_name.") - /// waiting for network... - internal static let waitingForNetwork = Localizable.tr("Localizable", "waiting for network...") } -// swiftlint:enable identifier_name line_length type_body_length +// swiftlint:enable function_parameter_count identifier_name line_length type_body_length + +// MARK: - Implementation Details extension Localizable { private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { diff --git a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/Contents.json deleted file mode 100644 index f9494e456..000000000 --- a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "next-bttn.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "next-bttn@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "next-bttn@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn.png b/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn.png deleted file mode 100644 index f061ce4675c1a3b3567832bd3469f070a6412555..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6428 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 F{{i&&!mI!Q diff --git a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@2x.png b/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@2x.png deleted file mode 100644 index 5218593807eadb0f24fe48aa0517bbd14f54bfa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10317 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 V0M${G1b^Si%1SCpREQY`{6Ey5raJ%t diff --git a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@3x.png b/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@3x.png deleted file mode 100644 index f146fc4fab21a000abeb394be241e6988f6e35bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11261 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ℑXYTH4~?mVfHR9580f`6)?p>Cbxz`vz?l*A zOLhq~_OOpLbnxb|$hgYizc~n*$kYzbUxo11HBI7@J-vew2Vw>$nKFYgo^}o(rFg#C zIF>sOaBodzgE{L((DT4lz++hR#VGBCke9cF(eR+?-bg$QPNks4?uOxTG%Ei&>zU(u zY@}4E(BR_9s|i(SSkaGah;hw7}LuT8O}zJ$FCMs{0c|xaB=HWq^W3qPrFL7GKh=yH(XC z{1+!|CT&vtQs7;~=HtCy_78m_?GW6p2AG$TV0$?s!2xcf{!YLZuxJQ-hhLnQ@uXxr zTk?qVa>k_a&`2|6C$n=!edPl0!AXDH)`SYiRXqL3 zfROD}(ydS>s?B2j>V$ck?G#6Tn0VH6pe2&suh8ykA+JvO!@;h%y37GoDulaN?6&B5 z*X6h_-6sz{KYPZ^dRIvM;Vr@2tUf($Nlw0~l6S>fGY%h%zyAs#=wo{C*!=jY-S@;S zCcYNafZEqI@b2L;#Y9kCPZQaDN|7CnyCon1N|S%&!Jf5xLNxgEU4NIq+7JyG0zdTi zC5NhjzU=pK{dp(?F7 zrKVHC*q}q0cL&bi^2wNL4d(lenIF;P(d|+MG+Bu%e;$r>MmM;!t#UMmeRF(nr0B>zgu45?%MR{k2mi7Jb`zTNn^Md6 zsHYuz=8*rD5@!Ja)Lw4oX{51H5L_~>?ZC9V^%4~QyO)sjX24&?e|as%J5Y$Oxn_PX z$#)0J1te(9U8UO#96iDp@gGdhqG&}RCfB6HN5E$)&zGWk*brjTsAEn~r z?F0+{0xQw!$fm^+W?6J)WYSgFmkJ=YA#R(rH1v4>ISK~x(lGBgZ1F(_Vm3a-IUgy= zdxV>asINrre~a6lCm-?pt@hRkX! zOYiZnGW+HqTqzkD#aFwNd72QrH$#m#fA4#^ws>K8ye(?W;1+z&JDLOUs>dpYqI>g4w6YoRGqU3!oa2O7eJ`yj6QxS* zsSTf?EhZstX1eZpH>5~?Euo(`m-FJ?FIXuYaQ*6sQ*nx*$K!KhoV`b)*%308Kk$3x z>j;LRiI?8}*>pwjUU}Wc6OB}TbCu?bbMsBuOVa&@6EBUk?OBEzf~+eH@(mcB#ulN` z2)QOT4DSgYwM2YV;by4FnRw`+jNcY1ePKsa|L5F~j?-k0F>~QGV&bh9EDitQgoBjU zPe>ugg9hD|+*Z%9@eJ=H={6oDm)nloOg#}VgHUd^?^`nfds4G>xiMY&lsaT}e&9he z(l*GsOuE1MFM5dxFJTMP_=AX}WQ`TUk0^)lv)rC&I7)LzW<{(pTe2$RQAfw1G=^!7 z8O%3oyu);W7a!{qlc1F*D;TQKrwav^wkagQW;{bbW{Y5Mb=e*5j0^#&v(%; diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/Contents.json deleted file mode 100644 index d92d61871..000000000 --- a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "qr-code.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "qr-code@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "qr-code@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code.png b/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code.png deleted file mode 100644 index 3efacc09e94b5daf50e5520cd3518f192451ee2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1672 zcmaJ?X;2eq7>?R{P$&WhwJ^@Q;qoAx%|S>?z?>Q(Tm}pklufdM0Lg~UQUch6dT@A# zL4k_4V6}`oAkbPXwb&Ry6sf>ytD{BYK^-X;oe?WgyFt+Yu-%#6@B7|)p7(p+_kCuz zBsO}ro6CDH6bi*nzD5#HuHNKJojaTSihw2i$Yn7hRT1kj9g&ORC`G8n)F>b~AZchk zifHq)JJB!-Wmb@0sUlR0D82?W&=DtwZZVihHiZ%vVKE_^Oq2lBXqw(A03Wrqf`DEt z0M~OA424OIrt8<_ra1A*R9c`LS_U`{Laso*;dF`!05E)jrags#zFqFw+ zayUXZ3`@B}CWplnOSxuYZGzP?3zum9Rw|4sWso z5l_Yyg-Ru%B92G~i&)?kSNn$Qr?}$(7i4IH`jj(=5mR@?i79s^&Xmmj*lgMjB!SgD=Kkv<{9= z+}eGhQ`^nk%@U3;m}R4Gt6e`dKhv&$>b|47yFVfLb7}pW(w!GP#L||I!eUEsDNj4r zVpqcQY7cXpZ?wLPwzsqXd+cvnS{eZ|Ozuk50@Qm=as@6kgg zkh*2=jt)ny%6cAsKRIx?I9na!Sz!0bPVtMqO5g8MA6{Nw+@iKFGPs1PD;!r`>4Zmv z>!IAk?Ko`v>$~Lc50byubeIHcFRwY)jNfi8bOR+SU5Ry3qYIQ@R9@96vvyG!v(i{w@vtUZo1~{-qv&OM^1NCW1nj))yL{{nOS}G zPoFmJ@^hOKXRK(0U0cettkf+DQaAk}9E+x1O{&UT$Y+f~N)clzMGAu+tgS`}F^I)C&sv7z|Esp`VJSM-sURR5r3 zH3tpDD>;uG4$Acty3>_+nM(Fv;9;FRTL$%B+S?!FaA{=S9zd4i;U)POp^`2a&m8HA zdm8UPuh3o_Wo^%#r!~6PXtDh$6|xoL&0FhNKMrbifAlg_C{(6=b+`RtMEj|yJc@@=%+Puc_{Wgj;zSof@M0!{hW({_QA(T~<% zb*!o!zsU|9a(JDoU-2-aVN1<$!&@1r$`)2M?Xdc^R)r)Tp72Pe2u}@2bxnn9o&N^8 LG+I(GO8x8~VOWg! diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@2x.png b/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@2x.png deleted file mode 100644 index c2b449fb527f886f9bdf647a189a8116c22f8017..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2505 zcmaJ@dpwi-AD*CBMB$*r(wW1Gz_GqZ^Z)%YQ^Jy>kd+8(FTaMU_fm`*|$T}Y%T z$&1`g(K+e3Riqm!y2#~Mshm#zrc?cXe{_D|*X#LSKCk!t^Lc+Re>{5wHh61mF4aUJ z5ZX)zEl_#dDnA>fnsVR3ym3i+n8IExI0%Y{cd$hu!h;J%foLY59Rmh}Y;NNA-#`}x zLe-iV%z{~dz7!6`w`b2|>?M4ml8r#PxJ!g=P8w^b57FY_L?MU`1jSH-h@rUO+4aL*iISi1zfH?-mJd?Z@Wu1oK9l`2v5{3yOF@fn2%>;-kM* zh{F4U3MVQZPog#w!V|CMZk@dm+ovH zI-f~gTUhBjH8nsU6%R}1KCOILuI~0R<=w-nQEB(PM#xgDebMppa3)g26uaUtFBW6a zr|!TsdIZ^}I~}%uxJG^XYb*VgR?Cly+(%xy%1s1|_ZO2F9&e#0lW5mRn+gN=;FgH@ z-><%DRhfJ&%6`O|s=*#SmgAq_{yIW&?fryDk#9FD&%n~);njK%*;Vc&UGX|6a;3jt z*A2Z9EyemqOY9K~EtH#Oe0%HpQ6Fwq9HwBn{gW)~&lPcX&x`>(YSkJW^~nYDkgd~uuS|GigPGCX)<{$M> zMOC(MWeoZ|^*J~vtekvSMT(h_P7yJwn!MX zOuL<&@ZhklMly+7_^WiEs|5<09(lhd@mkQC4D$Ik?+mLC>ym;wnUKl;SxNeF$zP$0 zUYnA0doCR`P1z^(rfH=P1Augi0H*4T2Y~jN(e?||`O<%HQ;;8yoLY)rH@M1|;iX#V zbv$V5T$>mKnZS?JVZcm?Vc;wxmWyxfGz3FSZvLFdlwPU zWt7VGFE3lr^?r?K96cfwx*r_6bh8;bV`e3}tnDmAb(Snzy6kZ$=G6-3aKlWyR_9sG zs>nassotm8X0em7yKc|OoEIDVn7(JHGodV0G`ERT-tjteT=Ww(n2 z)=juE>XB;BbWj*#G9Bo>C`Gv8qaJW9cqX)*e)sq$HItJ=gL`ljp;lB#wdRv<*R(Hp zi-*b;8cCFvjnSFagh}$6Cm--h=DxZHV`=OaX#>?dk1BR~o4xVV*AgR~OsAaDz1vR9 zOVMfLwn7)JS_72|REJAj`eY)|oLwMgH8&V#hdJ-8JmQZp=>|+*IK*W%NDWn95OV`V z2wQLQG|I>63$rsPKQ?A*lJ;8eW>;Y3XQivtwbgUvF8U*C7F9#z*XLw2sYMIYSGp`q zUKHNH-B7CIsumY^$Z4IaG#)#r>)j@_t;RGZT93U_^c&E~_0L%cJNFl&*F*n$C6fO8#_JTttTRr>@JrN0L>JB-9q~v-iLF$t6k8 zH??!M+k}c6aHsoGYGp^&WD2i5wqv034|!N;1E63=I%Tqnt?U>xV_pU`qgnlNS6}?$#!pJw}u8->$H;d>&UJ{5V#$ z(y^qr?U1&jHe!$M%X-7(>EqJ0+47ge)Y16yQe(QqO-A8^-_!N|^mO+Io_kIT(=<;~ zqX$1`c;8@`C3IZKANfELp>&?gg#Fr6|e z`>Ay)kyzKJN^$SkBZ{+M4t(Bz>r7-=Bi-VSr1XVz+=B~0%d@r_-(t7e{EGar#wt84 zG9t%6vTjvF<*u0~di5T~+56SA4)wok(sIj4qxtjcS k&L{6XPq(xreAWOE?9qT#Z*?{r&;ML8y*AJ)sgb+>1Gp0qb^rhX diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@3x.png b/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@3x.png deleted file mode 100644 index 5e37700c0ee3e79c9807123814b7240f5a330b37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4980 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 H-J<>nrt$YZ diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json deleted file mode 100644 index bab7f7d16..000000000 --- a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "left_image.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf deleted file mode 100644 index a21b5c9f751612124205c9b533bb03172339cabe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253015 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

&;T-tZpE#vBi3lJg5@cY=V zj5LmuGf1d2#F4WcC_V5KJ(v-_Sr%)e3?E3amRcdOeI&PZ|a@ zkzXr(@C(3_$_Z(07=$QNe#NXx_#*qKJa-u&Y3%r;MPgAWd~-B+YSl`u;`wAoN6r(~ zmHze=K?W0@!m4IO+J2Z1X7dPqk|e;ju7_fKIDqMgMxnd$l9Vt~yy);8nxc)oKln&1ZoKCJx~Du-ieg5n0`zol>Lb zynp|q`4l3T0H7404EEG)K*Y(znARvS)3?GL-pB~5;&}~g@%-jg2GBV%AW3U zX(J5PRPrRp*Y->-rGf#@$+iif-sVeEU@1?qcCpZ321Vk4zeBuZYl{pPo({OHtRS?R zl+TS6Jn60IE7&5ontstd9Lj7RW|^5IL%}`m+}mXY4bO!8>%Tjo(HFk)R>zB^q!8|| z;@STA>ATqY1Gx7TqJle270)zuo)R+R>LhA<#{}7Qqqg`}dMp?=0@cY^>k>Je-gUQT znS8+yypFusswyr~$JrGTyjY}l$Jhxv6%D`}d?Oumb<^ACiccRYz@eHWly8nM^V$Ii z6=$ccEHuE8CGT1R3IV{a6#v_Xhm2a5e03uB&S_3uevxlyBv+2t;Y8EZq9B4gnXh!n zFWCQv`$)9Qg>_)+cU&ZT`BgrAb3TXq-!D=;nQ;PJCvpw6?Sl5P z)G2WfaNS4DzD*Lj3oKGG%T7o0WPfdstG{3{9QUE=#k<&f?vt)g zS(K7lQy++ca67B`am3sU|QTqiX!EjItJsf3nu(`D{FSXk;d-^ zy^Ss$_vaa#;i<`9xP&nBcKiu){|S^&fykS4=7k}5YCiGB(@|eIlNHhIGtJXex_Zle!0Sd(T8Jz}68F&+=`rXa63Jw$HINXginl-F%w3DC1rYBYh{TBEc(Iw>!)=r5{z zt#!{FUCcj40jZ2{WpY0zZI>LC(3ejG8S?+ao8z6^#G^&Q1#+Kp+KV__>jmD~J6|(e z&D8Ktobo4-Ogx4s_XB;tc%SQ0>fdFXwb~BUe0DQe!KB_VNvmgH!;%uuCbV0`g2FX^ z08IFzRC44(_;P_q+5XSeLe?r&; z?I&5!rhFOuUP=H@myT;J)QewMk+>^Wa4 ze?J#`y-7XP!wyWY*1JD0_D$jNmMdu!xi+TvpYNlbU0ZwI{Jq8|N4%ZfXcyc8k7&!K z7hl6u+vzA}8FICin;%6i@OJ7r?R#6U-|ln_ktFr@dJSUCY`^0W8>s3dieC}(a`xB# zJMo9MYLA;ZnNVefij;@4Ph^v+s#UJ6Y)GsBBpJfXZM9((V{^C>@CqO81t{Dx@FzbD z&0drK6M#OfA0aA7g2u*FWm0Sor!vp~c1F)^EUS_tXVl*8B_hJPqh1IehdrQlSf(vI$!R{P^pgI$Ip}VF;OTY!N6GvJTi|X$`2%4);>-Jk@^nsXMRzUI z{kXn2hxH9CR@j=|MM#QPJWZZ}5{>oXxtYr`jMRFCvyocxbbXNPt zG_b#zeJ;n-IEkdjc%Pm8_wod|2iRF5RQvd*pI-;$;W z1v+Sbi9f(^wvhf@Oi`a9(9-zf_)KCGJl*j?7Ny>?2~L zRtt6!4)u^D)Zq$1*Zx9aiqlof4a@e0vDnW~Tf_%3ph??j02(_gon;@8V4vx$yEbYa z?-dL1HjGGlqPbaBOP>iXu%U1?MGz#Uf|Wt`;?zintBNzkBLB*8KFMQdZym({6W0o z&HJb)pAd=#^}5KT9$Xg^r(gpCA^|9HrvK~X1Z7(Px2Nm?VS8}(jAbS)lx_gltMNxh zXxN!bZ|mz2z}8+u0_j(3e6?(|`M3`lrZfZS1azA$8L^b^ezZCamT$lWy#f-P(97|( z%Ak`V8j*mYad){MyYzXpUCD)x_g^xI~Jodq|kzoGb8MGUvqJl)aR6(IVnRv?=^3o zj#s8BAK_Nd&ECz|jeDpzfoJqftAZks(=$yuZs;t{w<=k{4I>)uD0Gf=>@~)BJch&7RXfV=TI_JK@ZLQKKxlZjh~F z0t+%oj^SDsVAsOx%f6)Rndce75R&(dEM+q*2V*PrGV`%#Wt9DpmIu6I9n}NWr4sFG zf0~K7>>}yx`FwW7n)O}->Ii!$00}Y0rbY5BVO^Ms>X&sU(s&_fVIGnzhoWuZBy?21 zaZ_jpz<{8Wv4W1O+a&cB4>GGe#&OGJdUIjNtq^|WFa{s-i8M)z@H96-Gp{(B9b1q8 z%`#=hrAX`q8$d=_kJDZnmXQUYPxRei?p!y7+;@5MZP#eJ%{Sj@ptw44ZV5kY7AQWn z??0&oy$%MkQ3LT#93H>ZNvJvG0?e{~0`RMl@tCnp33W z;pcUPbw==YjCyt78u(n_$Y(`-DI_BEWmJp*?bBOo*z9tgzraZYI9|$G6t)9LQa%UT zC{vrj(&I*PRT%ZB0IM{k&M#H^wOmij=Gn5DJm{7{`~UUqK6~eO1a`lys~0oi4ul^H!;O)~Sr-nYi$ z8f03;Xd%{H>#O};JP83gsC>l?jub7Hs(4frl!XG;BGaVzd>c&mYsAy{X=MAX105`S z!_ER8i?}|VZV$DiSF2=g1C@01H6@#5C%lTO71 zIvA8dsBAy;{sr#yhMhM=L}>+5LBotYfu_;l>RmurUT$5D3@-#MJfy5Hc|JpV1$%Va zFwMznLxT;9Yi7{2HeP|$bb1^K@YHv7ySk7Q( zGF1&EML zu+aR^d*DJLUd$vw^QW@yzTvoD-|fn-#V%@qd(vO!Z6)UaY_=;=BG7?g0YwpIZ><~V*x(%-%+IGz=Fw?c) zWdH`P&g7bue^nKlCu#R~nXmigqh1u$%^z`HU+S1uhI1w~ZoywEh>8XA56`Sh;OoHm zVVgiCFARDD zyLKIl{Y!eBsSwY1;fv%f7_ZctpB4GDCF0 zq_H;IQZ9qVPgKSxDBlL#S*u%>tEH{vP;$FBkLX@|y!%}OB=>I?Lk`kb5!$>7=Hf!` zdd)m9^AUO+4Q_cbxW69P_X#cprIxp!w~{4)zM|kOyI+6LdHKoO>n-zJ_&061lZ=Xa z{(ugYf0_ry_|_>lZVp&o^r*De zWi0A2K&Ngz6R>nm{mUq1J6Y>A-}E0Ge|lnSD?^ZY!BB>nWMGHd=~`wfnw8K)>)J%g zM-TFpMpz#qc#^L*uvIb~(8*fxv{}Ws5gymKzB`ikWs;>9ixbD`&d*rctX5ey z3nR2ll}~4`o7rqONiPdV{!3?Krgh&3#BQ&~n3y|n!a~E;%2cpbYhW>2>h0^CwXmx? zFdhT7`!~JcJ-KaQ7Jj=AU8UKpjSBi(;!Ks0(~paW=?2a0Qcr8vq?GPIMW#1HmRf;U zI^deLS*f!DAi!%}M$=T=o($X#CYC`0~=9#C|4=*^2@Vb`+%2&PSUAX-79 z+Rw$^5p|!+qXi{`_@juqmh*UrotzEJ1bj{Tspl$Oe`~N*>wVE})rwmaY?=U3)@}R7 zF(rs)w~OZ%I36&19BoVTP+pz2>vx`RuQ2VV1rc5ac)iD=S7RpL(uP z1wSgwrf5iTAvaLgFCD_edO7-H2>#WWkFOR3jj@*zY+?3x+lp(!jz0LVU^4n~*dGvb}YYS zhou{>PyvND)!C=Qcc6`0zo??j#$QkqH;=eL4}UN;Q7MX3hmm{UYZmI4-^sei|A!&= z6U^Ct!XFUy@`LY1Ozau^rzLW{I;IEy@AQ|DPJC=-*=Yn5Etk@|@ zY~rXLCWMsytAj%+UdJ`KhOwC}^9eGf-%o`_@AXze_6=g@QAo=z0Wj|~qh)hCGnW`s zu!#o+V|SUJ_Pqo1h(={$^f(T#Ae&{T7|8shUbKq6YAS(g9u?0C$)XcN6LE~o9Wg(mru6u24kWEy2O|wS%>c%Iig`OUM6VSn$nh(7X1|>t zx*;CK!C8Ep>3p41XZc6!c3}wRboJi-ynen|wYf)QM>s%_JPu=LTx(9)NIC*+^&^D* z4>{n=e}Tz*+t@mry)!2h3!g>|%GUo#u+4*t_wdVpif)(v`&cDgbPNe)t-&S>v6j>- z&a}hun=2z6!+8j6|4x1?y+!ZH<3yk?bnx$A^?a=(r=PXS2qt0-s;f+=2fEv|>zT?U zE_?I|e%W&Q+krJhYhI;?+0)ZL_ywa`M89JFiz;i+58%+f-TMk3(W5k}>-)wJ15A=& z8Z&Z9Th1koOd~KwIL7qbD&}RHaX%rPTqJkD?3e4muE_g7u@`*Z z3=l{dzIVV|Mx#HA~lLknq$|cAkB17o19%h99f0?Ro}T@ccl8l)RA?u`NmJ z8Mmqgyg=bt@oWXjI~6#!6ji-B^w{QGN~%2eukx3I1;}!_vz~^%KG%Uu5TP7O6@+4Y z;yx2C2DThg>)%9S*MU|2SZ@4?=^VL#%O|&@RZlo^FuEYMFpbG5cWp)c8sKgsp!>ZJ z$@)qXut;XO>AAUoQybefo{Zy_Sgs>lSn%yOuDvkKp2|3lh{6Uuea*Y`CoLD_X`3-ekINQiHI$+r#H{ zgP@wFLfbE2$zE!dUHfh67}<>5c#BqIAMQ}5LeeeAm}&+-N~4SKTok_<5@RphT5Q}2 zXA~-^eI|m^)DZk;1K5+HgxKmZIzBMB&~Q?#v@(y<+e%}+2`45#{*V%3ype3}iyOq;DvORqH_y_ZE)_A%`Zt{WP};C{ zOr%f(M}y5X6AI(nwowF>rf#`sE#e3rZ!uv==!657DMG{j)p)g0lc7~6zw<$ow%ssU zizNZ%qtHXHuuw1m$lYSM57X@D!{^QCuJ6$#LIhT*e?E|Zk1zeW+`{}L84k|%?V~59 z*!8RD!SbO+!e9@e{Y@x=SL`cUeI#Y<{0S&@fqBCR z4->apmZC3uN`iGBjrzN8J9WxaEXD@l^Xq)q9A-6IxjX2O;6;mm$92+HwZ;2vBgy&MP>^7t_cjtP7S z#5a9tdpPxr`rbDfAN5-qrn4;oM(k7GI2#xb)1dtW>s#?+LkPzBgqDf_&X6{Si;k;~7$Ia8-*W>DHAqBJoLysuCO5QPt172ksNy9HPu0Sze-) za~-sjKq5mZBe$smS}YrI8eRxi>Pz&Z>>)mjNGeI8WR+aPwlp93^2xf zmAgrhQR?g>BxTSqdYZb)to9gjoQ3;eB5xeXChhj)Hd|$uw&evfTx6ZRimAI!geTY* zEcAyJX~J%;;$~|*BOkTDIGAyKm1~xnmtdb?l}x7;kGA7NtA{exe;;Z_Fz5FflNsvA z(|@_nl_VQ3TxFjY4_A-DNU=6q8s(z1wI+x>vlRV8RE2QfwjeT$ zmmygB?`!1+(UYpnX^}4SICaejo6&74nDqARmA&=LGq`ep(V~eDo+bZTI<2=^(-$03 zm}~gt_CCDPZOvYf_{~=B&+L%?&S5dad;%CKyv@(quea62p2n|{LaaOAIx8yAsK+Rk zW#cZQ_N?!PBtx-CtMe@BHcDxAOnuc> z2+O7299tziyXJVUctzvYX(Y(;bMSlGO!l9vza!H$?hzDKzw}aS$SMK$- zTL)!qk<~t~K6=>#igBSnZ^F91Xrh8SE-aZsScW?CA#gw(v@Q#>AF zSVoc{+emjz?#u87%xlAp{gFTE3_Qj-qaFjq%*l21%Z?@0DaKa9+%?5RGO3o--ITb9 zEz-hakXe7h!``rg+hsTH1t{@f0LauApky2deh<=HvTUsJqp}g}Mit}o$6AA!zFQ4ek-9S zUwht?c`ORtqZ4puWy0CgFle+n+`oweGVy^6fRq=f_NuI&^Fcb|RU1<{=M$93kx<6b zB4SF~>ktq?n_q|nI_Y4lKWfi#5?>8@N74xmK}~vpUZfV;k1Bp za#d362d=!BCpvz^x}(-a`w{AvSag8FT_Hdf^g}i#GL$aT{C)Ayp~k`UGZA)Lg}DQa zm0mD;p^_Mjz|RMbJG2d-LtPLyv8gEAJd@pI0wXWXkEPlB#?m-#M4dQ7LhC7<1&OBi zYS|3}GQS+^say3QsX$lPG>Y+f#+Z64vpXalvq`}*HKZlhkffJ`=7a4FmW-c9p*0OR zo+hcXEJF>~ci^%RnwYrWA~EJlMn+;N&=?!}NZ2*5qsFwD)<1mkN!wdyTle>V+JAS3 z=B`%~xz#&}P)gU9BkjdS&8^_hP-HsE!Z8gk!lD;pj>%$qN!?mTy8-84^D^*RAym4h zl)ZLl(lCNM4X^*E1x32UlIL*^X3yjZSN9{MqRd0mKs1)%j~0cZn}^Cne(j|udy=00 zxtc)~oX?oQb(e6G(6(>|tZb!j8>n?vI~QHS#`2;%s>qaiWn;EF{M&Q#Qw@9TGk$9tlqn~kiNdyPPiq7 zkh1kz1tcpD{S-@v3XES%Ki4&wyQx&!R!O6v>nBA);lFaqrYK#RbR0{`Ubby?!aAq@FH2a>-}$VsWLPuK77Lcimhyciq~E z?9B{kefV>c>|bw0HX(#||F zqD=Ea!s?D&QCqO}f@w*faK+xDo-a(W!)N>%?YGP5Cmssrk=KuCyHF3n@{4iL4n*<@ z&cea<#MG z_yrBeei#nhsk#cw1x_kQy|>#DN##<7cX^Sfc%8qi-*^{HXuALq$3!si{76Ek3q^HEk|bBNK{d2&V2;PLgGdbs*}tF;)R z2n`=_nX1-5E6D1GCOj1jy{4vAW)Sf>KQR6A7jFM zV$fbkn4E8G2_A%Oz$u{DWShUEcG+6f8ZoLp9_W{H1+2+~fmCteIiVEGCC`x}%#gY0 zzEGxjC9ZvI2j%?%wA%qWtEODwAE?yN7Cg}`s5R)E*; z4E8VcW5MtWOus~c_OB``F9Xl{g`kP0mP&A^17m8cH98OeY*PS^-xl#Rn-Sn(6oS}?N5{D%} z77;$Ta%;r1Q)C^n+b@-Io&b%Id^8qh&Aow9W#Np-uDL*DC)EU4prbx(6fKtnn{~H- zKG%~J(#@OjNj&$}(yzQMjT`PeykyHveHqB><~K>ZORyNdGIL5TbmGExo81qa1lYCX z2vT;R*C$k~#VR6J2N{@}<%>Z9{_>wv5^w&0Gt%}dNDxy2ekHS0FffM>T|e{UK^1Yp z$|u(%Jvn`#b4ny zFxZch0B#|1oCyhd0fkBzX^AwXSpJ#@6igYBcK8yD)y$r8^A3ZF)p^EdJkLA1jyq{PDLU1U#lsN%k;p@C=<*O~$a8YY^APBy zV#)K~-rnUwG2!W_wsEo!?Q0wHhgqX0W^C-Kk7s;#Bx;RXl{=Kb!9JU-h%}LR9!@4{}+s z6F+2s1AWio#kgcPjn;?}JVe|419563(b=D~;hp}3|DCPi=t2SxXV22X3zW@n@D20W zFT?{G*5)2V3Dw$SHg7VHQrgKlei7%O>q=_UBQnxY8(EU(QNV&Nyt~hIOijwcgg!wi z*F9?~Hnm?}D0^~gCQj>|Khb^vdO0+&PZ2DZ#P3}%>4XxQr@2+=Vd7O-;y>U`PoiDo zreg@}@&OGG(ROqXmo@Sa&YE$CWK~a4I)l{%2jl~Rq_G3x)cGm!EO9IWkWDnJ(p zt(46p14BF;WwTK?Te-sna97eKF78h*dE>|_ve?bdC*X!j>pnh>?H<&%e$dS`c8Zpp zRb^c&9|p5}QTdg+W$dgpphsSD+ix=e5wz>@6DTZ4Dp=7A{+I>NDu`_y{>ABRVeYFv zTW+dZ<;uS_RixcmrHJ=g7Ql17>^MJ8!ozON0LzX7c@A3cKphfyxs zI2t}I3eiwYYcKTHHKhDgVghNH6C>SyAXr(m)qu6rQ_AxZg9p|shu}+>c-hSRf(>8V zFUbSW-dgN-MhF&t61FN-a`CvuLe$;T6kjR7^3O5K<|%XXziz}imQZL}>ki^mC`jU_ zay-25Q(Y>(a{=FCtDf@z^nT&I;Hho&@^CIh7cdOvfKrlmN;`jRUK_(>@B@8bDZ|NsGqx^l6ObL{2(k{&Lbw{vtl~BPP6KZiE}YXd(Z2js z!NXXzk8br=SEap-%W+QmjgHg)_6vyLs!sKoLWwb29y~Ahheoq%ETh%&wl7LxA4t+H zP@k6lyOL?R2%g5TtGz(jEOO0%baN-?YD5elnlP;HhYByB4CE9$)KTnWR?Ngd-Bf~6 zzZRm?E?~0kRoyPT92~I!is~IvPUY-2QiE~VlTh1iU_$4tDxK$3k<%DHUn$@@%u;td zK>+lZOj|r-^6i(n%i9cvu6`O7t<95rV{hn>F#c}zW~k1mWdmIih6nMrShBP4|DaLQ z8n0^TmU$gwjDU)=Y2>C#F4qxVoUnFdBC@KJN8`)sXQ>le#ScRBEu{&Ih^^Yep23k~ zkWcTlaLbU7L=X^G@AT%sx@6KL&rPTjpu`DM!Gd|y5-HopsdQVGnr%m+uE|d0^o+8> zH+>Mtulh%5Jk9U_6f6MVL!}^Uk``>tWQ3rM^{+cfkaK|pQD~Cv#ctt5EPG!N$qMa< zS)4dw-^VszoN#qLr5M(}wz2eQm_n57wLEPU6t3 zltccyb2b0|zQr*{T|#R5Xb7xK?w12(qGcsvWzf{6rb$K>C zb^@7$-7I7Fc9(x4W>}?y-|l-nm2B1uN+~3wu~vuEmlFu;yLs<9bKM{K-fy`87E#T+ z@lFN-q*J(zq7bCdW%PG^C0Jtghh|?O8QzJ!S5R!^S?;6VYS3*do(P^ZQl!>-4S}Mq z47NDD**J2lxo`Hc=~({h^ln-Jf%UqEH(g`k1PlFAqxt4^_Y1G%nHQ$->n+|dmy>8A zP)BE}>8f1YT}ydiN|p-d_?qwfWh^WFSjxRsro*?_K^( zZuc-rmdjFT2%qaUH;udB;_QI|=iP*9C2=p2NkHQnAGfiu{1yO6AQMO2jK)s?%cdUE z6D%!+&o&Kzs#998CTiqSFF!6_!puHuu)O4suR^MD7Yz1VT#A=a`VgpA=cRh-+%Fj| z;PfzZDKE5}^Uw67z0#g?Nns&-I4Weg*s^z4zDtj$Z8=JmRS+*cdwhj5_y)AL^D?zawu)DPe^^HKXzQbTZ? zOHylKv(4sU3)#yn*CpfwQUg$gnNbw)1cd$=$e8_=)b*Ipp$o-6w8;`fXV6kf`acNcE6RQvPXo zWajPE_gqL??0B?Q1Eq3dm+YYP-gdgMe>Xhpm7p-TI~C?r2rZp6crEjk9=PKxdaR0EiU-6;LV&Xe8Q10RDX`O2IY zz66ygF{6@BMs)#&U}(g9?Y76oH8Yp%dGcZ#$4~5rRfxPC@!SEBf26MN;TA=sn?9b< zS4gMQ3En*@WtOYfY=29)V`iZSrgCtuIm{(7(}?uzJES1#!swsZNFEUr?pr3ir=C;R zO^zGXX~*F*>bJOGQ_+hpZdP~eT|1CAsDG6CbMmM0SOpm?23fy%WwTt%K@Prmv}1BH z)hO{pxsc$ChWo_J;X>t!_J->}^a$=Qs$r#KFGoADqsojFmt<9w&3Q6>-fi!P^R-q! zt26&T$}z>dZ}ThcX5DN*muS=*Rj%9b|D7;%7oD14FFd_8-A*!wTrCPF%Q_o+W*<}5 z^%C88pVE(WlZb+?RL?Q2t<3$lho(<%y0K)p90HqgVQ7>2Z^nXjVE})_7958F7(me zDc>()Khu|)8F{Bhc88mBsey}R;y#OGLhI4rae0fjZhUIi8qBegrkWEfiO&?0f@VKn zE{7oi%Heyr{3CMF%4$K2m8ZW@&+|8*fFM7& zwv}u_UmBLq;Q|eq2N7m{S>Ou73s2lxZwk2EbKE8QcSZg;$G)ncf0F3 zZF(Jf@sAcXQ*>QVT)eIoUy6s+Ih+#_b@?^CE;dKOO}OJ-H$d@=gjZvlgiPp^e!0H_*dM8wa9QhjrF^LUL%g8}R!V4zKfYlo2r zy_4YeA=hSL%l!z5!>T0|AG&V6(=!=fI=NJ%>KhK@;!OVV+4}R`fMAP-WnuutVn;54 z>(=L=Ovz}gd;0^zYE^;Kbxw1M<7cWES8XYZ52gY7nVkw_vD8&E>Em zmeM4iRC=`2HrKmyef19D?X<7py4lUGLg!u^ExeS$s4hKL%QPb(A9sAFRy}uA8JSLS z>`yuF3~{PpRt1)Z4g6ki)nR1FD%j_ba-hxb9&O#8&7mB!H9lgNPK5fMP5ldW?R+EJ zr-p`?#rB^&j~$XA+}zMy@o2A` z3l6&CWnSlFxi?olW+0ZMQY{nyO0ocwyBs!1>DIdLq)PpbgLFGXV&$`4#*FL`v~#pT;-grQ0=J#g==g4N?LMTod8crk?Kk1l&kdEZPsk}Jv zYkfd^cI~WOU+N-qI7=xuF!V@wR_+pUGJHg<<6ZBNeXdJ{Jzu_9enKbpi+s5y;%aDz z^7}`jQv1I%q+Be^1?m@dK(ew3T~4bl`n)pJqC){#^m&@HZt|P>^mt zt|r(_8WWkSi8YID{>JFPrF$n)`|GUDsYzeRTeBS*tuE#Ye-c} zzgQC#x(#%a?hBeb@}q`5g!oBk_}S@yn&?sGbWkluDD8cJdkmf{`KK(+4n9+v_{o4@ zyk5MY1XH?JfA*VpF12J9r`)1cn!u;`6>d1;_4My;?riu_?^t+fk{Z3td>sqivPK3@ z-X!8{g;&&PQd0F~g@=at<8k2+-tJWY;xPALhnTpjlzO-G(u1du4 zmz;Wyn`<1VvRLWDB@vCGWsR<;d~17?HVfH0gqkBnP$FBK7dqgaxoGh+;xJEq`q|_u z)8#|9nr3@9&EpnnT@?XzW#Vx1#ty;47yI{f|YZ!JiXxCp-V2)4y0xw z+t?6uoX9DW4G=qc@PmbmmXPStvoD@Y3l=UeM>b3OY)B+c++~kdQ%?rtz5~^@fB*dR z+Pb&CZv4Wj$Rfwn<@zi~4C=UeY5VKP%k1tupMGw&A-u;~-TG8!c!ESM7TGKf8W+5A~IOS+#IITps>NV?bc=PQq zzx;|e@d1Khv}Nmd^7bvI*_NeE)X4I(d9`@+{P=(L%+zJQM6d5>^N@`B>J#HZVp2Kagna$|^5uziUjK`{Wqrex|Izs*xrL5uCI>u3Wj=v)7AGW2sub4&EuN)u3}!WzXmR?laJW+-ci{$sW{MikX^Ut!Ovbc~ zI#9-sW?)Wm9{E>bxb!*6OCGv$z5u?fJfgY}-?Z7@LPcEuWnM zVkML)TuQ!`Oi$OU-{hw&SJ_8|MR;%f&MrOrh_B#GiUvrZeuGe9^5~TE<+0=BM3FD2 z^*RZ6KjHmD1#-fa1L^kIVtj=&C8~(4k(flt@juUJ#IFT%jre$PePa>_0lIzF>eq)3 z(;5fi+`03nTf`@M$aS9u2Y>I_QtN$y0Ag~Edc7tsSFC;=LhjbBTf`?N^0G&(gsQdb z`!bqIeOt76v55p5pjWYgU_*+d8}DsM4>WxNWIOI40%;THGymSZcd_>%V++{(NWslp zv@sC73I`zC$wDn9iF#~o1_HHwd7J>_85(~)B)e0K8TnUpxBM7AnXwz9^? z2MqE9Zd>PWJv9zTdoj_eQR^^oV-W1ASPNwPB7doe3-XDK0}D!q=V%7iDDs3RXtpq-oG2tg?I*_4FnWp`$9s5e`fy8G)l z2iYV88Qj!!YuG2ltc>P+{q=Hy2C7dA{!esZU)z}W^auMXRs!MO@$A8m-M6i~*z;Z+ z-U8WHu|P3XwxRn*YSe8kDzHf&+lbajru$mbQ(q3Oj4QdCb@e$7md52i(J)3}g>6q0 zVqoXWtDHN2LLT}S*EGmU^?KiuR-@_RN9A3$EuAFM?xZ>Nk;JFLd4!)lhpr>*;qDh_ zq>iDNdm_CFu^4_FJxOZQ=7eoz8^%_IkC!~Q{qpNI?LtxzV9=0ZK1i+6rH)>B_Hd7E zgnilL@UU7j%Bwh}U!Oir$RTN6op0FM8=8$@Lldp_ZG$rTi#q4h?JZWCy~inm`6nwu zTpQRI5d2+G2TB!$_ecyv@16S^a|3a_XG>SVef0R>*KhD2J}t-COM^z?v*?GRV@?Ex zQHBtHkbGxKRt3twtXxl2D>_%JDQ%uDTT?nO*g1Ow*=9>Xd0>rc#5>KLc?&yt??W8Y z%a?!BZhl2@gvnE;`5L&581=FTbRsU57uW1e!m=5hEkeFA^`?(anx43#QU%@r;m7M+ zwi3OXh!gsKzhQI(b;o;Ood=pw>b`&J2Z9m6N<@U`yRmaGwJ?^)-Nzq?KN8I3FaN|0Q0vCFom3>C{CC}6N|FJ-mNths zyH8_X&Yp6QZBwStW>!0P>!qQlaUCrzG$3{!jm4V3JVR|ws>_W5kyAj5mv zBc*k9p>rRmop9b#j)BCB**zd_e8Hln%2e>C&(_tgnonOeOu_;DvivpNDl%_G;l!`S z-Os=H`**=vG`uMSsf(vJv}iC72lOa>ee<>l*M5WMIajB-f$6oa$`|pa83h)gFe?Bz z@x25=V3@ZK9>#?cF4us&b2Xd%edFdQPX8870bT z74I6rG_gvF1i@V!5h}gW{Wvyp`uTb-MG}I|_}d$Ad%4CIr_1RIdCHcrf^9oW9eCus z_S7j5S5IB#Z+Iq)_Ti)6{NT6W@!@l2`YZnl z$&@m~nPQcoc0fGX?_b1~bZ#?M5X2A1w;x7~cYdUwNVcd22upB@#y`y*Asib^S10}R zf@QzG`3?tFC_13;v#gQdw(Sn_Chr4bTmV3vQeK_2=$9ocDMLKI+s8i>%UtZy|JH%S zq&c7RD;2^d(PZf zk`~;6H3&#u-8Ni!mBTvTo6u2M#53o&KRNMP&e+=@A7e59@xhVv^;;QUX1ktL_Z(iJ zP_d7Xesb{8;WFhaL$-D7+?^a(2&2sJ-m}lX{Rfo!u5w;ou}bb^+*L7oeu5rl4?JMw zy65hx({6&Eb2{U64@sly_<~G`w{Kc@GCb9mRs^>>LW-~Wan!^+Cr)w*Wns|guc|eK zNE5uVJ)7-xp|Y$W5%&ZpTZ#;ihn~MYrLc-&t>#4nei?58xI) z`uTuUolvB-;oYaPB6_(1+Vokk;Om1bNK5%;=T>8NsaUznHKxy)nN;A0S%lSxP-&zj z2IUfr5oQPFmhVtqY5cRn%OzmPy)Vzf%ViDo^U7NNapVMP0q9CVVR}R zJHvlCV{S(MS@vi)_??qqNE*(r3Pauqb9??9)p#gS%YR+{8GLY6Jqh!n%6=R(MeG1Q z{Qa%Xhyb{STL{Vl*90CF1q1-s{qMeKj3QED&jO8ibMHX{C0u)b163{!c^R&n*c4>y z@0+%gK3BULR4avUyQU*!y8ko!05HOW%)+4wT8)50ItK=J5 zW4dVR3PXpFv>)91_8VY@>YAHT*NPK#N_VCOWuznR8!RAg9^DIx?F!$A9U)VkOE@lHbAc6Q=-!HEPx45k5mCn(gAn?*TV5Yu9ba ze^;0GF>=!6EFg9Qh=s&BRQJ`ik$T z2`OYtH2IeX4n1(_@ZW#`&0^p*TCsW^H~ZTCqqZHpVAf<0zQNyX(6pud{#f*Mw<9!d zx#&W!Hmv)6$)_$q#0d4oHAZM}0q8pSF}CCeWmOR(K!$zx*-59)-~)X7_MKwpwCS@T zw@8C*2M!z(Gx1hLyiwB5$kwza)Y|K7*Z=d+KaHEV1h_y*v5NR#j%=^3Sqoc_7D|_? zC@&*V_syP9<6u9{l8?#3zoN$S6cfaY*M37~g7X?bBH|?qPk)*^2j-RbrUE5(&A{7F zXGV?3$($LZ;Ez%yo=JOz#aXmB<<0)^<7L!z_wGM{Q9OpFJStME0@`jLeE3n>$~EOB zwWK+jj(T~F)!>GOm$_!4T81W-nHwzVehkc(PrgsJrzK>&*mD573evKxc|UG@U2pn8uDUN`oI&3(miPsR1@&S`PoHBi>FW{@XukU zsb2U_K^KBz=j*nh3amNOXuZOO=Ms5S9OWd*qw4DdnZ{;d{WDo#$n7%f(CmzfoQF4` z%_{9C>k@6+cd1xSpwoc0UM~(ru??+V!oghr=}Mi3&5XSlYHIS7saD|I*zpsv$~V?4 zvubtEBY4DJWi*ztglxvvk7_NVb4rLIC7_pn{P8kjkpUty@%s%|ZNR{xFwh8&T)J$f zW5-X79XF9js>n8B;*`^;&xoyUeeeA>udgEw&ke+&LBy;G(?##QA}4LAq@uix3 zn+&V0em+~Hym9jHt4vpq{7<~C#?gh%+K+D90l}87SpD-azp`jKYoG*DrKSn!(XeUD zG2=iki9YQ^gAb=+G?PH&cg)u%t`}Feo zX{r|-c*&L=Zn=2s{IXJA)Kj@cn(gMUeHZ%-*7)i`G50fohrrtT;#EJfGIQngiLrZ@ zr?Vp_>4!%?2GhR({?gp}i%h{ZIUtm;QnP!nz9?oGgE+#|x9{8y%{h6SNuJ*HOjf>l zGc~@w@`F0%ScGgX@~8+A#8)HPghG>d1Fzsn0pvNVX>>)<7!e+^44yb? zvKv9ef&~jEOq|sF#ePj%w6RS~P47r2W)YYDsveA})EFogy_<-aIU26IS4fHHLUz`T+Y(@-!d0p{@QbG{Y6d4NVPjIz=UFBPZY=={-2oYTM zAqn$`WeL}^kK|?W;e#^U&kL8!Gu@|ATg5*HCTxfj5I{>8&M^YW=6>pwPcbK)l`*uK z8Y5TlLPDkdpiCy-D|d|5TOKu!8FS`|LQ>wF{pdLO-j` zB4lGhmWlD|_f+pHHCEnO+&wsE|NHyz|JaKDml%`pM=X+9y(ra?iQ)%@T;g@`@Y!;; zjD3A-O_q5X-C&FQhlmMpv&;k2j%OI8|o)z71NvJhojdQ4NrjhRZ!$5)?l}PZ2~Je(viLSU9N(Ei^tiWXZ>#x7i zYU4j~mfh^`m8W2{fz_|DjZmn;IJYW$7w>GLXQ9 zprlbREkgH+O2!n8^lNVD8Zw1(`{2Ik9-sdB@?;$3DlZHTaO)vB{CQ+r<*}(i0ci%d zr4DHv?!hoh@3+=ceuB*V$J*zz*#IMhhmXb{5QZC=hSu)>gCD%LakCBY^~YCq%qy6} z=?xsOL^jd84oq@_#e-%znfd4TZTvq2fYJ)onCO<%gz%$UR;J!E;~gO+G>#%r%!V1j zz^zWwMbN03FVTz?u1@86%VYM9ojB#jjho;MzCK=E^%{z8@9)^l5mHy#M>Ze;HdZG-JxTnQ&0DuMZPgBUyTe9}wc)+~_=*KV zg0s*bIip;CK4t#-Pd~ddYXN$v5}%DAFR;P!^@>|mTKi?$Y6J>M7I1$m+6qYvz+>Vl zLIj<%T4fJS&oK~p&*Mk~1+>oq$Gv4~bCifx@dji1x7OYujuB8TkVerY%5uL(#7B^O z)+HJ#lv_X#L=|`kIKm>#$*99Le)H3bM!}*40ST#-(|!a8c-fUcx~@dENUdIFv@b$$ zN>+~47k5wBU;~BS&j6VAN`_oT%rK1dW(~Ni3-d(k|LovU)h%XjCjKXno%r;X1xtuY zibqQP?vgh56^}=2jbak`H1F!MMu*QPea(l+CJPxaJu;jam{p&w*43&rD}Vj7g2$)8 zW%CSR)I239cOm_D&1QXfE$y_=ofvCoZ>mg%t&flJ#)h|{)9&243p?ogjT<;1M1%M7 zz#?-;fs!E+Ncm;dZW=E~HdIUrHsmoLldesHg144MWltHVMOWtog}t8vj(B52@)&cA zYR{^W-oo<0gr3 znwan@ON1F2LrokS;$y){cnZOT2Ts48j_@YF$WNQX*mE$keteJ7DnXetA&-x+D1#l2 zcG34(`P$k5aCw{oQD1kgX4819k&Wf;JHOZFKt#$6L_f#$0AEIE!Vn}~`zfA4hoVxA zIXyA&Sj`X6=*)S{>J>IhM8Q+yhVSR#3tE~4%)1h-Kah?ZI}t~19`|*iK-X{~SjpUy1}0pE{gihI5Hv>f_7vh1|RA)s?8=;riVBC=s`h57C6x*jjx0)0Ej*kK)z=BP@)qpza{^ z1Z&eDenbTtPaqV2@XN1vzc`~;oksq4!)f@jGlD`($=uQ+$cEK#pg}aB7pBkQBR^pp zSH7!aGi3q$=4Z4+Lh2*X;p^QfT821QEKb$vQOd7r1G2Gwl<&1Ur$OGtQ-*<^D^)pD zq6(%NPBB?TWCOlXnEAf{a8bjVD#Rou!V3}^{0V$nK?pW_bN7CUX1jO(8^DcBd=5zM z+m=}KT-I7@&J(7{JTYqN3=%7NQozy3mGATJ;y85rN8gH|jKIZid7N@^gYM_eH63Uq2qjfHGtOGeYug zc{+=^7Dl>nT8YB-`#fS#L08D zn+XwH(&ZxJo+`2tf%MMFFL(DpyF;9H;LxbFu8Q{*HEmOmAf3#OD ztt~PIoVO2B3JeY9g<%>oaZHJ>;_<5`VF9_yu01xS8n?DT7As#+fU=pH@@#z+ulzu& zpd}e{VK7xUt_U%NwGWE&tjR4;WsZu8_js9tY)U9|%6Tzc-q`$wO}Sj*o0zY<9pur# zmAh4M4kt~V13b9)8zdQ8AH;lDMK*YDch7u@atJVZZS9*PvYoHng3!r`6%ei7JNNaU z-+uS!Hy3!Sw-0*D?|3p(gSEkWEPDp4F6D?2jC?YsyAk0iCpo2pujhQ8N=MGjKBX=rUJn(1CRIM0$t9aLNoPg2Ftu;7yY|`<@J5 zlrgk_XiP2dpiDV+8y(h>74pV-1UCc7=7eDKTM5df#E}%~K1`AB?bMl+pk#WED#}gO zDeUtGNqna+frjY}+%Ut4G$M0iB-Wj&GZ}8jPhqg1Oyu;&Gg*~eEIH>?azNRf;4sl` z8Y^w@lbIG~41EbGvzWe}?eH1fIeT6ckxhi0hvtJ@1@PV?L9zR$UkDuL44P%G?Ep`gh zTs2LK*eh1Umz(w{1&2vvpBwa;8zWSWFV;IOc3x1}&PPFbccl&i%KC)GRZk~XW*8DJ{Tar_hxo5FdM zlVg6a0$g$)wU8+sDjgD4@!U$qhj{p3tP^NCwWDjBT#-_T!8awuN$b|H6JksUyq{SS zLS62Rad}$Zak~HeP37O$57Mo}65X{ue!3sv6{rPvfnehL-zN3-oI(rrzdZozYzhlJ#A7#1C8 zhJ1}i1h|VUED&-~eKM4Dg1A?o$pSG+Z>7#ZmiV^e>8#}~i++f%h#dBP%|o)K$cgV? zl+2~!SMlxr?h$Y}MRJw|$~aewgW{ygd6QEoXHSpSeW!rvsI6}9C|$@VL79~JI7NC1 z86->-Cg(qRf&iL za_J{$WF@{yqZ1R->h-V0hbb~P)@DQJ4FbF&^Fll+Ql4OSG7_RW$ zMLG?+0GRe(%5+oHXR95d!*a)I<5+ZCHzICE`fNK>3cYoMZ%S|jbyL!!M+FKMv#4eF&4TK3&nMNAmLFBLOdSErqA|L}^io`7xpMWZ zug+DdRNb~AOD_V+rLklAXe(S4ErSg>mg90L+xm1C9A#i*j-|P+o$)WGJSs;vH>4WKAo9}$IxR)IxzA^*k)27dwF>4M@9XN34`|mHE zJOA~mQ>Tv|JO07p5BKcZ_ujVc8#irUyKeo8m8%ynS~`2qykR3od--0my4=caQHGr6 zZc^vgfU?*k@R62g2tAnU;QmboKMSu-{_(mHqKhBQI)!>LE&EeSDAq|=t9%dXKkvelE>EoY=O z8+)D%wliwap$`r(TfXAt=cl@M>%}mqOr5dmop;~gzGLsc10R0)(XrzvzWCzw`LDmZ zbm@nmfBxm>&6|Jz`6qsl|M=r}LeWy*PYe3V(!fpEYAal{OnnEGy_!Ckp zs7#^K;Z#n%`P#{JN9s&9B7NwjpYUH#v(56vx}RzjpB67!Zv4b4oxAkV)n`9y86D)P#CdZ%nI;w6 zO*E3~80w4W=ki#b(|sVeg(-_cOC#9KN@QE7@cg=f0w`-3lQ1-A6l8`3W$y@3mbYWx z_|o3tq^_PkPKA*O^F?O3Rs7hPN*&fXeu~o1Eh*BC&6vYZH3Kb+&C4s;<0+mJi>AkuYIY4ZS!k1cHG1Z7ruLa?He9@Xi)*VXMm|8d9*`> zG+mpX$x0yL4iN~tXu!~2ynLl;f%yHu<4IlaPfPq7M^?9jo zy~bwsp=+*09^<;_vbouDPLI&`4?vmIoS1Tp(JQ|i+fuu88~#?RM8+C=5z+~CYl?KQ zr3tF;KLSu=%NVh#C{|$XO12vUQyY$@2c8J7XOiC`W0Ro0*32M>H(&?lMqEzq6%Cgx zT%;tD(B6Ifoj!B6VdLi9?muu)Rb)V59r@q4-(E2MQm2Ylq7vOQ<7&8|^?~C}+5V4l zBMC*z!2CpU-3`t5W~zsbH4)aZlY&~~1iI*%Or>IzKq=H|QQ2@?I=0}7XF^1_8*&ry z4VkHAs^A1}()q&s#1V(Jbx%|M4p}{2&GRINZH0cb=Rn%3P5U?B+Sq@<5ci`4^h%Yf zP_c52nziaTXw=MHHf($f8x@l>%`n3$y z{T5I@Ww3M8X0r|4j-(X0RnvlU`(3~klsyP8ETKA_H|1nLmN~0sVG(+TB`+_OCc=@4 zO}(Br2kWp#9+w_J-bnQj)*h;Mq)0a*eYS#v!!$e0*nQ_M40(F`?8p=lm|91N@JavfF+L(+v;*%MsNsAf(B$FKiQwiJBV*Pu!Dnst#inAj!! z07#UoE+K+^GX~No&cK*#`S>EkLc(0J+VV%lEO;hFgf*zOWD9HJE&$xHGnI~|PDTob zd2XVa+W@=#4w<;4l}({ABag*twS|837HvNHP22OApkj;%Rqln1Zn5g{Y(q^5SHfw?8cL-H3b&jQS zo|_16XflgN3UX1Y^ds&jA%egw>2ndBSNSeK&5<*mUdkR}^(f}doqyuf&xQ^g>GUF~ zGPFUE6x66yuYSX(U|`$!ow2~`)3^V?!Ncm*Yvfeko7?#KA|J>ic~vav)Bd!52Y;;V zN!xsaz?v9su4uN1d@+&H@sUyat?;7n&00#rx(F0rbx2D^RS;2Bsu$cuEwwFaG84)< z3CCi^s|RL_kidLd+8kEA!Ux;jAhSehA5p-%JsW6Q%M1{1o<&G}F;~czH(yM?==?&M zmE_7^Qyn;}Afld|T!~jC+q9fXMM(i6LR1^~_sr@tr$)piwc5+>OZ^AUo4=?+#cD}A zZybCiJRg-UpO)PhC}Z6Wlqy_`&vEF9AeYD%5rq@lq+K(<`F50eu~2~L<{sRn)h;Hs zrO5=HHX=(N+q_}as?%WC?mb(!yoV~6Z8N#!QNSZcjgp&n@9o~R4}3Q43pkMSma^rm zU}eBzlOrbwPmZ4)LOF_ZAmv!f;nZBgfqYzYN_9a7tAg}>{K>I#lcrh+2#-_CA2?hJ z6pD;f1-HmZzj3oBUuO2CF~34}SrnCeWVrG3F*s{p2pD1>+0#AoEoaVtDYDa1&bRHo za$|&%xR0}p;GZ~Y>Vk!fOOz^)lOoC$WIKS3l{Mng)vvE}0&4kfoAzBSdapk= zyC!XRzDTpse5y2C0NLEHf0P_Vmq&7Gf?Q6z=Gm;Z^X7Noh;>gf3}@xD$FTt?fqA%f z?~=ESvWK+IITYA-^p^L*?lj`jM;_G}dwI(|+>RVGVf|Yhx9{Aoyy`S;W)B3FELWv* z^VVIu_ZmEO_@v2GXU(27fBu4{jz1FIlF1_qP>jO%^vdZiMCsx~bjzjg@+*LB9ydsx zh&VA)47LgKGwFM&f;;4j6av{hE|2Y)UI6Ol!Gi}VveGh+7B64^36HX-IFi-URXpoT z;8rKXhn!?;>AbNr8=uZB4Y@^RA*YS@7~EE^RUZO)>a^+0b^cL>id46A67X@y=NRNd zO*DXPS{C`pOretD72vinMTWs4VFhCgA$&ZomafpgbGK>JXXwN; zmEtr)Q#5D;#0}3Oc$XUQg3TL1HZ2RDGpXe4Xj-})rK94(H1U#Fhbl|qWYfNPV&V!$#}tk*SZ(PrVoMJC~vre=5@FEy+GQafU>`1jLfR{f>{WK*3MSD6rO zK0>w?D^V7&7p#Bj>^X4IP;SqiJ71tsF<1RJH(wsjs=o^nmAP+B+SX8R5$)zgO0{T_ z($X$7J<)H_u${a25<&IQ;UlfvbvCPgB$%4O$bv$=+N@EC?G#H846F{O3|3tg0c2C1 z7FU_RL$^Ch+!lM{RRxiF=N5P#YH~bSqlgo9eW<$$WSq zb$upvQ`h@P*hn5bcKpDBLwD}n!5Jgu8TxEM9Df@(ZDx-X8~xgEza9DL<0D5tM&Zr- zH@azo6R5P!IhndCY2u@WiDLDh(M&Iwo#X zc~djy%scqOVdb@W*$R0v?P>62)XQUaV31`+>4u5mw)aWlhXx;0%5NP&HszeS&xuVs zZW|Wgm`|bxz#mYyV&&?_O-*uo0qT9CC#J zl5IAqe?Y3S&e6yP!75j+iAozfY=1ibP=69_#(5-?Y8aDJ__Z}_4Gd6wmEAH+1RFfj zO_@4_Ng$LJmZ4%N$@%ExqohedscP@Jv6ACL1o!*mH8D%LmmAY#bre7P^dEfl=B-){ zntCkksz4I**5rUDPn*Sa+wfdAzD3P^D#q^tWHYeBWAhh6j$pHpbpC?HAQcXaoI2dC zM{iDHhYufV)VPKG5&So_cBwLW$Ow79x#?B1pib>_TOP`a12~F&xHiIhxHyp6bLN3{ zq;b7#gSQg|B@(jD4YJCYksPCnm#qY~Het$id8YO>LP;3H0P;yi5~YP}=EM2YSFE8{EZ~t=n-1hfr({t7+ocoEMIy0?3Y|;&VKdB?c4l3wQAQ%R|{fc6v=E>@X(H&bj*7_H_<4*ORh-e zhYKK^a!$15M8^qao0HbAMaYJ4BA83JZ{Mz4ud!3#M6MbuU;JDC>VHfUDBQ7gcMhsQ z{O}{%ZccSgxy2=&gltv^>4PM@a^))Uj_p6JCxTcp1elFTgh`;Ey!qX&rVPqWBh6&W zy4HDVTeW80#qTeP*NIP0PMkW!0IX5PHg#O>fF>_V_l%88sx{qV?>0a!7)Jov+$>`^ zk7DBsN5?wIHWymEZtCiNgl|ct1E}W8D?+W|nhezX_S*|v-+P};4PPAqY|^Zi6J0lB z<{WTOd5o2}W;op;G+IJ7OJgEbt5FBB2)=X0t|AHj{`*U$*RER++l|D!H#Th8@V4QB zXsjsrl#c&oX^Z(_0?Q^jjQD2SOY^I7Sq0|67&W8#0J3RZ(#m4^yR(68t6jGtxP zJ#C@l$Fk)s;U!8P#>*}S<4v2lM%QJ{>+8rzVq&$pgtuTsCL#Gj%luFF?LTDhg2nQy zaf=spfhX4JmfD1xg_CsB8^ifwIfOW{)6^n*2arwLEP50697s{yfH;vLiv*~3^7B)~ z(}cxD^b@JulpHmBET=QbGtdwEO# zM)8K_Hzz1m`EditrkoRvIk@|<=RjJzY$aS_17dK_olctWy9#d&8`Ay*hHy#Fj@(x< z@Ko+;wnVJ0F(3@|*83{^Qw2$2Uan9{`9?D}WVOj{)+&BxBji8 zpB(38+Uv!E+8Z~;JFXQ7=BiCPtbKIyk{u$1Nb#uOC_iEV*_3mlJtrm>-4AEKMiR|N zLVHvGTXJ58#1O+>)ijIzcWA0TCsvn)FeyISmDd$9c4y?9^64JOr(t$L&AscFJ(oVNpSy%g~v96>% z8bYtISdD=^L?SB_to1D@qsa*ihI&)Go?`BR(I z9YY0eN%7*OX7k(af8AJ_yN|J1^U8t6`}FVEuM>vPa67~`whno$Zkf*O9k}Q)e5;zn zaQkUCU`^CQ+`4s}7I4FvgQT@68Q0jqo?X2WYRy5m3BeJRtTtD*+~ilRT!SgXW(p@B zu8Lt_0IBBcsm=Lv|C6t&aXf79kJYb}pS~w&HrbI8A*-;0M=!T?_dd$2RE4VU_dY?} zt;Qeg+NbmGN2h#Rv0!n2Qn#lLQGRs$kqv51LMo5oCWp0S$4_WMLgg4O%>m8aAe#a4 z=*(GjKREm$ZosjPl6pR4)*Rfg-MxDcyYDyFzsUsRI9H4g|H&uEd-WC!tbqO3*RCfx z0=S2YCppk{Uw{1#?tRF_HKk~vMH~M)$6!Nwn>wx>ctb^DNpC!k52it zOreq>+ny(bl?&K@WTOvSz|AjUjj`bL!=d<5K=m}Lmu73;q76UUL~OyiBCIxy*?9$q z-cCG5C9(li_+b;>95KDOZvBBbGZERuAbnM<*8%0g##(h6V0n7*;0HjZay=NxsT3vX z6p0u5`373>uPX9*$_q2(Qf{{-%$YTNj_)9q-k#8Ty;E1Jun@kA2#LxilqASTH{64p zzrY#;0lOTEALbw%K6!{4$^nxLR+|{IFYz=TjmZq#D2_@gc8Yzk!rNL^d8# zu~L;94rCJ?S3(MMyyY~yc*%09O~Z-uvM8L%Ay|BOS)_REdh~8aE#+TXu;i2DpUSW9 zz5A_vZS91~9#kXd&tISyPkg65o!2rs@-ih81e)!@6TU*TiJQn3xcL#R2?H??+2DYI zNchMV=Le5u&~Jd%_=!_~{q-8O7Ww5W2rz}GT#>g%S~`t_g!n8%V+4L0zS?-Cifpt% z8J-dw#gxDS@;EPhQmL#l-t{XP)9&}?sPc>9-MugR|gLq_b`cL3{n`My@B zmo8oEJ4mIsCv;vpBKJ9B$Wc`%Gf73WiA!AxZm_!{EyO>c<*og9p9(Y^LIk-=n=xfw zd`rUWmn(Zfxh-6@1jKuF#j2}Uuim(E1OJM=9cN|t@81{sW)ia1tKS590sgdM$_*Sm zjAE+DhT9O@q1O4YzvaGAm-%H6qhmy@ZM7LuE$Zi0i) z_K_M7B*~FY4r?4jq1OBj+!B&GvIQgvS};df8N%@4lTEz7{OTM#V#LeiwX~4jtgDJT zkWH1lo3zO01Z!}`op^43K)E@zJ|x*_GUT%bv_7<8j;=EB*@(l)pZF_}6dNBPT3SeM z7V)Ci$00R|d=es?64u0XrGJ52Y_dnTaxzU_RSUi(XBU{;0UI=RB@ioKwi4Fy#6hUr zsJWEsCXw8{82O;4t}9scJ8%>1Mn&1E!`DYLq}c-2Rc@fh`$rY4)q&5ZJxqME@;q6( zuwa9*Pfb5ONoY3ru;yRj7K^Ws`*w9HgD5G zW3$Q1TX<%|8uCJ$xqUT@9mSi0$*cg;Okn9^EwEMANpHVDa;W|Mm{dOP67 z$P8p7Iy@n(#B0Z{Jwr#1(Fki4OI8tHL@!3We8WE*-f=~~sC!sbrQ9&3GzE@KY$PV8 zfOceS93Bht{9c-n70+kK>2$yB5rna;5>=pJyb-e|6G!1qQ_b@TgA%WR=Sl4lWL?SR z6P53w=SejWGxpPLIOGA@a3iVF-x`z~)EZSViqsw*z?-eAt1ePe@q)u9UU1%ASyvUz zpU^Bkwoi_H<1>XVPM3oi#79zQlwjeb6dCbvwk&Oq37K+JrbV7uSXgNI>*|mFh2_>p z0dL`^;MM4H6W@IM9r=aNNgRFj=|5OY`^hJgvQK#nXY1j=0=&|-2cgz{5Y`xisLUp4 zeW-a02ja906ZiteTI7j&DSO1EOksfAmZw6{td>CUaLS;KPiFxPal+p-M`Y9RI3$w! zeFO9Y5ni;Qjjl3E>nilwPJMCuop-ltX{TuE3JVtajXNW{N{6+KOIb#Y@UqD_BD`qX z>Hbm%;EIgO?@(s*3fbI@mxF4x$O5gyq6y_WH8?i`*|(+1Bq1J&gHHq#wR}PPoTD@6 zK_Q`0-nh~+h1@h~`XhN;uSLLSJkgp%cnX;r9rS&glA?a^DW6QQIk zPggY#N?ELatl*6y5S7pLO&yyb!Kk&!=BCe>HPy@)-!6Cb;H-IP1ck!m+L1a$4thw# z-*`6b+zh!!WDf6=E3$q>{t~f;-85+Ikvx)e|7;P)8u6oAHbhdc*SJNyPTl1fx*y7Y z?Ix#qnM(8K&rd3_(*9mLNmtzr#Qh^NlJe_nafR&Xv-!~)_SjV+aFvK`afz|30@KkxnS?*GGn+b_?>TrhK()2F+-s`^*b)lVuu|M6Y^ z_=Wayr421I;nV40DX=0yE8yaMp)fb@5*uOJQP@mbL%F~w({`b_-6K@C{EOR%kctw7?MOR8;WT`|bmC<18(0`%hV7$X{!T5?XskB&5O@2@QL=H=V z&`6GlXkO-1QA5C69Uw&Ne3{y}L|tCvSCF23w0Iz??%JaAbiKPv_qwxF{i{A6TufXp zAAK3Q{E+WnPO$jT3b%;2T|n&|O`1UmsM+lZY`&(vAy(j#^~>WDI}tZ`}COG{D|Ly-L-|3n&GqT(yE_o6t_3<-*6Y5@jltiPdX z{A1jXU1Ghc#xJiSJccq)PFwm_kipJlh_n;-qw&O~mZ~?Qh)-q8=RuC=jTU5$4Q&ar z^u86mC7x+{^8BJ^r7j8Z&(tGy{W_Co4@g?z;yEz-A=gLyQd|YSVcn?bbpAP9a*du6 zFGP{wurg9w)P|c28Jwvg6jmFi+i{!lo^>exenZJH>IK33tWqM+LN?6`@@V{k9q@J$mm) zPwoa2{;$`>Qy+Vh4j9Dhp=OA@lD!{A!q4sPxBEs4Jj?}}DSKA3r>lZ2#N$4^|A)0JW@M@_+A=@amn@@?573HfXM5V;zg!AS{di_^#?x)qo2A$T} zDmAr9X0I=sNkk-U*s}zS(eXllu`ml74R7HFEUgP@_>g!or6)GU>J?E3nW43LsL>RQ z1@XlT`j|l%yWo7YIjD;zfi}zS)8`q5z^KcYTisGdigvOxB0PuCYCZ|!EQU?6+IqFO z7oAU!t!6+&94YTanRhvvI&IuHC$BBhEN^&;1=&}a+g3na zGAoCrRM7O3NJLNfpt%ua*-sf*N?K5N_7T{(WGw5OPDMxmn7JfOG!r#-F3~H7RSVi; zOk#m(ejx#Sa-t{Y+y|HYLL1A*IF(6D4J<>e&(!MtaEw!1o331#aT);!f1et=W;gG%N5Hzho$ zMKvxncKxv=o`b+04}F=mP7?v1Pt=U116LKOm&wZ!%TgB%(QS?(e zOE-H6l;Ul-pQKtBoej5fLomY=DeE5Y&sop$Sx)Q3XZE#ubZqtHHVg@s%RKaQZgM2# z+srFJqei@_+XG`?r$&VcX-DDh(}w zx{?MGyqKDS81^T?&`50U{PDysZw6^$d{m4lAt%N2ucKGQd}~QrC!Xv~8Vugt%&#)t zB7{T4Y<1lkg%)jEY--#}3>gqx+^**+*KQs_ZB@o3 zhJ<2My-~m?_Ky`{h`-3ugp;PtXF5Z|eEOcp+266B2nPKyW{U8{cxG&zlab9vCx#|m4g*xCJ!M7XLosYIVrqNYW zFw=8YOHR-|nrXq%Q8A8vt70G=+G%Fy6(f(XUdb-46&iR6ICtf-T6tIQL_R|xt7CRo z^%+|N2DvzRS4%yga>k(1Ub2J0{4}?i2hqb-&Q&=i$r_O`c6p7_jk`CNo@-G^+pYrIx>~X0cG+evW## z=AmY-Vm!3OH>A4C2i;HbY+{0l7qA#&KN%6BC#PabIa(0RV{ zgcs*@d6&-=0cR;4w#wnL{N4%q*OU_~v@CYsCurn=03a^^gO-CE?3pua3TAn^@VDii&R1hIdDr5huS+|NYYmh{El%Xme1-OcpUH4%YuT9|GrWsjr9 z5cptb*5l+P7S(d0eZxz6tjmE)Or!$xi+(ZKB7G-haG0>~Rn)pQ?&p-R+0fETu!|?F ze5LOuD4s~5?%NFeUyLfd4;yhyFGM)mpza>sAS0_a$YO=PpTLB#Q`Er60C?Cq3#}4M z{5Ezp7n5(qVmw!$XMD5~c42qDTOxen__@@@K64wb2UqI~{p$V_pasW!uh-@5%$<_2bG!-{ zGF=INQI5tCOT};tityix;kB9ljQjTKVAc)|Is8}EwHvf5!Z4gD%-Q$t;vs3ek5I?C zpVBRM4YjwHkuu|nM1*M7Z9WxcOa(<5&o!R~j@$7%zk23c7p1nD+=R4-MRE5>IK)HV znA|tiUUAsaa?=4%ZptV90Umg-DZ+#W;7WN>P>#K9o326&;mFtKX&+jzmKoLe2q`~h7_A5 zfwd2mm{g&4XD_0LeS((8vr63ifXgQUePtQk@X}b45q0qoEDDosgtR z6DdY~reZsymnfMcaLw_i*i``{t+|r?V7l4dVA3+Jmb4QI*Zo~E z4y^~~G0#HgaGy z!|nAh-MF#!Y5pOFE@fU_loC=;7MJ(pF8hY_K$x$h1$% zt3ndxPNwa$sE&akcW>#59Z=&45YOYJlL5a7W@UV#`)c44Y5D6k0{Y@ykWni9EG2ar z416ZfWrk!;SQ6G1_gwXA*H~e{P7zw{(A(q9o)I(2c7(-HC2M}?O5Jze8NH-VRDrnCoAV;NNIYOR zOZMqBXtWy{&o*|&7kpjr&S1ZNI8Sgrt1JH*4`k6)gBO=-x_BM z!WQ(jU(EuH<$7pIDk>7O-;suhw)Re&Y4`&|(>~5RIv@o}`EZ4e4NjUc(Xc_=Fs z@kH(ptu2=}l=VYzV;{C4p*b|=>cnp%t{tbkXHi)5BeyugZ9K3zQnxGhz0@|{gqKT^=IVacO zPw#BpTvaSwBpe-_903*pH*#nzQa1L`W}ttc+j(zc=4fsKP2WfPXG~r&ka2dH7cX9X z&%01|KoN})GwBL~fqRKYz-Ij4H~%HVe>vg5Qt)3X_^%ZFR|@_s1^>TE!8Hb24e|?^ z{{s|z##hf!jDv%N?LR<_gYBP-{|kt5JcG0UuRx5Qjg9SpkQh5JFCWVPhGOLZFDS;z z^SAM5yavTwtWX4IX5j=quipND@Yz3T?f;3-Q2x*OjQoG&v%~C|FW<_mKCSNL92qpu z86H2`r&7bKir>6;Q+Idg&;ERZ{YCu!&eOyFU10WOz|({1?H%s*-Oaff^aIh872{OJ z&uQ&WRks!i`M~S6p3&{M*9^U#bw$hm0Z}#Ru;efP^EyEahPJY_bqqc4?&0zE{oT#Y z@lI~1^1}kdg)KMtf+L~9QSKC}tAZ7hTnEUt1@{5pga}^j#eZIF*su=oePxOcxHcUf zV={g~0!0HKt!xG5fViP~fF}r_s}0?v&Dq6NeYuW*c!>DFHx9wq;n~Z4tx&m-Q$H0= zGTEj+SFIcEN-tlOZOAkoTd=7owX6X=Jq*oQv-9%v?z9lma1vnd|4+|fz+&PwW7if= zK6GwOzSb=c_)?6?vvWC_?Jh`HD(pOz$~oyjRHf&zYL|^B%<%$z1lP}l6!hYsS6fI| zHh{N6E>;(OsM82(kl7wnOk}J-?R+|0Bw(D@C~}+xADt%oNu;@zz=7H^m%_lIU;iC; zUQidUse>UHNi@~ilX6^0x`lr1V}$~=!2RMV=)tObNkc{u*56%FQK1=;(TdDPKp94V zq!#8sREG}YgdAp2bg5IZ$Zj;{Qb-%Pox1JeJ_qr&zORz!J*bTu^HnW+$)kFx1Z>Tq ziu}J#*;|;DI?dWns!I63xPjI?$-ASL@&Dg+6FJvWpE2U@26r z!Th`!_u$rQKrzL)TJ>!{r{V`G)(e<_U%QINZ2(W`w~A$tz+-*9ElXz`-<)kz97~GM z+I``-LjFU%XDW6j1dYge8m=lSUjW7sw82NApiax;NYj#c)J2YqGfg(*kmjNPHCf~* zFJKa;NCKH=M26msLzd9Kh5+Wu#G}Y`7q|9pC~Icg7MM1ItE3tKCGrb+d`dPIpkul% z_6$+cC%0Dbq4CF5L!hQu&;Ra9G~G#hR{0F1HOb|inESQN@k|3_&o_W)eBcvLpbaLl zLPBELmTdR(bOT!p{(puW9EZEcy+iGcxa(mk!@98N+R^SXn=#v_7*?lL|+CA?73Q=B0(1W5f9`t}=yTt}okbmIo zkkv{+Z`-=USkV`^YWYoDSAlQ#CFZ^s=*|NFGJlX5BCJ&5#E`^c?u0!xt5MN%<@h&+ z_a!+UfsmUqqZW|l;s@YWdYDW{JHT?HTdw|CIuGMtraEzfy^nQfEvS&Uo*n7N@K2AP znDhl#31tJSjp%yXr<5D0R%7{L|9xJi*jhizMS+{>z{IDs+7S0voz@xEWwPiJ zEcq3Hl1M#8bZ3@-9~abB4_~>h{)W}`ae;xx(7<+uQQChR;yW>P<8cMRAFA=vG_Ak# z{{ktdXlo_`2~Six5&V1iuxwYk5%93rLE-7USd};Lx6HyNZ`JTBafU-9Lo*2ne*M~Y zX~@)fLi{mlC(3{C3lhVGRTBF=oEflp#{_z)nH8?LYw}i;$Pajn0uruQW9z;_f$`?} zvFs4~=^r)JMTE4QNqv+2rH#Y9({!_pN0(VZyrcac(D5sn$$~!u5=NFjGqeJWl8*p; z0pA3}_W=2E+xHdzkH`jJ;m*^o@5v6^kUAEoEvYOUzIL~6G*}(A5tx8T0_iI4n>*Yf zz~7lF+s6-0x6`k1<_uDTNP;Yb#DJ_zktg0Tc?+>9xpOrFu>5CKWu!01%n&S$GAfGW1qCg z?{-crcMh^`8M+epb`G-jQAqVf0w@6t5lai&k+>8cKZ!rcF<}W2_)7pZ+Ai6`2TbNS z-ak>NFB~B5UktU}T&JhL*w9EB)TY-?MzutuH2tn|?EtIKDCns-WM(9})n;LUa~6(i zN}Z9hkg(_dkWvOiuZ=5%|0pLkax87(cY}!mdxno*>7WNNjgN_M!1nSe?6O-B&F)&! zvEDF***{$`Z-a36642SWnUAmUpcujJb;CboUaB7*Jvb-fTBCkjGfFu(T4BZ>bH&o?!8hUcE>ZCq+ z@EuBBEH`Sr9KCk_+1|y8h%C?Zb`2XWL{Fe~tl6Vh&f`E5ks8RelTxMh=S7B{}`fV5~^k0`7#lLG#4}e*DszG2-bJ1iC&1 zKA{iuu^i0)q*Y}?QjqSNgHVIr-%z|n`iG~{sN=sag?vw*m2B-S_+n*I1$wB|70z#! z>^V1aS7_Kgnw-<$W#V7XG~ZnshuKkECe=G9ztj}8p_F1=0!u!xb~RLMZmn;aIF4#c zD@W;$PrknW%eq{!4sLfkVvAY#sy%vm3>2)__ZtDS$dNzSOl-cn=8IQXuwUt~)B2Xq zz+6;l?fCg>jyUK#zHcns&L0#En|~2}jDxi6k?H=#lE|bL_7AGi#o8@e8soTD`k5#) z*E-fQt|9zpyk`$v4fT*YNGCmay4tXPQ-j&>D8d$Z2Os~_JF&G-@J&|>J|Wojo?d8w zdsYkIw|80ph1XYR_`DU7>a)LEn|t|W3T~xDPj)w7hsC~}yKfI+PSh`7c zVS~>he<)d^tLc;FCK?Fp;`w~FF%iABxsG=J^6x4YEC-vo=kc>^=;?gX>+0JBgGP^N z#Zt`n0_&pku>C=MSi2Hm>CtVR7bV`;SMUI2d(V-}4eXvBd=iYm%eu9+u>kl8bprh5 zTB3Pb{o!GF%NhJQW`wt%<)W;YV5)o?{Chs^#F{tbND+HZHz{Ze63+g%htA=BkI+BM zaNd`$Y0v`}?brV>V*{0#cuBHTc0MuVh69cQUTeh7D06c9yUo}Z zqr_L}obP}VjtxZ8^!}gC@M+g_>@&VH>aeVR{TZI3QQc@C10 z)utv*1zkC2^i9rJ4PFyLXPh0Dx8O^W@1g!7;y?420>8;pBYQq+w8M3*H`JNbtwf!u z)G-Tb${HmJMq4lSb{=y6EFdZ!6n;|V*!$|bWu^Nq9Q58`R-BWZ>^95~j>)S(AhwlD zta$I7X~2>CORR+ag$PKmu;Sf00N3!mfRaswxgYd@*^S&u+E^X~u?N@VC9=L{4R4{R zEWfY$3C=f<&(@tvcdKuZxkQotBE-G``WZD&x)oCC=wX{~K{8f$A@M{Hg+-On zvEhZ#TIBT5&5!UT0$N(~cqMWJpqW6H5}x#5l5kO<++FpzKMY5-%Ql=2<xDR1qp$~+T!kEmFO-tSRc z{it^k0j1T{_Pg1lZuTyRFk0fxiy{Q0ft4oxKMVA%{R6AIu)4lpr8VG#Nx5)z$cSE# zt@sqq;!;-OHNY3XLQ2A|QDSwl&%h2ZvKHr!$o@MtWvu!4#RmStKRAiUH*P$r2=oVq z-;(}b;xfU?feI{Y$&P?Ws=8XFIVP$$x_ zUL9QOYs4dUkCr>lJ^r+k$fH>iBliq_{AF-N;P&@nYB+4^G&A9a>$o&qjF?lK9x9~+ z@H=$ZK2{|ti>B)V$VR72i6h2{CUi*ek(;q?`@DXUbH}pt&9ndhN2u2*U`r4}A{#<*Pc=tT zc)i|8%PmYo(Kk&XqDHW8}rSDNR17X7xIpc{m- z8tZ;L^LMWJ6OaURi=1^qbf$3#?M)YN=kSJTjnp4*fx+694NB1t zZSavPX2~WxR`tdkYg70UY9ChSnyX#S4*AgLBbkU7ogDW79n}{6pqrqqXM_hI&0k)_ zfFG_yF{~SX(rCDfZ>(mWDzz?{)G1XfSCniV0V1YW-w!N-UENwcW5N0#tq`~BHU%Tq z8eYMpAWiRj3>TvYZM^#15IKxS+T9^&d3_`yp;~KW51mUF%=BfwQ&=Md{gYPBfNR-a zDx37}6pT}kSNy< z;or7=st9{dUdaA#_~UXFdBo6XQm9le*?f*!HQsj$AxJ@f257m|#(@%{U*6rPvXD5N zM~hJ_EG2Vx59@&dslUMh+n_WgsEL>^76VA7am|fp&FI)&7Boivb01*&#PfLb~U=-l_kGCkcZ) zK-pX+ooJRr71A56g}-iOS@2JysrWb?|MXBa&fNE`tXs$`(R;#0evn-t$8@_I@@LoM&JGe zdEMd9zQ_B2YS!7YnRT51ba024NuuSrKOIzU_(m8p_o#Vdf7U@#O}$|wTvtd=gxx-* zixbMd)g1@@_19JzLF@>B%!3+{U`DVKh165@_*Mkf2hRHqaS7|gRsQ5C$d<*^CHR?QLhWL z+<~eWFJJxBpZoLF!s-DRC4{MC_;gWe%S-Bf1EuN~J41J6=EbP>GLa9{DJ5{jPin$N zuZaWg+d^J=Ry2Mf1;}(dDp^Ld07{o)1i!NxeVtO3eV(kexS%PVs816wWN#I5@lGtw z02id0a^!i4KC_uJRd?UGl`0nUmQ!5$_8F92A@kZD^Q=9~WAT-C^QHX%^yn4t$q|WD z7B@$kWL7&;w?pSI-`(Qvp)x}C>y#k8fXlF`Y%|K!fFXx3a2fj^86A)#dwgG^EU4)J z@z7M@@|Aj0eUoKk{U9h)pU?)X zajc8pd+ZO9-gS43UFm!xUA;*LABeM;$Jl7ll`sV!%#yU?TY@9!G~;ssm5R zM#wq^j1E7glh2!A77%!NWBkY8UPFJ&LFb2vEZhPt8@9KqQ`jP$W8H5Wd2CIiC zkSfC;we@B>YKJw7Tk&pd+77_91E^uWjU6c{`lkse39;9pyKtL(ZksU>mc2!bf7dtz zyH+vXK+o&RH}EMfx9#T%4&2|H5d_Z1-|R{cC@c-%u?)>Z8q{CbU)cB(<2~ax7%T$$ zE_&PgvtIe51<7M~P`3Gv$wFHzaa1ilSSgCqsbKjyL)l@7v75CwN6u4sSyH~FruaM2 zj~>OMb^xz|Bgm^`wSLKaXg$l>{f`a|j>ExPkmz=vu(7+(On32Ao%&wf%B?PDP-!m=lz{LKx-{(5IJpl1yuGUcz zE}-b6{^O{!r0#;5K-r!;(BXog+w~Z%$|V}55d^b*jw(uxn!_pETkhK1fvw#E!_OPH z*<{xzu7z29X^-OK;=2<%xVI_|=2%a(O5^Jy{uOYn;A~<6RQpT<=NP6R;kU=7_?1hmrYSd!yMuYmE_%7%k~ z8|SYZ;52BzM051hbPp@TpkYSaEOl_6I*>Z1+A0AshYPhTEiX_}zjN9Bu8bJvF#R%l zTvLiIzq)kbA=ujQSQ5J{(oFc5ge^$p`$mrjL=&7!_Tg?E;cF3H9&~*?>VgZ~df-N* z{s6u?IiI`3{V~j6uaNBFE6KT(1;~bJx2TqcnEm_BYdD|T&5ixRg8_v359vhScWuvS z3weAD=f2dLEd3xS?*f`MmvC>zH=S;72`8nO!z||iv|=lrCUP~-A&ED(Hl`5?i}Us6Ny7W-;ymi#cfKFpdq!DtYSh{&GhbEU z4mE5w&m3po_$S!5-Waoj0e2Xf^j#`~*v497ov&UH9lR&_0W||QvF6p~WKMpMLl7+j zP7^G#T7msg7mt7xGJ#$rF!NPcZDE(^;uJufiTjR--wv^%fRU&io3lU!w&QQQu%bVL2ik7gER{Bn&Lr|^M zg9$TKHyDwVgC5>B^q!-=`oyYae>NiyJOIOZsbV$Lj!RO4$YXnb6C4{{b7^zd^^vYH zjw1ErQ2mIgjAPXL`8wIe1L>Ja;mwLnM4$ECZwhipyEv;Zz7c{pw^gh|L<`aOGEc;h z8sC+$oZt@&W*ZdReZI9i##rj5b}WS#U{57-x5*VW_weV9-j-gy&q6nbgReq58PLc~ zYPH#3M-6!Vev2N^s9xu7WPtj|mnen`d$p+dLEGnBdlaY0H=FySnus^Bf$FGV#OZSOTb+ z$;Dzr-PCNjFc(Ux?9)>aAE6<4?0G~_tsB*Lnk`NP)e_^y1RF#$@9|Ou!;lKXyGxhH(-F&u=eWyhbb=yQt~)oe^hCsBnRv)?o9hJf*ugt ze9A?7ht=MZ_miH!6iDfOXPG1NPVpH(AeFw0ZDxHf@H6fwiish$y$Gn9&@t`I zp`i%W@|_0jZe)FYOw=lJH+nZqsw)luO)@W;+sj#7Ga`DA+sJV;a&p{r&Ec^vr@B|7 zgZSjft>bf!OZS+V-arSqH&D-Z%7*Fq98AZ`yKylr?f4f5mjRSQ_2$?+x3L&inQ~Y4 zfTtZ+Z!MggafnU1yZD_w883wSOlDf7|MZuzeeGaUG8$Syau@?ZU<$oXy~IbdZ4lg=?wK$6T}KYBh+?Hh+nZhjT8o-l4|*LVq`a zR4xMszb6NMVC@zwl=EwouXZ##;57?xM9^CZI56OnVJ?*KH-R0oy2G{Yvl@I$HGb!h zp)-1(YFOm!k=dXAg91eYnCdP(S_gj=V8=8RhKotN2RQH2Y!7F862e~rB7z?L{ROHd zJM@)_eZqSVWeU3qqD3|qVZ+g$)gUtbdrr48PZIsDn*!Ukyu^lNuKt0@aR{<75p`s? z3GA52YW^aXJ`i}t9S0y;v>F>XAL}-2BiS25r@x-OFcqAb2)9UKp_R4x68#m~7d*fy>Gzgm;+u5-Q~%9DEsm>b|Xw0-y+=JmwXxPhPZ?mc* zRzu4j52-6f z0Wvs0zx+M6N%v6I>wyVtuDUn7v?t@wm|Zs9>U!+)!-X!oN_zsb&*hVbK3GXjp0j+n zYIS=b+~Q;*z2*kh7P3fQkMUZ_poq)sJ_p!d(mcSuTblzRz^CfVhs?H|b0!K-GquWx z_9lm8=iis_1TZsti3$isEoVS?YN=J?Gr<;2Cbux5OvsPP zGbT0pC1`3x=5WU$?tT|2Q><7OK3BGqNA2{BZIhkS&VM!47x;l0Tk_Oo-wNMj;{Yv} zXux9~bnC#lcxUa-PLTb2S^bbZbOH<7(PU)nmpyzx-Q8P#i=tVK=l7s$MdQ}jQ3+?= zQYcv5hr=ME{LC*s8#9BFz8$l}#`S(a(qIBrN~z(%VET7(ye~DNv69Kyv?=s~{6rHr zGk5sHy(f0g%eLHS0<}Nv8U${CivuEgY@vs#1DU{4oW2t>WRV(lF+$g+Gdn>MSWdPR zkoo{o{J?nG`LjXB^Y?4W^s!a~P2*#nSdSzWv5QD_g}vKhz@_P?3NHifysnYfA~ihl zaAlFN6m|MeKAzH*G-=?Pygfp6Fs1>GFQ+)EI6EN9R5qBQxpWWk-LnTv729AzaubBv z)p`T4w1c|5Fm*Po%?3s z!XMeW?X9;Cm9BIW!>euGRo$hEhI6HoYS#C%K+lh^ESgT=L>JPX;mdF}jU0|pVlrY$ z1Qw+bmjhx}I&&rhu)d3}g~0Ow`M1tX5a;$q>f*So9v2g>na0J8H>xk>Zy|~lbE#cG zaxAK@mpofFEdMj-3}g?3`VXoG^FVwTtV`^l4G#fRM?>~|}ux>lQ$MmdD@zMh4977AX4lI%*e!_btX`0*<^&M8jwAIpE~R~=wiq==zCINeivkc}hvY-4A3 zrM$5DbybIwzZs^3*Mn-N-Nl6kDTeh18PMP=zOkPjVAttqXSqRI7w}S>ibP

r3Hw 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

7LO)^p*; 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 diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json deleted file mode 100644 index 3c7f1cbf1..000000000 --- a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "right_image.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/right_image.pdf b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/right_image.pdf deleted file mode 100644 index bf18d530ee760b9e9df2cc3b63238e98fc1a2391..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 260993 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 TxNSuspRnsER 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

lU;#cc|lb~B8L1%{-_cAU_ku}S3(6+vZG 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 diff --git a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json deleted file mode 100644 index 0fc3cb506..000000000 --- a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_new_group.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "preserves-vector-representation" : true - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/ic_new_group.pdf b/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/ic_new_group.pdf deleted file mode 100644 index a70db86f74b1cf89aba6f0900029833911427ca6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7638 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 ZKkVk_PO`@Lf*>T}Xt02QvZf07{{Te;HERF> -- GitLab From 1bf2509d2e3bebea7dfb4fa25e85446bd5383d3c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 12:59:07 +0200 Subject: [PATCH 033/138] [NY-4699] Remove hardcodes account statuses from profile and chat list. --- .../ChatListMessageCell/Model/ChatListMessageCellModel.swift | 5 ++--- .../Profile/View/DetailsView/ProfileDetailsView.swift | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 8d12b5eed..c5ee537d9 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -43,10 +43,9 @@ final class ChatListMessageCellModel: CellViewModel { // MARK: - Avatar private func setupAvatar(in cell: Cell) { - cell.avatarImageView.imageView + cell.avatarImageView + .imageView .setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) - - cell.avatarImageView.update(model is Contact ? .color(UIColor.nynja.callGreen) : .none) } diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift index cb5b5b71f..3a44d47a9 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift @@ -198,8 +198,6 @@ class ProfileDetailsView: UIView { phoneLabel.isHidden = false walletButton.isHidden = false infoView.isHidden = false - - avatarImageView.update(.color(UIColor.nynja.callGreen)) } // MARK: Recognizer -- GitLab From c4bbaff15b538dc66b69a0433751e4c13ef1ab1f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 14:54:50 +0200 Subject: [PATCH 034/138] [NY-4699] Added TypingDisplayModel. --- Nynja.xcodeproj/project.pbxproj | 8 ++ .../Interactor/MessageInteractor.swift | 6 +- .../Models/Statuses/ActionStatus.swift | 22 ++++- .../Message/Presenter/MessagePresenter.swift | 10 ++- .../Message/Protocols/MessageProtocols.swift | 2 +- Nynja/Statuses/AccountStatus.swift | 2 + Nynja/Statuses/TypingData.swift | 23 ++++++ Nynja/Statuses/TypingDisplayModel.swift | 29 +++++++ Nynja/Statuses/TypingStatusProvider.swift | 81 +++++++++++++++---- 9 files changed, 157 insertions(+), 26 deletions(-) create mode 100644 Nynja/Statuses/TypingData.swift create mode 100644 Nynja/Statuses/TypingDisplayModel.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 6ec362192..28dcfa7e2 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -910,6 +910,8 @@ 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */; }; 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; + 8542B8102188741100A286E5 /* TypingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B80F2188741100A286E5 /* TypingData.swift */; }; + 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B811218879B100A286E5 /* TypingDisplayModel.swift */; }; 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */; }; 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */; }; 85433F24204D596D00B373A7 /* WebFullScreenProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */; }; @@ -3141,6 +3143,8 @@ 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDelegate.swift; sourceTree = ""; }; 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; + 8542B80F2188741100A286E5 /* TypingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingData.swift; sourceTree = ""; }; + 8542B811218879B100A286E5 /* TypingDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingDisplayModel.swift; sourceTree = ""; }; 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenPresenter.swift; sourceTree = ""; }; 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenViewController.swift; sourceTree = ""; }; 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenProtocols.swift; sourceTree = ""; }; @@ -9109,6 +9113,8 @@ 85EB37FE21837304003A2D6F /* AccountStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, + 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, + 8542B80F2188741100A286E5 /* TypingData.swift */, ); path = Statuses; sourceTree = ""; @@ -15339,6 +15345,7 @@ E7E06C681F792B0200BFC8FA /* LoginWheelContainerDataSource.swift in Sources */, 854751492093BDD300F8D5F8 /* CollectionViewScrollProxy.swift in Sources */, 850C301B204DA87A00DB26C2 /* PrivacyListPresenter.swift in Sources */, + 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */, 267BE28E1FDE9FCC00C47E18 /* SettingsGroupWireFrame.swift in Sources */, 85BDD2B821465EFA00695DE5 /* ScrollDirection.swift in Sources */, A4679BA620B2DD0F0021FE9C /* SubscribersSelectorWireFrame.swift in Sources */, @@ -16199,6 +16206,7 @@ 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */, A42D51C7206A361400EEB952 /* timeoutEvent.swift in Sources */, B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */, + 8542B8102188741100A286E5 /* TypingData.swift in Sources */, 267BE2B11FE13AB600C47E18 /* ParticipantsViewController.swift in Sources */, 8503B527205046A6006F0593 /* NotificationSettingsProtocols.swift in Sources */, FDECF3B609DB36ABEED91F70 /* EditUsernamePresenter.swift in Sources */, diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 5e276b318..cbd024f04 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -189,12 +189,12 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H mqttService.addSubscriber(self) if let chatId = chat.id { - typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingStatus in + typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in guard let `self` = self else { return } - self.presenter?.actionStatusChanged(typingStatus) + self.presenter?.didReceiveTyping(typingInfo) - if case .done = typingStatus { + if case .done = typingInfo { return } dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in diff --git a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift index 5c3c25c45..85e25e835 100644 --- a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift @@ -12,6 +12,27 @@ enum ActionStatus { case sending(SendingStatus) case recording(RecordingStatus) + var isTyping: Bool { + if case .typing = self { + return true + } + return false + } + + var isSendingFile: Bool { + if case .sending = self { + return true + } + return false + } + + var isRecording: Bool { + if case .recording = self { + return true + } + return false + } + var title: String { switch self { case .done: @@ -47,5 +68,4 @@ enum ActionStatus { self = .recording(.voice) } } - } diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 4e558edb5..a5d12add6 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -939,11 +939,13 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract lastStatus = status.title } - func actionStatusChanged(_ status: ActionStatus) { - if case .done = status { + func didReceiveTyping(_ typing: TypingDisplayModel) { + switch typing { + case .done: restoreStatus() - } else { - view.updateHeaderStatus(status.title) + case let .typing(sender, status): + let displayString = sender.displayName.flatMap { "\($0) \(status.title)" } ?? status.title + view.updateHeaderStatus(displayString) } } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 5258417d6..6e4d7e5ea 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -151,7 +151,7 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func internetStatusChanged(_ status: InternetStatus) func presenceStatusChanged(_ status: PresenceStatus) - func actionStatusChanged(_ status: ActionStatus) + func didReceiveTyping(_ typing: TypingDisplayModel) func restoreStatus() func messageSent(_ localId: MessageLocalId) diff --git a/Nynja/Statuses/AccountStatus.swift b/Nynja/Statuses/AccountStatus.swift index f9f0890d4..2a1bfe2f6 100644 --- a/Nynja/Statuses/AccountStatus.swift +++ b/Nynja/Statuses/AccountStatus.swift @@ -8,6 +8,8 @@ typealias AccountId = String +typealias FeedId = String + enum AccountStatus { case active case inactive diff --git a/Nynja/Statuses/TypingData.swift b/Nynja/Statuses/TypingData.swift new file mode 100644 index 000000000..fc4d1b803 --- /dev/null +++ b/Nynja/Statuses/TypingData.swift @@ -0,0 +1,23 @@ +// +// TypingData.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct TypingData { + let feedId: String + let displayInfo: DisplayInfo + + struct Sender { + let senderId: String + let senderName: String? + let status: ActionStatus + } + + enum DisplayInfo { + case p2p(Sender) + case room([Sender]) + } +} diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift new file mode 100644 index 000000000..bed92f469 --- /dev/null +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -0,0 +1,29 @@ +// +// TypingDisplayModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum TypingDisplayModel { + case typing(Sender, ActionStatus) + case done + + enum Sender { + case name(String) + case names([String]) + case none + + var displayName: String? { + switch self { + case let .name(name): + return name + case let .names(names): + return names.joined(separator: ", ") + case .none: + return nil + } + } + } +} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index ecf1c9a46..db0701060 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -9,25 +9,24 @@ import Foundation protocol TypingStatusObservable: class { - typealias Callback = (AccountId, ActionStatus) -> Void + typealias Callback = (FeedId, TypingDisplayModel) -> Void func addObserver(_ observer: AnyObject, callback: @escaping Callback) - func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: FeedId, callback: @escaping Callback) func removeObserver(_ observer: AnyObject) - func removeObserver(_ observer: AnyObject, for key: AccountId) - func notify(_ key: AccountId, with value: ActionStatus) + func removeObserver(_ observer: AnyObject, for key: FeedId) + func notify(_ key: FeedId, with value: TypingDisplayModel) } protocol TypingStatusProvider: TypingStatusObservable { - func status(for accountId: AccountId) -> ActionStatus - func update(_ status: ActionStatus, for accountId: AccountId) + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { - private var data: [AccountId: ActionStatus] = [:] + private var data: [FeedId: TypingData] = [:] - private(set) var observable = KeyedObservable() + private(set) var observable = KeyedObservable() // MARK: - Dependencies @@ -51,23 +50,71 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - TypingStatusProvider - func status(for accountId: AccountId) -> ActionStatus { - return data[accountId] ?? .done - } - - func update(_ status: ActionStatus, for accountId: AccountId) { - data[accountId] = status - observable.notify(accountId, with: status) + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { + return data[feedId].flatMap { displayInfo(for: $0) } } // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { - guard let accountId = typing.phone_id, let typingType = typing.type else { + guard let feedId = typing.phone_id, let typingType = typing.type else { return } let status = ActionStatus(typingModelType: typingType) - notify(accountId, with: status) + + // FIXME: implement logic for rooms. + let senderInfo = TypingData.Sender(senderId: feedId, senderName: nil, status: status) + + let data = TypingData(feedId: feedId, displayInfo: .p2p(senderInfo)) + + update(data) + } + + private func update(_ typingData: TypingData) { + let feedId = typingData.feedId + + guard let oldTyping = data[feedId] else { + save(typingData) + return + } + switch (oldTyping.displayInfo, typingData.displayInfo) { + case (.p2p, .p2p): + save(typingData) + + case let (.room(oldDisplayInfo), .room(newDisplayInfo)): + var displayInfo = oldDisplayInfo + displayInfo.append(contentsOf: newDisplayInfo) + + let newTypingModel = TypingData(feedId: feedId, displayInfo: .room(displayInfo)) + save(newTypingModel) + + default: + break + } + } + + private func save(_ typingData: TypingData) { + data[typingData.feedId] = typingData + observable.notify(typingData.feedId, with: displayInfo(for: typingData)) + } + + private func displayInfo(for typing: TypingData) -> TypingDisplayModel { + switch typing.displayInfo { + case let .p2p(senderInfo): + let sender: TypingDisplayModel.Sender = senderInfo.senderName.flatMap { .name($0) } ?? .none + return .typing(sender, senderInfo.status) + + case let .room(senderArrayInfo): + guard let lastStatus = senderArrayInfo.last?.status else { + break + } + let senders: TypingDisplayModel.Sender = .names(senderArrayInfo.compactMap { + // FIXME: + $0.senderName + }) + return .typing(senders, lastStatus) + } + return .done } } -- GitLab From 411a0533f53f375945b56a1d9bf0cba0e47409db Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 16:39:38 +0200 Subject: [PATCH 035/138] [NY-4699] Conform ActionStatus to Equatable. --- Nynja/Modules/Message/Models/Statuses/ActionStatus.swift | 2 +- Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift | 2 +- Nynja/Modules/Message/Models/Statuses/SendingStatus.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift index 85e25e835..0f0a4e9c0 100644 --- a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum ActionStatus { +enum ActionStatus: Equatable { case done case typing case sending(SendingStatus) diff --git a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift b/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift index bfb75367e..a65d33387 100644 --- a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum RecordingStatus: String { +enum RecordingStatus: String, Equatable { case video = "video" case voice = "voice_message" diff --git a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift b/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift index 7ab436017..3a5ad85bc 100644 --- a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum SendingStatus: String { +enum SendingStatus: String, Equatable { case file = "file" case video = "video" case voice = "voice_message" -- GitLab From 525fb290ad9f84ee35728d0ac49e471a69e8348c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 16:46:21 +0200 Subject: [PATCH 036/138] [NY-4699] Prepare display model for rooms --- .../Message/Presenter/MessagePresenter.swift | 2 +- Nynja/Statuses/TypingData.swift | 6 ++-- Nynja/Statuses/TypingDisplayModel.swift | 29 +++++++++++++------ Nynja/Statuses/TypingStatusProvider.swift | 24 +++++++++------ 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index a5d12add6..77c263d7e 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -944,7 +944,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract case .done: restoreStatus() case let .typing(sender, status): - let displayString = sender.displayName.flatMap { "\($0) \(status.title)" } ?? status.title + let displayString = sender?.displayName.flatMap { "\($0) \(status.title)" } ?? status.title view.updateHeaderStatus(displayString) } } diff --git a/Nynja/Statuses/TypingData.swift b/Nynja/Statuses/TypingData.swift index fc4d1b803..3fc7a2a9f 100644 --- a/Nynja/Statuses/TypingData.swift +++ b/Nynja/Statuses/TypingData.swift @@ -10,14 +10,14 @@ struct TypingData { let feedId: String let displayInfo: DisplayInfo - struct Sender { + struct SenderInfo { let senderId: String let senderName: String? let status: ActionStatus } enum DisplayInfo { - case p2p(Sender) - case room([Sender]) + case p2p(SenderInfo) + case room([SenderInfo]) } } diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index bed92f469..cb9155c5e 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -7,23 +7,34 @@ // enum TypingDisplayModel { - case typing(Sender, ActionStatus) + case typing(SenderInfo?, ActionStatus) case done - enum Sender { - case name(String) - case names([String]) - case none + enum SenderInfo { + case p2p(String) + case room([String]) var displayName: String? { switch self { - case let .name(name): + case let .p2p(name): return name - case let .names(names): + case let .room(names): return names.joined(separator: ", ") - case .none: - return nil } } } + + var displayName: String? { + guard case let .typing(senderInfo, _) = self else { + return nil + } + return senderInfo?.displayName + } + + var status: ActionStatus? { + guard case let .typing(_, status) = self else { + return nil + } + return status + } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index db0701060..8e2dd82da 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -64,7 +64,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let status = ActionStatus(typingModelType: typingType) // FIXME: implement logic for rooms. - let senderInfo = TypingData.Sender(senderId: feedId, senderName: nil, status: status) + let senderInfo = TypingData.SenderInfo(senderId: feedId, senderName: nil, status: status) let data = TypingData(feedId: feedId, displayInfo: .p2p(senderInfo)) @@ -84,9 +84,13 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta case let (.room(oldDisplayInfo), .room(newDisplayInfo)): var displayInfo = oldDisplayInfo + displayInfo.removeAll { sender in + newDisplayInfo.contains { $0.senderId == sender.senderId } + } displayInfo.append(contentsOf: newDisplayInfo) let newTypingModel = TypingData(feedId: feedId, displayInfo: .room(displayInfo)) + save(newTypingModel) default: @@ -102,19 +106,21 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private func displayInfo(for typing: TypingData) -> TypingDisplayModel { switch typing.displayInfo { case let .p2p(senderInfo): - let sender: TypingDisplayModel.Sender = senderInfo.senderName.flatMap { .name($0) } ?? .none - return .typing(sender, senderInfo.status) + return .typing(senderInfo.senderName.flatMap { .p2p($0) }, senderInfo.status) case let .room(senderArrayInfo): - guard let lastStatus = senderArrayInfo.last?.status else { + guard !senderArrayInfo.isEmpty, let lastStatus = senderArrayInfo.last?.status else { break } - let senders: TypingDisplayModel.Sender = .names(senderArrayInfo.compactMap { - // FIXME: - $0.senderName - }) - return .typing(senders, lastStatus) + let members: [String] = senderArrayInfo.compactMap { + guard lastStatus == $0.status else { + return nil + } + return $0.senderName + } + return .typing(.room(members), lastStatus) } + return .done } } -- GitLab From 6a47735f38ffeb3b72546a99fe3d2dd706b0a5d9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 17:38:02 +0200 Subject: [PATCH 037/138] [NY-4699] Refactored TypingStatusProvider --- Nynja.xcodeproj/project.pbxproj | 4 - Nynja/Statuses/TypingData.swift | 23 ---- Nynja/Statuses/TypingDisplayModel.swift | 12 +-- Nynja/Statuses/TypingStatusProvider.swift | 125 +++++++++++++++------- 4 files changed, 89 insertions(+), 75 deletions(-) delete mode 100644 Nynja/Statuses/TypingData.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 28dcfa7e2..63e89967f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -910,7 +910,6 @@ 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */; }; 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; - 8542B8102188741100A286E5 /* TypingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B80F2188741100A286E5 /* TypingData.swift */; }; 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B811218879B100A286E5 /* TypingDisplayModel.swift */; }; 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */; }; 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */; }; @@ -3143,7 +3142,6 @@ 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDelegate.swift; sourceTree = ""; }; 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; - 8542B80F2188741100A286E5 /* TypingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingData.swift; sourceTree = ""; }; 8542B811218879B100A286E5 /* TypingDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingDisplayModel.swift; sourceTree = ""; }; 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenPresenter.swift; sourceTree = ""; }; 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenViewController.swift; sourceTree = ""; }; @@ -9114,7 +9112,6 @@ 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, - 8542B80F2188741100A286E5 /* TypingData.swift */, ); path = Statuses; sourceTree = ""; @@ -16206,7 +16203,6 @@ 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */, A42D51C7206A361400EEB952 /* timeoutEvent.swift in Sources */, B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */, - 8542B8102188741100A286E5 /* TypingData.swift in Sources */, 267BE2B11FE13AB600C47E18 /* ParticipantsViewController.swift in Sources */, 8503B527205046A6006F0593 /* NotificationSettingsProtocols.swift in Sources */, FDECF3B609DB36ABEED91F70 /* EditUsernamePresenter.swift in Sources */, diff --git a/Nynja/Statuses/TypingData.swift b/Nynja/Statuses/TypingData.swift deleted file mode 100644 index 3fc7a2a9f..000000000 --- a/Nynja/Statuses/TypingData.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// TypingData.swift -// Nynja -// -// Created by Anton Poltoratskyi on 30.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -struct TypingData { - let feedId: String - let displayInfo: DisplayInfo - - struct SenderInfo { - let senderId: String - let senderName: String? - let status: ActionStatus - } - - enum DisplayInfo { - case p2p(SenderInfo) - case room([SenderInfo]) - } -} diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index cb9155c5e..24e962cf4 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -10,17 +10,11 @@ enum TypingDisplayModel { case typing(SenderInfo?, ActionStatus) case done - enum SenderInfo { - case p2p(String) - case room([String]) + struct SenderInfo { + var senders: [String] var displayName: String? { - switch self { - case let .p2p(name): - return name - case let .room(names): - return names.joined(separator: ", ") - } + return senders.joined(separator: ", ") } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 8e2dd82da..5d2404131 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -23,9 +23,11 @@ protocol TypingStatusProvider: TypingStatusObservable { } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { - + private var data: [FeedId: TypingData] = [:] + private var workItems: [FeedId: DispatchWorkItem] = [:] + private(set) var observable = KeyedObservable() @@ -64,63 +66,108 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let status = ActionStatus(typingModelType: typingType) // FIXME: implement logic for rooms. - let senderInfo = TypingData.SenderInfo(senderId: feedId, senderName: nil, status: status) + let senderInfo = TypingInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) - let data = TypingData(feedId: feedId, displayInfo: .p2p(senderInfo)) - - update(data) + update(with: senderInfo) } - private func update(_ typingData: TypingData) { - let feedId = typingData.feedId + private func update(with typingInfo: TypingInfo) { + let feedId = typingInfo.feed.identifier guard let oldTyping = data[feedId] else { - save(typingData) + // Save and notify if didn't exists any other typing status for this feed + switch typingInfo.feed { + case .p2p: + save(.p2p(typingInfo), for: feedId) + case .room: + save(.room([typingInfo]), for: feedId) + } return } - switch (oldTyping.displayInfo, typingData.displayInfo) { - case (.p2p, .p2p): - save(typingData) - - case let (.room(oldDisplayInfo), .room(newDisplayInfo)): - var displayInfo = oldDisplayInfo - displayInfo.removeAll { sender in - newDisplayInfo.contains { $0.senderId == sender.senderId } - } - displayInfo.append(contentsOf: newDisplayInfo) - - let newTypingModel = TypingData(feedId: feedId, displayInfo: .room(displayInfo)) + + switch oldTyping { + case .p2p: + save(.p2p(typingInfo), for: feedId) + case let .room(oldTypingSendersInfo): + var newTypingInfo = oldTypingSendersInfo - save(newTypingModel) + newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } + newTypingInfo.append(typingInfo) - default: - break + save(.room(newTypingInfo), for: feedId) } } - private func save(_ typingData: TypingData) { - data[typingData.feedId] = typingData - observable.notify(typingData.feedId, with: displayInfo(for: typingData)) + private func save(_ typing: TypingData, for feedId: FeedId) { + let workItem = DispatchWorkItem { + + } + data[feedId] = typing + observable.notify(feedId, with: displayInfo(for: typing)) } private func displayInfo(for typing: TypingData) -> TypingDisplayModel { - switch typing.displayInfo { - case let .p2p(senderInfo): - return .typing(senderInfo.senderName.flatMap { .p2p($0) }, senderInfo.status) - - case let .room(senderArrayInfo): - guard !senderArrayInfo.isEmpty, let lastStatus = senderArrayInfo.last?.status else { - break + let typingInfo = typing.senders + + guard !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { + return .done + } + let senders: [String] = typingInfo.compactMap { + guard lastStatus == $0.status else { + return nil + } + return $0.senderName + } + let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) + + return .typing(senderInfo, lastStatus) + } +} + + +// MARK: - Private Data Types + +private extension TypingStatusProviderImpl { + + enum TypingData { + case p2p(TypingInfo) + case room([TypingInfo]) + + var senders: [TypingInfo] { + switch self { + case let .p2p(sender): + return [sender] + case let .room(senders): + return senders } - let members: [String] = senderArrayInfo.compactMap { - guard lastStatus == $0.status else { - return nil + } + } + + final class TypingInfo { + enum Feed { + case p2p(FeedId) + case room(FeedId) + + var identifier: String { + switch self { + case let .p2p(id): + return id + case let .room(id): + return id } - return $0.senderName } - return .typing(.room(members), lastStatus) } - return .done + let feed: Feed + let senderId: String + let senderName: String? + let status: ActionStatus + + init(feed: Feed, senderId: String, senderName: String?, status: ActionStatus) { + self.feed = feed + self.senderId = senderId + self.senderName = senderName + self.status = status + } } } -- GitLab From 1a56ee869f97fc142d3ec97a2e67a79caf89a052 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 17:59:10 +0200 Subject: [PATCH 038/138] [NY-4699] Implemented restoring status in TypingStatusProvider. --- .../Interactor/MessageInteractor.swift | 11 +------- Nynja/Statuses/TypingStatusProvider.swift | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index cbd024f04..b200a435b 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -190,16 +190,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H if let chatId = chat.id { typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in - guard let `self` = self else { return } - - self.presenter?.didReceiveTyping(typingInfo) - - if case .done = typingInfo { - return - } - dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in - self?.presenter?.restoreStatus() - } + self?.presenter?.didReceiveTyping(typingInfo) } } ConnectionService.shared.addSubscriber(self) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 5d2404131..16472906e 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -98,18 +98,29 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } } - private func save(_ typing: TypingData, for feedId: FeedId) { + private func save(_ typing: TypingData?, for feedId: FeedId) { + data[feedId] = typing + + let displayInfo = self.displayInfo(for: typing) + observable.notify(feedId, with: displayInfo) + + workItems[feedId]?.cancel() + + if case .done = displayInfo { + return + } + let workItem = DispatchWorkItem { - + self.data[feedId] = nil + self.save(nil, for: feedId) } - data[feedId] = typing - observable.notify(feedId, with: displayInfo(for: typing)) + + workItems[feedId] = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: workItem) } - private func displayInfo(for typing: TypingData) -> TypingDisplayModel { - let typingInfo = typing.senders - - guard !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { + private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { + guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { return .done } let senders: [String] = typingInfo.compactMap { -- GitLab From e4aed91335f1d397c0ea8db49753e498a72a84a2 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 18:41:16 +0200 Subject: [PATCH 039/138] [NY-4699] Implemented dismiss work item for rooms. --- Nynja/Statuses/TypingStatusProvider.swift | 72 +++++++++++++---------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 16472906e..1f2defe9a 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -28,7 +28,9 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private var workItems: [FeedId: DispatchWorkItem] = [:] - private(set) var observable = KeyedObservable() + let observable = KeyedObservable() + + private let typingDismissInterval = 10.0 // MARK: - Dependencies @@ -67,56 +69,66 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // FIXME: implement logic for rooms. let senderInfo = TypingInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) - - update(with: senderInfo) + save(senderInfo) } - private func update(with typingInfo: TypingInfo) { + private func save(_ typingInfo: TypingInfo) { let feedId = typingInfo.feed.identifier - - guard let oldTyping = data[feedId] else { + + if let oldTyping = data[feedId] { + switch oldTyping { + case .p2p: + update(.p2p(typingInfo), for: feedId) + + case let .room(oldTypingSendersInfo): + var newTypingInfo = oldTypingSendersInfo + + newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } + newTypingInfo.append(typingInfo) + + update(.room(newTypingInfo), for: feedId) + } + } else { // Save and notify if didn't exists any other typing status for this feed switch typingInfo.feed { case .p2p: - save(.p2p(typingInfo), for: feedId) + update(.p2p(typingInfo), for: feedId) case .room: - save(.room([typingInfo]), for: feedId) + update(.room([typingInfo]), for: feedId) } - return } - switch oldTyping { - case .p2p: - save(.p2p(typingInfo), for: feedId) - case let .room(oldTypingSendersInfo): - var newTypingInfo = oldTypingSendersInfo - - newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } - newTypingInfo.append(typingInfo) - - save(.room(newTypingInfo), for: feedId) - } + dismiss(typingInfo, after: typingDismissInterval, for: feedId) } - private func save(_ typing: TypingData?, for feedId: FeedId) { + private func update(_ typing: TypingData?, for feedId: FeedId) { data[feedId] = typing let displayInfo = self.displayInfo(for: typing) observable.notify(feedId, with: displayInfo) - + } + + private func dismiss(_ typing: TypingInfo, after delay: TimeInterval, for feedId: FeedId) { workItems[feedId]?.cancel() - if case .done = displayInfo { - return - } - let workItem = DispatchWorkItem { - self.data[feedId] = nil - self.save(nil, for: feedId) + guard let currentTyping = self.data[feedId] else { + return + } + switch currentTyping { + case let .p2p(info) where info === typing: + self.update(nil, for: feedId) + + case let .room(info): + self.update(.room(info.filter { $0 !== typing }), for: feedId) + + default: + break + } } workItems[feedId] = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: workItem) + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { @@ -136,7 +148,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } -// MARK: - Private Data Types +// MARK: - Inner Types private extension TypingStatusProviderImpl { -- GitLab From c0244f907c07928e22fca41d7330059ce5e8c614 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 19:04:25 +0200 Subject: [PATCH 040/138] [NY-4699] Make TypingStatusProvider asynchronous. --- .../Interactor/MessageInteractor.swift | 2 +- Nynja/Statuses/TypingStatusProvider.swift | 87 ++++++++++--------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index b200a435b..50578f17a 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -178,7 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingStatusProvider = TypingStatusProviderImpl(dependencies: TypingHandler.shared) + typingStatusProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) super.init() diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 1f2defe9a..16d6666a9 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -19,23 +19,30 @@ protocol TypingStatusObservable: class { } protocol TypingStatusProvider: TypingStatusObservable { - func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { + let observable = KeyedObservable() + private var data: [FeedId: TypingData] = [:] private var workItems: [FeedId: DispatchWorkItem] = [:] - let observable = KeyedObservable() - private let typingDismissInterval = 10.0 + private let processingQueue: DispatchQueue + + private let notifyQueue: DispatchQueue + // MARK: - Dependencies - typealias Dependencies = TypingHandler + struct Dependencies { + let typingHandler: TypingHandler + let processingQueue = DispatchQueue.global(qos: .default) + let notifyQueue = DispatchQueue.main + } private let typingHandler: TypingHandler @@ -43,7 +50,9 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - Init init(dependencies: Dependencies) { - typingHandler = dependencies + typingHandler = dependencies.typingHandler + processingQueue = dependencies.processingQueue + notifyQueue = dependencies.notifyQueue typingHandler.addObserver(self) } @@ -52,13 +61,6 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } - // MARK: - TypingStatusProvider - - func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { - return data[feedId].flatMap { displayInfo(for: $0) } - } - - // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { @@ -68,11 +70,14 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let status = ActionStatus(typingModelType: typingType) // FIXME: implement logic for rooms. - let senderInfo = TypingInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) - save(senderInfo) + let senderInfo = SenderInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) + + processingQueue.async { + self.save(senderInfo) + } } - private func save(_ typingInfo: TypingInfo) { + private func save(_ typingInfo: SenderInfo) { let feedId = typingInfo.feed.identifier if let oldTyping = data[feedId] { @@ -101,34 +106,38 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta dismiss(typingInfo, after: typingDismissInterval, for: feedId) } + private func dismiss(_ typing: SenderInfo, after delay: TimeInterval, for feedId: FeedId) { + workItems[feedId]?.cancel() + + let workItem = DispatchWorkItem { + self.remove(typing, for: feedId) + } + workItems[feedId] = workItem + + processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) + } + private func update(_ typing: TypingData?, for feedId: FeedId) { data[feedId] = typing let displayInfo = self.displayInfo(for: typing) - observable.notify(feedId, with: displayInfo) + notifyQueue.async { + self.observable.notify(feedId, with: displayInfo) + } } - private func dismiss(_ typing: TypingInfo, after delay: TimeInterval, for feedId: FeedId) { - workItems[feedId]?.cancel() - - let workItem = DispatchWorkItem { - guard let currentTyping = self.data[feedId] else { - return - } - switch currentTyping { - case let .p2p(info) where info === typing: - self.update(nil, for: feedId) - - case let .room(info): - self.update(.room(info.filter { $0 !== typing }), for: feedId) - - default: - break + private func remove(_ typing: SenderInfo, for feedId: FeedId) { + guard let currentTyping = data[feedId] else { + return + } + switch currentTyping { + case let .p2p(info): + if info === typing { + update(nil, for: feedId) } + case let .room(info): + update(.room(info.filter { $0 !== typing }), for: feedId) } - - workItems[feedId] = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { @@ -153,10 +162,10 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private extension TypingStatusProviderImpl { enum TypingData { - case p2p(TypingInfo) - case room([TypingInfo]) + case p2p(SenderInfo) + case room([SenderInfo]) - var senders: [TypingInfo] { + var senders: [SenderInfo] { switch self { case let .p2p(sender): return [sender] @@ -166,7 +175,7 @@ private extension TypingStatusProviderImpl { } } - final class TypingInfo { + final class SenderInfo { enum Feed { case p2p(FeedId) case room(FeedId) -- GitLab From 60d93a21c7a1f08ef04fec9510b80f8bfda8f56f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 19:06:54 +0200 Subject: [PATCH 041/138] [NY-4699] Update MessageProtocols. --- Nynja/Modules/Message/Presenter/MessagePresenter.swift | 2 +- Nynja/Modules/Message/Protocols/MessageProtocols.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 77c263d7e..fc4d0a0ce 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -949,7 +949,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } } - func restoreStatus() { + private func restoreStatus() { view.updateHeaderStatus(lastStatus) } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 6e4d7e5ea..89a876acc 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -152,7 +152,6 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func internetStatusChanged(_ status: InternetStatus) func presenceStatusChanged(_ status: PresenceStatus) func didReceiveTyping(_ typing: TypingDisplayModel) - func restoreStatus() func messageSent(_ localId: MessageLocalId) func messageRead(_ localId: MessageLocalId) -- GitLab From 953163d554a6cf3c1c323b135bbfb87e8c26b549 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 10:53:11 +0200 Subject: [PATCH 042/138] [NY-4699] Implemented typing logic in chat header. --- Nynja.xcodeproj/project.pbxproj | 42 ++++++++-- Nynja/Generated/LocalizableConstants.swift | 6 +- .../Message/Models/Statuses/ChatStatus.swift | 13 +++ .../Statuses/ChatStatusDisplayInfo.swift | 12 +++ .../{ => Internet}/InternetStatus.swift | 0 .../{ => Presence}/PresenceStatus.swift | 0 .../Statuses/{ => Typing}/ActionStatus.swift | 0 .../{ => Typing}/RecordingStatus.swift | 0 .../Statuses/{ => Typing}/SendingStatus.swift | 0 .../Message/Presenter/MessagePresenter.swift | 84 +++++++++++-------- .../Message/Protocols/MessageProtocols.swift | 2 +- Nynja/Modules/Message/View/MessageVC.swift | 2 +- .../View/Views/AvatarView/AvatarView.swift | 39 ++++++--- Nynja/Resources/en.lproj/Localizable.strings | 6 +- Nynja/Statuses/TypingDisplayModel.swift | 2 +- Nynja/Statuses/TypingStatusProvider.swift | 2 +- 16 files changed, 146 insertions(+), 64 deletions(-) create mode 100644 Nynja/Modules/Message/Models/Statuses/ChatStatus.swift create mode 100644 Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift rename Nynja/Modules/Message/Models/Statuses/{ => Internet}/InternetStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Presence}/PresenceStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/ActionStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/RecordingStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/SendingStatus.swift (100%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 63e89967f..95085d324 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -984,6 +984,8 @@ 855EF421202CC6F800541BE3 /* GetExtendedStarsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */; }; 855EF423202CC85300541BE3 /* MQTTServiceStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */; }; 855EF425202CCADB00541BE3 /* ExtendedStarHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */; }; + 8560C4C6218997DD006635AE /* ChatStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8560C4C5218997DD006635AE /* ChatStatus.swift */; }; + 8560C4C8218999E3006635AE /* ChatStatusDisplayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */; }; 8562853220D140FC000C9739 /* InputBar+ButtonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853120D140FC000C9739 /* InputBar+ButtonType.swift */; }; 8562853420D16242000C9739 /* StickerPreviewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853320D16242000C9739 /* StickerPreviewing.swift */; }; 8562853620D164B5000C9739 /* ScaleAnimatableGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */; }; @@ -3193,6 +3195,8 @@ 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExtendedStarsModel.swift; sourceTree = ""; }; 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTServiceStars.swift; sourceTree = ""; }; 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedStarHandler.swift; sourceTree = ""; }; + 8560C4C5218997DD006635AE /* ChatStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStatus.swift; sourceTree = ""; }; + 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStatusDisplayInfo.swift; sourceTree = ""; }; 8562853120D140FC000C9739 /* InputBar+ButtonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InputBar+ButtonType.swift"; sourceTree = ""; }; 8562853320D16242000C9739 /* StickerPreviewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewing.swift; sourceTree = ""; }; 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleAnimatableGrid.swift; sourceTree = ""; }; @@ -8545,6 +8549,32 @@ path = CollectionView; sourceTree = ""; }; + 8560C4C221899793006635AE /* Typing */ = { + isa = PBXGroup; + children = ( + A45F10BE20B4218D00F45004 /* ActionStatus.swift */, + A45F10BC20B4218D00F45004 /* RecordingStatus.swift */, + A45F10BF20B4218D00F45004 /* SendingStatus.swift */, + ); + path = Typing; + sourceTree = ""; + }; + 8560C4C32189979B006635AE /* Internet */ = { + isa = PBXGroup; + children = ( + A45F10BD20B4218D00F45004 /* InternetStatus.swift */, + ); + path = Internet; + sourceTree = ""; + }; + 8560C4C4218997A4006635AE /* Presence */ = { + isa = PBXGroup; + children = ( + A45F10BB20B4218D00F45004 /* PresenceStatus.swift */, + ); + path = Presence; + sourceTree = ""; + }; 8562853720D164BE000C9739 /* ScaleAnimation */ = { isa = PBXGroup; children = ( @@ -10262,11 +10292,11 @@ A45F10BA20B4218D00F45004 /* Statuses */ = { isa = PBXGroup; children = ( - A45F10BB20B4218D00F45004 /* PresenceStatus.swift */, - A45F10BC20B4218D00F45004 /* RecordingStatus.swift */, - A45F10BD20B4218D00F45004 /* InternetStatus.swift */, - A45F10BE20B4218D00F45004 /* ActionStatus.swift */, - A45F10BF20B4218D00F45004 /* SendingStatus.swift */, + 8560C4C5218997DD006635AE /* ChatStatus.swift */, + 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */, + 8560C4C4218997A4006635AE /* Presence */, + 8560C4C32189979B006635AE /* Internet */, + 8560C4C221899793006635AE /* Typing */, ); path = Statuses; sourceTree = ""; @@ -14752,6 +14782,7 @@ F119E66E20D24BBF0043A532 /* MultiplePreviewWireframe.swift in Sources */, 2648C40F2069B52100863614 /* ChangeNumberStep3Presenter.swift in Sources */, A42D51A0206A361400EEB952 /* reader.swift in Sources */, + 8560C4C8218999E3006635AE /* ChatStatusDisplayInfo.swift in Sources */, A42D52BB206A53AA00EEB952 /* Vox_Spec.swift in Sources */, A418DA3420ED0D1300FE780B /* CountView.swift in Sources */, 4B8FC3082163ABC300602D6B /* Desc+Sticker.swift in Sources */, @@ -15362,6 +15393,7 @@ 8580BADA20BD98E700239D9D /* ChatListMessageContentView.swift in Sources */, E757B53D1FE9225C00467BA2 /* TypingExtension.swift in Sources */, C940514C204C7FAF00D72B04 /* DataAndStorageInteractor.swift in Sources */, + 8560C4C6218997DD006635AE /* ChatStatus.swift in Sources */, F1A9FA3590CC1F834B727955 /* AddContactPresenter.swift in Sources */, 6DD72F601F1547AC008CFF83 /* GCD.swift in Sources */, A49CC1D820E4AB2C00879D41 /* DisplayModeConfigFactory.swift in Sources */, diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index ed092328b..2fb3716d7 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -616,11 +616,11 @@ internal extension String { static var messageDelay: String { return localizable.tr("Localizable", "message_delay") } /// New messages static var messageNewMessages: String { return localizable.tr("Localizable", "message_new_messages") } - /// ...sending a + /// sending static var messageSending: String { return localizable.tr("Localizable", "message_sending") } /// edited static var messageStatusEdited: String { return localizable.tr("Localizable", "message_status_edited") } - /// ...typing + /// typing static var messageStatusTyping: String { return localizable.tr("Localizable", "message_status_typing") } /// meters static var meters: String { return localizable.tr("Localizable", "meters") } @@ -802,7 +802,7 @@ internal extension String { static var questionEndCall: String { return localizable.tr("Localizable", "question_end_call") } /// Are you sure you want to leave the call? static var questionEndCallP2p: String { return localizable.tr("Localizable", "question_end_call_p2p") } - /// ...recording a + /// recording static var recording: String { return localizable.tr("Localizable", "recording") } /// Remove static var remove: String { return localizable.tr("Localizable", "remove") } diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift new file mode 100644 index 000000000..1cedf26b2 --- /dev/null +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift @@ -0,0 +1,13 @@ +// +// ChatStatus.swift +// Nynja +// +// Created by Anton Poltoratskyi on 31.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct ChatStatus { + var presence: PresenceStatus? + var internet: InternetStatus + var typing: TypingDisplayModel +} diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift new file mode 100644 index 000000000..dd1a460ba --- /dev/null +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift @@ -0,0 +1,12 @@ +// +// ChatStatusDisplayInfo.swift +// Nynja +// +// Created by Anton Poltoratskyi on 31.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum ChatStatusDisplayInfo { + case text(String) + case typing(TypingDisplayModel.SenderInfo?, ActionStatus) +} diff --git a/Nynja/Modules/Message/Models/Statuses/InternetStatus.swift b/Nynja/Modules/Message/Models/Statuses/Internet/InternetStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/InternetStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Internet/InternetStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/PresenceStatus.swift b/Nynja/Modules/Message/Models/Statuses/Presence/PresenceStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/PresenceStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Presence/PresenceStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/ActionStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/SendingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index fc4d0a0ce..836148571 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -10,23 +10,19 @@ import UIKit import CoreLocation.CLLocation -class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol, ForwardSelectorDelegate { +final class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol, ForwardSelectorDelegate { + + // MARK: - Properties - //MARK: - Properties var isMyselfChat: Bool { return interactor.isMyselfChat } - private var lastStatus: String = "" { - didSet { updateStatus(lastStatus) } - } - - private var internetStatus: InternetStatus = .connected private var isActionsEnabled: Bool = true private var wasViewDisappeared: Bool = false private let chatScreenAlertFactory: ChatScreenAlertFactoryProtocol = ChatScreenAlertFactory() - // -- unread mention counter + // MARK: Unread mention counter var uniqueUnreadMentionIds: Set = [] var unreadMentionIds: [MessageServerId] = [] @@ -39,10 +35,44 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } } } + + // MARK: Header Status + + private var headerStatus = ChatStatus(presence: nil, internet: .connected, typing: .none) { + didSet { + let internetStatus = headerStatus.internet + let presenceStatus = headerStatus.presence + let typing = headerStatus.typing + + switch internetStatus { + case .waiting, .connecting: + // Always show internet status when not connected + view?.updateHeaderStatus(.text(internetStatus.rawValue.localized)) + + case .connected: + // Show typing if exists, otherwise - show presence + + switch typing { + case let .typing(sender, status): + if case .done = status { + fallthrough + } + view.updateHeaderStatus(.typing(sender, status)) + + case .none: + if let presence = presenceStatus { + view?.updateHeaderStatus(.text(presence.title)) + } else { + view.updateHeaderStatus(.text(internetStatus.rawValue.localized)) + } + } + } + } + } + - // -- - - //MARK: - BasePresenter + // MARK: - BasePresenter + override var itemsFactory: WCItemsFactory? { if isMyselfChat { return MySelfItemsFactory(isActionsEnabled: true) @@ -75,7 +105,8 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } - //MARK: - MessagePresenterProtocol + // MARK: - MessagePresenterProtocol + weak var view: MessageViewProtocol! var wireFrame: MessageWireframeProtocol! var interactor: MessageInteractorInputProtocol! { @@ -926,31 +957,15 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func internetStatusChanged(_ status: InternetStatus) { - internetStatus = status - switch status { - case .waiting, .connecting: - view?.updateHeaderStatus(status.rawValue.localized) - case .connected: - restoreStatus() - } + headerStatus.internet = status } func presenceStatusChanged(_ status: PresenceStatus) { - lastStatus = status.title + headerStatus.presence = status } func didReceiveTyping(_ typing: TypingDisplayModel) { - switch typing { - case .done: - restoreStatus() - case let .typing(sender, status): - let displayString = sender?.displayName.flatMap { "\($0) \(status.title)" } ?? status.title - view.updateHeaderStatus(displayString) - } - } - - private func restoreStatus() { - view.updateHeaderStatus(lastStatus) + headerStatus.typing = typing } func messageSent(_ localId: String) { @@ -992,12 +1007,9 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract self?.openProfileScreen(contact: contact) } } + + // MARK: - Utils - private func updateStatus(_ status: String) { - if internetStatus == .connected { - view.updateHeaderStatus(status) - } - } func getPreviousMessages(id: MessageServerId) { self.interactor.fetchMessages(from: id, isNew: true) diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 89a876acc..c501e69f7 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -289,7 +289,7 @@ protocol MessageViewProtocol: class { func scrollToBottomIfNeeded() func scrollToBottom() - func updateHeaderStatus(_ status: String) + func updateHeaderStatus(_ status: ChatStatusDisplayInfo) func updateDeliveryStatus(_ status: DeliveryStatus, messageId: String) func removeMessage(_ messageId: MessageLocalId, isForAllUsers: Bool) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index fb54b6343..6dcb86247 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -1116,7 +1116,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw avatarView.setup(with: viewModel) } - func updateHeaderStatus(_ status: String) { + func updateHeaderStatus(_ status: ChatStatusDisplayInfo) { avatarView.status = status } diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index bf039f735..c7d450f76 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -15,28 +15,41 @@ final class AvatarView: BaseView { return [statusLabel, separatorView] } - var status: String? { + var status: ChatStatusDisplayInfo? { didSet { - if let title = status?.replacingOccurrences(of: "...", with: ""), title.contains("typing") { - statusLabel.isHidden = true - typingView.isHidden = false + guard let status = status else { + return + } + + switch status { + case let .text(statusString): + statusLabel.text = statusString + statusLabel.accessibilityValue = statusString + + statusLabel.isHidden = false + typingView.isHidden = true + + case let .typing(sender, status): + let indicator: TypingView.Appearance.Indicator + switch status { + case .recording: + indicator = .circle(UIColor.nynja.white) + case .sending, .typing, .done: + indicator = .dots(UIColor.nynja.white) + } - let appearance = TypingView.Appearance(indicator: .dots(UIColor.white), + let appearance = TypingView.Appearance(indicator: indicator, textColor: titleLabel.textColor, textFont: statusLabel.font, - senderInfo: "typing", - typingInfo: "", + senderInfo: sender?.displayName, + typingInfo: status.title, isTypingInfoPinned: false ) typingView.update(appearance) - } else { - statusLabel.isHidden = false - typingView.isHidden = true - statusLabel.text = status + statusLabel.isHidden = true + typingView.isHidden = false } - statusLabel.text = status - statusLabel.accessibilityValue = status } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index ebadea743..5d1b71a0f 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -482,9 +482,9 @@ "deleted_message_replied_preview" = "Deleted message"; // MARK: Message -"message_status_typing"="...typing"; +"message_status_typing"="typing"; "message_new_messages"="New messages"; -"message_sending"="...sending a"; +"message_sending"="sending"; // MARK: Sending Status "file"="file"; @@ -492,7 +492,7 @@ // MARK: Recording Status "video"="video"; -"recording"="...recording a"; +"recording"="recording"; // MARK: Presence status "active"="active"; diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index 24e962cf4..7da267f34 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -8,7 +8,7 @@ enum TypingDisplayModel { case typing(SenderInfo?, ActionStatus) - case done + case none struct SenderInfo { var senders: [String] diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 16d6666a9..ba0be82e9 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -142,7 +142,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { - return .done + return .none } let senders: [String] = typingInfo.compactMap { guard lastStatus == $0.status else { -- GitLab From a8a1ac73363df7c4c975936d1a58c898611e2e5c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 11:12:30 +0200 Subject: [PATCH 043/138] [NY-4699] Fixed left offset for typing view. --- Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 5ffd98f8d..9cc14b774 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -139,7 +139,7 @@ public final class TypingView: BaseView { } enum senderInfo { - static let leftOffset: CGFloat = 8 + static let leftOffset: CGFloat = 4 } } } -- GitLab From 3367cb9276973d310bebf4cf88b501d58b85785e Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 11:23:13 +0200 Subject: [PATCH 044/138] [NY-4699] Don't remake layout if the same views exists in typing container. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 9cc14b774..6e3bc5c57 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -77,7 +77,7 @@ public final class TypingView: BaseView { // MARK: - Layout public func update(_ appearance: Appearance) { - indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + switch appearance.indicator { case let .dots(color): @@ -93,6 +93,10 @@ public final class TypingView: BaseView { } private func setupDotsIndicator(color: UIColor) { + guard !indicatorContainer.subviews.contains(where: { $0 is TypingIndicatorView }) else { return } + + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + let indicatorView = TypingIndicatorView() indicatorView.itemColor = color indicatorView.itemSize = Constraints.indicator.dotsSize.adjustedByWidth @@ -107,6 +111,10 @@ public final class TypingView: BaseView { } private func setupCircleIndicator(color: UIColor) { + guard !indicatorContainer.subviews.contains(where: { $0 is TypingBoldIndicatorView }) else { return } + + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + let indicatorView = TypingBoldIndicatorView() indicatorContainer.addSubview(indicatorView) -- GitLab From 6dffbd91e7dd406e0fd33373384eb81b5573b2d1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 17:24:13 +0200 Subject: [PATCH 045/138] [NY-4699] Implemented notifying UI on chat lists. --- Nynja/Extensions/Models/StarExtension.swift | 4 ++ .../Cell/ChatListMessageContentView.swift | 14 ++--- .../Cell/ChatListMessageTableViewCell.swift | 53 +++++++++++++++++++ .../Model/ChatListMessageCellModel.swift | 7 ++- .../ChatsList/ChatsListProtocols.swift | 9 +++- .../Interactor/ChatsListInteractor.swift | 23 ++++++++ .../Presenter/ChatsListPresenter.swift | 8 +++ .../ChatsList/View/ChatListTableDS.swift | 11 +++- .../View/ChatsListViewController.swift | 9 +++- .../Presenter/Contact+DialogCellModel.swift | 5 +- .../Profile/Presenter/DialogCellModel.swift | 1 + .../Presenter/Room+DialogCellModel.swift | 5 +- 12 files changed, 129 insertions(+), 20 deletions(-) diff --git a/Nynja/Extensions/Models/StarExtension.swift b/Nynja/Extensions/Models/StarExtension.swift index 95cbaf53a..450e052fc 100644 --- a/Nynja/Extensions/Models/StarExtension.swift +++ b/Nynja/Extensions/Models/StarExtension.swift @@ -12,6 +12,10 @@ extension Star: DialogCellModel { private static let deletedAccountTitle = MessageSender.deleted.fullname + var feedId: String! { + return message?.chatId + } + var title: String! { if let feedName = self.message?.feedName { let sender = self.message?.senderName ?? Star.deletedAccountTitle.localized diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index 71c5eda1e..1ad7e579f 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -75,17 +75,9 @@ final class ChatListMessageContentView: BaseView { textView.setup(sender: sender, image: image, text: text) } - func showTyping(sender: String?) { + func showTyping(with appearance: TypingView.Appearance) { typingView.isHidden = false textView.isHidden = true - - let appearance = TypingView.Appearance(indicator: .circle(UIColor.nynja.white), - textColor: titleLabel.textColor, - textFont: typingFont, - senderInfo: sender, - typingInfo: "typing", - isTypingInfoPinned: false) - typingView.update(appearance) } @@ -97,8 +89,8 @@ final class ChatListMessageContentView: BaseView { // MARK: - Layout - private let typingFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, - height: Constraints.typingView.height.adjustedByWidth)! + static let typingFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, + height: Constraints.typingView.height.adjustedByWidth)! private enum Constraints { diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 0230b9458..7a9b71f5e 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -12,6 +12,10 @@ import NynjaUIKit final class ChatListMessageTableViewCell: UITableViewCell { + private var observable: TypingObservable? + private var feedId: FeedId? + + // MARK: - Views private(set) lazy var avatarImageView: AvatarStatusView = { @@ -106,6 +110,55 @@ final class ChatListMessageTableViewCell: UITableViewCell { messageContentView.isHidden = false separatorView.isHidden = false } + + func setup(observable: TypingObservable?, feedId: FeedId) { + self.observable = observable + self.feedId = feedId + + self.observable?.observeChanges(for: feedId) { [weak self] typing in + self?.handleTyping(typing) + } + } + + + // MARK: - Life Cycle + + override func prepareForReuse() { + super.prepareForReuse() + messageContentView.hideTyping() + + guard let feedId = feedId else { + return + } + observable?.removeObserver(for: feedId) + self.feedId = nil + } + + + private func handleTyping(_ typing: TypingDisplayModel) { + switch typing { + case let .typing(sender, status): + let indicator: TypingView.Appearance.Indicator + switch status { + case .recording: + indicator = .circle(UIColor.nynja.white) + case .sending, .typing, .done: + indicator = .dots(UIColor.nynja.white) + } + + let appearance = TypingView.Appearance(indicator: indicator, + textColor: UIColor.nynja.white, + textFont: ChatListMessageContentView.typingFont, + senderInfo: sender?.displayName, + typingInfo: status.title, + isTypingInfoPinned: false + ) + messageContentView.showTyping(with: appearance) + + case .none: + messageContentView.hideTyping() + } + } } // MARK: - Layout diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index c5ee537d9..5cfa7a1b4 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -17,8 +17,9 @@ final class ChatListMessageCellModel: CellViewModel { return "chatList_cell" } - private let payloadParser: MessagePayloadParserInput private let model: DialogCellModel + private let observable: TypingObservable? + private let payloadParser: MessagePayloadParserInput private weak var delegate: ChatListMessageCellModelDelegate? private var sender: String? { @@ -26,9 +27,11 @@ final class ChatListMessageCellModel: CellViewModel { } init(model: DialogCellModel, + observable: TypingObservable? = nil, payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate?) { self.model = model + self.observable = observable self.payloadParser = payloadParser self.delegate = delegate } @@ -37,6 +40,7 @@ final class ChatListMessageCellModel: CellViewModel { setupAvatar(in: cell) setupAccessory(in: cell) setupMessage(model.message, in: cell) + cell.setup(observable: observable, feedId: model.feedId) } @@ -74,7 +78,6 @@ final class ChatListMessageCellModel: CellViewModel { let type = SendMessageType(rawValue: mime) else { return } - cell.messageContentView.showTyping(sender: sender) switch type { case .text: diff --git a/Nynja/Modules/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index c5aafbdb5..cbce84e51 100644 --- a/Nynja/Modules/ChatsList/ChatsListProtocols.swift +++ b/Nynja/Modules/ChatsList/ChatsListProtocols.swift @@ -8,6 +8,11 @@ import UIKit +protocol TypingObservable: class { + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) + func removeObserver(for feedId: FeedId) +} + protocol ChatsListWireFrameProtocol: class { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) @@ -28,7 +33,7 @@ protocol ChatsListViewProtocol: class { func setup(with state: CollectionState<[Contact]>, displayMode: CollectionDisplayMode) } -protocol ChatsListPresenterProtocol: BasePresenterProtocol { +protocol ChatsListPresenterProtocol: BasePresenterProtocol, TypingObservable { var view: ChatsListViewProtocol! { get set } var interactor: ChatsListInteractorInputProtocol! { get set } @@ -50,7 +55,7 @@ protocol ChatsListInteractorOutputProtocol: class { func didFilter(chats: [Contact]) } -protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol { +protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var presenter: ChatsListInteractorOutputProtocol! { get set } diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 02aa17311..c110a358c 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -12,10 +12,13 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingStatusProvider private var chats: [Contact] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable @@ -27,6 +30,14 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + // FIXME: move to factory + typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + + super.init() + } + + deinit { + typingProvider.removeObserver(self) } @@ -39,6 +50,10 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini override func loadData() { super.loadData() fetchChats() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } } @@ -49,6 +64,14 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini applyFilter(with: searchText) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + // MARK: - StorageSubscriber diff --git a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift index 6456463eb..2db4c11d7 100644 --- a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift +++ b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift @@ -33,6 +33,14 @@ class ChatsListPresenter: BasePresenter, ChatsListPresenterProtocol, ChatsListIn wireFrame.showChatWith(contact: contact) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + // MARK: - ChatsListInteractorOutputProtocol diff --git a/Nynja/Modules/ChatsList/View/ChatListTableDS.swift b/Nynja/Modules/ChatsList/View/ChatListTableDS.swift index 174497229..1a8c94bc1 100644 --- a/Nynja/Modules/ChatsList/View/ChatListTableDS.swift +++ b/Nynja/Modules/ChatsList/View/ChatListTableDS.swift @@ -14,10 +14,14 @@ final class ChatListTableDS: NSObject, UITableViewDataSource { var chatList = [Contact]() private let payloadParser: MessagePayloadParserInput + + private let typingObservable: TypingObservable + private weak var delegate: ChatListMessageCellModelDelegate? - init(payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate) { + init(payloadParser: MessagePayloadParserInput, typingObservable: TypingObservable, delegate: ChatListMessageCellModelDelegate) { self.payloadParser = payloadParser + self.typingObservable = typingObservable self.delegate = delegate } @@ -30,6 +34,9 @@ final class ChatListTableDS: NSObject, UITableViewDataSource { } private func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { - return ChatListMessageCellModel(model: chatList[indexPath.row], payloadParser: payloadParser, delegate: delegate) + return ChatListMessageCellModel(model: chatList[indexPath.row], + observable: typingObservable, + payloadParser: payloadParser, + delegate: delegate) } } diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index b5bd6f8a1..d4899b1ad 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -126,7 +126,7 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height tableView.estimatedRowHeight = tableView.rowHeight - dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), typingObservable: presenter, delegate: self) emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) tableView.dataSource = emptyStateDS @@ -138,6 +138,13 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable extension ChatsListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let feedId = dataSource.chatList[indexPath.row].phone_id else { + return + } + presenter.removeObserver(for: feedId) + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) prepareForDissappear() diff --git a/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift index 1001ca59a..45a78599e 100644 --- a/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift @@ -10,7 +10,10 @@ import Foundation extension Contact : DialogCellModel { - // MARK: DialogCellModel methods + var feedId: String! { + return phone_id + } + var title: String! { return fullName } diff --git a/Nynja/Modules/Profile/Presenter/DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/DialogCellModel.swift index c247a3172..38e1392c2 100644 --- a/Nynja/Modules/Profile/Presenter/DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/DialogCellModel.swift @@ -9,6 +9,7 @@ import Foundation protocol DialogCellModel : CellModel { + var feedId: String! { get } var title: String! { get } var message: Message? { get } var hasMentions: Bool { get } diff --git a/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift index 1e8cd4a00..b22acc76f 100644 --- a/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift @@ -10,7 +10,10 @@ import Foundation extension Room: DialogCellModel { - // MARK: DialogCellModel methods + var feedId: String! { + return id + } + var title: String! { return name != nil ? name! : "" } -- GitLab From 2fb295ed60f46082a175b8c58b7d4a829cabb6ef Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 18:05:49 +0200 Subject: [PATCH 046/138] [NY-4699] Fixed `done` typing status. --- .../Cell/ChatListMessageTableViewCell.swift | 25 +++++++++++++------ .../View/ChatsListViewController.swift | 7 ------ .../Models/Statuses/Typing/ActionStatus.swift | 7 ++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 7a9b71f5e..c12c21936 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -101,6 +101,10 @@ final class ChatListMessageTableViewCell: UITableViewCell { setup() } + deinit { + removeObserver() + } + // MARK: - Setup @@ -126,18 +130,17 @@ final class ChatListMessageTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() messageContentView.hideTyping() - - guard let feedId = feedId else { - return - } - observable?.removeObserver(for: feedId) - self.feedId = nil + removeObserver() } - private func handleTyping(_ typing: TypingDisplayModel) { switch typing { case let .typing(sender, status): + guard !status.isDone else { + messageContentView.hideTyping() + break + } + let indicator: TypingView.Appearance.Indicator switch status { case .recording: @@ -159,6 +162,14 @@ final class ChatListMessageTableViewCell: UITableViewCell { messageContentView.hideTyping() } } + + private func removeObserver() { + guard let feedId = feedId else { + return + } + observable?.removeObserver(for: feedId) + self.feedId = nil + } } // MARK: - Layout diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index d4899b1ad..15702f53d 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -138,13 +138,6 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable extension ChatsListViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let feedId = dataSource.chatList[indexPath.row].phone_id else { - return - } - presenter.removeObserver(for: feedId) - } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) prepareForDissappear() diff --git a/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift index 0f0a4e9c0..13c65fdba 100644 --- a/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift @@ -12,6 +12,13 @@ enum ActionStatus: Equatable { case sending(SendingStatus) case recording(RecordingStatus) + var isDone: Bool { + if case .done = self { + return true + } + return false + } + var isTyping: Bool { if case .typing = self { return true -- GitLab From 23f2ecbe8ccc228324e10a50a35ac2987e8eadaa Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 18:44:50 +0200 Subject: [PATCH 047/138] [NY-4699] Implemented thread-safe access to data dictionary inside a TypingStatusProvider. --- .../Interactor/ChatsListInteractor.swift | 1 + Nynja/Statuses/TypingStatusProvider.swift | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index c110a358c..0e5138c35 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -66,6 +66,7 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } } func removeObserver(for feedId: FeedId) { diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index ba0be82e9..1405e3d86 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -19,6 +19,7 @@ protocol TypingStatusObservable: class { } protocol TypingStatusProvider: TypingStatusObservable { + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { @@ -31,6 +32,8 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private let typingDismissInterval = 10.0 + private let isolationQueue = DispatchQueue(label: "com.nynja.typing-status-provider", attributes: .concurrent) + private let processingQueue: DispatchQueue private let notifyQueue: DispatchQueue @@ -61,6 +64,13 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } + // MARK: - TypingStatusProvider + + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { + return typing(for: feedId).flatMap { displayInfo(for: $0) } + } + + // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { @@ -80,7 +90,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private func save(_ typingInfo: SenderInfo) { let feedId = typingInfo.feed.identifier - if let oldTyping = data[feedId] { + if let oldTyping = typing(for: feedId) { switch oldTyping { case .p2p: update(.p2p(typingInfo), for: feedId) @@ -118,7 +128,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } private func update(_ typing: TypingData?, for feedId: FeedId) { - data[feedId] = typing + set(typing, for: feedId) let displayInfo = self.displayInfo(for: typing) notifyQueue.async { @@ -127,7 +137,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } private func remove(_ typing: SenderInfo, for feedId: FeedId) { - guard let currentTyping = data[feedId] else { + guard let currentTyping = self.typing(for: feedId) else { return } switch currentTyping { @@ -154,6 +164,25 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta return .typing(senderInfo, lastStatus) } + + + // MARK: - Thread-Safe Data Access + + private func set(_ data: TypingData?, for feedId: FeedId) { + isolationQueue.async(flags: .barrier) { + self.data[feedId] = data + } + } + + private func typing(for feedId: FeedId) -> TypingData? { + var typing: TypingData? + + isolationQueue.sync { + typing = data[feedId] + } + + return typing + } } -- GitLab From 37a2c95b331f2065818f18e30d2646cb0e9eb46b Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 18:58:16 +0200 Subject: [PATCH 048/138] [NY-4699] Ask for typing status when chat screen is loaded. --- Nynja/Modules/Message/Interactor/MessageInteractor.swift | 7 +++++++ Nynja/Modules/Message/Presenter/MessagePresenter.swift | 1 + Nynja/Modules/Message/Protocols/MessageProtocols.swift | 1 + 3 files changed, 9 insertions(+) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 50578f17a..d57eb3f51 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -444,6 +444,13 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H presenter?.internetStatusChanged(.waiting) } } + + func askForTypingStatus() { + guard let feedId = chat.id, let typing = typingStatusProvider.typingStatus(for: feedId) else { + return + } + presenter?.didReceiveTyping(typing) + } // MARK: - Send Message func sendMessage(_ message: InputTextMessage) { diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 836148571..52796e279 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -101,6 +101,7 @@ final class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageIn super.screenLoaded() interactor.askForInternetStatus() + interactor.askForTypingStatus() interactor.room.map { fetchMentionedMessages(in: $0) } } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index c501e69f7..78e3eb66b 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -244,6 +244,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func sender(for message: Message) -> MessageSender func askForInternetStatus() + func askForTypingStatus() func editMessage(_ message: InputTextMessage) func clearEditMessageObject() -- GitLab From 8e74ab3e44c8632aaaf1fc2e0dc6fbf3d0e7ceb1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 19:22:54 +0200 Subject: [PATCH 049/138] [NY-4699] Pass typing data from GroupsListInteractor to group list cells. --- .../GroupsList/GroupsListProtocols.swift | 4 ++-- .../Interactor/GroupsListInteractor.swift | 20 +++++++++++++++++++ .../Presenter/GroupsListPresenter.swift | 8 ++++++++ .../GroupsList/View/GroupsListTableDS.swift | 15 +++++++++++--- .../View/GroupsListViewController.swift | 2 +- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Nynja/Modules/GroupsList/GroupsListProtocols.swift b/Nynja/Modules/GroupsList/GroupsListProtocols.swift index 6f973d065..f10ba5bd2 100644 --- a/Nynja/Modules/GroupsList/GroupsListProtocols.swift +++ b/Nynja/Modules/GroupsList/GroupsListProtocols.swift @@ -30,7 +30,7 @@ protocol GroupsListViewProtocol: class { func setup(with state: CollectionState<[Room]>, displayMode: CollectionDisplayMode) } -protocol GroupsListPresenterProtocol: BasePresenterProtocol { +protocol GroupsListPresenterProtocol: BasePresenterProtocol, TypingObservable { var view: GroupsListViewProtocol! { get set } var wireFrame: GroupsListWireFrameProtocol! { get set } @@ -55,7 +55,7 @@ protocol GroupsListInteractorOutputProtocol: class { func didFilter(groups: [Room]) } -protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol { +protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var presenter: GroupsListInteractorOutputProtocol! { get set } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index c16ee18b5..d0109e5dd 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -12,10 +12,13 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingStatusProvider private var chats: [Room] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable @@ -27,6 +30,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + // FIXME: move to factory + typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + + super.init() } // MARK: - BaseInteractor @@ -42,6 +49,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I override func loadData() { super.loadData() fetchGroups() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } } @@ -52,6 +63,15 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I applyFilter(with: searchText) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + // MARK: - StorageSubscriber diff --git a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift index 4cd181f2e..8600c93ae 100644 --- a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift +++ b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift @@ -43,6 +43,14 @@ class GroupsListPresenter: BasePresenter, GroupsListPresenterProtocol, GroupsLis self.interactor.filter(with: text) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + // MARK: - GroupsListInteractorOutputProtocol diff --git a/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift b/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift index c7a9dbc10..49470a986 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift @@ -14,10 +14,17 @@ class GroupsListTableDS: NSObject, UITableViewDataSource { var rooms = [Room]() private let payloadParser: MessagePayloadParserInput + + private let typingObservable: TypingObservable? + private weak var delegate: ChatListMessageCellModelDelegate? - init(payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate? = nil) { + init(payloadParser: MessagePayloadParserInput, + typingObservable: TypingObservable? = nil, + delegate: ChatListMessageCellModelDelegate? = nil) { + self.payloadParser = payloadParser + self.typingObservable = typingObservable self.delegate = delegate } @@ -30,7 +37,9 @@ class GroupsListTableDS: NSObject, UITableViewDataSource { } private func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { - return ChatListMessageCellModel(model: rooms[indexPath.row], payloadParser: payloadParser, delegate: delegate) + return ChatListMessageCellModel(model: rooms[indexPath.row], + observable: typingObservable, + payloadParser: payloadParser, + delegate: delegate) } - } diff --git a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift index 81fac1e09..1c779d577 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift @@ -109,7 +109,7 @@ final class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipab tableView.register(viewModel: ChatListMessageCellModel.self) tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height - dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), typingObservable: presenter, delegate: self) emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) tableView.dataSource = emptyStateDS -- GitLab From b2ffca421b5941c7208446400c5898d39b8088d6 Mon Sep 17 00:00:00 2001 From: AshCenso Date: Thu, 1 Nov 2018 12:59:31 +0200 Subject: [PATCH 050/138] in progress --- Nynja copy-Info.plist | 122 -- Nynja.xcodeproj/project.pbxproj | 98 +- Nynja/Generated/AssetsConstants.swift | 1099 +++++------------ Nynja/Generated/ColorsConstants.swift | 76 +- Nynja/Generated/FontsConstants.swift | 65 +- Nynja/Generated/LocalizableConstants.swift | 21 +- .../AccountSettingsProtocols.swift | 29 + .../View/AccountSettingsViewController.swift | 97 ++ .../AccountSettingsViewsFactory.swift | 18 + .../Wireframe/AccountSettingsWireframe.swift | 27 + .../AccountSettingsCoordinator.swift | 9 + Nynja/Modules/Auth/AuthCoordinator.swift | 4 +- .../Contents.json | 0 .../next bttn@2x.png | Bin .../next bttn@3x.png | Bin .../Contents.json | 0 .../qr-code.png | Bin .../qr-code@2x.png | Bin .../qr-code@3x.png | Bin .../wheel_left_image.imageset/Contents.json | 12 - .../wheel_left_image.imageset/left_image.pdf | Bin 253015 -> 0 bytes .../wheel_right_image.imageset/Contents.json | 12 - .../right_image.pdf | Bin 260993 -> 0 bytes .../ic_camera_frame.imageset/Contents.json | 35 - .../ic_camera_frame.pdf | Bin 3953 -> 0 bytes .../ic_new_group.imageset/Contents.json | 15 - .../ic_new_group.imageset/ic_new_group.pdf | Bin 7638 -> 0 bytes .../ic_search.imageset/Contents.json | 21 - .../ic_search.imageset/ic_search.pdf | Bin 4243 -> 0 bytes Nynja/Resources/LaunchScreen.storyboard | 2 +- 30 files changed, 697 insertions(+), 1065 deletions(-) delete mode 100644 Nynja copy-Info.plist create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift create mode 100644 Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift rename Nynja/Resources/Assets.xcassets/Main Wheel/{next_bttn.imageset => next_bttn1.imageset}/Contents.json (100%) rename Nynja/Resources/Assets.xcassets/Main Wheel/{next_bttn.imageset => next_bttn1.imageset}/next bttn@2x.png (100%) rename Nynja/Resources/Assets.xcassets/Main Wheel/{next_bttn.imageset => next_bttn1.imageset}/next bttn@3x.png (100%) rename Nynja/Resources/Assets.xcassets/New Folder/{qr-code.imageset => qr-code1.imageset}/Contents.json (100%) rename Nynja/Resources/Assets.xcassets/New Folder/{qr-code.imageset => qr-code1.imageset}/qr-code.png (100%) rename Nynja/Resources/Assets.xcassets/New Folder/{qr-code.imageset => qr-code1.imageset}/qr-code@2x.png (100%) rename Nynja/Resources/Assets.xcassets/New Folder/{qr-code.imageset => qr-code1.imageset}/qr-code@3x.png (100%) delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/right_image.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/ic_camera_frame.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/ic_new_group.imageset/ic_new_group.pdf delete mode 100644 Nynja/Resources/Assets.xcassets/ic_search.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/ic_search.imageset/ic_search.pdf diff --git a/Nynja copy-Info.plist b/Nynja copy-Info.plist deleted file mode 100644 index 5472ab2b7..000000000 --- a/Nynja copy-Info.plist +++ /dev/null @@ -1,122 +0,0 @@ - - - - - AppGroup - $(AppGroup) - CFBundleDevelopmentRegion - en - CFBundleDisplayName - $(AppName) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 0.2.158 - ConfServerAddress - $(ConfServerAddress) - ConfServerPort - $(ConfServerPort) - ConfServerSecure - $(ConfServerSecure) - Config - $(Config) - Fabric - - APIKey - 595b68a8c4deb3533dcdfc24ca73fd3cffd99f3c - Kits - - - KitInfo - - KitName - Crashlytics - - - - LSApplicationQueriesSchemes - - cydia - comgooglemapsurl - comgooglemaps - googlechromes - - LSRequiresIPhoneOS - - ModelsVersion - $(ModelsVersion) - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSCameraUsageDescription - NYNJA needs it to allow you make photos and video calls. - NSContactsUsageDescription - NYNJA needs it to allow you add new contacts from your phone contact book. - NSLocationAlwaysUsageDescription - NYNJA needs to know your location so that you can be able to share it. - NSLocationWhenInUseUsageDescription - NYNJA needs to know your location so that you can be able to share it. - NSMicrophoneUsageDescription - NYNJA needs it to allow you send audio messages and make voice calls. - NSPhotoLibraryAddUsageDescription - NYNJA needs it to save photos and video to your device. - NSPhotoLibraryUsageDescription - NYNJA needs it so that you can use your local images. - ServerPort - $(ServerPort) - ServerURL - $(ServerURL) - UIAppFonts - - Avenir.ttc - LatoBlack.ttf - Myriad Pro Regular.ttf - NotoSans-Bold.ttf - NotoSans-Regular.ttf - NotoSans-Medium.ttf - NotoSans-Italic.ttf - - UIBackgroundModes - - audio - fetch - remote-notification - voip - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarStyle - UIStatusBarStyleLightContent - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - isServerConnectionSecure - $(isServerConnectionSecure) - - diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index d3f83f3b4..645ec28a5 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -670,8 +670,12 @@ 5E7E9FC2215BA681004D306B /* CountryTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */; }; 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */; }; 5EB13FDBA6153EE67366115F /* ScheduleMessageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */; }; - 5EC8C841216648B6003D4731 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EC8C840216648B6003D4731 /* ViewController.swift */; }; 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD49CF323041B47A752603E /* MapSearchInteractor.swift */; }; + 5EDD454F21885ED200C50BC8 /* AccountSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */; }; + 5EDD455121885EE300C50BC8 /* AccountSettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */; }; + 5EDD455321885F7800C50BC8 /* AccountSettingsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */; }; + 5EDD45552188601400C50BC8 /* AccountSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */; }; + 5EDD455821899BCE00C50BC8 /* AccountSettingsViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */; }; 5EEB73A4215D00E300D8ECE6 /* CountrySelectorInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */; }; 5EEB73A6215D00F100D8ECE6 /* CountrySelectorWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */; }; 5EEB73A8215D00FD00D8ECE6 /* CountrySelectorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A7215D00FD00D8ECE6 /* CountrySelectorPresenter.swift */; }; @@ -2822,13 +2826,16 @@ 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileWireframe.swift; sourceTree = ""; }; 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileContentView.swift; sourceTree = ""; }; 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; - 5E7E9FB2215BA059004D306B /* Nynja copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Nynja copy-Info.plist"; path = "/Users/ash/Projects/NynjaIOSWallet/Nynja copy-Info.plist"; sourceTree = ""; }; 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorProtocols.swift; sourceTree = ""; }; 5E7E9FBB215BA19B004D306B /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewController.swift; sourceTree = ""; }; 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVCell.swift; sourceTree = ""; }; 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVHeader.swift; sourceTree = ""; }; - 5EC8C840216648B6003D4731 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsCoordinator.swift; sourceTree = ""; }; + 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsProtocols.swift; sourceTree = ""; }; + 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsWireframe.swift; sourceTree = ""; }; + 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewController.swift; sourceTree = ""; }; + 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewsFactory.swift; sourceTree = ""; }; 5EEA3D18EFB98D7959F993E4 /* AddParticipantsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsProtocols.swift; sourceTree = ""; }; 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorInteractor.swift; sourceTree = ""; }; 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorWireframe.swift; sourceTree = ""; }; @@ -5907,7 +5914,6 @@ 767274D7745E7E490BE6C79C /* Pods */, E853D758816E611EE4809ED3 /* Frameworks */, 851EBD7D20B403B90065C644 /* Recovered References */, - 5E7E9FB2215BA059004D306B /* Nynja copy-Info.plist */, ); sourceTree = ""; }; @@ -5930,7 +5936,6 @@ 3A768DE41ECB3E7600108F7C /* Library */, 3ABCE9021EC9357900A80B15 /* Resources */, 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */, - 5EC8C840216648B6003D4731 /* ViewController.swift */, 3AC07E2E1F05572400ADBE26 /* Nynja-Bridging-Header.h */, F11786B320A8A5EB007A9A1B /* Coordinators */, 49E75E252CE2F3C96A626230 /* Modules */, @@ -6134,6 +6139,7 @@ 49E75E252CE2F3C96A626230 /* Modules */ = { isa = PBXGroup; children = ( + 5EDD454621885EC400C50BC8 /* AccountSettings */, 4B749F0E214FEFC8002F3A33 /* Auth */, 260531122127407A002E1CF1 /* LogOutput */, FBCE83C320E52351003B7558 /* Payment */, @@ -6997,6 +7003,82 @@ path = Headers; sourceTree = ""; }; + 5EDD454621885EC400C50BC8 /* AccountSettings */ = { + isa = PBXGroup; + children = ( + 5EDD454721885EC400C50BC8 /* Coordinator */, + 5EDD454821885EC400C50BC8 /* AccountSettings */, + ); + path = AccountSettings; + sourceTree = ""; + }; + 5EDD454721885EC400C50BC8 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; + 5EDD454821885EC400C50BC8 /* AccountSettings */ = { + isa = PBXGroup; + children = ( + 5EDD454921885EC400C50BC8 /* Presenter */, + 5EDD454A21885EC400C50BC8 /* Wireframe */, + 5EDD454B21885EC400C50BC8 /* View */, + 5EDD454C21885EC400C50BC8 /* Interactor */, + 5EDD454D21885EC400C50BC8 /* Entities */, + 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */, + ); + path = AccountSettings; + sourceTree = ""; + }; + 5EDD454921885EC400C50BC8 /* Presenter */ = { + isa = PBXGroup; + children = ( + ); + path = Presenter; + sourceTree = ""; + }; + 5EDD454A21885EC400C50BC8 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 5EDD454B21885EC400C50BC8 /* View */ = { + isa = PBXGroup; + children = ( + 5EDD45562188617A00C50BC8 /* ViewsFactory */, + 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 5EDD454C21885EC400C50BC8 /* Interactor */ = { + isa = PBXGroup; + children = ( + ); + path = Interactor; + sourceTree = ""; + }; + 5EDD454D21885EC400C50BC8 /* Entities */ = { + isa = PBXGroup; + children = ( + ); + path = Entities; + sourceTree = ""; + }; + 5EDD45562188617A00C50BC8 /* ViewsFactory */ = { + isa = PBXGroup; + children = ( + 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */, + ); + path = ViewsFactory; + sourceTree = ""; + }; 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */ = { isa = PBXGroup; children = ( @@ -14130,6 +14212,7 @@ buildActionMask = 2147483647; files = ( A432CF1B20B4347D00993AFB /* MaterialTextView.swift in Sources */, + 5EDD455121885EE300C50BC8 /* AccountSettingsProtocols.swift in Sources */, B7EF8ED0210C501F00E0E981 /* InterpretationTypeTableDataSource.swift in Sources */, F11DF06620BD96D000F3E005 /* GalleryFilterGroupType.swift in Sources */, F11786F120AC5482007A9A1B /* ServiceFactory.swift in Sources */, @@ -14180,6 +14263,7 @@ 855AC532208E441500DC2335 /* StickersInputPresenter.swift in Sources */, 3A1DC73C1EF15330006A8E9F /* HandlerService.swift in Sources */, E7302A951FC86424002892F8 /* P2pTable.swift in Sources */, + 5EDD45552188601400C50BC8 /* AccountSettingsViewController.swift in Sources */, F117871120ACF018007A9A1B /* CameraQualitySettingsInteractor.swift in Sources */, 8503B51920503683006F0593 /* NynjaCellButton.swift in Sources */, 8596CEF62048AEB8006FC65D /* ThemeItemsFactory.swift in Sources */, @@ -14585,6 +14669,7 @@ FBD885782147F9640099B8C3 /* FontsConstants.swift in Sources */, 263D662D1FE8D03400A509F8 /* TypingModel.swift in Sources */, 853D0F9020C00806008C3684 /* StickerPreviewState.swift in Sources */, + 5EDD455821899BCE00C50BC8 /* AccountSettingsViewsFactory.swift in Sources */, A42D51BE206A361400EEB952 /* Vox.swift in Sources */, E77764C21FBDA9BD0042541D /* ImageWheelItemModel.swift in Sources */, 857A06612035E3360097C49B /* ForwardAvatarCollectionViewCell.swift in Sources */, @@ -15575,6 +15660,7 @@ FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, E79061B61FBF1C8C009FD83A /* DescTable.swift in Sources */, + 5EDD454F21885ED200C50BC8 /* AccountSettingsCoordinator.swift in Sources */, 5EEB73D62161DBF100D8ECE6 /* EmailLoginView.swift in Sources */, 5EEB73B4216047E000D8ECE6 /* CodeConfirmationViewController.swift in Sources */, 2910A0129CA29C35161DD692 /* EditPhotoInteractor.swift in Sources */, @@ -15675,7 +15761,6 @@ 6547BE911E492D790E0D4390 /* EditGroupNameInteractor.swift in Sources */, 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */, 263A60AE1FB51C22006F9D52 /* MemberExtension.swift in Sources */, - 5EC8C841216648B6003D4731 /* ViewController.swift in Sources */, E709383F1FBEE41D006CCDC6 /* Describable.swift in Sources */, A42D52DA206A53AB00EEB952 /* Desc_Spec.swift in Sources */, A433D9A120A5C18C00C946F9 /* ContactsProvider.swift in Sources */, @@ -15809,6 +15894,7 @@ 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */, F1607B2A20B2DE6500BDF60A /* CameraQRPreviewWireframe.swift in Sources */, E3BE59F069959DA2523EF3DC /* MapSearchWireframe.swift in Sources */, + 5EDD455321885F7800C50BC8 /* AccountSettingsWireframe.swift in Sources */, B7EF8ED4210C511C00E0E981 /* InterpretationTypeCell.swift in Sources */, 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */, F1313B0220888FE600E04092 /* ThirdPartyServices.swift in Sources */, diff --git a/Nynja/Generated/AssetsConstants.swift b/Nynja/Generated/AssetsConstants.swift index f2fd83e47..2bdcfba6d 100644 --- a/Nynja/Generated/AssetsConstants.swift +++ b/Nynja/Generated/AssetsConstants.swift @@ -1,379 +1,278 @@ +// swiftlint:disable all // Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen #if os(OSX) import AppKit.NSImage internal typealias AssetColorTypeAlias = NSColor - internal typealias Image = NSImage + internal typealias AssetImageTypeAlias = NSImage #elseif os(iOS) || os(tvOS) || os(watchOS) import UIKit.UIImage internal typealias AssetColorTypeAlias = UIColor - internal typealias Image = UIImage + internal typealias AssetImageTypeAlias = UIImage #endif // swiftlint:disable superfluous_disable_command // swiftlint:disable file_length -@available(*, deprecated, renamed: "ImageAsset") -internal typealias AssetType = ImageAsset - -internal struct ImageAsset { - internal fileprivate(set) var name: String - - internal var image: Image { - let bundle = Bundle(for: BundleToken.self) - #if os(iOS) || os(tvOS) - let image = Image(named: name, in: bundle, compatibleWith: nil) - #elseif os(OSX) - let image = bundle.image(forResource: NSImage.Name(name)) - #elseif os(watchOS) - let image = Image(named: name) - #endif - guard let result = image else { fatalError("Unable to load image named \(name).") } - return result - } -} - -internal struct ColorAsset { - internal fileprivate(set) var name: String - - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) - internal var color: AssetColorTypeAlias { - return AssetColorTypeAlias(asset: self) - } -} +// MARK: - Asset Catalogs // swiftlint:disable identifier_name line_length nesting type_body_length type_name internal enum Asset { internal static let appIcon = ImageAsset(name: "App_Icon") - internal enum Background { - internal static let background = ImageAsset(name: "background") - internal static let backgroundLight = ImageAsset(name: "background_light") - internal static let bgSecurityPlaceholderDark = ImageAsset(name: "bg_security_placeholder_dark") - internal static let bgSecurityPlaceholderLight = ImageAsset(name: "bg_security_placeholder_light") - } - internal enum Buttons { - internal enum Checkable { - internal static let icChecked = ImageAsset(name: "ic_checked") - internal static let icPartialySelected = ImageAsset(name: "ic_partialy_selected") - internal static let icUnchecked = ImageAsset(name: "ic_unchecked") - } - } - internal enum CallItems { - internal static let icAcceptCall = ImageAsset(name: "ic_accept_call") - internal static let icAcceptCallBig = ImageAsset(name: "ic_accept_call_big") - internal static let icDeclineCall = ImageAsset(name: "ic_decline_call") - internal static let icGoToChat = ImageAsset(name: "ic_go_to_chat") - internal static let icMoreVoiceCall = ImageAsset(name: "ic_more_voice_call") - internal static let icMuteVoiceCall = ImageAsset(name: "ic_mute_voice_call") - internal static let icOutgoingCall = ImageAsset(name: "ic_outgoing_call") - internal static let icPortOut1 = ImageAsset(name: "ic_port_out-1") - internal static let icPortOut = ImageAsset(name: "ic_port_out") - internal static let icSpeakerOff = ImageAsset(name: "ic_speaker_off") - internal static let icSpeakerOn = ImageAsset(name: "ic_speaker_on") - internal static let icUnmuteVoiceCall = ImageAsset(name: "ic_unmute_voice_call") - internal static let icVideoOffVoiceCall = ImageAsset(name: "ic_video_off_voice_call") - internal static let icVideoOnVoiceCall = ImageAsset(name: "ic_video_on_voice_call") - } - internal enum CameraItems { - internal static let icCameraFrame = ImageAsset(name: "ic_camera_frame") - internal static let icFlashlightActive = ImageAsset(name: "ic_flashlight_active") - internal static let icFlashlightInactive = ImageAsset(name: "ic_flashlight_inactive") - } + internal static let background = ImageAsset(name: "background") + internal static let backgroundLight = ImageAsset(name: "background_light") + internal static let bgSecurityPlaceholderDark = ImageAsset(name: "bg_security_placeholder_dark") + internal static let bgSecurityPlaceholderLight = ImageAsset(name: "bg_security_placeholder_light") + internal static let icChecked = ImageAsset(name: "ic_checked") + internal static let icPartialySelected = ImageAsset(name: "ic_partialy_selected") + internal static let icUnchecked = ImageAsset(name: "ic_unchecked") + internal static let icAcceptCall = ImageAsset(name: "ic_accept_call") + internal static let icAcceptCallBig = ImageAsset(name: "ic_accept_call_big") + internal static let icDeclineCall = ImageAsset(name: "ic_decline_call") + internal static let icGoToChat = ImageAsset(name: "ic_go_to_chat") + internal static let icMoreVoiceCall = ImageAsset(name: "ic_more_voice_call") + internal static let icMuteVoiceCall = ImageAsset(name: "ic_mute_voice_call") + internal static let icOutgoingCall = ImageAsset(name: "ic_outgoing_call") + internal static let icPortOut1 = ImageAsset(name: "ic_port_out-1") + internal static let icPortOut = ImageAsset(name: "ic_port_out") + internal static let icSpeakerOff = ImageAsset(name: "ic_speaker_off") + internal static let icSpeakerOn = ImageAsset(name: "ic_speaker_on") + internal static let icUnmuteVoiceCall = ImageAsset(name: "ic_unmute_voice_call") + internal static let icVideoOffVoiceCall = ImageAsset(name: "ic_video_off_voice_call") + internal static let icVideoOnVoiceCall = ImageAsset(name: "ic_video_on_voice_call") + internal static let icCameraFrame = ImageAsset(name: "ic_camera_frame") + internal static let icFlashlightActive = ImageAsset(name: "ic_flashlight_active") + internal static let icFlashlightInactive = ImageAsset(name: "ic_flashlight_inactive") internal static let combinedShape1960 = ImageAsset(name: "Combined_shape_1960") - internal enum Contacts { - internal static let avaPlaceholder = ImageAsset(name: "ava_placeholder") - internal static let circleRed = ImageAsset(name: "circle_red") - internal static let contactSeparatop = ImageAsset(name: "contact_separatop") - } - internal enum Forward { - internal static let icForwardContacts = ImageAsset(name: "ic_forward_contacts") - internal static let icForwardContactsSelected = ImageAsset(name: "ic_forward_contacts_selected") - internal static let icForwardGroups = ImageAsset(name: "ic_forward_groups") - internal static let icForwardGroupsSelected = ImageAsset(name: "ic_forward_groups_selected") - internal static let icForwardSendIcon = ImageAsset(name: "ic_forward_send_icon") - } - internal enum GroupStorage { - internal static let icAudioFormat = ImageAsset(name: "ic_audio_format") - internal static let icUnknownFormat = ImageAsset(name: "ic_unknown_format") - } + internal static let avaPlaceholder = ImageAsset(name: "ava_placeholder") + internal static let circleRed = ImageAsset(name: "circle_red") + internal static let contactSeparatop = ImageAsset(name: "contact_separatop") + internal static let icForwardContacts = ImageAsset(name: "ic_forward_contacts") + internal static let icForwardContactsSelected = ImageAsset(name: "ic_forward_contacts_selected") + internal static let icForwardGroups = ImageAsset(name: "ic_forward_groups") + internal static let icForwardGroupsSelected = ImageAsset(name: "ic_forward_groups_selected") + internal static let icForwardSendIcon = ImageAsset(name: "ic_forward_send_icon") + internal static let icAudioFormat = ImageAsset(name: "ic_audio_format") + internal static let icUnknownFormat = ImageAsset(name: "ic_unknown_format") internal static let group1 = ImageAsset(name: "Group_1") internal static let group2 = ImageAsset(name: "Group_2") + internal static let iconsGeneralIcAcceptCall = ImageAsset(name: "Icons_General_ic_accept_call") internal static let iconsGeneralIcClose = ImageAsset(name: "Icons_General_ic_close") + internal static let iconsGeneralIcEmail = ImageAsset(name: "Icons_General_ic_email") internal static let iconsGeneralIcEye = ImageAsset(name: "Icons_General_ic_eye") + internal static let iconsGeneralIcGoogle = ImageAsset(name: "Icons_General_ic_google") internal static let iconsGeneralIcGreyClose = ImageAsset(name: "Icons_General_ic_grey_close") internal static let iconsGeneralIcTranslate = ImageAsset(name: "Icons_General_ic_translate") - internal enum LastMessageType { - internal static let icLastContact = ImageAsset(name: "ic_last_contact") - internal static let icLastEmoji = ImageAsset(name: "ic_last_emoji") - internal static let icLastFile = ImageAsset(name: "ic_last_file") - internal static let icLastLocation = ImageAsset(name: "ic_last_location") - internal static let icLastPhoto = ImageAsset(name: "ic_last_photo") - internal static let icLastVideo = ImageAsset(name: "ic_last_video") - internal static let icLastVideoCall = ImageAsset(name: "ic_last_video_call") - internal static let icLastVoiceMsg = ImageAsset(name: "ic_last_voice_msg") - } - internal enum LinkField { - internal static let icRefresh = ImageAsset(name: "ic_refresh") - } - internal enum Logo { - internal static let authLightLogo = ImageAsset(name: "auth_light_logo") - internal static let darkLogo = ImageAsset(name: "dark_logo") - internal static let icLogo = ImageAsset(name: "ic_logo") - internal static let lightLogo = ImageAsset(name: "light_logo") - internal static let logo1 = ImageAsset(name: "logo-1") - internal static let logo = ImageAsset(name: "logo") - } - internal enum MainWheel { - internal enum Chat { - internal static let disableFamily = ImageAsset(name: "disable_family") - internal static let disableFavorites = ImageAsset(name: "disable_favorites") - internal static let disableWork = ImageAsset(name: "disable_work") - internal static let family = ImageAsset(name: "family") - internal static let favorites = ImageAsset(name: "favorites") - internal static let list = ImageAsset(name: "list") - internal static let work = ImageAsset(name: "work") - } - internal enum NewContact { - internal static let history = ImageAsset(name: "History") - internal static let qrCode = ImageAsset(name: "QR code") - internal static let username = ImageAsset(name: "Username") - internal static let byContacts = ImageAsset(name: "by_contacts") - internal static let code = ImageAsset(name: "code") - internal static let number = ImageAsset(name: "number") - } - internal enum Actions { - internal static let file = ImageAsset(name: "File") - internal static let contact = ImageAsset(name: "contact") - internal static let event = ImageAsset(name: "event") - internal static let galery = ImageAsset(name: "galery") - internal static let location = ImageAsset(name: "location") - internal static let photos = ImageAsset(name: "photos") - internal static let videoCall = ImageAsset(name: "video_call") - internal static let voiceCall = ImageAsset(name: "voice_call") - } - internal static let actions = ImageAsset(name: "actions") - internal static let chats = ImageAsset(name: "chats") - internal static let contacts = ImageAsset(name: "contacts") - internal static let disableGroups = ImageAsset(name: "disable_groups") - internal static let disableNewGroup = ImageAsset(name: "disable_new group") - internal static let disableSearch = ImageAsset(name: "disable_search") - internal static let fav = ImageAsset(name: "fav") - internal static let groups = ImageAsset(name: "groups") - internal static let icCloseWheel = ImageAsset(name: "ic_close_wheel") - internal static let myProfile = ImageAsset(name: "my_profile") - internal enum NewGroup { - internal static let done = ImageAsset(name: "done") - } - internal static let newContact = ImageAsset(name: "new_contact") - internal static let newGroup = ImageAsset(name: "new_group") - internal static let nextBttn = ImageAsset(name: "next_bttn") - internal static let search = ImageAsset(name: "search") - internal static let send = ImageAsset(name: "send") - internal static let settings = ImageAsset(name: "settings") - internal static let wheelInactive = ImageAsset(name: "wheel_inactive") - } - internal enum Marketplace { - internal static let marketplaceSwapButton = ImageAsset(name: "marketplace_swap_button") - internal enum Menu { - internal enum Access { - internal static let marketplaceApps = ImageAsset(name: "marketplace_apps") - internal static let marketplaceBots = ImageAsset(name: "marketplace_bots") - internal static let marketplaceGroupsChannels = ImageAsset(name: "marketplace_groups_channels") - } - internal enum Freelance { - internal static let marketplaceDesign = ImageAsset(name: "marketplace_design") - internal static let marketplaceInterpretation = ImageAsset(name: "marketplace_interpretation") - internal static let marketplaceSupport = ImageAsset(name: "marketplace_support") - } - internal enum Main { - internal static let marketplaceAccess = ImageAsset(name: "marketplace_access") - internal static let marketplaceFreelance = ImageAsset(name: "marketplace_freelance") - internal static let marketplaceVirtualGoods = ImageAsset(name: "marketplace_virtual_goods") - } - internal enum VirtualGoods { - internal static let marketplaceMediaContent = ImageAsset(name: "marketplace_media_content") - internal static let marketplaceSticker = ImageAsset(name: "marketplace_sticker") - } - } - } - internal enum MentionIndicator { - internal static let icChatMentionIndicator = ImageAsset(name: "ic_chat_mention_indicator") - internal static let icHomeMentionIndicator = ImageAsset(name: "ic_home_mention_indicator") - } - internal enum Messages { - internal enum ContextMenu { - internal static let contextMenuNext = ImageAsset(name: "contextMenuNext") - internal static let contextMenuPrevious = ImageAsset(name: "contextMenuPrevious") - internal static let icAnotherLanguageTranscribeContextMenu = ImageAsset(name: "ic_another_language_transcribe_context_menu") - internal static let icAnotherLanguageTranslateContextMenu = ImageAsset(name: "ic_another_language_translate_context_menu") - internal static let icCopyContextMenu = ImageAsset(name: "ic_copy_context_menu") - internal static let icDeleteContextMenu = ImageAsset(name: "ic_delete_context_menu") - internal static let icEditContextMenu = ImageAsset(name: "ic_edit_context_menu") - internal static let icForwardContextMenu = ImageAsset(name: "ic_forward_context_menu") - internal static let icReplyContextMenu = ImageAsset(name: "ic_reply_context_menu") - internal static let icSaveToDownloadsContextMenu = ImageAsset(name: "ic_save_to_downloads_context_menu") - internal static let icSaveToGalleryContextMenu = ImageAsset(name: "ic_save_to_gallery_context_menu") - internal static let icShareContextMenu = ImageAsset(name: "ic_share_context_menu") - internal static let icStarContextMenu = ImageAsset(name: "ic_star_context_menu") - internal static let icTranscribeContextMenu = ImageAsset(name: "ic_transcribe_context_menu") - internal static let icTranslateContextMenu = ImageAsset(name: "ic_translate_context_menu") - } - internal enum Counter { - internal static let icEyeBubleGray = ImageAsset(name: "ic_eye_buble_gray") - internal static let icReplyBubbleGray = ImageAsset(name: "ic_reply_bubble_gray") - internal static let icShareBubbleGray = ImageAsset(name: "ic_share_bubble_gray") - } - internal enum Delivery { - internal static let iicMessageUnsent = ImageAsset(name: "Iic_message_unsent") - internal static let icMessageRead = ImageAsset(name: "ic_message_read") - internal static let icMessageSent = ImageAsset(name: "ic_message_sent") - } - internal enum Loading { - internal static let icBtnStartDownload = ImageAsset(name: "ic_btn_start_download") - internal static let icBtnStopDownload = ImageAsset(name: "ic_btn_stop_download") - internal static let startDownload = ImageAsset(name: "start_download") - internal static let stopDownload = ImageAsset(name: "stop_download") - } - internal enum Media { - internal static let icBtnPlay = ImageAsset(name: "ic_btn_play") - internal static let icPauseBubble = ImageAsset(name: "ic_pause_bubble") - internal static let icPlayBubble = ImageAsset(name: "ic_play_bubble") - } - } - internal enum NewFolder { - internal static let delayMessage = ImageAsset(name: "Delay-Message") - internal static let emoji = ImageAsset(name: "Emoji") - internal static let acceptAudioBttn = ImageAsset(name: "accept-audio-bttn") - internal static let acceptBttn = ImageAsset(name: "accept-bttn") - internal static let activeMap = ImageAsset(name: "active-map") - internal static let cancelCall = ImageAsset(name: "cancel-call") - internal static let cancelVideoCall = ImageAsset(name: "cancel_video_call") - internal static let centerIcon = ImageAsset(name: "center-icon") - internal static let changePhoto = ImageAsset(name: "change-photo") - internal static let country = ImageAsset(name: "country") - internal static let delay = ImageAsset(name: "delay") - internal static let detailsMap = ImageAsset(name: "details_map") - internal static let editIcon1 = ImageAsset(name: "edit-icon-1") - internal static let evntsInactive = ImageAsset(name: "evnts-inactive") - internal static let familyInactive = ImageAsset(name: "family-inactive") - internal static let favoritesActive = ImageAsset(name: "favorites-active") - internal static let favoritesInactive = ImageAsset(name: "favorites-inactive") - internal static let icIncomingCallWhite = ImageAsset(name: "ic_incoming_call_white") - internal static let icOutgoingCallWhite = ImageAsset(name: "ic_outgoing_call_white") - internal static let iconNinja = ImageAsset(name: "icon_ninja") - internal static let incomingDark = ImageAsset(name: "incoming_dark") - internal static let incomingLight = ImageAsset(name: "incoming_light") - internal static let incomingVideoDark = ImageAsset(name: "incoming_video_dark") - internal static let incomingVideoLight = ImageAsset(name: "incoming_video_light") - internal static let lastVideoCall = ImageAsset(name: "last-Video-Call") - internal static let lastLocation = ImageAsset(name: "last-location") - internal static let lastPhoto = ImageAsset(name: "last-photo") - internal static let lastVideo = ImageAsset(name: "last-video") - internal static let lastVoiceMsg = ImageAsset(name: "last-voice-msg") - internal static let messageBttn = ImageAsset(name: "message-bttn") - internal static let microphoneBttn = ImageAsset(name: "microphone-bttn") - internal static let nextBttn = ImageAsset(name: "next-bttn") - internal static let outgoingDark = ImageAsset(name: "outgoing_dark") - internal static let outgoingLight = ImageAsset(name: "outgoing_light") - internal static let outgoingVideoDark = ImageAsset(name: "outgoing_video_dark") - internal static let outgoingVideoLight = ImageAsset(name: "outgoing_video_light") - internal static let phoneNumber = ImageAsset(name: "phone-number") - internal static let qrCode = ImageAsset(name: "qr-code") - internal static let recBar = ImageAsset(name: "rec-bar") - internal static let recProcess = ImageAsset(name: "rec-process") - internal static let recLight = ImageAsset(name: "rec_light") - internal static let schedule = ImageAsset(name: "schedule") - internal static let searchInactive = ImageAsset(name: "search-inactive") - internal static let sendBttn = ImageAsset(name: "send-bttn") - internal static let separators = ImageAsset(name: "separators") - internal static let speakerBttn = ImageAsset(name: "speaker-bttn") - internal static let switchCameraBttn = ImageAsset(name: "switch-camera-bttn") - internal static let textBar = ImageAsset(name: "text-bar") - internal static let workInactive = ImageAsset(name: "work-inactive") - } + internal static let icLastContact = ImageAsset(name: "ic_last_contact") + internal static let icLastEmoji = ImageAsset(name: "ic_last_emoji") + internal static let icLastFile = ImageAsset(name: "ic_last_file") + internal static let icLastLocation = ImageAsset(name: "ic_last_location") + internal static let icLastPhoto = ImageAsset(name: "ic_last_photo") + internal static let icLastVideo = ImageAsset(name: "ic_last_video") + internal static let icLastVideoCall = ImageAsset(name: "ic_last_video_call") + internal static let icLastVoiceMsg = ImageAsset(name: "ic_last_voice_msg") + internal static let icRefresh = ImageAsset(name: "ic_refresh") + internal static let authLightLogo = ImageAsset(name: "auth_light_logo") + internal static let darkLogo = ImageAsset(name: "dark_logo") + internal static let icLogo = ImageAsset(name: "ic_logo") + internal static let lightLogo = ImageAsset(name: "light_logo") + internal static let logo1 = ImageAsset(name: "logo-1") + internal static let logo = ImageAsset(name: "logo") + internal static let disableFamily = ImageAsset(name: "disable_family") + internal static let disableFavorites = ImageAsset(name: "disable_favorites") + internal static let disableWork = ImageAsset(name: "disable_work") + internal static let family = ImageAsset(name: "family") + internal static let favorites = ImageAsset(name: "favorites") + internal static let list = ImageAsset(name: "list") + internal static let work = ImageAsset(name: "work") + internal static let history = ImageAsset(name: "History") + internal static let qrCode = ImageAsset(name: "QR code") + internal static let username = ImageAsset(name: "Username") + internal static let byContacts = ImageAsset(name: "by_contacts") + internal static let code = ImageAsset(name: "code") + internal static let number = ImageAsset(name: "number") + internal static let file = ImageAsset(name: "File") + internal static let contact = ImageAsset(name: "contact") + internal static let event = ImageAsset(name: "event") + internal static let galery = ImageAsset(name: "galery") + internal static let location = ImageAsset(name: "location") + internal static let photos = ImageAsset(name: "photos") + internal static let videoCall = ImageAsset(name: "video_call") + internal static let voiceCall = ImageAsset(name: "voice_call") + internal static let actions = ImageAsset(name: "actions") + internal static let chats = ImageAsset(name: "chats") + internal static let contacts = ImageAsset(name: "contacts") + internal static let disableGroups = ImageAsset(name: "disable_groups") + internal static let disableNewGroup = ImageAsset(name: "disable_new group") + internal static let disableSearch = ImageAsset(name: "disable_search") + internal static let fav = ImageAsset(name: "fav") + internal static let groups = ImageAsset(name: "groups") + internal static let icCloseWheel = ImageAsset(name: "ic_close_wheel") + internal static let myProfile = ImageAsset(name: "my_profile") + internal static let done = ImageAsset(name: "done") + internal static let newContact = ImageAsset(name: "new_contact") + internal static let newGroup = ImageAsset(name: "new_group") + internal static let nextBttn1 = ImageAsset(name: "next_bttn1") + internal static let search = ImageAsset(name: "search") + internal static let send = ImageAsset(name: "send") + internal static let settings = ImageAsset(name: "settings") + internal static let wheelInactive = ImageAsset(name: "wheel_inactive") + internal static let marketplaceSwapButton = ImageAsset(name: "marketplace_swap_button") + internal static let marketplaceApps = ImageAsset(name: "marketplace_apps") + internal static let marketplaceBots = ImageAsset(name: "marketplace_bots") + internal static let marketplaceGroupsChannels = ImageAsset(name: "marketplace_groups_channels") + internal static let marketplaceDesign = ImageAsset(name: "marketplace_design") + internal static let marketplaceInterpretation = ImageAsset(name: "marketplace_interpretation") + internal static let marketplaceSupport = ImageAsset(name: "marketplace_support") + internal static let marketplaceAccess = ImageAsset(name: "marketplace_access") + internal static let marketplaceFreelance = ImageAsset(name: "marketplace_freelance") + internal static let marketplaceVirtualGoods = ImageAsset(name: "marketplace_virtual_goods") + internal static let marketplaceMediaContent = ImageAsset(name: "marketplace_media_content") + internal static let marketplaceSticker = ImageAsset(name: "marketplace_sticker") + internal static let icChatMentionIndicator = ImageAsset(name: "ic_chat_mention_indicator") + internal static let icHomeMentionIndicator = ImageAsset(name: "ic_home_mention_indicator") + internal static let contextMenuNext = ImageAsset(name: "contextMenuNext") + internal static let contextMenuPrevious = ImageAsset(name: "contextMenuPrevious") + internal static let icAnotherLanguageTranscribeContextMenu = ImageAsset(name: "ic_another_language_transcribe_context_menu") + internal static let icAnotherLanguageTranslateContextMenu = ImageAsset(name: "ic_another_language_translate_context_menu") + internal static let icCopyContextMenu = ImageAsset(name: "ic_copy_context_menu") + internal static let icDeleteContextMenu = ImageAsset(name: "ic_delete_context_menu") + internal static let icEditContextMenu = ImageAsset(name: "ic_edit_context_menu") + internal static let icForwardContextMenu = ImageAsset(name: "ic_forward_context_menu") + internal static let icReplyContextMenu = ImageAsset(name: "ic_reply_context_menu") + internal static let icSaveToDownloadsContextMenu = ImageAsset(name: "ic_save_to_downloads_context_menu") + internal static let icSaveToGalleryContextMenu = ImageAsset(name: "ic_save_to_gallery_context_menu") + internal static let icShareContextMenu = ImageAsset(name: "ic_share_context_menu") + internal static let icStarContextMenu = ImageAsset(name: "ic_star_context_menu") + internal static let icTranscribeContextMenu = ImageAsset(name: "ic_transcribe_context_menu") + internal static let icTranslateContextMenu = ImageAsset(name: "ic_translate_context_menu") + internal static let icEyeBubleGray = ImageAsset(name: "ic_eye_buble_gray") + internal static let icReplyBubbleGray = ImageAsset(name: "ic_reply_bubble_gray") + internal static let icShareBubbleGray = ImageAsset(name: "ic_share_bubble_gray") + internal static let iicMessageUnsent = ImageAsset(name: "Iic_message_unsent") + internal static let icMessageRead = ImageAsset(name: "ic_message_read") + internal static let icMessageSent = ImageAsset(name: "ic_message_sent") + internal static let icBtnStartDownload = ImageAsset(name: "ic_btn_start_download") + internal static let icBtnStopDownload = ImageAsset(name: "ic_btn_stop_download") + internal static let startDownload = ImageAsset(name: "start_download") + internal static let stopDownload = ImageAsset(name: "stop_download") + internal static let icBtnPlay = ImageAsset(name: "ic_btn_play") + internal static let icPauseBubble = ImageAsset(name: "ic_pause_bubble") + internal static let icPlayBubble = ImageAsset(name: "ic_play_bubble") + internal static let delayMessage = ImageAsset(name: "Delay-Message") + internal static let emoji = ImageAsset(name: "Emoji") + internal static let acceptAudioBttn = ImageAsset(name: "accept-audio-bttn") + internal static let acceptBttn = ImageAsset(name: "accept-bttn") + internal static let activeMap = ImageAsset(name: "active-map") + internal static let cancelCall = ImageAsset(name: "cancel-call") + internal static let cancelVideoCall = ImageAsset(name: "cancel_video_call") + internal static let centerIcon = ImageAsset(name: "center-icon") + internal static let changePhoto = ImageAsset(name: "change-photo") + internal static let country = ImageAsset(name: "country") + internal static let delay = ImageAsset(name: "delay") + internal static let detailsMap = ImageAsset(name: "details_map") + internal static let editIcon1 = ImageAsset(name: "edit-icon-1") + internal static let evntsInactive = ImageAsset(name: "evnts-inactive") + internal static let familyInactive = ImageAsset(name: "family-inactive") + internal static let favoritesActive = ImageAsset(name: "favorites-active") + internal static let favoritesInactive = ImageAsset(name: "favorites-inactive") + internal static let icIncomingCallWhite = ImageAsset(name: "ic_incoming_call_white") + internal static let icOutgoingCallWhite = ImageAsset(name: "ic_outgoing_call_white") + internal static let iconNinja = ImageAsset(name: "icon_ninja") + internal static let incomingDark = ImageAsset(name: "incoming_dark") + internal static let incomingLight = ImageAsset(name: "incoming_light") + internal static let incomingVideoDark = ImageAsset(name: "incoming_video_dark") + internal static let incomingVideoLight = ImageAsset(name: "incoming_video_light") + internal static let lastVideoCall = ImageAsset(name: "last-Video-Call") + internal static let lastLocation = ImageAsset(name: "last-location") + internal static let lastPhoto = ImageAsset(name: "last-photo") + internal static let lastVideo = ImageAsset(name: "last-video") + internal static let lastVoiceMsg = ImageAsset(name: "last-voice-msg") + internal static let messageBttn = ImageAsset(name: "message-bttn") + internal static let microphoneBttn = ImageAsset(name: "microphone-bttn") + internal static let nextBttn = ImageAsset(name: "next-bttn") + internal static let outgoingDark = ImageAsset(name: "outgoing_dark") + internal static let outgoingLight = ImageAsset(name: "outgoing_light") + internal static let outgoingVideoDark = ImageAsset(name: "outgoing_video_dark") + internal static let outgoingVideoLight = ImageAsset(name: "outgoing_video_light") + internal static let phoneNumber = ImageAsset(name: "phone-number") + internal static let qrCode1 = ImageAsset(name: "qr-code1") + internal static let recBar = ImageAsset(name: "rec-bar") + internal static let recProcess = ImageAsset(name: "rec-process") + internal static let recLight = ImageAsset(name: "rec_light") + internal static let schedule = ImageAsset(name: "schedule") + internal static let searchInactive = ImageAsset(name: "search-inactive") + internal static let sendBttn = ImageAsset(name: "send-bttn") + internal static let separators = ImageAsset(name: "separators") + internal static let speakerBttn = ImageAsset(name: "speaker-bttn") + internal static let switchCameraBttn = ImageAsset(name: "switch-camera-bttn") + internal static let textBar = ImageAsset(name: "text-bar") + internal static let workInactive = ImageAsset(name: "work-inactive") internal static let oval14 = ImageAsset(name: "Oval_14") internal static let oval17 = ImageAsset(name: "Oval_17") internal static let oval6 = ImageAsset(name: "Oval_6") internal static let sendAsFile = ImageAsset(name: "SendAsFile") - internal enum Stickers { - internal static let stickerStub = ImageAsset(name: "sticker_stub") - internal static let stickersIcAdd = ImageAsset(name: "stickers_ic_add") - internal static let stickersIcDefaultPack = ImageAsset(name: "stickers_ic_default_pack") - internal static let stickersIcRecent = ImageAsset(name: "stickers_ic_recent") - internal static let stickersIcSearch = ImageAsset(name: "stickers_ic_search") - } - internal enum Wallet { - internal static let icArrowTransferDown = ImageAsset(name: "ic_arrow_transfer_down") - internal static let icArrowTransferUp = ImageAsset(name: "ic_arrow_transfer_up") - internal static let icBtc = ImageAsset(name: "ic_btc") - internal static let icEos = ImageAsset(name: "ic_eos") - internal static let icNyn = ImageAsset(name: "ic_nyn") - internal static let icPay = ImageAsset(name: "ic_pay") - internal static let icWallet = ImageAsset(name: "ic_wallet") - } - internal enum Wheel { - internal enum Button { - internal static let btnWheelInactive = ImageAsset(name: "btn_wheel_inactive") - internal static let btnWheelInactiveLight = ImageAsset(name: "btn_wheel_inactive_light") - internal static let wheel = ImageAsset(name: "wheel") - } - internal enum Placeholders { - internal static let icBrokenImagePlaceholder = ImageAsset(name: "ic_broken_image_placeholder") - internal static let icEmptyImagePlaceholder = ImageAsset(name: "ic_empty_image_placeholder") - internal static let icEmptyLocationPlaceholder = ImageAsset(name: "ic_empty_location_placeholder") - } - internal enum WheelItems { - internal static let icActions = ImageAsset(name: "ic_actions") - internal static let icByContacts = ImageAsset(name: "ic_by_contacts") - internal static let icByNumber = ImageAsset(name: "ic_by_number") - internal static let icByPassword = ImageAsset(name: "ic_by_password") - internal static let icByQrCode = ImageAsset(name: "ic_by_qr_code") - internal static let icByUsername = ImageAsset(name: "ic_by_username") - internal static let icCalls = ImageAsset(name: "ic_calls") - internal static let icCamera = ImageAsset(name: "ic_camera") - internal static let icChannelInactive = ImageAsset(name: "ic_channel_inactive") - internal static let icChannelNew = ImageAsset(name: "ic_channel_new") - internal static let icChats = ImageAsset(name: "ic_chats") - internal static let icContacts = ImageAsset(name: "ic_contacts") - internal static let icEditProfile = ImageAsset(name: "ic_edit_profile") - internal static let icFamily = ImageAsset(name: "ic_family") - internal static let icFile = ImageAsset(name: "ic_file") - internal static let icFriends = ImageAsset(name: "ic_friends") - internal static let icGallery = ImageAsset(name: "ic_gallery") - internal static let icGroupCall = ImageAsset(name: "ic_group_call") - internal static let icGroupCalls = ImageAsset(name: "ic_group_calls") - internal static let icGroupSettings = ImageAsset(name: "ic_group_settings") - internal static let icGroupVideo = ImageAsset(name: "ic_group_video") - internal static let icGroups = ImageAsset(name: "ic_groups") - internal static let icHistory = ImageAsset(name: "ic_history") - internal static let icHome = ImageAsset(name: "ic_home") - internal static let icList = ImageAsset(name: "ic_list") - internal static let icLocation = ImageAsset(name: "ic_location") - internal static let icMyself = ImageAsset(name: "ic_myself") - internal static let icNew = ImageAsset(name: "ic_new") - internal static let icNewAudioCall = ImageAsset(name: "ic_new_audio_call") - internal static let icNewChat = ImageAsset(name: "ic_new_chat") - internal static let icNewContact = ImageAsset(name: "ic_new_contact") - internal static let icNewGroup = ImageAsset(name: "ic_new_group") - internal static let icNewPhoneCall = ImageAsset(name: "ic_new_phone_call") - internal static let icNewVideoCall = ImageAsset(name: "ic_new_video_call") - internal static let icOptions = ImageAsset(name: "ic_options") - internal static let icRecents = ImageAsset(name: "ic_recents") - internal static let icSearch = ImageAsset(name: "ic_search") - internal static let icStarred = ImageAsset(name: "ic_starred") - internal static let icVideo = ImageAsset(name: "ic_video") - internal static let icVideoIndicator = ImageAsset(name: "ic_video_indicator") - internal static let icWork = ImageAsset(name: "ic_work") - } - internal enum WheelPosition { - internal static let wheelLeftImage = ImageAsset(name: "wheel_left_image") - internal static let wheelRightImage = ImageAsset(name: "wheel_right_image") - } - } - internal enum WheelPosition { - internal static let wheelLeftImage = ImageAsset(name: "wheel_left_image") - internal static let wheelRightImage = ImageAsset(name: "wheel_right_image") - } + internal static let stickerStub = ImageAsset(name: "sticker_stub") + internal static let stickersIcAdd = ImageAsset(name: "stickers_ic_add") + internal static let stickersIcDefaultPack = ImageAsset(name: "stickers_ic_default_pack") + internal static let stickersIcRecent = ImageAsset(name: "stickers_ic_recent") + internal static let stickersIcSearch = ImageAsset(name: "stickers_ic_search") + internal static let icArrowTransferDown = ImageAsset(name: "ic_arrow_transfer_down") + internal static let icArrowTransferUp = ImageAsset(name: "ic_arrow_transfer_up") + internal static let icBtc = ImageAsset(name: "ic_btc") + internal static let icEos = ImageAsset(name: "ic_eos") + internal static let icNyn = ImageAsset(name: "ic_nyn") + internal static let icPay = ImageAsset(name: "ic_pay") + internal static let icWallet = ImageAsset(name: "ic_wallet") + internal static let btnWheelInactive = ImageAsset(name: "btn_wheel_inactive") + internal static let btnWheelInactiveLight = ImageAsset(name: "btn_wheel_inactive_light") + internal static let wheel = ImageAsset(name: "wheel") + internal static let icBrokenImagePlaceholder = ImageAsset(name: "ic_broken_image_placeholder") + internal static let icEmptyImagePlaceholder = ImageAsset(name: "ic_empty_image_placeholder") + internal static let icEmptyLocationPlaceholder = ImageAsset(name: "ic_empty_location_placeholder") + internal static let icActions = ImageAsset(name: "ic_actions") + internal static let icByContacts = ImageAsset(name: "ic_by_contacts") + internal static let icByNumber = ImageAsset(name: "ic_by_number") + internal static let icByPassword = ImageAsset(name: "ic_by_password") + internal static let icByQrCode = ImageAsset(name: "ic_by_qr_code") + internal static let icByUsername = ImageAsset(name: "ic_by_username") + internal static let icCalls = ImageAsset(name: "ic_calls") + internal static let icCamera = ImageAsset(name: "ic_camera") + internal static let icChannelInactive = ImageAsset(name: "ic_channel_inactive") + internal static let icChannelNew = ImageAsset(name: "ic_channel_new") + internal static let icChats = ImageAsset(name: "ic_chats") + internal static let icContacts = ImageAsset(name: "ic_contacts") + internal static let icEditProfile = ImageAsset(name: "ic_edit_profile") + internal static let icFamily = ImageAsset(name: "ic_family") + internal static let icFile = ImageAsset(name: "ic_file") + internal static let icFriends = ImageAsset(name: "ic_friends") + internal static let icGallery = ImageAsset(name: "ic_gallery") + internal static let icGroupCall = ImageAsset(name: "ic_group_call") + internal static let icGroupCalls = ImageAsset(name: "ic_group_calls") + internal static let icGroupSettings = ImageAsset(name: "ic_group_settings") + internal static let icGroupVideo = ImageAsset(name: "ic_group_video") + internal static let icGroups = ImageAsset(name: "ic_groups") + internal static let icHistory = ImageAsset(name: "ic_history") + internal static let icHome = ImageAsset(name: "ic_home") + internal static let icList = ImageAsset(name: "ic_list") + internal static let icLocation = ImageAsset(name: "ic_location") + internal static let icMyself = ImageAsset(name: "ic_myself") + internal static let icNew = ImageAsset(name: "ic_new") + internal static let icNewAudioCall = ImageAsset(name: "ic_new_audio_call") + internal static let icNewChat = ImageAsset(name: "ic_new_chat") + internal static let icNewContact = ImageAsset(name: "ic_new_contact") + internal static let icNewGroup = ImageAsset(name: "ic_new_group") + internal static let icNewPhoneCall = ImageAsset(name: "ic_new_phone_call") + internal static let icNewVideoCall = ImageAsset(name: "ic_new_video_call") + internal static let icOptions = ImageAsset(name: "ic_options") + internal static let icRecents = ImageAsset(name: "ic_recents") + internal static let icSearch = ImageAsset(name: "ic_search") + internal static let icStarred = ImageAsset(name: "ic_starred") + internal static let icVideo = ImageAsset(name: "ic_video") + internal static let icVideoIndicator = ImageAsset(name: "ic_video_indicator") + internal static let icWork = ImageAsset(name: "ic_work") + internal static let wheelLeftImage = ImageAsset(name: "wheel_left_image") + internal static let wheelRightImage = ImageAsset(name: "wheel_right_image") internal static let arrowCollapse = ImageAsset(name: "arrow_collapse") internal static let arrowExpand = ImageAsset(name: "arrow_expand") internal static let barsTabBarOverridesCenterButtonBtnPlayVideo = ImageAsset(name: "bars_tab_bar_overrides_center_button_btn_play_video") @@ -402,7 +301,6 @@ internal enum Asset { internal static let icBack = ImageAsset(name: "ic_back") internal static let icBackNavigation = ImageAsset(name: "ic_back_navigation") internal static let icBottomArrow = ImageAsset(name: "ic_bottom_arrow") - internal static let icCameraFrame = ImageAsset(name: "ic_camera_frame") internal static let icChangeCameraIos = ImageAsset(name: "ic_change_camera_ios") internal static let icCheckmarkRed = ImageAsset(name: "ic_checkmark_red") internal static let icClock = ImageAsset(name: "ic_clock") @@ -413,6 +311,8 @@ internal enum Asset { internal static let icDeleteRedCircle = ImageAsset(name: "ic_delete_red_circle") internal static let icEditDone = ImageAsset(name: "ic_edit_done") internal static let icEmailStorage = ImageAsset(name: "ic_email_storage") + internal static let icEmptyAvatar = ImageAsset(name: "ic_empty_avatar") + internal static let icFacebook = ImageAsset(name: "ic_facebook") internal static let icFlashlightAuto = ImageAsset(name: "ic_flashlight_auto") internal static let icFlashlightOff = ImageAsset(name: "ic_flashlight_off") internal static let icFlashlightOn = ImageAsset(name: "ic_flashlight_on") @@ -427,14 +327,12 @@ internal enum Asset { internal static let icMarketplaceWheelContextMenu = ImageAsset(name: "ic_marketplace_wheel_context_menu") internal static let icMic = ImageAsset(name: "ic_mic") internal static let icMicDarkGray = ImageAsset(name: "ic_mic_dark_gray") - internal static let icNewGroup = ImageAsset(name: "ic_new_group") internal static let icParticipantsSearch = ImageAsset(name: "ic_participants_search") internal static let icPhoneStorage = ImageAsset(name: "ic_phone_storage") internal static let icPhotoPlaceholder = ImageAsset(name: "ic_photo_placeholder") internal static let icPlaceStar = ImageAsset(name: "ic_place_star") internal static let icQrCode = ImageAsset(name: "ic_qr_code") internal static let icScheduledMsgCheck = ImageAsset(name: "ic_scheduled_msg_check") - internal static let icSearch = ImageAsset(name: "ic_search") internal static let icSearchEmpty = ImageAsset(name: "ic_search_empty") internal static let icSendAsFile = ImageAsset(name: "ic_send_as_file") internal static let icSoundOff = ImageAsset(name: "ic_sound_off") @@ -458,30 +356,29 @@ internal enum Asset { internal static let imgEmptyStatesP2p = ImageAsset(name: "img_empty_states_p2p") internal static let imgEmptyStatesScheduled = ImageAsset(name: "img_empty_states_scheduled") internal static let imgEmptyStatesStarred = ImageAsset(name: "img_empty_states_starred") - internal enum Input { - internal static let arrowUp = ImageAsset(name: "arrow_up") - internal static let delayRed = ImageAsset(name: "delay_red") - internal static let editMsgButton = ImageAsset(name: "edit_msg_button") - internal static let icDelete = ImageAsset(name: "ic_delete") - internal static let icPause = ImageAsset(name: "ic_pause") - internal static let icPlay = ImageAsset(name: "ic_play") - internal static let icSend = ImageAsset(name: "ic_send") - internal static let icVoice = ImageAsset(name: "ic_voice") - internal static let inputKeyboard = ImageAsset(name: "input_keyboard") - internal static let inputLeftEmojiButton = ImageAsset(name: "input_left_emoji_button") - internal static let inputLeftKeyboardButton = ImageAsset(name: "input_left_keyboard_button") - internal static let inputMicrophone = ImageAsset(name: "input_microphone") - internal static let pause = ImageAsset(name: "pause") - internal static let playBtn = ImageAsset(name: "play_btn") - internal static let recBackground = ImageAsset(name: "rec_background") - internal static let roundedRedBig = ImageAsset(name: "rounded_red_big") - internal static let sendButton = ImageAsset(name: "send_button") - internal static let trash = ImageAsset(name: "trash") - internal static let voiceButton = ImageAsset(name: "voice_button") - } + internal static let arrowUp = ImageAsset(name: "arrow_up") + internal static let delayRed = ImageAsset(name: "delay_red") + internal static let editMsgButton = ImageAsset(name: "edit_msg_button") + internal static let icDelete = ImageAsset(name: "ic_delete") + internal static let icPause = ImageAsset(name: "ic_pause") + internal static let icPlay = ImageAsset(name: "ic_play") + internal static let icSend = ImageAsset(name: "ic_send") + internal static let icVoice = ImageAsset(name: "ic_voice") + internal static let inputKeyboard = ImageAsset(name: "input_keyboard") + internal static let inputLeftEmojiButton = ImageAsset(name: "input_left_emoji_button") + internal static let inputLeftKeyboardButton = ImageAsset(name: "input_left_keyboard_button") + internal static let inputMicrophone = ImageAsset(name: "input_microphone") + internal static let pause = ImageAsset(name: "pause") + internal static let playBtn = ImageAsset(name: "play_btn") + internal static let recBackground = ImageAsset(name: "rec_background") + internal static let roundedRedBig = ImageAsset(name: "rounded_red_big") + internal static let sendButton = ImageAsset(name: "send_button") + internal static let trash = ImageAsset(name: "trash") + internal static let voiceButton = ImageAsset(name: "voice_button") internal static let inputLeftButton = ImageAsset(name: "input_left_button") internal static let leftButton = ImageAsset(name: "left_button") internal static let lock = ImageAsset(name: "lock") + internal static let logo2 = ImageAsset(name: "logo-2") internal static let maximaze = ImageAsset(name: "maximaze") internal static let minimaze = ImageAsset(name: "minimaze") internal static let myLocation = ImageAsset(name: "my_location") @@ -496,416 +393,86 @@ internal enum Asset { internal static let transcribed = ImageAsset(name: "transcribed") internal static let translated = ImageAsset(name: "translated") internal static let world = ImageAsset(name: "world") - - // swiftlint:disable trailing_comma - internal static let allColors: [ColorAsset] = [ - ] - internal static let allImages: [ImageAsset] = [ - appIcon, - Background.background, - Background.backgroundLight, - Background.bgSecurityPlaceholderDark, - Background.bgSecurityPlaceholderLight, - Buttons.Checkable.icChecked, - Buttons.Checkable.icPartialySelected, - Buttons.Checkable.icUnchecked, - CallItems.icAcceptCall, - CallItems.icAcceptCallBig, - CallItems.icDeclineCall, - CallItems.icGoToChat, - CallItems.icMoreVoiceCall, - CallItems.icMuteVoiceCall, - CallItems.icOutgoingCall, - CallItems.icPortOut1, - CallItems.icPortOut, - CallItems.icSpeakerOff, - CallItems.icSpeakerOn, - CallItems.icUnmuteVoiceCall, - CallItems.icVideoOffVoiceCall, - CallItems.icVideoOnVoiceCall, - CameraItems.icCameraFrame, - CameraItems.icFlashlightActive, - CameraItems.icFlashlightInactive, - combinedShape1960, - Contacts.avaPlaceholder, - Contacts.circleRed, - Contacts.contactSeparatop, - Forward.icForwardContacts, - Forward.icForwardContactsSelected, - Forward.icForwardGroups, - Forward.icForwardGroupsSelected, - Forward.icForwardSendIcon, - GroupStorage.icAudioFormat, - GroupStorage.icUnknownFormat, - group1, - group2, - iconsGeneralIcClose, - iconsGeneralIcEye, - iconsGeneralIcGreyClose, - iconsGeneralIcTranslate, - LastMessageType.icLastContact, - LastMessageType.icLastEmoji, - LastMessageType.icLastFile, - LastMessageType.icLastLocation, - LastMessageType.icLastPhoto, - LastMessageType.icLastVideo, - LastMessageType.icLastVideoCall, - LastMessageType.icLastVoiceMsg, - LinkField.icRefresh, - Logo.authLightLogo, - Logo.darkLogo, - Logo.icLogo, - Logo.lightLogo, - Logo.logo1, - Logo.logo, - MainWheel.Chat.disableFamily, - MainWheel.Chat.disableFavorites, - MainWheel.Chat.disableWork, - MainWheel.Chat.family, - MainWheel.Chat.favorites, - MainWheel.Chat.list, - MainWheel.Chat.work, - MainWheel.NewContact.history, - MainWheel.NewContact.qrCode, - MainWheel.NewContact.username, - MainWheel.NewContact.byContacts, - MainWheel.NewContact.code, - MainWheel.NewContact.number, - MainWheel.Actions.file, - MainWheel.Actions.contact, - MainWheel.Actions.event, - MainWheel.Actions.galery, - MainWheel.Actions.location, - MainWheel.Actions.photos, - MainWheel.Actions.videoCall, - MainWheel.Actions.voiceCall, - MainWheel.actions, - MainWheel.chats, - MainWheel.contacts, - MainWheel.disableGroups, - MainWheel.disableNewGroup, - MainWheel.disableSearch, - MainWheel.fav, - MainWheel.groups, - MainWheel.icCloseWheel, - MainWheel.myProfile, - MainWheel.NewGroup.done, - MainWheel.newContact, - MainWheel.newGroup, - MainWheel.nextBttn, - MainWheel.search, - MainWheel.send, - MainWheel.settings, - MainWheel.wheelInactive, - Marketplace.marketplaceSwapButton, - Marketplace.Menu.Access.marketplaceApps, - Marketplace.Menu.Access.marketplaceBots, - Marketplace.Menu.Access.marketplaceGroupsChannels, - Marketplace.Menu.Freelance.marketplaceDesign, - Marketplace.Menu.Freelance.marketplaceInterpretation, - Marketplace.Menu.Freelance.marketplaceSupport, - Marketplace.Menu.Main.marketplaceAccess, - Marketplace.Menu.Main.marketplaceFreelance, - Marketplace.Menu.Main.marketplaceVirtualGoods, - Marketplace.Menu.VirtualGoods.marketplaceMediaContent, - Marketplace.Menu.VirtualGoods.marketplaceSticker, - MentionIndicator.icChatMentionIndicator, - MentionIndicator.icHomeMentionIndicator, - Messages.ContextMenu.contextMenuNext, - Messages.ContextMenu.contextMenuPrevious, - Messages.ContextMenu.icAnotherLanguageTranscribeContextMenu, - Messages.ContextMenu.icAnotherLanguageTranslateContextMenu, - Messages.ContextMenu.icCopyContextMenu, - Messages.ContextMenu.icDeleteContextMenu, - Messages.ContextMenu.icEditContextMenu, - Messages.ContextMenu.icForwardContextMenu, - Messages.ContextMenu.icReplyContextMenu, - Messages.ContextMenu.icSaveToDownloadsContextMenu, - Messages.ContextMenu.icSaveToGalleryContextMenu, - Messages.ContextMenu.icShareContextMenu, - Messages.ContextMenu.icStarContextMenu, - Messages.ContextMenu.icTranscribeContextMenu, - Messages.ContextMenu.icTranslateContextMenu, - Messages.Counter.icEyeBubleGray, - Messages.Counter.icReplyBubbleGray, - Messages.Counter.icShareBubbleGray, - Messages.Delivery.iicMessageUnsent, - Messages.Delivery.icMessageRead, - Messages.Delivery.icMessageSent, - Messages.Loading.icBtnStartDownload, - Messages.Loading.icBtnStopDownload, - Messages.Loading.startDownload, - Messages.Loading.stopDownload, - Messages.Media.icBtnPlay, - Messages.Media.icPauseBubble, - Messages.Media.icPlayBubble, - NewFolder.delayMessage, - NewFolder.emoji, - NewFolder.acceptAudioBttn, - NewFolder.acceptBttn, - NewFolder.activeMap, - NewFolder.cancelCall, - NewFolder.cancelVideoCall, - NewFolder.centerIcon, - NewFolder.changePhoto, - NewFolder.country, - NewFolder.delay, - NewFolder.detailsMap, - NewFolder.editIcon1, - NewFolder.evntsInactive, - NewFolder.familyInactive, - NewFolder.favoritesActive, - NewFolder.favoritesInactive, - NewFolder.icIncomingCallWhite, - NewFolder.icOutgoingCallWhite, - NewFolder.iconNinja, - NewFolder.incomingDark, - NewFolder.incomingLight, - NewFolder.incomingVideoDark, - NewFolder.incomingVideoLight, - NewFolder.lastVideoCall, - NewFolder.lastLocation, - NewFolder.lastPhoto, - NewFolder.lastVideo, - NewFolder.lastVoiceMsg, - NewFolder.messageBttn, - NewFolder.microphoneBttn, - NewFolder.nextBttn, - NewFolder.outgoingDark, - NewFolder.outgoingLight, - NewFolder.outgoingVideoDark, - NewFolder.outgoingVideoLight, - NewFolder.phoneNumber, - NewFolder.qrCode, - NewFolder.recBar, - NewFolder.recProcess, - NewFolder.recLight, - NewFolder.schedule, - NewFolder.searchInactive, - NewFolder.sendBttn, - NewFolder.separators, - NewFolder.speakerBttn, - NewFolder.switchCameraBttn, - NewFolder.textBar, - NewFolder.workInactive, - oval14, - oval17, - oval6, - sendAsFile, - Stickers.stickerStub, - Stickers.stickersIcAdd, - Stickers.stickersIcDefaultPack, - Stickers.stickersIcRecent, - Stickers.stickersIcSearch, - Wallet.icArrowTransferDown, - Wallet.icArrowTransferUp, - Wallet.icBtc, - Wallet.icEos, - Wallet.icNyn, - Wallet.icPay, - Wallet.icWallet, - Wheel.Button.btnWheelInactive, - Wheel.Button.btnWheelInactiveLight, - Wheel.Button.wheel, - Wheel.Placeholders.icBrokenImagePlaceholder, - Wheel.Placeholders.icEmptyImagePlaceholder, - Wheel.Placeholders.icEmptyLocationPlaceholder, - Wheel.WheelItems.icActions, - Wheel.WheelItems.icByContacts, - Wheel.WheelItems.icByNumber, - Wheel.WheelItems.icByPassword, - Wheel.WheelItems.icByQrCode, - Wheel.WheelItems.icByUsername, - Wheel.WheelItems.icCalls, - Wheel.WheelItems.icCamera, - Wheel.WheelItems.icChannelInactive, - Wheel.WheelItems.icChannelNew, - Wheel.WheelItems.icChats, - Wheel.WheelItems.icContacts, - Wheel.WheelItems.icEditProfile, - Wheel.WheelItems.icFamily, - Wheel.WheelItems.icFile, - Wheel.WheelItems.icFriends, - Wheel.WheelItems.icGallery, - Wheel.WheelItems.icGroupCall, - Wheel.WheelItems.icGroupCalls, - Wheel.WheelItems.icGroupSettings, - Wheel.WheelItems.icGroupVideo, - Wheel.WheelItems.icGroups, - Wheel.WheelItems.icHistory, - Wheel.WheelItems.icHome, - Wheel.WheelItems.icList, - Wheel.WheelItems.icLocation, - Wheel.WheelItems.icMyself, - Wheel.WheelItems.icNew, - Wheel.WheelItems.icNewAudioCall, - Wheel.WheelItems.icNewChat, - Wheel.WheelItems.icNewContact, - Wheel.WheelItems.icNewGroup, - Wheel.WheelItems.icNewPhoneCall, - Wheel.WheelItems.icNewVideoCall, - Wheel.WheelItems.icOptions, - Wheel.WheelItems.icRecents, - Wheel.WheelItems.icSearch, - Wheel.WheelItems.icStarred, - Wheel.WheelItems.icVideo, - Wheel.WheelItems.icVideoIndicator, - Wheel.WheelItems.icWork, - Wheel.WheelPosition.wheelLeftImage, - Wheel.WheelPosition.wheelRightImage, - WheelPosition.wheelLeftImage, - WheelPosition.wheelRightImage, - arrowCollapse, - arrowExpand, - barsTabBarOverridesCenterButtonBtnPlayVideo, - bgChannelsEmptyState, - btnRecordDefault, - btnRecordHightlighted, - btnRecording, - btnScheduleMessage, - btnStopPlayingVideo, - btnStopRecording, - btnTakePhotoDefault, - btnTakePhotoHighlighted, - btnWheelDone, - callKitImage, - camera, - cancel, - editIcon, - emojiWhite, - frame, - icAddParticipants, - icAddPhotoPlaceholder, - icArrowDown, - icArrowRight1, - icArrowRight, - icArrowUpRed, - icBack, - icBackNavigation, - icBottomArrow, - icCameraFrame, - icChangeCameraIos, - icCheckmarkRed, - icClock, - icClose, - icCloseRed, - icContactsEmpty, - icCurrentLocation, - icDeleteRedCircle, - icEditDone, - icEmailStorage, - icFlashlightAuto, - icFlashlightOff, - icFlashlightOn, - icIncomingCallDark, - icKeyboard, - icLinkStorage, - icMapCurrent, - icMapMode, - icMapPin, - icMapSearch, - icMarketplaceContextMenu, - icMarketplaceWheelContextMenu, - icMic, - icMicDarkGray, - icNewGroup, - icParticipantsSearch, - icPhoneStorage, - icPhotoPlaceholder, - icPlaceStar, - icQrCode, - icScheduledMsgCheck, - icSearch, - icSearchEmpty, - icSendAsFile, - icSoundOff, - icStar, - icStyle, - icUnmute, - icVoiceCallOutgoingDark, - iconRedCheck, - iconShare, - icon, - iconInvitation, - iconMe, - iconsCameraQrCodeIcFlashlightActive, - iconsGeneralIcAddMember, - iconsGeneralIcDone, - iconsGeneralIcEditDone, - iconsGeneralIcSoundOn, - iconsIcMic, - imgEmptyStatesContactRequests, - imgEmptyStatesGroups, - imgEmptyStatesP2p, - imgEmptyStatesScheduled, - imgEmptyStatesStarred, - Input.arrowUp, - Input.delayRed, - Input.editMsgButton, - Input.icDelete, - Input.icPause, - Input.icPlay, - Input.icSend, - Input.icVoice, - Input.inputKeyboard, - Input.inputLeftEmojiButton, - Input.inputLeftKeyboardButton, - Input.inputMicrophone, - Input.pause, - Input.playBtn, - Input.recBackground, - Input.roundedRedBig, - Input.sendButton, - Input.trash, - Input.voiceButton, - inputLeftButton, - leftButton, - lock, - maximaze, - minimaze, - myLocation, - phone, - profileBig, - star, - staticmap, - statusIcon, - switchCamera, - tableArrow, - tableOverridesRightOverridesCheckboxIcUnchecked, - transcribed, - translated, - world, - ] - // swiftlint:enable trailing_comma - @available(*, deprecated, renamed: "allImages") - internal static let allValues: [AssetType] = allImages } // swiftlint:enable identifier_name line_length nesting type_body_length type_name -internal extension Image { - @available(iOS 1.0, tvOS 1.0, watchOS 1.0, *) - @available(OSX, deprecated, - message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") - convenience init!(asset: ImageAsset) { - #if os(iOS) || os(tvOS) +// MARK: - Implementation Details + +internal struct ColorAsset { + internal fileprivate(set) var name: String + + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) + internal var color: AssetColorTypeAlias { + return AssetColorTypeAlias(asset: self) + } +} + +internal extension AssetColorTypeAlias { + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) + convenience init!(asset: ColorAsset) { let bundle = Bundle(for: BundleToken.self) + #if os(iOS) || os(tvOS) self.init(named: asset.name, in: bundle, compatibleWith: nil) #elseif os(OSX) - self.init(named: NSImage.Name(asset.name)) + self.init(named: NSColor.Name(asset.name), bundle: bundle) #elseif os(watchOS) self.init(named: asset.name) #endif } } -internal extension AssetColorTypeAlias { - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, OSX 10.13, *) - convenience init!(asset: ColorAsset) { +internal struct DataAsset { + internal fileprivate(set) var name: String + + #if os(iOS) || os(tvOS) || os(OSX) + @available(iOS 9.0, tvOS 9.0, OSX 10.11, *) + internal var data: NSDataAsset { + return NSDataAsset(asset: self) + } + #endif +} + +#if os(iOS) || os(tvOS) || os(OSX) +@available(iOS 9.0, tvOS 9.0, OSX 10.11, *) +internal extension NSDataAsset { + convenience init!(asset: DataAsset) { let bundle = Bundle(for: BundleToken.self) #if os(iOS) || os(tvOS) + self.init(name: asset.name, bundle: bundle) + #elseif os(OSX) + self.init(name: NSDataAsset.Name(asset.name), bundle: bundle) + #endif + } +} +#endif + +internal struct ImageAsset { + internal fileprivate(set) var name: String + + internal var image: AssetImageTypeAlias { + let bundle = Bundle(for: BundleToken.self) + #if os(iOS) || os(tvOS) + let image = AssetImageTypeAlias(named: name, in: bundle, compatibleWith: nil) + #elseif os(OSX) + let image = bundle.image(forResource: NSImage.Name(name)) + #elseif os(watchOS) + let image = AssetImageTypeAlias(named: name) + #endif + guard let result = image else { fatalError("Unable to load image named \(name).") } + return result + } +} + +internal extension AssetImageTypeAlias { + @available(iOS 1.0, tvOS 1.0, watchOS 1.0, *) + @available(OSX, deprecated, + message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") + convenience init!(asset: ImageAsset) { + #if os(iOS) || os(tvOS) + let bundle = Bundle(for: BundleToken.self) self.init(named: asset.name, in: bundle, compatibleWith: nil) #elseif os(OSX) - self.init(named: NSColor.Name(asset.name), bundle: bundle) + self.init(named: NSImage.Name(asset.name)) #elseif os(watchOS) self.init(named: asset.name) #endif diff --git a/Nynja/Generated/ColorsConstants.swift b/Nynja/Generated/ColorsConstants.swift index 61eef0faf..a9606800c 100644 --- a/Nynja/Generated/ColorsConstants.swift +++ b/Nynja/Generated/ColorsConstants.swift @@ -10,89 +10,89 @@ internal typealias SGColor = UIColor internal extension SGColor { enum nynja { /// 0x262626ff (r: 38, g: 38, b: 38, a: 255) - static let almostBlack = #colorLiteral(red: 0.14902, green: 0.14902, blue: 0.14902, alpha: 1.0) + static let almostBlack = #colorLiteral(red: 0.14901961, green: 0.14901961, blue: 0.14901961, alpha: 1.0) /// 0xe5e5e5ff (r: 229, g: 229, b: 229, a: 255) - static let almostWhite = #colorLiteral(red: 0.898039, green: 0.898039, blue: 0.898039, alpha: 1.0) + static let almostWhite = #colorLiteral(red: 0.8980392, green: 0.8980392, blue: 0.8980392, alpha: 1.0) /// 0x6dbee1ff (r: 109, g: 190, b: 225, a: 255) - static let aquaBlue = #colorLiteral(red: 0.427451, green: 0.745098, blue: 0.882353, alpha: 1.0) + static let aquaBlue = #colorLiteral(red: 0.42745098, green: 0.74509805, blue: 0.88235295, alpha: 1.0) /// 0x272a30ff (r: 39, g: 42, b: 48, a: 255) - static let backgroundColor = #colorLiteral(red: 0.152941, green: 0.164706, blue: 0.188235, alpha: 1.0) + static let backgroundColor = #colorLiteral(red: 0.15294118, green: 0.16470589, blue: 0.1882353, alpha: 1.0) /// 0xddddddff (r: 221, g: 221, b: 221, a: 255) - static let backgroundColorLight = #colorLiteral(red: 0.866667, green: 0.866667, blue: 0.866667, alpha: 1.0) + static let backgroundColorLight = #colorLiteral(red: 0.8666667, green: 0.8666667, blue: 0.8666667, alpha: 1.0) /// 0x3f3f3fff (r: 63, g: 63, b: 63, a: 255) - static let backgroundGray = #colorLiteral(red: 0.247059, green: 0.247059, blue: 0.247059, alpha: 1.0) + static let backgroundGray = #colorLiteral(red: 0.24705882, green: 0.24705882, blue: 0.24705882, alpha: 1.0) /// 0x000000ff (r: 0, g: 0, b: 0, a: 255) static let black = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) /// 0x45a5ffff (r: 69, g: 165, b: 255, a: 255) - static let blue = #colorLiteral(red: 0.270588, green: 0.647059, blue: 1.0, alpha: 1.0) + static let blue = #colorLiteral(red: 0.27058825, green: 0.64705884, blue: 1.0, alpha: 1.0) /// 0x957348ff (r: 149, g: 115, b: 72, a: 255) - static let brown = #colorLiteral(red: 0.584314, green: 0.45098, blue: 0.282353, alpha: 1.0) + static let brown = #colorLiteral(red: 0.58431375, green: 0.4509804, blue: 0.28235295, alpha: 1.0) /// 0x00e359ff (r: 0, g: 227, b: 89, a: 255) - static let callGreen = #colorLiteral(red: 0.0, green: 0.890196, blue: 0.34902, alpha: 1.0) + static let callGreen = #colorLiteral(red: 0.0, green: 0.8901961, blue: 0.34901962, alpha: 1.0) /// 0x00000000 (r: 0, g: 0, b: 0, a: 0) static let clear = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) /// 0x505255ff (r: 80, g: 82, b: 85, a: 255) - static let contentDisabledGray = #colorLiteral(red: 0.313726, green: 0.321569, blue: 0.333333, alpha: 1.0) + static let contentDisabledGray = #colorLiteral(red: 0.3137255, green: 0.32156864, blue: 0.33333334, alpha: 1.0) /// 0x35383eff (r: 53, g: 56, b: 62, a: 255) - static let contextMenuBackGray = #colorLiteral(red: 0.207843, green: 0.219608, blue: 0.243137, alpha: 1.0) + static let contextMenuBackGray = #colorLiteral(red: 0.20784314, green: 0.21960784, blue: 0.24313726, alpha: 1.0) /// 0x067655ff (r: 6, g: 118, b: 85, a: 255) - static let darkGreen = #colorLiteral(red: 0.0235294, green: 0.462745, blue: 0.333333, alpha: 1.0) + static let darkGreen = #colorLiteral(red: 0.023529412, green: 0.4627451, blue: 0.33333334, alpha: 1.0) /// 0x2c2e33ff (r: 44, g: 46, b: 51, a: 255) - static let darkLight = #colorLiteral(red: 0.172549, green: 0.180392, blue: 0.2, alpha: 1.0) + static let darkLight = #colorLiteral(red: 0.17254902, green: 0.18039216, blue: 0.2, alpha: 1.0) /// 0xa4000dff (r: 164, g: 0, b: 13, a: 255) - static let darkRed = #colorLiteral(red: 0.643137, green: 0.0, blue: 0.0509804, alpha: 1.0) + static let darkRed = #colorLiteral(red: 0.6431373, green: 0.0, blue: 0.050980393, alpha: 1.0) /// 0x3891ffff (r: 56, g: 145, b: 255, a: 255) - static let dodgerBlue = #colorLiteral(red: 0.219608, green: 0.568627, blue: 1.0, alpha: 1.0) + static let dodgerBlue = #colorLiteral(red: 0.21960784, green: 0.5686275, blue: 1.0, alpha: 1.0) /// 0x969696ff (r: 150, g: 150, b: 150, a: 255) - static let dustyGray = #colorLiteral(red: 0.588235, green: 0.588235, blue: 0.588235, alpha: 1.0) + static let dustyGray = #colorLiteral(red: 0.5882353, green: 0.5882353, blue: 0.5882353, alpha: 1.0) /// 0x666666ff (r: 102, g: 102, b: 102, a: 255) static let gray = #colorLiteral(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) /// 0x63666aff (r: 99, g: 102, b: 106, a: 255) - static let lightGray = #colorLiteral(red: 0.388235, green: 0.4, blue: 0.415686, alpha: 1.0) + static let lightGray = #colorLiteral(red: 0.3882353, green: 0.4, blue: 0.41568628, alpha: 1.0) /// 0xc90010ff (r: 201, g: 0, b: 16, a: 255) - static let mainRed = #colorLiteral(red: 0.788235, green: 0.0, blue: 0.0627451, alpha: 1.0) + static let mainRed = #colorLiteral(red: 0.7882353, green: 0.0, blue: 0.0627451, alpha: 1.0) /// 0x959699ff (r: 149, g: 150, b: 153, a: 255) - static let manatee = #colorLiteral(red: 0.584314, green: 0.588235, blue: 0.6, alpha: 1.0) + static let manatee = #colorLiteral(red: 0.58431375, green: 0.5882353, blue: 0.6, alpha: 1.0) /// 0x86bc5fff (r: 134, g: 188, b: 95, a: 255) - static let mantis = #colorLiteral(red: 0.52549, green: 0.737255, blue: 0.372549, alpha: 1.0) + static let mantis = #colorLiteral(red: 0.5254902, green: 0.7372549, blue: 0.37254903, alpha: 1.0) /// 0xe7e7e7ff (r: 231, g: 231, b: 231, a: 255) - static let mercury = #colorLiteral(red: 0.905882, green: 0.905882, blue: 0.905882, alpha: 1.0) + static let mercury = #colorLiteral(red: 0.90588236, green: 0.90588236, blue: 0.90588236, alpha: 1.0) /// 0xccccccff (r: 204, g: 204, b: 204, a: 255) static let middleGray = #colorLiteral(red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0) /// 0xf5b758ff (r: 245, g: 183, b: 88, a: 255) - static let orange = #colorLiteral(red: 0.960784, green: 0.717647, blue: 0.345098, alpha: 1.0) + static let orange = #colorLiteral(red: 0.9607843, green: 0.7176471, blue: 0.34509805, alpha: 1.0) /// 0x2f353bff (r: 47, g: 53, b: 59, a: 255) - static let outerSpace = #colorLiteral(red: 0.184314, green: 0.207843, blue: 0.231373, alpha: 1.0) + static let outerSpace = #colorLiteral(red: 0.18431373, green: 0.20784314, blue: 0.23137255, alpha: 1.0) /// 0xe16d9dff (r: 225, g: 109, b: 157, a: 255) - static let pink = #colorLiteral(red: 0.882353, green: 0.427451, blue: 0.615686, alpha: 1.0) + static let pink = #colorLiteral(red: 0.88235295, green: 0.42745098, blue: 0.6156863, alpha: 1.0) /// 0xe0e0e0ff (r: 224, g: 224, b: 224, a: 255) - static let profilePhoneNumberGray = #colorLiteral(red: 0.878431, green: 0.878431, blue: 0.878431, alpha: 1.0) + static let profilePhoneNumberGray = #colorLiteral(red: 0.8784314, green: 0.8784314, blue: 0.8784314, alpha: 1.0) /// 0x676bb9ff (r: 103, g: 107, b: 185, a: 255) - static let purple = #colorLiteral(red: 0.403922, green: 0.419608, blue: 0.72549, alpha: 1.0) + static let purple = #colorLiteral(red: 0.40392157, green: 0.41960785, blue: 0.7254902, alpha: 1.0) /// 0xe06356ff (r: 224, g: 99, b: 86, a: 255) - static let roman = #colorLiteral(red: 0.878431, green: 0.388235, blue: 0.337255, alpha: 1.0) + static let roman = #colorLiteral(red: 0.8784314, green: 0.3882353, blue: 0.3372549, alpha: 1.0) /// 0xd8d8d8ff (r: 216, g: 216, b: 216, a: 255) - static let selfBubleColor = #colorLiteral(red: 0.847059, green: 0.847059, blue: 0.847059, alpha: 1.0) + static let selfBubleColor = #colorLiteral(red: 0.84705883, green: 0.84705883, blue: 0.84705883, alpha: 1.0) /// 0x212226ff (r: 33, g: 34, b: 38, a: 255) - static let selfTextColor = #colorLiteral(red: 0.129412, green: 0.133333, blue: 0.14902, alpha: 1.0) + static let selfTextColor = #colorLiteral(red: 0.12941177, green: 0.13333334, blue: 0.14901961, alpha: 1.0) /// 0x1f1f1fff (r: 31, g: 31, b: 31, a: 255) - static let shadowBlack = #colorLiteral(red: 0.121569, green: 0.121569, blue: 0.121569, alpha: 1.0) + static let shadowBlack = #colorLiteral(red: 0.12156863, green: 0.12156863, blue: 0.12156863, alpha: 1.0) /// 0x2b2e32ff (r: 43, g: 46, b: 50, a: 255) - static let shark = #colorLiteral(red: 0.168627, green: 0.180392, blue: 0.196078, alpha: 1.0) + static let shark = #colorLiteral(red: 0.16862746, green: 0.18039216, blue: 0.19607843, alpha: 1.0) /// 0xbfbfbfff (r: 191, g: 191, b: 191, a: 255) - static let silver = #colorLiteral(red: 0.74902, green: 0.74902, blue: 0.74902, alpha: 1.0) + static let silver = #colorLiteral(red: 0.7490196, green: 0.7490196, blue: 0.7490196, alpha: 1.0) /// 0x696a6bff (r: 105, g: 106, b: 107, a: 255) - static let subtitleGray = #colorLiteral(red: 0.411765, green: 0.415686, blue: 0.419608, alpha: 1.0) + static let subtitleGray = #colorLiteral(red: 0.4117647, green: 0.41568628, blue: 0.41960785, alpha: 1.0) /// 0x4ecfb1ff (r: 78, g: 207, b: 177, a: 255) - static let turquoise = #colorLiteral(red: 0.305882, green: 0.811765, blue: 0.694118, alpha: 1.0) + static let turquoise = #colorLiteral(red: 0.30588236, green: 0.8117647, blue: 0.69411767, alpha: 1.0) /// 0xaaaaaaff (r: 170, g: 170, b: 170, a: 255) - static let tutorialGray = #colorLiteral(red: 0.666667, green: 0.666667, blue: 0.666667, alpha: 1.0) + static let tutorialGray = #colorLiteral(red: 0.6666667, green: 0.6666667, blue: 0.6666667, alpha: 1.0) /// 0x9f68a8ff (r: 159, g: 104, b: 168, a: 255) - static let violet = #colorLiteral(red: 0.623529, green: 0.407843, blue: 0.658824, alpha: 1.0) + static let violet = #colorLiteral(red: 0.62352943, green: 0.40784314, blue: 0.65882355, alpha: 1.0) /// 0x45484dff (r: 69, g: 72, b: 77, a: 255) - static let wheelBackHighlitedGray = #colorLiteral(red: 0.270588, green: 0.282353, blue: 0.301961, alpha: 1.0) + static let wheelBackHighlitedGray = #colorLiteral(red: 0.27058825, green: 0.28235295, blue: 0.3019608, alpha: 1.0) /// 0x32353bff (r: 50, g: 53, b: 59, a: 255) - static let wheelTopLevelSeparatorColor = #colorLiteral(red: 0.196078, green: 0.207843, blue: 0.231373, alpha: 1.0) + static let wheelTopLevelSeparatorColor = #colorLiteral(red: 0.19607843, green: 0.20784314, blue: 0.23137255, alpha: 1.0) /// 0xffffffff (r: 255, g: 255, b: 255, a: 255) static let white = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) } diff --git a/Nynja/Generated/FontsConstants.swift b/Nynja/Generated/FontsConstants.swift index 4d205bcb0..4ce35f59f 100644 --- a/Nynja/Generated/FontsConstants.swift +++ b/Nynja/Generated/FontsConstants.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen #if os(OSX) @@ -11,6 +12,41 @@ // swiftlint:disable superfluous_disable_command // swiftlint:disable file_length +// MARK: - Fonts + +// swiftlint:disable identifier_name line_length type_body_length +internal enum FontFamily { + internal enum Avenir { + internal static let black = FontConvertible(name: "Avenir-Black", family: "Avenir", path: "Avenir.ttc") + internal static let blackOblique = FontConvertible(name: "Avenir-BlackOblique", family: "Avenir", path: "Avenir.ttc") + internal static let book = FontConvertible(name: "Avenir-Book", family: "Avenir", path: "Avenir.ttc") + internal static let bookOblique = FontConvertible(name: "Avenir-BookOblique", family: "Avenir", path: "Avenir.ttc") + internal static let heavy = FontConvertible(name: "Avenir-Heavy", family: "Avenir", path: "Avenir.ttc") + internal static let heavyOblique = FontConvertible(name: "Avenir-HeavyOblique", family: "Avenir", path: "Avenir.ttc") + internal static let light = FontConvertible(name: "Avenir-Light", family: "Avenir", path: "Avenir.ttc") + internal static let lightOblique = FontConvertible(name: "Avenir-LightOblique", family: "Avenir", path: "Avenir.ttc") + internal static let medium = FontConvertible(name: "Avenir-Medium", family: "Avenir", path: "Avenir.ttc") + internal static let mediumOblique = FontConvertible(name: "Avenir-MediumOblique", family: "Avenir", path: "Avenir.ttc") + internal static let oblique = FontConvertible(name: "Avenir-Oblique", family: "Avenir", path: "Avenir.ttc") + internal static let roman = FontConvertible(name: "Avenir-Roman", family: "Avenir", path: "Avenir.ttc") + internal static let all: [FontConvertible] = [black, blackOblique, book, bookOblique, heavy, heavyOblique, light, lightOblique, medium, mediumOblique, oblique, roman] + } + internal enum NotoSans { + internal static let bold = FontConvertible(name: "NotoSans-Bold", family: "Noto Sans", path: "NotoSans-Bold.ttf") + internal static let italic = FontConvertible(name: "NotoSans-Italic", family: "Noto Sans", path: "NotoSans-Italic.ttf") + internal static let medium = FontConvertible(name: "NotoSans-Medium", family: "Noto Sans", path: "NotoSans-Medium.ttf") + internal static let regular = FontConvertible(name: "NotoSans-Regular", family: "Noto Sans", path: "NotoSans-Regular.ttf") + internal static let all: [FontConvertible] = [bold, italic, medium, regular] + } + internal static let allCustomFonts: [FontConvertible] = [Avenir.all, NotoSans.all].flatMap { $0 } + internal static func registerAllCustomFonts() { + allCustomFonts.forEach { $0.register() } + } +} +// swiftlint:enable identifier_name line_length type_body_length + +// MARK: - Implementation Details + internal struct FontConvertible { internal let name: String internal let family: String @@ -21,9 +57,9 @@ internal struct FontConvertible { } internal func register() { + // swiftlint:disable:next conditional_returns_on_newline guard let url = url else { return } - var errorRef: Unmanaged? - CTFontManagerRegisterFontsForURL(url as CFURL, .process, &errorRef) + CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) } fileprivate var url: URL? { @@ -48,29 +84,4 @@ internal extension Font { } } -// swiftlint:disable identifier_name line_length type_body_length -internal enum FontFamily { - internal enum Avenir { - internal static let black = FontConvertible(name: "Avenir-Black", family: "Avenir", path: "Avenir.ttc") - internal static let blackOblique = FontConvertible(name: "Avenir-BlackOblique", family: "Avenir", path: "Avenir.ttc") - internal static let book = FontConvertible(name: "Avenir-Book", family: "Avenir", path: "Avenir.ttc") - internal static let bookOblique = FontConvertible(name: "Avenir-BookOblique", family: "Avenir", path: "Avenir.ttc") - internal static let heavy = FontConvertible(name: "Avenir-Heavy", family: "Avenir", path: "Avenir.ttc") - internal static let heavyOblique = FontConvertible(name: "Avenir-HeavyOblique", family: "Avenir", path: "Avenir.ttc") - internal static let light = FontConvertible(name: "Avenir-Light", family: "Avenir", path: "Avenir.ttc") - internal static let lightOblique = FontConvertible(name: "Avenir-LightOblique", family: "Avenir", path: "Avenir.ttc") - internal static let medium = FontConvertible(name: "Avenir-Medium", family: "Avenir", path: "Avenir.ttc") - internal static let mediumOblique = FontConvertible(name: "Avenir-MediumOblique", family: "Avenir", path: "Avenir.ttc") - internal static let oblique = FontConvertible(name: "Avenir-Oblique", family: "Avenir", path: "Avenir.ttc") - internal static let roman = FontConvertible(name: "Avenir-Roman", family: "Avenir", path: "Avenir.ttc") - } - internal enum NotoSans { - internal static let bold = FontConvertible(name: "NotoSans-Bold", family: "Noto Sans", path: "NotoSans-Bold.ttf") - internal static let italic = FontConvertible(name: "NotoSans-Italic", family: "Noto Sans", path: "NotoSans-Italic.ttf") - internal static let medium = FontConvertible(name: "NotoSans-Medium", family: "Noto Sans", path: "NotoSans-Medium.ttf") - internal static let regular = FontConvertible(name: "NotoSans-Regular", family: "Noto Sans", path: "NotoSans-Regular.ttf") - } -} -// swiftlint:enable identifier_name line_length type_body_length - private final class BundleToken {} diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index b2a681105..af13205c6 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen import Foundation @@ -5,7 +6,9 @@ import Foundation // swiftlint:disable superfluous_disable_command // swiftlint:disable file_length -// swiftlint:disable identifier_name line_length type_body_length +// MARK: - Strings + +// swiftlint:disable function_parameter_count identifier_name line_length type_body_length internal enum Localizable { /// Accept Audio internal static let acceptAudio = Localizable.tr("Localizable", "accept_audio") @@ -267,6 +270,8 @@ internal enum Localizable { internal static let confirmContryShort = Localizable.tr("Localizable", "confirm_contry_short") /// connected internal static let connected = Localizable.tr("Localizable", "connected") + /// connecting... + internal static let connecting = Localizable.tr("Localizable", "connecting...") /// Can't connect to the server. Try again later. internal static let connectionToServerFailed = Localizable.tr("Localizable", "connection_to_server_failed") /// Contact Requests @@ -487,6 +492,8 @@ internal enum Localizable { internal static let invited = Localizable.tr("Localizable", "invited") /// Join internal static let join = Localizable.tr("Localizable", "join") + /// Keep your existing name. + internal static let keepYourExistingName = Localizable.tr("Localizable", "Keep_your_existing_name.") /// Keep your existing username. internal static let keepYourExistingUsername = Localizable.tr("Localizable", "Keep_your_existing_username") /// Language for interpretation @@ -1091,6 +1098,8 @@ internal enum Localizable { internal static let voiceMessage = Localizable.tr("Localizable", "voice_message") /// Voice Messages internal static let voiceMessages = Localizable.tr("Localizable", "voice_messages") + /// waiting for network... + internal static let waitingForNetwork = Localizable.tr("Localizable", "waiting for network...") /// Add\nWallet internal static let walletAddTitle = Localizable.tr("Localizable", "wallet_add_title") /// Your balance has not enough funds to transfer this amount @@ -1291,14 +1300,10 @@ internal enum Localizable { internal static let youWasRemoved = Localizable.tr("Localizable", "you_was_removed") /// Your device appears to be rooted. The security of your app can be compromised. internal static let yourDeviceIsRooted = Localizable.tr("Localizable", "your_device_is_rooted") - /// connecting... - internal static let connecting = Localizable.tr("Localizable", "connecting...") - /// Keep your existing name. - internal static let keepYourExistingName = Localizable.tr("Localizable", "Keep_your_existing_name.") - /// waiting for network... - internal static let waitingForNetwork = Localizable.tr("Localizable", "waiting for network...") } -// swiftlint:enable identifier_name line_length type_body_length +// swiftlint:enable function_parameter_count identifier_name line_length type_body_length + +// MARK: - Implementation Details extension Localizable { private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { diff --git a/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift new file mode 100644 index 000000000..61197db7d --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift @@ -0,0 +1,29 @@ +// +// AccountSettingsProtocols.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AccountSettingsWireframeProtocol: WireframeProtocol { + +} + +protocol AccountSettingsViewProtocol: class where Self: UIViewController { + +} + +protocol AccountSettingsPresenterProtocol: NavigationProtocol { + +} + +protocol AccountSettingsInputInteractorProtocol { + +} + +protocol AccountSettingsOutputInteractorProtocol { + +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift new file mode 100644 index 000000000..8105451d8 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift @@ -0,0 +1,97 @@ +// +// AccountSettingsViewController.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AccountSettingsViewController: UIViewController, AccountSettingsViewProtocol, InitializeInjectable, UITableViewDelegate, UITableViewDataSource { + private let viewsFactory: AccountSettingsViewsFactoryProtocol + private let presenter: AccountSettingsPresenterProtocol + + private lazy var topHeaderLayoutGuide: UILayoutGuide = { + let layoutGuide = UILayoutGuide() + view.addLayoutGuide(layoutGuide) + + layoutGuide.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.height.equalTo(20 + UIWindow.safeAreaTopPadding()) + } + + return layoutGuide + }() + + private lazy var headerView: NavigationView = { + let header = UIView.makeHeaderView( + on: view, + top: topHeaderLayoutGuide, + config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: "Account settings".localized.uppercased(), + navigationHandler: presenter, + backButtonImage: UIImage.backButtonImage)) + + return header + }() + + private lazy var tableView: UITableView = { + let table = UITableView(frame: CGRect.zero, style: UITableViewStyle.grouped) + view.addSubview(table) + + table.snp.makeConstraints{ (make) in + make.top.equalTo(headerView.snp.bottom) + make.left.right.equalToSuperview() + make.bottom.equalTo(saveButton.snp.top).offset(-28) + } + + return table + }() + + private lazy var saveButton: UIButton = { + let button = UIButton() + view.addSubview(button) + + button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.mainRed), for: .normal) + button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.darkRed), for: .disabled) + + button.setTitle("Save".localized.uppercased(), for: .normal) + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + + button.addTarget(self, action: #selector(saveAction(sender:)), for: .touchUpInside) + + button.layer.cornerRadius = 22 + + button.snp.makeConstraints { (make) in + make.height.equalTo(44) + make.left.right.equalToSuperview().inset(16) + make.bottom.equalToSuperview().offset(-28) + } + + return button + }() + + init(dependencies: AccountSettingsViewController.Dependencies) { + viewsFactory = dependencies.viewsFactory + presenter = dependencies.presenter + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + struct Dependencies { + let viewsFactory: AccountSettingsViewsFactoryProtocol + let presenter: AccountSettingsPresenterProtocol + } + + @objc func saveAction(sender: UIButton) { + + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift new file mode 100644 index 000000000..5ea3d648e --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift @@ -0,0 +1,18 @@ +// +// AccountSettingsViewsFactory.swift +// Nynja +// +// Created by Ash on 10/31/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +protocol AccountSettingsViewsFactoryProtocol { + +} + +final class AccountSettingsViewsFactory: AccountSettingsViewsFactoryProtocol { + +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift new file mode 100644 index 000000000..9d2d1977c --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -0,0 +1,27 @@ +// +// AccountSettingsWireframe.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AccountSettingsCoordinatorProtocol { + func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) +} + +final class AccountSettingsWireframe: AccountSettingsWireframeProtocol { + struct Parameters {} + + struct Dependencies {} + + enum State {} + + func prepareModule(parameters: AccountSettingsWireframe.Parameters, dependencies: AccountSettingsWireframe.Dependencies) -> UIViewController { + let view = UIViewController() + + return view + } +} diff --git a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift new file mode 100644 index 000000000..05412c444 --- /dev/null +++ b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift @@ -0,0 +1,9 @@ +// +// AccountSettingsCoordinator.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 66298e0d4..e9dd34474 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -128,8 +128,8 @@ extension AuthCoordinator { private func titleForPopup(loginOption: LoginOption) -> String { switch loginOption { - case .email(let email): return "Please confirm the email you entered is correct".localized - case .phoneNumber(let number): return "Please confirm the number you entered is correct".localized + case .email: return "Please confirm the email you entered is correct".localized + case .phoneNumber: return "Please confirm the number you entered is correct".localized default: return "" } } diff --git a/Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn1.imageset/Contents.json similarity index 100% rename from Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn.imageset/Contents.json rename to Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn1.imageset/Contents.json diff --git a/Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn.imageset/next bttn@2x.png b/Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn1.imageset/next bttn@2x.png similarity index 100% rename from Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn.imageset/next bttn@2x.png rename to Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn1.imageset/next bttn@2x.png diff --git a/Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn.imageset/next bttn@3x.png b/Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn1.imageset/next bttn@3x.png similarity index 100% rename from Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn.imageset/next bttn@3x.png rename to Nynja/Resources/Assets.xcassets/Main Wheel/next_bttn1.imageset/next bttn@3x.png diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/Contents.json similarity index 100% rename from Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/Contents.json rename to Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/Contents.json diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code.png b/Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/qr-code.png similarity index 100% rename from Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code.png rename to Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/qr-code.png diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@2x.png b/Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/qr-code@2x.png similarity index 100% rename from Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@2x.png rename to Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/qr-code@2x.png diff --git a/Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@3x.png b/Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/qr-code@3x.png similarity index 100% rename from Nynja/Resources/Assets.xcassets/New Folder/qr-code.imageset/qr-code@3x.png rename to Nynja/Resources/Assets.xcassets/New Folder/qr-code1.imageset/qr-code@3x.png diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json deleted file mode 100644 index bab7f7d16..000000000 --- a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "left_image.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf deleted file mode 100644 index a21b5c9f751612124205c9b533bb03172339cabe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253015 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

&;T-tZpE#vBi3lJg5@cY=V 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

r3Hw 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

7LO)^p*; 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 diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json deleted file mode 100644 index 3c7f1cbf1..000000000 --- a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "right_image.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/right_image.pdf b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/right_image.pdf deleted file mode 100644 index bf18d530ee760b9e9df2cc3b63238e98fc1a2391..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 260993 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 TxNSuspRnsER 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

lU;#cc|lb~B8L1%{-_cAU_ku}S3(6+vZG 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 diff --git a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json deleted file mode 100644 index 0fc3cb506..000000000 --- a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_new_group.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "preserves-vector-representation" : true - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/ic_new_group.pdf b/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/ic_new_group.pdf deleted file mode 100644 index a70db86f74b1cf89aba6f0900029833911427ca6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7638 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 ZKkVk_PO`@Lf*>T}Xt02QvZf07{{Te;HERF> diff --git a/Nynja/Resources/LaunchScreen.storyboard b/Nynja/Resources/LaunchScreen.storyboard index 2e44e96b2..ac3b99c6d 100644 --- a/Nynja/Resources/LaunchScreen.storyboard +++ b/Nynja/Resources/LaunchScreen.storyboard @@ -1,5 +1,5 @@ - + -- GitLab From c0e439f27f9ad9a45044cab9f3fa4833ab59bc52 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 13:20:00 +0200 Subject: [PATCH 051/138] [NY-4699] Fetch member alias by roomId and phoneId. --- Nynja/DB/Models/DBMember.swift | 46 ++++++++++++++----- Nynja/Services/Member/MemberDAO.swift | 6 +++ Nynja/Services/Member/MemberDAOProtocol.swift | 3 +- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Nynja/DB/Models/DBMember.swift b/Nynja/DB/Models/DBMember.swift index 2b456cbcb..1ef475ada 100644 --- a/Nynja/DB/Models/DBMember.swift +++ b/Nynja/DB/Models/DBMember.swift @@ -176,6 +176,10 @@ class DBMember: Record, DBModelProtocol { return member } + static func memberAlias(from db: Database, roomId: String, phoneId: String) throws -> String? { + return try requestAlias(roomId: roomId, phoneId: phoneId).fetchOne(db) + } + private func construct(_ db: Database) throws { let memberId = "\(self.id)" self.features = (try? DBMember.requestFeature(targetId: memberId).fetchAll(db)) ?? [] @@ -202,16 +206,35 @@ class DBMember: Record, DBModelProtocol { } // MARK: - Requests + static private func requestFeature(targetId: String) -> QueryInterfaceRequest { return DBFeature.request(targetId: targetId, targetType: DBFeature.TargetType.member) } + static private func requestAlias(roomId: String, phoneId: String) -> AnyTypedRequest { + let sql = sqlMember(roomId: roomId, phoneId: phoneId, selection: MemberTable.Column.alias.title) + return SQLRequest(sql).asRequest(of: String.self) + } + static private func requestMember(roomId: String, phoneId: String) -> AnyTypedRequest { + let sql = sqlMember(roomId: roomId, phoneId: phoneId) + return SQLRequest(sql).asRequest(of: DBMember.self) + } + + static private func requestMember(roomId: String, isAdmin: Bool) -> AnyTypedRequest { + let sql = sqlAdmin(roomId: roomId, isAdmin: isAdmin) + return SQLRequest(sql).asRequest(of: DBMember.self) + } + + + // MARK: SQL + + static private func sqlMember(roomId: String, phoneId: String, selection: String = "*") -> String { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name let sql = """ - SELECT \(memberTable).* + SELECT \(memberTable).\(selection) FROM \(roomMemberTable) LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = \(memberTable).\(MemberTable.Column.id.title) @@ -219,23 +242,22 @@ class DBMember: Record, DBModelProtocol { AND \(MemberTable.Column.phoneId.title) = '\(phoneId)' """ - return SQLRequest(sql).asRequest(of: DBMember.self) + return sql } - static private func requestMember(roomId: String, isAdmin: Bool) -> AnyTypedRequest { + static private func sqlAdmin(roomId: String, isAdmin: Bool) -> String { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name let sql = """ - SELECT \(memberTable).* - FROM \(roomMemberTable) - LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = - \(memberTable).\(MemberTable.Column.id.title) - WHERE \(RoomMemberTable.Column.roomId.title) = '\(roomId)' - AND \(RoomMemberTable.Column.isAdmin.title) = \(isAdmin ? 1 : 0) - """ + SELECT \(memberTable).* + FROM \(roomMemberTable) + LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = + \(memberTable).\(MemberTable.Column.id.title) + WHERE \(RoomMemberTable.Column.roomId.title) = '\(roomId)' + AND \(RoomMemberTable.Column.isAdmin.title) = \(isAdmin ? 1 : 0) + """ - return SQLRequest(sql).asRequest(of: DBMember.self) + return sql } - } diff --git a/Nynja/Services/Member/MemberDAO.swift b/Nynja/Services/Member/MemberDAO.swift index 9cf7ed62a..4375b187a 100644 --- a/Nynja/Services/Member/MemberDAO.swift +++ b/Nynja/Services/Member/MemberDAO.swift @@ -52,6 +52,12 @@ class MemberDAO: MemberDAOProtocol { return Member(member: member) } + static func fetchMemberAlias(roomId: String, phoneId: String) -> String? { + return dbManager.fetch { db in + return try DBMember.memberAlias(from: db, roomId: roomId, phoneId: phoneId) + } + } + // MARK: - Update diff --git a/Nynja/Services/Member/MemberDAOProtocol.swift b/Nynja/Services/Member/MemberDAOProtocol.swift index a8d461120..bb5b49b1a 100644 --- a/Nynja/Services/Member/MemberDAOProtocol.swift +++ b/Nynja/Services/Member/MemberDAOProtocol.swift @@ -15,10 +15,11 @@ protocol MemberDAOProtocol: DAOProtocol { static func findMemberBy(id: Int64) -> Member? static func findMemberBy(roomId: String, phoneId: String) -> Member? + static func fetchMemberAlias(roomId: String, phoneId: String) -> String? + // MARK: - Update static func updateColumns(_ columns: Set, member: Member) static func updateReader(_ reader: Int64, roomId: String, phoneId: String) - } -- GitLab From 1c87030595791cdf9b990e6c1da884bfb9b85899 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 14:09:16 +0200 Subject: [PATCH 052/138] [NY-4699] Implemented in-chat action based statuses for rooms. --- Nynja/MQTTModels/TypingExtension+BERT.swift | 18 ++++--- Nynja/ServerModel/Model/Typing.swift | 6 ++- Nynja/ServerModel/Source/Decoder.swift | 9 ++-- Nynja/ServerModel/Spec/Typing_Spec.swift | 6 +++ Nynja/Services/Models/TypingModel.swift | 4 +- Nynja/Statuses/TypingStatusProvider.swift | 14 ++++-- .../Messaging/TypingSenderService.swift | 47 ++++++++++--------- 7 files changed, 66 insertions(+), 38 deletions(-) diff --git a/Nynja/MQTTModels/TypingExtension+BERT.swift b/Nynja/MQTTModels/TypingExtension+BERT.swift index 54369b5a1..7f52c0327 100644 --- a/Nynja/MQTTModels/TypingExtension+BERT.swift +++ b/Nynja/MQTTModels/TypingExtension+BERT.swift @@ -9,15 +9,21 @@ import Foundation extension Typing { + func getBert() -> BertObject { let topic = BertAtom(fromString: "Typing") - let _phone_id = Bert.getBin(self.phone_id) - var _comments: BertObject = BertNil() - if let com = self.comments as? String { - _comments = Bert.getBin(com) + + let feedId = Bert.getBin(self.feed_id) + let senderId = Bert.getBin(self.sender_id) + let senderAlias = Bert.getBin(self.sender_alias) + + let comments: BertObject + if let _comments = self.comments as? String { + comments = Bert.getBin(_comments) + } else { + comments = BertNil() } - return BertTuple(fromElements: [topic, _phone_id, _comments]) + return BertTuple(fromElements: [topic, feedId, senderId, senderAlias, comments]) } } - diff --git a/Nynja/ServerModel/Model/Typing.swift b/Nynja/ServerModel/Model/Typing.swift index f37b18267..e807bc87c 100644 --- a/Nynja/ServerModel/Model/Typing.swift +++ b/Nynja/ServerModel/Model/Typing.swift @@ -1,5 +1,7 @@ class Typing { - var phone_id: String? + var feed_id: String? + var sender_id: String? + var sender_alias: String? var comments: AnyObject? -} \ No newline at end of file +} diff --git a/Nynja/ServerModel/Source/Decoder.swift b/Nynja/ServerModel/Source/Decoder.swift index bf56d8420..844a4646b 100644 --- a/Nynja/ServerModel/Source/Decoder.swift +++ b/Nynja/ServerModel/Source/Decoder.swift @@ -302,10 +302,12 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? a_History.status = body[5].parse(bert: tuple.elements[6]) as? AnyObject return a_History case "Typing": - if body.count != 2 { return nil } + if body.count != 4 { return nil } let a_Typing = Typing() - a_Typing.phone_id = body[0].parse(bert: tuple.elements[1]) as? String - a_Typing.comments = body[1].parse(bert: tuple.elements[2]) as? AnyObject + a_Typing.feed_id = body[0].parse(bert: tuple.elements[1]) as? String + a_Typing.sender_id = body[1].parse(bert: tuple.elements[2]) as? String + a_Typing.sender_alias = body[2].parse(bert: tuple.elements[3]) as? String + a_Typing.comments = body[3].parse(bert: tuple.elements[4]) as? AnyObject return a_Typing case "Contact": if body.count != 14 || tuple.elements.count != 15 { return nil } @@ -538,3 +540,4 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? default: return nil } } + diff --git a/Nynja/ServerModel/Spec/Typing_Spec.swift b/Nynja/ServerModel/Spec/Typing_Spec.swift index 41ceda8a5..bdc43d54c 100644 --- a/Nynja/ServerModel/Spec/Typing_Spec.swift +++ b/Nynja/ServerModel/Spec/Typing_Spec.swift @@ -1,4 +1,10 @@ func get_Typing() -> Model { return Model(value:Tuple(name:"Typing",body:[ Model(value:Binary()), + Model(value:Chain(types:[ + Model(value:List(constant:"")), + Model(value:Binary())])), + Model(value:Chain(types:[ + Model(value:List(constant:"")), + Model(value:Binary())])), Model(value:Chain(types:[Model(value:Tuple()),Model(value:Atom()),Model(value:Binary()),Model(value:Number()),Model(value:List(constant:""))]))]))} diff --git a/Nynja/Services/Models/TypingModel.swift b/Nynja/Services/Models/TypingModel.swift index 1306e2159..8f7464947 100644 --- a/Nynja/Services/Models/TypingModel.swift +++ b/Nynja/Services/Models/TypingModel.swift @@ -86,7 +86,9 @@ enum TypingModelType: String { final class TypingModel: BaseMQTTModel { enum Topic { - case p2p(phone: String) + /// phone number without '_{roster_id}' + case p2p(phoneNumber: String) + /// room id case room(id: String) fileprivate var path: String { diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 1405e3d86..ddd2830fa 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -74,13 +74,17 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { - guard let feedId = typing.phone_id, let typingType = typing.type else { + guard let feedId = typing.feed_id, let typingType = typing.type else { return } + let senderId = typing.sender_id + let senderAlias = typing.sender_alias + + let feed: SenderInfo.Feed = senderId != nil ? .p2p(feedId) : .room(feedId) + let status = ActionStatus(typingModelType: typingType) - // FIXME: implement logic for rooms. - let senderInfo = SenderInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) + let senderInfo = SenderInfo(feed: feed, senderId: senderId, senderName: senderAlias, status: status) processingQueue.async { self.save(senderInfo) @@ -220,11 +224,11 @@ private extension TypingStatusProviderImpl { } let feed: Feed - let senderId: String + let senderId: String? let senderName: String? let status: ActionStatus - init(feed: Feed, senderId: String, senderName: String?, status: ActionStatus) { + init(feed: Feed, senderId: String?, senderName: String?, status: ActionStatus) { self.feed = feed self.senderId = senderId self.senderName = senderName diff --git a/Shared/Services/Messaging/TypingSenderService.swift b/Shared/Services/Messaging/TypingSenderService.swift index 5b6ac91d9..bc1cf19c7 100644 --- a/Shared/Services/Messaging/TypingSenderService.swift +++ b/Shared/Services/Messaging/TypingSenderService.swift @@ -45,10 +45,10 @@ final class TypingSenderService: TypingSenderServiceProtocol, InitializeInjectab return } if let contact = chat as? Contact, let phone = contact.phoneNumber { - sendTyping(for: .p2p(phone: phone), type: type, ownPhoneId: ownPhoneId) + sendTyping(for: .p2p(phoneNumber: phone), type: type, feedId: ownPhoneId, senderId: nil, senderAlias: nil) - } else if let room = chat as? Room, let roomId = room.id { - sendTyping(for: .room(id: roomId), type: type, ownPhoneId: ownPhoneId) + } else if let room = chat as? Room, let roomId = room.id, let alias = memberAlias(roomId: roomId, phoneId: ownPhoneId) { + sendTyping(for: .room(id: roomId), type: type, feedId: roomId, senderId: ownPhoneId, senderAlias: alias) } } @@ -61,31 +61,36 @@ final class TypingSenderService: TypingSenderServiceProtocol, InitializeInjectab return } if message.feed_id is p2p, let phoneId = message.to, let phone = Contact.phoneNumber(from: phoneId) { - sendTyping(for: .p2p(phone: phone), type: type, ownPhoneId: ownPhoneId) + sendTyping(for: .p2p(phoneNumber: phone), type: type, feedId: ownPhoneId, senderId: nil, senderAlias: nil) - } else if message.feed_id is muc, let roomId = message.to { - sendTyping(for: .room(id: roomId), type: type, ownPhoneId: ownPhoneId) + } else if message.feed_id is muc, let roomId = message.to, let alias = memberAlias(roomId: roomId, phoneId: ownPhoneId) { + sendTyping(for: .room(id: roomId), type: type, feedId: roomId, senderId: ownPhoneId, senderAlias: alias) } } - private func sendTyping(for destination: TypingModel.Topic, type: TypingModelType, ownPhoneId: String) { - let typingModel = self.typingModel(for: destination, type: type, ownPhoneId: ownPhoneId) - mqttService.sendTyping(typingModel) - } - - private func typingModel(for destination: TypingModel.Topic, type: TypingModelType, ownPhoneId: String) -> TypingModel { + private func sendTyping(for topic: TypingModel.Topic, type: TypingModelType, + feedId: String, senderId: String?, senderAlias: String?) { let typing = Typing() - switch destination { - case .p2p: - // Send own phone id for p2p - typing.phone_id = ownPhoneId - case let .room(id): - // Send room id for muc - typing.phone_id = id - } + typing.feed_id = feedId + typing.sender_id = senderId + typing.sender_alias = senderAlias typing.comments = type.rawValue as AnyObject - return TypingModel(typing: typing, to: destination) + let typingModel = TypingModel(typing: typing, to: topic) + + mqttService.sendTyping(typingModel) + } + + + // MARK: - Database + + private func memberAlias(roomId: String, phoneId: String) -> String? { + #if !SHARE_EXTENSION + // FIXME: inject MessageDAO as a dependency + return MemberDAO.fetchMemberAlias(roomId: roomId, phoneId: phoneId) + #else + return nil + #endif } } -- GitLab From 170870c6f0d9e4c923a48d3cd47741c6a02627c1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 14:19:09 +0200 Subject: [PATCH 053/138] [NY-4699] Refactored TypingStatusProvider. --- Nynja/Statuses/TypingStatusProvider.swift | 45 ++++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index ddd2830fa..24de72bf5 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -80,11 +80,16 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let senderId = typing.sender_id let senderAlias = typing.sender_alias - let feed: SenderInfo.Feed = senderId != nil ? .p2p(feedId) : .room(feedId) + let feed: SenderInfo.Feed + if let senderId = typing.sender_id, let alias = typing.sender_alias { + feed = .room(feedId, senderId: senderId, senderName: alias) + } else { + feed = .p2p(feedId) + } let status = ActionStatus(typingModelType: typingType) - let senderInfo = SenderInfo(feed: feed, senderId: senderId, senderName: senderAlias, status: status) + let senderInfo = SenderInfo(feed: feed, status: status) processingQueue.async { self.save(senderInfo) @@ -211,27 +216,49 @@ private extension TypingStatusProviderImpl { final class SenderInfo { enum Feed { case p2p(FeedId) - case room(FeedId) + case room(FeedId, senderId: String, senderName: String) var identifier: String { switch self { case let .p2p(id): return id - case let .room(id): + case let .room(id, _, _): return id } } + + var senderId: String? { + switch self { + case let .room(_, senderId, _): + return senderId + case .p2p: + return nil + } + } + + var senderName: String? { + switch self { + case let .room(_, _, senderName): + return senderName + case .p2p: + return nil + } + } } let feed: Feed - let senderId: String? - let senderName: String? let status: ActionStatus - init(feed: Feed, senderId: String?, senderName: String?, status: ActionStatus) { + var senderId: String? { + return feed.senderId + } + + var senderName: String? { + return feed.senderName + } + + init(feed: Feed, status: ActionStatus) { self.feed = feed - self.senderId = senderId - self.senderName = senderName self.status = status } } -- GitLab From 7690fae46d7714c701807a1e012487fa1dd30d40 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 15:27:45 +0200 Subject: [PATCH 054/138] [NY-4699] Ignore typing from rooms without sender alias. --- Nynja/Statuses/TypingDisplayModel.swift | 6 ++++- Nynja/Statuses/TypingStatusProvider.swift | 31 +++++++++-------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index 7da267f34..1f548cc0d 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -14,7 +14,11 @@ enum TypingDisplayModel { var senders: [String] var displayName: String? { - return senders.joined(separator: ", ") + guard !senders.isEmpty, senders.count > 1 else { + return senders.first + } + // FIXME: localize + return "\(senders.count) people ...".localized } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 24de72bf5..d766aab03 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -30,14 +30,14 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private var workItems: [FeedId: DispatchWorkItem] = [:] - private let typingDismissInterval = 10.0 - private let isolationQueue = DispatchQueue(label: "com.nynja.typing-status-provider", attributes: .concurrent) private let processingQueue: DispatchQueue private let notifyQueue: DispatchQueue + private let typingDismissInterval = 10.0 + // MARK: - Dependencies @@ -77,11 +77,12 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta guard let feedId = typing.feed_id, let typingType = typing.type else { return } - let senderId = typing.sender_id - let senderAlias = typing.sender_alias - + let feed: SenderInfo.Feed - if let senderId = typing.sender_id, let alias = typing.sender_alias { + if let senderId = typing.sender_id { + guard let alias = typing.sender_alias else { + return + } feed = .room(feedId, senderId: senderId, senderName: alias) } else { feed = .p2p(feedId) @@ -107,7 +108,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta case let .room(oldTypingSendersInfo): var newTypingInfo = oldTypingSendersInfo - newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } + newTypingInfo.removeAll { typingInfo.feed.senderId == $0.feed.senderId } newTypingInfo.append(typingInfo) update(.room(newTypingInfo), for: feedId) @@ -164,10 +165,10 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta return .none } let senders: [String] = typingInfo.compactMap { - guard lastStatus == $0.status else { - return nil - } - return $0.senderName +// guard lastStatus == $0.status else { +// return nil +// } + return $0.feed.senderName } let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) @@ -249,14 +250,6 @@ private extension TypingStatusProviderImpl { let feed: Feed let status: ActionStatus - var senderId: String? { - return feed.senderId - } - - var senderName: String? { - return feed.senderName - } - init(feed: Feed, status: ActionStatus) { self.feed = feed self.status = status -- GitLab From bfee43d27216c8c9e0a44a7006cb1bbad3576790 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 16:37:29 +0200 Subject: [PATCH 055/138] [NY-4699] Handle case when 2 or more people are typing in a group. --- Nynja/Generated/LocalizableConstants.swift | 6 +++ .../Cell/ChatListMessageTableViewCell.swift | 6 +-- .../Statuses/ChatStatusDisplayInfo.swift | 2 +- .../Message/Presenter/MessagePresenter.swift | 2 +- .../View/Views/AvatarView/AvatarView.swift | 25 ++++++--- Nynja/Resources/en.lproj/Localizable.strings | 2 + Nynja/Statuses/TypingDisplayModel.swift | 54 ++++++++++++++----- Nynja/Statuses/TypingStatusProvider.swift | 11 ++-- 8 files changed, 76 insertions(+), 32 deletions(-) diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 8df7d47f9..cd000668d 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -642,6 +642,12 @@ internal extension String { static var messageStatusEdited: String { return localizable.tr("Localizable", "message_status_edited") } /// typing static var messageStatusTyping: String { return localizable.tr("Localizable", "message_status_typing") } + /// %@ people + static func messageTypingStatusPeople(_ p1: String) -> String { + return localizable.tr("Localizable", "message_typing_status_people", p1) + } + /// ... + static var messageTypingStatusUndefined: String { return localizable.tr("Localizable", "message_typing_status_undefined") } /// meters static var meters: String { return localizable.tr("Localizable", "meters") } /// microphone diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index c12c21936..0199e67f9 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -142,10 +142,10 @@ final class ChatListMessageTableViewCell: UITableViewCell { } let indicator: TypingView.Appearance.Indicator - switch status { - case .recording: + switch status.action { + case .recording?: indicator = .circle(UIColor.nynja.white) - case .sending, .typing, .done: + default: indicator = .dots(UIColor.nynja.white) } diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift index dd1a460ba..dfd9c1d6f 100644 --- a/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift @@ -8,5 +8,5 @@ enum ChatStatusDisplayInfo { case text(String) - case typing(TypingDisplayModel.SenderInfo?, ActionStatus) + case typing(TypingDisplayModel.SenderInfo?, TypingDisplayModel.Status) } diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 52796e279..d13ecf56f 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -54,7 +54,7 @@ final class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageIn switch typing { case let .typing(sender, status): - if case .done = status { + if status.isDone { fallthrough } view.updateHeaderStatus(.typing(sender, status)) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index 633c993fd..e2b5566cc 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -26,15 +26,14 @@ final class AvatarView: BaseView { statusLabel.text = statusString statusLabel.accessibilityValue = statusString - statusLabel.isHidden = false - typingView.isHidden = true + hideTyping() case let .typing(sender, status): let indicator: TypingView.Appearance.Indicator - switch status { - case .recording: + switch status.action { + case .recording?: indicator = .circle(UIColor.nynja.white) - case .sending, .typing, .done: + default: indicator = .dots(UIColor.nynja.white) } @@ -47,8 +46,7 @@ final class AvatarView: BaseView { ) typingView.update(appearance) - statusLabel.isHidden = true - typingView.isHidden = false + showTyping() } } } @@ -192,4 +190,17 @@ final class AvatarView: BaseView { muteLeftConstraint?.update(offset: leftInset) } } + + + // MARK: - Layout + + private func showTyping() { + statusLabel.isHidden = true + typingView.isHidden = false + } + + private func hideTyping() { + statusLabel.isHidden = false + typingView.isHidden = true + } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 7a5e97526..a82d4f762 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -495,6 +495,8 @@ "message_status_typing"="typing"; "message_new_messages"="New messages"; "message_sending"="sending"; +"message_typing_status_people"="%@ people"; +"message_typing_status_undefined"="..."; // MARK: Sending Status "file"="file"; diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index 1f548cc0d..99ce063bb 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -7,32 +7,60 @@ // enum TypingDisplayModel { - case typing(SenderInfo?, ActionStatus) + case typing(SenderInfo?, Status) case none + var displayName: String? { + guard case let .typing(senderInfo, _) = self else { + return nil + } + return senderInfo?.displayName + } + + var status: Status? { + guard case let .typing(_, status) = self else { + return nil + } + return status + } +} + +extension TypingDisplayModel { + struct SenderInfo { - var senders: [String] + let senders: [String] var displayName: String? { guard !senders.isEmpty, senders.count > 1 else { return senders.first } - // FIXME: localize - return "\(senders.count) people ...".localized + return String.localizable.messageTypingStatusPeople(String(senders.count)) } } - var displayName: String? { - guard case let .typing(senderInfo, _) = self else { - return nil + enum Status { + case action(ActionStatus) + /// undefined means that there are more then one active typing in chat with different statuses. + case undefined + + var title: String { + switch self { + case let .action(actionStatus): + return actionStatus.title + case .undefined: + return String.localizable.messageTypingStatusUndefined + } } - return senderInfo?.displayName - } - - var status: ActionStatus? { - guard case let .typing(_, status) = self else { + + var action: ActionStatus? { + if case let .action(actionStatus) = self { + return actionStatus + } return nil } - return status + + var isDone: Bool { + return action?.isDone ?? false + } } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index d766aab03..a6f0d7853 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -164,15 +164,12 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { return .none } - let senders: [String] = typingInfo.compactMap { -// guard lastStatus == $0.status else { -// return nil -// } - return $0.feed.senderName - } + let senders = typingInfo.compactMap { $0.feed.senderName } let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) - return .typing(senderInfo, lastStatus) + let shouldDisplayStatus = !typingInfo.contains { $0.status != lastStatus } + + return .typing(senderInfo, shouldDisplayStatus ? .action(lastStatus) : .undefined) } -- GitLab From 62c029d4e780d731b8a88339763acb4f8665397d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:13:53 +0200 Subject: [PATCH 056/138] [NY-4699] Remove unused AccountStatusProvider. Update interface of TypingStatusProvider. --- Nynja.xcodeproj/project.pbxproj | 4 --- Nynja/Statuses/AccountStatusProvider.swift | 40 ---------------------- Nynja/Statuses/TypingStatusProvider.swift | 7 ++-- 3 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 Nynja/Statuses/AccountStatusProvider.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 6bc64895a..a42efb344 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1133,7 +1133,6 @@ 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2620BEE961008AD211 /* ScalableCell.swift */; }; 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; - 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */; }; 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; @@ -3352,7 +3351,6 @@ 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageVC+StickerInputModuleDelegate.swift"; sourceTree = ""; }; 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; - 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatusProvider.swift; sourceTree = ""; }; 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; @@ -9238,7 +9236,6 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, - 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); @@ -15159,7 +15156,6 @@ 4B4266C1204D917800194BC1 /* ActionsView+Layout.swift in Sources */, AF440BA5CEBE5170D082FF60 /* LoginProtocols.swift in Sources */, 85C16C3520D2520E00EDB77E /* StickersDownloadingService.swift in Sources */, - 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */, 6D6234F81F1E158600EF375F /* HistoryCell.swift in Sources */, FEA655F62167777E00B44029 /* PaymentInteractor.swift in Sources */, E74EC9EF1FC2DE23007268E6 /* MemberTable.swift in Sources */, diff --git a/Nynja/Statuses/AccountStatusProvider.swift b/Nynja/Statuses/AccountStatusProvider.swift deleted file mode 100644 index 65d15f633..000000000 --- a/Nynja/Statuses/AccountStatusProvider.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// AccountStatusProvider.swift -// Nynja -// -// Created by Anton Poltoratskyi on 26.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol AccountStatusObservable: class { - typealias Callback = (AccountId, AccountStatus) -> Void - - func addObserver(_ observer: AnyObject, callback: @escaping Callback) - func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) - func removeObserver(_ observer: AnyObject) - func removeObserver(_ observer: AnyObject, for key: AccountId) - func notify(_ key: AccountId, with value: AccountStatus) -} - -protocol AccountStatusProvider: AccountStatusObservable { - func status(for accountId: AccountId) -> AccountStatus - func update(_ status: AccountStatus, for accountId: AccountId) -} - -final class AccountStatusProviderImpl: AccountStatusProvider, KeyedObservableContainer { - - private var data: [AccountId: AccountStatus] = [:] - - private(set) var observable = KeyedObservable() - - func status(for accountId: AccountId) -> AccountStatus { - return data[accountId] ?? .none - } - - func update(_ status: AccountStatus, for accountId: AccountId) { - data[accountId] = status - observable.notify(accountId, with: status) - } -} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index a6f0d7853..aa6fde968 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -8,17 +8,14 @@ import Foundation -protocol TypingStatusObservable: class { +protocol TypingStatusProvider: class { typealias Callback = (FeedId, TypingDisplayModel) -> Void func addObserver(_ observer: AnyObject, callback: @escaping Callback) func addObserver(_ observer: AnyObject, for key: FeedId, callback: @escaping Callback) func removeObserver(_ observer: AnyObject) func removeObserver(_ observer: AnyObject, for key: FeedId) - func notify(_ key: FeedId, with value: TypingDisplayModel) -} - -protocol TypingStatusProvider: TypingStatusObservable { + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -- GitLab From e0bc9e79459c87d3c763012e34722430afa2182d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:26:22 +0200 Subject: [PATCH 057/138] [NY-4699] Don't display typing from ourself. --- .../Interactor/ChatsListInteractor.swift | 5 ++++- .../Interactor/GroupsListInteractor.swift | 5 ++++- .../Message/Interactor/MessageInteractor.swift | 5 ++++- Nynja/Statuses/TypingStatusProvider.swift | 15 +++++++++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 0e5138c35..7994cb33d 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -31,7 +31,10 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + typingProvider = TypingStatusProviderImpl(dependencies: + .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) super.init() } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index d0109e5dd..86de8260e 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -31,7 +31,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + typingProvider = TypingStatusProviderImpl(dependencies: + .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) super.init() } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 9902585bc..e5dacf53a 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -178,7 +178,10 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingStatusProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + typingStatusProvider = TypingStatusProviderImpl(dependencies: + .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) super.init() diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index aa6fde968..8e26b9816 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -39,17 +39,21 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - Dependencies struct Dependencies { + let storageService: StorageService let typingHandler: TypingHandler let processingQueue = DispatchQueue.global(qos: .default) let notifyQueue = DispatchQueue.main } + private let storageService: StorageService + private let typingHandler: TypingHandler // MARK: - Init init(dependencies: Dependencies) { + storageService = dependencies.storageService typingHandler = dependencies.typingHandler processingQueue = dependencies.processingQueue notifyQueue = dependencies.notifyQueue @@ -71,10 +75,9 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { - guard let feedId = typing.feed_id, let typingType = typing.type else { + guard let feedId = typing.feed_id, let typingType = typing.type, shouldHandleTyping(typing) else { return } - let feed: SenderInfo.Feed if let senderId = typing.sender_id { guard let alias = typing.sender_alias else { @@ -94,6 +97,14 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } } + private func shouldHandleTyping(_ typing: Typing) -> Bool { + // Don't handle typing from us (in group chat) + if let senderId = typing.sender_id, senderId == storageService.phoneId { + return false + } + return true + } + private func save(_ typingInfo: SenderInfo) { let feedId = typingInfo.feed.identifier -- GitLab From 9dcb2dcf3285b2d3071f535b428beaee71628673 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:36:40 +0200 Subject: [PATCH 058/138] [NY-4699] Rename TypingStatusProvider to TypingProvider. --- Nynja.xcodeproj/project.pbxproj | 8 ++++---- .../ChatsList/Interactor/ChatsListInteractor.swift | 4 ++-- .../GroupsList/Interactor/GroupsListInteractor.swift | 4 ++-- .../Modules/Message/Interactor/MessageInteractor.swift | 10 +++++----- ...TypingStatusProvider.swift => TypingProvider.swift} | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) rename Nynja/Statuses/{TypingStatusProvider.swift => TypingProvider.swift} (96%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index a42efb344..aaadae856 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -972,7 +972,7 @@ 85482848204EA56600DCBEC8 /* PrivacyListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */; }; 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */; }; 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548340D207769E800604051 /* DocumentInteractionInput.swift */; }; - 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingStatusProvider.swift */; }; + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingProvider.swift */; }; 8548341B2187449F002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* Observable.swift */; }; 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* ObservableContainer.swift */; }; 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */; }; @@ -3201,7 +3201,7 @@ 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyListDataSource.swift; sourceTree = ""; }; 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastScrollable.swift; sourceTree = ""; }; 8548340D207769E800604051 /* DocumentInteractionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentInteractionInput.swift; sourceTree = ""; }; - 854834172186FADB002064E1 /* TypingStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusProvider.swift; sourceTree = ""; }; + 854834172186FADB002064E1 /* TypingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingProvider.swift; sourceTree = ""; }; 8548341A2187449F002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 8548341C218744AC002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; @@ -9236,7 +9236,7 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, - 854834172186FADB002064E1 /* TypingStatusProvider.swift */, + 854834172186FADB002064E1 /* TypingProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); path = Statuses; @@ -16378,7 +16378,7 @@ 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, - 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */, + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */, F11DF06520BD96D000F3E005 /* GalleryFilterType.swift in Sources */, FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 7994cb33d..3e2846761 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -12,7 +12,7 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini private let storageService: StorageService private let conversationsProvider: ConversationsProviding - private let typingProvider: TypingStatusProvider + private let typingProvider: TypingProvider private var chats: [Contact] = [] private var searchText: String = "" @@ -31,7 +31,7 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: + typingProvider = TypingProviderImpl(dependencies: .init(storageService: StorageService.sharedInstance, typingHandler: TypingHandler.shared) ) diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index 86de8260e..a497798e9 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -12,7 +12,7 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I private let storageService: StorageService private let conversationsProvider: ConversationsProviding - private let typingProvider: TypingStatusProvider + private let typingProvider: TypingProvider private var chats: [Room] = [] private var searchText: String = "" @@ -31,7 +31,7 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: + typingProvider = TypingProviderImpl(dependencies: .init(storageService: StorageService.sharedInstance, typingHandler: TypingHandler.shared) ) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index e5dacf53a..200973fb3 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -99,7 +99,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private var presenceProvider: PresenceStatusProvider! - private let typingStatusProvider: TypingStatusProvider + private let typingProvider: TypingProvider private let historyRequestFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() @@ -178,7 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingStatusProvider = TypingStatusProviderImpl(dependencies: + typingProvider = TypingProviderImpl(dependencies: .init(storageService: StorageService.sharedInstance, typingHandler: TypingHandler.shared) ) @@ -192,7 +192,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H mqttService.addSubscriber(self) if let chatId = chat.id { - typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in + typingProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in self?.presenter?.didReceiveTyping(typingInfo) } } @@ -207,7 +207,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) - typingStatusProvider.removeObserver(self) + typingProvider.removeObserver(self) MessageHandler.shared.removeSubscriber(self) HistoryHandler.shared.removeSubscriber(self) ConnectionService.shared.removeSubscriber(self) @@ -448,7 +448,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } func askForTypingStatus() { - guard let feedId = chat.id, let typing = typingStatusProvider.typingStatus(for: feedId) else { + guard let feedId = chat.id, let typing = typingProvider.typingStatus(for: feedId) else { return } presenter?.didReceiveTyping(typing) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingProvider.swift similarity index 96% rename from Nynja/Statuses/TypingStatusProvider.swift rename to Nynja/Statuses/TypingProvider.swift index 8e26b9816..bef782d56 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -1,5 +1,5 @@ // -// TypingStatusProvider.swift +// TypingProvider.swift // Nynja // // Created by Anton Poltoratskyi on 29.10.2018. @@ -8,7 +8,7 @@ import Foundation -protocol TypingStatusProvider: class { +protocol TypingProvider: class { typealias Callback = (FeedId, TypingDisplayModel) -> Void func addObserver(_ observer: AnyObject, callback: @escaping Callback) @@ -19,7 +19,7 @@ protocol TypingStatusProvider: class { func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { +final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { let observable = KeyedObservable() @@ -65,7 +65,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } - // MARK: - TypingStatusProvider + // MARK: - TypingProvider func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { return typing(for: feedId).flatMap { displayInfo(for: $0) } @@ -203,7 +203,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - Inner Types -private extension TypingStatusProviderImpl { +private extension TypingProviderImpl { enum TypingData { case p2p(SenderInfo) -- GitLab From bf425d649b2606885bc5327c5da88cd3889ac6ad Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:49:35 +0200 Subject: [PATCH 059/138] [NY-4699] Move TypingProvider initialization to ServiceFactrory. --- Nynja.xcodeproj/project.pbxproj | 8 +++ .../Interactor/ChatsListInteractor.swift | 7 +-- .../WireFrame/ChatsListWireframe.swift | 8 ++- .../Interactor/GroupsListInteractor.swift | 7 +-- .../WireFrame/GroupsListWireframe.swift | 9 +-- .../MQTTHandlerFactoryProtocol.swift | 13 ++++ .../ServiceFactory/ServiceFactory.swift | 59 ++++++------------- .../ServiceFactoryProtocol.swift | 49 +++++++++++++++ 8 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift create mode 100644 Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index aaadae856..4fac69477 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -868,6 +868,8 @@ 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */; }; 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872BE20CD457F007CD6CA /* StickersProviding.swift */; }; 851872C120CD45B3007CD6CA /* StickersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872C020CD45B3007CD6CA /* StickersProvider.swift */; }; + 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */; }; + 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */; }; 851EBD7F20B418890065C644 /* StickersInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EBD7E20B418890065C644 /* StickersInputView.swift */; }; 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F520D4194A007C0036 /* DBRecentSticker.swift */; }; 852003F820D419E9007C0036 /* RecentStickerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F720D419E9007C0036 /* RecentStickerTable.swift */; }; @@ -3127,6 +3129,8 @@ 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDetailsPreviewView.swift; sourceTree = ""; }; 851872BE20CD457F007CD6CA /* StickersProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProviding.swift; path = Services/StickersProvider/StickersProviding.swift; sourceTree = ""; }; 851872C020CD45B3007CD6CA /* StickersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProvider.swift; path = Services/StickersProvider/StickersProvider.swift; sourceTree = ""; }; + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceFactoryProtocol.swift; sourceTree = ""; }; + 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTHandlerFactoryProtocol.swift; sourceTree = ""; }; 851EBD7E20B418890065C644 /* StickersInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputView.swift; sourceTree = ""; }; 852003F520D4194A007C0036 /* DBRecentSticker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRecentSticker.swift; sourceTree = ""; }; 852003F720D419E9007C0036 /* RecentStickerTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentStickerTable.swift; sourceTree = ""; }; @@ -12903,6 +12907,8 @@ F11786EF20AC5474007A9A1B /* ServiceFactory */ = { isa = PBXGroup; children = ( + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */, + 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */, F11786F020AC5482007A9A1B /* ServiceFactory.swift */, ); name = ServiceFactory; @@ -15130,6 +15136,7 @@ 26342CA920ECBAEF00D2196B /* TranscribeNetworkClient.swift in Sources */, 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */, 267BE2831FDE905D00C47E18 /* SettingsProtocols.swift in Sources */, + 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */, 264638231FFFE269002590E6 /* RepliesHeaderView.swift in Sources */, 263D66331FE8D95100A509F8 /* TypingHandler.swift in Sources */, 4B8996F5204EF75500DCB183 /* FeedDAOProtocol.swift in Sources */, @@ -16565,6 +16572,7 @@ 8505445720627C7C00E0F2B3 /* HistoryCellModel.swift in Sources */, 2F2A5C12A7202E7834F923DC /* GroupRulesWireframe.swift in Sources */, 2625DBF820EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift in Sources */, + 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */, D3A30AF05BD7C46A9A8C1FC1 /* GroupStorageProtocols.swift in Sources */, 8520040720D4F436007C0036 /* StickerPreviewConfig.swift in Sources */, F1607B1D20B20F7800BDF60A /* GridView.swift in Sources */, diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 3e2846761..29ca6e092 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -25,16 +25,13 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider - // FIXME: move to factory - typingProvider = TypingProviderImpl(dependencies: - .init(storageService: StorageService.sharedInstance, - typingHandler: TypingHandler.shared) - ) + typingProvider = dependencies.typingProvider super.init() } diff --git a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift index 608c9433c..a1bbd89b0 100644 --- a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift +++ b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift @@ -14,13 +14,17 @@ class ChatsListWireFrame: ChatsListWireFrameProtocol { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) { + let serviceFactory = ServiceFactory() + // Componenets let view = ChatsListViewController() let presenter = ChatsListPresenter() let interactor = ChatsListInteractor( - dependencies: .init(storageService: StorageService.sharedInstance, - conversationsProvider: ConversationsProvider())) + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) self.main = main diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index a497798e9..9b1bf236a 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -25,16 +25,13 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider - // FIXME: move to factory - typingProvider = TypingProviderImpl(dependencies: - .init(storageService: StorageService.sharedInstance, - typingHandler: TypingHandler.shared) - ) + typingProvider = dependencies.typingProvider super.init() } diff --git a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift index 3db6d4df5..bb52986b2 100644 --- a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift +++ b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift @@ -18,15 +18,16 @@ class GroupsListWireFrame: GroupsListWireFrameProtocol { self.main = main // Dependencies - let conversationsProvider = ConversationsProvider() - + let serviceFactory = ServiceFactory() // Compomentes let view = GroupsListViewController() let presenter = GroupsListPresenter() let interactor = GroupsListInteractor( - dependencies: .init(storageService: StorageService.sharedInstance, - conversationsProvider: ConversationsProvider())) + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) // Connecting view.presenter = presenter diff --git a/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift new file mode 100644 index 000000000..48fa93797 --- /dev/null +++ b/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift @@ -0,0 +1,13 @@ +// +// MQTTHandlerFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol MQTTHandlerFactoryProtocol: class { + func makeTypingHandler() -> TypingHandler +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index f581ec61b..5b164cd7d 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -8,48 +8,7 @@ import Foundation -protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol { - func makeMessageSendingService() -> MessageSendingServiceProtocol - func makeResourceManager() -> ResourceManagerProtocol - func makeMessageFactory() -> MessageFactoryProtocol - - func makeMessagePayloadBuilder() -> MessagePayloadBuilderInput - func makeMessagePayloadParser() -> MessagePayloadParserInput - - func makeCameraSettingsService(with flow: CameraSourceFlow) -> CameraSettingsServiceProtocol - - func makeMesageProcessingManager() -> MessageProcessingManagerInterface - - func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol - - func makeContactsProvider() -> ContactsProviding - func makeConversationsProvider() -> ConversationsProviding - func makeStickersProvider() -> StickersProviding - - func makeTextInputValidationService() -> TextInputValidationServiceProtocol - func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol - func makeWalletOpeningTextInputValidationService() -> WalletOpeningTextInputValidationServiceProtocol - func makeWalletFundingNetworkService() -> WalletFundingNetworkService - func makePermissionManager() -> PermissionManager - - func makeWalletService() -> WalletService - func makeSyncFileManager() -> SyncFileManager - - func makeMuteChatService() -> MuteChatServiceProtocol - - func makeConnectionService() -> ConnectionService - - func makeAlertManager() -> AlertManager - - func makeStatusCodeManager() -> StatusCodeManager - - func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol - func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol - - func makeAudioSessionManager() -> AudioSessionManager -} - -final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { +final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol, MQTTHandlerFactoryProtocol { func makeMessageSendingService() -> MessageSendingServiceProtocol { let dependencies = MessageSendingService.Dependencies( @@ -94,6 +53,13 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } + + func makeTypingProvider() -> TypingProvider { + // FIXME: typing handler must be injected + let dependencies = TypingProviderImpl.Dependencies(storageService: makeStorageService(), + typingHandler: makeTypingHandler()) + return TypingProviderImpl(dependencies: dependencies) + } func makeContactsProvider() -> ContactsProviding { return ContactsProvider() @@ -170,3 +136,12 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return AudioSessionManager.shared } } + +// MARK: - MQTT Handlers + +extension ServiceFactory { + + func makeTypingHandler() -> TypingHandler { + return TypingHandler.shared + } +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift new file mode 100644 index 000000000..f0dfad164 --- /dev/null +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -0,0 +1,49 @@ +// +// ServiceFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTHandlerFactoryProtocol { + func makeMessageSendingService() -> MessageSendingServiceProtocol + func makeResourceManager() -> ResourceManagerProtocol + func makeMessageFactory() -> MessageFactoryProtocol + + func makeMessagePayloadBuilder() -> MessagePayloadBuilderInput + func makeMessagePayloadParser() -> MessagePayloadParserInput + + func makeCameraSettingsService(with flow: CameraSourceFlow) -> CameraSettingsServiceProtocol + + func makeMesageProcessingManager() -> MessageProcessingManagerInterface + + func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol + + func makeTypingProvider() -> TypingProvider + func makeContactsProvider() -> ContactsProviding + func makeConversationsProvider() -> ConversationsProviding + func makeStickersProvider() -> StickersProviding + + func makeTextInputValidationService() -> TextInputValidationServiceProtocol + func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol + func makeWalletOpeningTextInputValidationService() -> WalletOpeningTextInputValidationServiceProtocol + func makeWalletFundingNetworkService() -> WalletFundingNetworkService + func makePermissionManager() -> PermissionManager + + func makeWalletService() -> WalletService + func makeSyncFileManager() -> SyncFileManager + + func makeMuteChatService() -> MuteChatServiceProtocol + + func makeConnectionService() -> ConnectionService + + func makeAlertManager() -> AlertManager + + func makeStatusCodeManager() -> StatusCodeManager + + func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol + func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol + + func makeAudioSessionManager() -> AudioSessionManager +} -- GitLab From acdb36ca0ab19733e96d3ae6c265256304b5e818 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:56:46 +0200 Subject: [PATCH 060/138] [NY-4699] Remove unused code. --- Nynja.xcodeproj/project.pbxproj | 4 ---- Nynja/Statuses/AccountStatus.swift | 19 ------------------- Nynja/Statuses/TypingProvider.swift | 2 ++ 3 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 Nynja/Statuses/AccountStatus.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 4fac69477..7b08c754c 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1137,7 +1137,6 @@ 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.swift */; }; - 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3357,7 +3356,6 @@ 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; - 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -9239,7 +9237,6 @@ 85EB37F9218365A6003A2D6F /* Statuses */ = { isa = PBXGroup; children = ( - 85EB37FE21837304003A2D6F /* AccountStatus.swift */, 854834172186FADB002064E1 /* TypingProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); @@ -15303,7 +15300,6 @@ A49B81B320B4BB6400980D36 /* NynjaMTIConfig.swift in Sources */, 8EC2AF6B20053FC300807B20 /* GroupCollectionCell.swift in Sources */, 4B8996D8204EDA7700DCB183 /* JobDAOProtocol.swift in Sources */, - 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */, A4CE80C320C95E7F00400713 /* CollectionDisplayMode.swift in Sources */, E74E53951FB45D6800463242 /* ScrollBar.swift in Sources */, FEA655CD2167777E00B44029 /* SeedVerificationWalletProtocols.swift in Sources */, diff --git a/Nynja/Statuses/AccountStatus.swift b/Nynja/Statuses/AccountStatus.swift deleted file mode 100644 index 2a1bfe2f6..000000000 --- a/Nynja/Statuses/AccountStatus.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccountStatus.swift -// Nynja -// -// Created by Anton Poltoratskyi on 26.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -typealias AccountId = String - -typealias FeedId = String - -enum AccountStatus { - case active - case inactive - case busy - case offline - case none -} diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index bef782d56..3620f9c31 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -8,6 +8,8 @@ import Foundation +typealias FeedId = String + protocol TypingProvider: class { typealias Callback = (FeedId, TypingDisplayModel) -> Void -- GitLab From f804dd6e298d675fb494241d1ed9150ab8269c23 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 18:07:07 +0200 Subject: [PATCH 061/138] [NY-4699] Make TypingProvider to be singleton. --- .../Message/Interactor/MessageInteractor.swift | 5 +---- Nynja/Services/ServiceFactory/ServiceFactory.swift | 5 +---- Nynja/Statuses/TypingProvider.swift | 13 +++++++++++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 200973fb3..431beb157 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -178,10 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingProvider = TypingProviderImpl(dependencies: - .init(storageService: StorageService.sharedInstance, - typingHandler: TypingHandler.shared) - ) + typingProvider = TypingProviderImpl.shared super.init() diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 5b164cd7d..d4367c0fb 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -55,10 +55,7 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol, MQTTHa } func makeTypingProvider() -> TypingProvider { - // FIXME: typing handler must be injected - let dependencies = TypingProviderImpl.Dependencies(storageService: makeStorageService(), - typingHandler: makeTypingHandler()) - return TypingProviderImpl(dependencies: dependencies) + return TypingProviderImpl.shared } func makeContactsProvider() -> ContactsProviding { diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 3620f9c31..83dd04a2d 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -21,7 +21,7 @@ protocol TypingProvider: class { func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { +final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate { let observable = KeyedObservable() @@ -38,6 +38,15 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing private let typingDismissInterval = 10.0 + // MARK: - Singleton + + // FIXME: singleton is a temporary solution until we don't have fullly implemented DI using ServiceFactory in the whole application. + static let shared: TypingProvider = TypingProviderImpl( + dependencies: .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) + + // MARK: - Dependencies struct Dependencies { @@ -54,7 +63,7 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing // MARK: - Init - init(dependencies: Dependencies) { + private init(dependencies: Dependencies) { storageService = dependencies.storageService typingHandler = dependencies.typingHandler processingQueue = dependencies.processingQueue -- GitLab From 99d7cc1bfd074ac0beede83f74ed9e1bfadf7659 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:01:26 +0200 Subject: [PATCH 062/138] [NY-4699] Implemented ability to work correctly both with senderId and without senderId sent in Typing model in p2p chat. --- Nynja/Statuses/TypingProvider.swift | 156 ++++++++++------------------ 1 file changed, 53 insertions(+), 103 deletions(-) diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 83dd04a2d..9edbc028e 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -89,19 +89,12 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing guard let feedId = typing.feed_id, let typingType = typing.type, shouldHandleTyping(typing) else { return } - let feed: SenderInfo.Feed - if let senderId = typing.sender_id { - guard let alias = typing.sender_alias else { - return - } - feed = .room(feedId, senderId: senderId, senderName: alias) - } else { - feed = .p2p(feedId) - } - let status = ActionStatus(typingModelType: typingType) - let senderInfo = SenderInfo(feed: feed, status: status) + let senderInfo = TypingSenderInfo(feedId: feedId, + senderId: typing.sender_id, + senderName: typing.sender_alias, + status: status) processingQueue.async { self.save(senderInfo) @@ -109,51 +102,33 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing } private func shouldHandleTyping(_ typing: Typing) -> Bool { - // Don't handle typing from us (in group chat) + // Ignore typing from myself (in group chat). if let senderId = typing.sender_id, senderId == storageService.phoneId { return false } return true } - private func save(_ typingInfo: SenderInfo) { - let feedId = typingInfo.feed.identifier + private func save(_ typingInfo: TypingSenderInfo) { + let feedId = typingInfo.feedId - if let oldTyping = typing(for: feedId) { - switch oldTyping { - case .p2p: - update(.p2p(typingInfo), for: feedId) - - case let .room(oldTypingSendersInfo): - var newTypingInfo = oldTypingSendersInfo - - newTypingInfo.removeAll { typingInfo.feed.senderId == $0.feed.senderId } - newTypingInfo.append(typingInfo) - - update(.room(newTypingInfo), for: feedId) - } + let typing: TypingData + if let oldTypingData = self.typing(for: feedId) { + + var newTypingData = oldTypingData + + newTypingData.senders.removeAll { $0.senderId == typingInfo.senderId } + newTypingData.senders.append(typingInfo) + + typing = newTypingData + } else { - // Save and notify if didn't exists any other typing status for this feed - switch typingInfo.feed { - case .p2p: - update(.p2p(typingInfo), for: feedId) - case .room: - update(.room([typingInfo]), for: feedId) - } + typing = TypingData(senders: [typingInfo]) } - dismiss(typingInfo, after: typingDismissInterval, for: feedId) - } - - private func dismiss(_ typing: SenderInfo, after delay: TimeInterval, for feedId: FeedId) { - workItems[feedId]?.cancel() - - let workItem = DispatchWorkItem { - self.remove(typing, for: feedId) - } - workItems[feedId] = workItem + update(typing, for: feedId) - processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) + dismiss(typingInfo, after: typingDismissInterval, for: feedId) } private func update(_ typing: TypingData?, for feedId: FeedId) { @@ -165,25 +140,38 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing } } - private func remove(_ typing: SenderInfo, for feedId: FeedId) { - guard let currentTyping = self.typing(for: feedId) else { + private func remove(_ typing: TypingSenderInfo, for feedId: FeedId) { + guard let currentTypingData = self.typing(for: feedId) else { return } - switch currentTyping { - case let .p2p(info): - if info === typing { - update(nil, for: feedId) - } - case let .room(info): - update(.room(info.filter { $0 !== typing }), for: feedId) + var newData = currentTypingData + newData.senders.removeAll { $0 === typing } + + update(newData, for: feedId) + } + + + // MARK: - Dismiss Timer + + private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval, for feedId: FeedId) { + workItems[feedId]?.cancel() + + let workItem = DispatchWorkItem { + self.remove(typing, for: feedId) } + workItems[feedId] = workItem + + processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) } + + // MARK: - Display Model + private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { return .none } - let senders = typingInfo.compactMap { $0.feed.senderName } + let senders = typingInfo.compactMap { $0.senderName } let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) let shouldDisplayStatus = !typingInfo.contains { $0.status != lastStatus } @@ -216,58 +204,20 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing private extension TypingProviderImpl { - enum TypingData { - case p2p(SenderInfo) - case room([SenderInfo]) - - var senders: [SenderInfo] { - switch self { - case let .p2p(sender): - return [sender] - case let .room(senders): - return senders - } - } + struct TypingData { + var senders: [TypingSenderInfo] } - final class SenderInfo { - enum Feed { - case p2p(FeedId) - case room(FeedId, senderId: String, senderName: String) - - var identifier: String { - switch self { - case let .p2p(id): - return id - case let .room(id, _, _): - return id - } - } - - var senderId: String? { - switch self { - case let .room(_, senderId, _): - return senderId - case .p2p: - return nil - } - } - - var senderName: String? { - switch self { - case let .room(_, _, senderName): - return senderName - case .p2p: - return nil - } - } - } - - let feed: Feed + final class TypingSenderInfo { + let feedId: FeedId + let senderId: String + let senderName: String? let status: ActionStatus - init(feed: Feed, status: ActionStatus) { - self.feed = feed + init(feedId: FeedId, senderId: String?, senderName: String?, status: ActionStatus) { + self.feedId = feedId + self.senderId = senderId ?? feedId + self.senderName = senderName self.status = status } } -- GitLab From 46c9af56123337fc30039949479d7913ee5f1822 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:11:50 +0200 Subject: [PATCH 063/138] [NY-4699] Added validation for tuple.elements in Decoder. --- Nynja/ServerModel/Source/Decoder.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Nynja/ServerModel/Source/Decoder.swift b/Nynja/ServerModel/Source/Decoder.swift index 844a4646b..d6116cd2e 100644 --- a/Nynja/ServerModel/Source/Decoder.swift +++ b/Nynja/ServerModel/Source/Decoder.swift @@ -1,5 +1,7 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? { + guard tuple.elements.count == body.count + 1 else { return nil } + switch name { case "writer": if body.count != 5 { return nil } -- GitLab From 16cf00d9fd18a2dcb706391ede1b75ef8b584a99 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:26:36 +0200 Subject: [PATCH 064/138] [NY-4699] Move TypingObservable to separate file. --- Nynja.xcodeproj/project.pbxproj | 4 ++++ Nynja/Modules/ChatsList/ChatsListProtocols.swift | 5 ----- Nynja/Statuses/TypingObservable.swift | 12 ++++++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 Nynja/Statuses/TypingObservable.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 7b08c754c..b5b008825 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1104,6 +1104,7 @@ 85CB25DC20D723D300D5E565 /* StickerPackDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CB25DB20D723D300D5E565 /* StickerPackDAOProtocol.swift */; }; 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */; }; 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */; }; + 85CEFBC0218C5D9500760F9E /* TypingObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */; }; 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */; }; 85D669E520BD956000FBD803 /* UIButtonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */; }; 85D669E620BD956000FBD803 /* UIView+Shadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E320BD956000FBD803 /* UIView+Shadow.swift */; }; @@ -3326,6 +3327,7 @@ 85CB25DB20D723D300D5E565 /* StickerPackDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackDAOProtocol.swift; sourceTree = ""; }; 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackExtension.swift; sourceTree = ""; }; 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticSelectionFeedbackGenerator.swift; sourceTree = ""; }; + 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingObservable.swift; sourceTree = ""; }; 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Int+AnyObject.swift"; sourceTree = ""; }; 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensions.swift; sourceTree = ""; }; 85D669E320BD956000FBD803 /* UIView+Shadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Shadow.swift"; sourceTree = ""; }; @@ -9238,6 +9240,7 @@ isa = PBXGroup; children = ( 854834172186FADB002064E1 /* TypingProvider.swift */, + 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); path = Statuses; @@ -15704,6 +15707,7 @@ A42D52C4206A53AA00EEB952 /* error2_Spec.swift in Sources */, 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */, 00E9824E205C2604008BF03D /* SessionItemView.swift in Sources */, + 85CEFBC0218C5D9500760F9E /* TypingObservable.swift in Sources */, 8520040920D4F9B4007C0036 /* MessageStickerRepliedView.swift in Sources */, 00102F40202C8E5300A877A9 /* NynjaCalendarView.swift in Sources */, 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */, diff --git a/Nynja/Modules/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index cbce84e51..e9ee87810 100644 --- a/Nynja/Modules/ChatsList/ChatsListProtocols.swift +++ b/Nynja/Modules/ChatsList/ChatsListProtocols.swift @@ -8,11 +8,6 @@ import UIKit -protocol TypingObservable: class { - func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) - func removeObserver(for feedId: FeedId) -} - protocol ChatsListWireFrameProtocol: class { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) diff --git a/Nynja/Statuses/TypingObservable.swift b/Nynja/Statuses/TypingObservable.swift new file mode 100644 index 000000000..34a6b75e9 --- /dev/null +++ b/Nynja/Statuses/TypingObservable.swift @@ -0,0 +1,12 @@ +// +// TypingObservable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 02.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol TypingObservable: class { + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) + func removeObserver(for feedId: FeedId) +} -- GitLab From 2a4813b4adc33ae8e5b06edd6ac82dd7f5144a6b Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:37:33 +0200 Subject: [PATCH 065/138] [NY-4699] Rename observables. --- Nynja.xcodeproj/project.pbxproj | 32 ++++----- Nynja/Observable/KeyedObservable.swift | 70 ++++++------------- .../Observable/KeyedObservableContainer.swift | 70 +++++++++++++------ Nynja/Observable/Observable.swift | 38 ++++------ Nynja/Observable/ObservableContainer.swift | 38 ++++++---- .../HandleServices/TypingHandler.swift | 4 +- Nynja/Statuses/TypingProvider.swift | 4 +- 7 files changed, 128 insertions(+), 128 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b5b008825..f42862bb3 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -975,8 +975,8 @@ 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */; }; 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548340D207769E800604051 /* DocumentInteractionInput.swift */; }; 854834182186FADB002064E1 /* TypingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingProvider.swift */; }; - 8548341B2187449F002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* Observable.swift */; }; - 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* ObservableContainer.swift */; }; + 8548341B2187449F002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* ObservableContainer.swift */; }; + 8548341D218744AC002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* Observable.swift */; }; 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */; }; 854A4B2D2080D68200759152 /* CellWithArrowCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */; }; 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */; }; @@ -1136,8 +1136,8 @@ 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2620BEE961008AD211 /* ScalableCell.swift */; }; 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; - 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; - 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.swift */; }; + 85EB37FB21837235003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservable.swift */; }; + 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3206,8 +3206,8 @@ 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastScrollable.swift; sourceTree = ""; }; 8548340D207769E800604051 /* DocumentInteractionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentInteractionInput.swift; sourceTree = ""; }; 854834172186FADB002064E1 /* TypingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingProvider.swift; sourceTree = ""; }; - 8548341A2187449F002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; - 8548341C218744AC002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; + 8548341A2187449F002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; + 8548341C218744AC002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowCellModel.swift; sourceTree = ""; }; 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; @@ -3356,8 +3356,8 @@ 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageVC+StickerInputModuleDelegate.swift"; sourceTree = ""; }; 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; - 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; - 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -8489,10 +8489,10 @@ 8548341921874434002064E1 /* Observable */ = { isa = PBXGroup; children = ( - 8548341A2187449F002064E1 /* Observable.swift */, - 8548341C218744AC002064E1 /* ObservableContainer.swift */, - 85EB37FC21837253003A2D6F /* KeyedObservable.swift */, - 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, + 8548341C218744AC002064E1 /* Observable.swift */, + 8548341A2187449F002064E1 /* ObservableContainer.swift */, + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */, + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */, ); path = Observable; sourceTree = ""; @@ -15331,7 +15331,7 @@ 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, - 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */, + 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15495,7 +15495,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, - 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */, + 85EB37FB21837235003A2D6F /* KeyedObservable.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, @@ -15720,7 +15720,7 @@ 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, - 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */, + 8548341D218744AC002064E1 /* Observable.swift in Sources */, 4B1D7DFE2029C41C00703228 /* AboutItemsFactory.swift in Sources */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */, @@ -15985,7 +15985,7 @@ E70938371FBEDA2B006CCDC6 /* ProfileTable.swift in Sources */, A416DA602075341C00FBF1BA /* CLLocationCoordinate2D+Payload.swift in Sources */, A4679BAE20B2DD100021FE9C /* SubscribersSelectorInteractor.swift in Sources */, - 8548341B2187449F002064E1 /* Observable.swift in Sources */, + 8548341B2187449F002064E1 /* ObservableContainer.swift in Sources */, FEA655FD2167777F00B44029 /* TransferDetailsInteractor.swift in Sources */, E70F78B91FD6C64E00385565 /* ChatCheckpointTable.swift in Sources */, 4B06D30620287060003B275B /* WCDataManagerProtocol.swift in Sources */, diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift index 6c1da682a..3ade428dc 100644 --- a/Nynja/Observable/KeyedObservable.swift +++ b/Nynja/Observable/KeyedObservable.swift @@ -6,67 +6,39 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +protocol KeyedObservable: class { + associatedtype Key: Hashable + associatedtype Value + typealias Callback = (Key, Value) -> Void + + var observable: KeyedObservableContainer { get } + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: Key) + func notify(_ key: Key, with value: Value) +} -final class KeyedObservable { - - private typealias Observers = [AnyWeakSubscriber] - - private struct Handler { - var callback: (Key, Value) -> Void - } - - private let lock = NSLock() - - private var allObservers: Observers = [] - - private var observers: [Key: Observers] = [:] +extension KeyedObservable { - func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { - let handler = Handler(callback: callback) - let container = AnyWeakSubscriber(object: observer, handler: handler) - - lock.lock() - - allObservers.append(container) - - lock.unlock() + func addObserver(_ observer: AnyObject, callback: @escaping Callback) { + observable.addObserver(observer, callback: callback) } - func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { - let handler = Handler(callback: callback) - let container = AnyWeakSubscriber(object: observer, handler: handler) - - lock.lock() - - var newObservers = observers[key] ?? [] - newObservers.append(container) - observers[key] = newObservers - - lock.unlock() + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) { + observable.addObserver(observer, for: key, callback: callback) } func removeObserver(_ observer: AnyObject) { - lock.lock() - allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } - for (key, _) in observers { - observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } - } - lock.unlock() + observable.removeObserver(observer) } func removeObserver(_ observer: AnyObject, for key: Key) { - lock.lock() - observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } - lock.unlock() + observable.removeObserver(observer, for: key) } func notify(_ key: Key, with value: Value) { - lock.lock() - for observer in allObservers { - observer.handler.callback(key, value) - } - observers[key]?.forEach { $0.handler.callback(key, value) } - lock.unlock() + observable.notify(key, with: value) } } diff --git a/Nynja/Observable/KeyedObservableContainer.swift b/Nynja/Observable/KeyedObservableContainer.swift index ce8dfedc0..f728dfe2e 100644 --- a/Nynja/Observable/KeyedObservableContainer.swift +++ b/Nynja/Observable/KeyedObservableContainer.swift @@ -6,39 +6,67 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol KeyedObservableContainer: class { - associatedtype Key: Hashable - associatedtype Value - typealias Callback = (Key, Value) -> Void - - var observable: KeyedObservable { get } - - func addObserver(_ observer: AnyObject, callback: @escaping Callback) - func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) - func removeObserver(_ observer: AnyObject) - func removeObserver(_ observer: AnyObject, for key: Key) - func notify(_ key: Key, with value: Value) -} +import Foundation -extension KeyedObservableContainer { +final class KeyedObservableContainer { + + private typealias Observers = [AnyWeakSubscriber] + + private struct Handler { + var callback: (Key, Value) -> Void + } + + private let lock = NSLock() + + private var allObservers: Observers = [] + + private var observers: [Key: Observers] = [:] - func addObserver(_ observer: AnyObject, callback: @escaping Callback) { - observable.addObserver(observer, callback: callback) + func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + allObservers.append(container) + + lock.unlock() } - func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) { - observable.addObserver(observer, for: key, callback: callback) + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + var newObservers = observers[key] ?? [] + newObservers.append(container) + observers[key] = newObservers + + lock.unlock() } func removeObserver(_ observer: AnyObject) { - observable.removeObserver(observer) + lock.lock() + allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + for (key, _) in observers { + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + } + lock.unlock() } func removeObserver(_ observer: AnyObject, for key: Key) { - observable.removeObserver(observer, for: key) + lock.lock() + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() } func notify(_ key: Key, with value: Value) { - observable.notify(key, with: value) + lock.lock() + for observer in allObservers { + observer.handler.callback(key, value) + } + observers[key]?.forEach { $0.handler.callback(key, value) } + lock.unlock() } } diff --git a/Nynja/Observable/Observable.swift b/Nynja/Observable/Observable.swift index fb149c81c..0f648eb7d 100644 --- a/Nynja/Observable/Observable.swift +++ b/Nynja/Observable/Observable.swift @@ -6,36 +6,26 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - -final class Observable { - - private typealias Observers = [WeakRef] +protocol Observable: class { + associatedtype Observer + var observable: ObservableContainer { get } - private let lock = NSLock() + func addObserver(_ observer: Observer) + func removeObserver(_ observer: Observer) + func notify(_ block: (Observer) -> Void) +} - private var observers: Observers = [] +extension Observable { - func addObserver(_ observer: T) { - let container = WeakRef(value: observer as AnyObject) - - lock.lock() - - observers.append(container) - - lock.unlock() + func addObserver(_ observer: Observer) { + observable.addObserver(observer) } - func removeObserver(_ observer: T) { - let observer = observer as AnyObject - lock.lock() - observers.removeAll { $0.value === observer || $0.value == nil } - lock.unlock() + func removeObserver(_ observer: Observer) { + observable.removeObserver(observer) } - func notify(_ block: (T) -> Void) { - lock.lock() - observers.forEach { ($0.value as? T).flatMap(block) } - lock.unlock() + func notify(_ block: (Observer) -> Void) { + observable.notify(block) } } diff --git a/Nynja/Observable/ObservableContainer.swift b/Nynja/Observable/ObservableContainer.swift index d995d3c90..183ee0dd5 100644 --- a/Nynja/Observable/ObservableContainer.swift +++ b/Nynja/Observable/ObservableContainer.swift @@ -6,26 +6,36 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol ObservableContainer: class { - associatedtype Observer - var observable: Observable { get } +import Foundation + +final class ObservableContainer { - func addObserver(_ observer: Observer) - func removeObserver(_ observer: Observer) - func notify(_ block: (Observer) -> Void) -} + private typealias Observers = [WeakRef] + + private let lock = NSLock() -extension ObservableContainer { + private var observers: Observers = [] - func addObserver(_ observer: Observer) { - observable.addObserver(observer) + func addObserver(_ observer: T) { + let container = WeakRef(value: observer as AnyObject) + + lock.lock() + + observers.append(container) + + lock.unlock() } - func removeObserver(_ observer: Observer) { - observable.removeObserver(observer) + func removeObserver(_ observer: T) { + let observer = observer as AnyObject + lock.lock() + observers.removeAll { $0.value === observer || $0.value == nil } + lock.unlock() } - func notify(_ block: (Observer) -> Void) { - observable.notify(block) + func notify(_ block: (T) -> Void) { + lock.lock() + observers.forEach { ($0.value as? T).flatMap(block) } + lock.unlock() } } diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index 7774b0c4e..ef56ab9ab 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -12,7 +12,7 @@ protocol TypingHandlerDelegate: class { func didReceiveTyping(_ typing: Typing) } -final class TypingHandler: BaseHandler, ObservableContainer { +final class TypingHandler: BaseHandler, Observable { // MARK: - Singleton @@ -23,7 +23,7 @@ final class TypingHandler: BaseHandler, ObservableContainer { // MARK: - ObservableContainer - let observable = Observable() + let observable = ObservableContainer() // MARK: - Handler diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 9edbc028e..840385e27 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -21,9 +21,9 @@ protocol TypingProvider: class { func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate { +final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDelegate { - let observable = KeyedObservable() + let observable = KeyedObservableContainer() private var data: [FeedId: TypingData] = [:] -- GitLab From 7048ec049d1b5bce95d89def29571e5f36ede6ef Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:42:38 +0200 Subject: [PATCH 066/138] [NY-4699] Remove unused code from TypingView. --- .../NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 5 +---- .../Cell/ChatListMessageTableViewCell.swift | 3 +-- Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 6e3bc5c57..665f1f922 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -23,20 +23,17 @@ public final class TypingView: BaseView { public let textFont: UIFont public let senderInfo: String? public let typingInfo: String - public let isTypingInfoPinned: Bool public init(indicator: Indicator, textColor: UIColor, textFont: UIFont, senderInfo: String?, - typingInfo: String, - isTypingInfoPinned: Bool) { + typingInfo: String) { self.indicator = indicator self.textColor = textColor self.textFont = textFont self.senderInfo = senderInfo self.typingInfo = typingInfo - self.isTypingInfoPinned = isTypingInfoPinned } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 0199e67f9..4885b1c82 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -153,8 +153,7 @@ final class ChatListMessageTableViewCell: UITableViewCell { textColor: UIColor.nynja.white, textFont: ChatListMessageContentView.typingFont, senderInfo: sender?.displayName, - typingInfo: status.title, - isTypingInfoPinned: false + typingInfo: status.title ) messageContentView.showTyping(with: appearance) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index e2b5566cc..44212f7e3 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -41,8 +41,7 @@ final class AvatarView: BaseView { textColor: titleLabel.textColor, textFont: statusLabel.font, senderInfo: sender?.displayName, - typingInfo: status.title, - isTypingInfoPinned: false + typingInfo: status.title ) typingView.update(appearance) -- GitLab From c39ace9781980d4b1ef7a6c90c594a4ec837bb30 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 13:07:53 +0200 Subject: [PATCH 067/138] [NY-4699] Implemented Typing on Profile screen. --- .../Interactor/ChatsListInteractor.swift | 2 +- .../Interactor/GroupsListInteractor.swift | 7 +++- .../Interactor/ProfileInteractor.swift | 39 +++++++++++++++++-- .../Profile/Presenter/ProfilePresenter.swift | 10 +++++ Nynja/Modules/Profile/ProfileProtocols.swift | 4 +- .../Profile/View/ProfileViewController.swift | 5 ++- .../View/TableView/ProfileTablewViewDS.swift | 5 +++ .../Profile/WireFrame/ProfileWireframe.swift | 11 +++--- 8 files changed, 69 insertions(+), 14 deletions(-) diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 29ca6e092..1dc1e96a3 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -49,11 +49,11 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini override func loadData() { super.loadData() - fetchChats() typingProvider.addObserver(self) { [weak self] feedId, typing in self?.typingHandlers[feedId]?(typing) } + fetchChats() } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index 9b1bf236a..030bc26d8 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -36,6 +36,11 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I super.init() } + deinit { + typingProvider.removeObserver(self) + } + + // MARK: - BaseInteractor override var subscribes: [SubscribeType]? { @@ -48,11 +53,11 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I override func loadData() { super.loadData() - fetchGroups() typingProvider.addObserver(self) { [weak self] feedId, typing in self?.typingHandlers[feedId]?(typing) } + fetchGroups() } diff --git a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift index 0b73d2bbf..aa441303d 100644 --- a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift +++ b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift @@ -20,17 +20,30 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { var starred: CellModels = [] var scheduled: CellModels = [] + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + private var contactsProvider: ContactsProviding! private var conversationsProvider: ConversationsProviding! + private var typingProvider: TypingProvider! private var mqttService: MQTTService! - //MARK: - BaseInteractor + deinit { + typingProvider.removeObserver(self) + } + + + // MARK: - BaseInteractor + override var subscribes: [SubscribeType]? { return [.contact(StorageService.sharedInstance.phoneId!), .contact(nil), .room(nil), .star(nil), .job(nil), .profile] } override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchContact() fetchRooms() fetchChats() @@ -40,6 +53,9 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { fetchLastEvents() } + + // MARK: - ProfileInteractorInputProtocol + func acceptContact(with phoneId: String) { mqttService.confirmFriend(friendPhoneId: phoneId) @@ -64,6 +80,16 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { } } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + + // MARK: - StorageSubscriber override func update(with changes: [StorageChange], type: SubscribeType) { @@ -88,8 +114,10 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { } } -//MARK: Private methods -fileprivate extension ProfileInteractor { + +// MARK: - Private methods + +private extension ProfileInteractor { func fetchContact() { guard let phoneId = StorageService.sharedInstance.phoneId, @@ -156,11 +184,15 @@ fileprivate extension ProfileInteractor { } + +// MARK: - SetInjectable + extension ProfileInteractor: SetInjectable { func inject(dependencies: ProfileInteractor.Dependencies) { presenter = dependencies.presenter contactsProvider = dependencies.contactsProvider conversationsProvider = dependencies.conversationsProvider + typingProvider = dependencies.typingProvider mqttService = dependencies.mqttService } @@ -168,6 +200,7 @@ extension ProfileInteractor: SetInjectable { let presenter: ProfileInteractorOutputProtocol let contactsProvider: ContactsProviding let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider let mqttService: MQTTService } } diff --git a/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift b/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift index 4728e19ed..806d35cd7 100644 --- a/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift +++ b/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift @@ -115,6 +115,16 @@ class ProfilePresenter: BasePresenter, ProfilePresenterProtocol, ProfileInteract return nil } } + + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + + // MARK: - ProfileInteractorOutputProtocol diff --git a/Nynja/Modules/Profile/ProfileProtocols.swift b/Nynja/Modules/Profile/ProfileProtocols.swift index 045570511..83626aca3 100644 --- a/Nynja/Modules/Profile/ProfileProtocols.swift +++ b/Nynja/Modules/Profile/ProfileProtocols.swift @@ -28,7 +28,7 @@ protocol ProfileWireFrameProtocol: class { func showFavorites() } -protocol ProfilePresenterProtocol: BasePresenterProtocol { +protocol ProfilePresenterProtocol: BasePresenterProtocol, TypingObservable { func showImagePreview(from imageView: UIImageView) func showQRGenerator() @@ -66,7 +66,7 @@ protocol ProfileInteractorOutputProtocol: class { func showWallet(with balance: NYNMoney?) } -protocol ProfileInteractorInputProtocol: BaseInteractorProtocol { +protocol ProfileInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var myContact: Contact! { get set } diff --git a/Nynja/Modules/Profile/View/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index 33f2fc770..81be85f01 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -91,7 +91,10 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { tableView.register(StarMessageCell.self, forCellReuseIdentifier: StarMessageCell.cellId) tableViewDelegate = ProfileTableViewDelegate(sectionDelegate: self) - tableViewDS = ProfileTablewViewDS(payloadParser: payloadParser, contactCellDelegate: self, chatCellDelegate: self) + tableViewDS = ProfileTablewViewDS(payloadParser: payloadParser, + typingObservable: presenter, + contactCellDelegate: self, + chatCellDelegate: self) tableView.delegate = tableViewDelegate tableView.dataSource = tableViewDS diff --git a/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift b/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift index f5b2e40fa..1c7cf6a6b 100644 --- a/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift +++ b/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift @@ -21,11 +21,15 @@ class ProfileTablewViewDS: NSObject, UITableViewDataSource { private let payloadParser: MessagePayloadParserInput + private let typingObservable: TypingObservable + init(payloadParser: MessagePayloadParserInput, + typingObservable: TypingObservable, contactCellDelegate: ProfileContactCellDelegate, chatCellDelegate: ChatListMessageCellModelDelegate) { self.payloadParser = payloadParser + self.typingObservable = typingObservable self.contactCellDelegate = contactCellDelegate self.chatCellDelegate = chatCellDelegate } @@ -68,6 +72,7 @@ class ProfileTablewViewDS: NSObject, UITableViewDataSource { if let cellModel = cellModel as? DialogCellModel { let model = ChatListMessageCellModel( model: cellModel, + observable: typingObservable, payloadParser: payloadParser, delegate: chatCellDelegate ) diff --git a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift index 119210b33..e64262d1e 100644 --- a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift +++ b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift @@ -16,9 +16,7 @@ class ProfileWireFrame: ProfileWireFrameProtocol { main: MainWireFrame?) { // Dependencies - let contactsProvider = ContactsProvider() - let conversationsProvider = ConversationsProvider() - let mqttService = MQTTService.sharedInstance + let serviceFactory = ServiceFactory() // Components let view = ProfileViewController() @@ -26,9 +24,10 @@ class ProfileWireFrame: ProfileWireFrameProtocol { let interactor = ProfileInteractor() let interactorDependencies = ProfileInteractor.Dependencies(presenter: presenter, - contactsProvider: contactsProvider, - conversationsProvider: conversationsProvider, - mqttService: mqttService) + contactsProvider: serviceFactory.makeContactsProvider(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider(), + mqttService: serviceFactory.makeMQTTService()) let presenterDependencies = ProfilePresenter.Dependencies(view: view, wireFrame: self, interactor: interactor) let viewDependencies = ProfileViewController.Dependencies(presenter: presenter) -- GitLab From 28734ce98b306c1eabcb2ab043c82be5120e691c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 17:09:37 +0200 Subject: [PATCH 068/138] [NY-4699] Fixed wrong logic of dismiss timer. --- Nynja/Statuses/TypingProvider.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 840385e27..ffb9895ed 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -128,7 +128,7 @@ final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDe update(typing, for: feedId) - dismiss(typingInfo, after: typingDismissInterval, for: feedId) + dismiss(typingInfo, after: typingDismissInterval) } private func update(_ typing: TypingData?, for feedId: FeedId) { @@ -140,7 +140,8 @@ final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDe } } - private func remove(_ typing: TypingSenderInfo, for feedId: FeedId) { + private func remove(_ typing: TypingSenderInfo) { + let feedId = typing.feedId guard let currentTypingData = self.typing(for: feedId) else { return } @@ -153,13 +154,15 @@ final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDe // MARK: - Dismiss Timer - private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval, for feedId: FeedId) { - workItems[feedId]?.cancel() + private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval) { + let dismissKey = "\(typing.feedId)\(typing.senderId)" + + workItems[dismissKey]?.cancel() let workItem = DispatchWorkItem { - self.remove(typing, for: feedId) + self.remove(typing) } - workItems[feedId] = workItem + workItems[dismissKey] = workItem processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) } -- GitLab From a36e4529eb41aa4d60b2627490fdb3c07302a1ec Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 20:09:23 +0200 Subject: [PATCH 069/138] [NY-4699] Fixed layout of ChatListMessageTableViewCell. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 1 - Nynja.xcodeproj/project.pbxproj | 8 +- .../Cell/ChatListMessageContentView.swift | 69 +++++++++++++++-- ...ft => ChatListMessageIndicatorsView.swift} | 76 ++++++------------- .../Cell/ChatListMessageTableViewCell.swift | 32 +------- .../Cell/CounterView.swift | 25 +++--- .../Model/ChatListMessageCellModel.swift | 4 +- 7 files changed, 111 insertions(+), 104 deletions(-) rename Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/{ChatListMessageAccessoryView.swift => ChatListMessageIndicatorsView.swift} (59%) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 665f1f922..9f001bf32 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -49,7 +49,6 @@ public final class TypingView: BaseView { private lazy var senderInfoLabel: UILabel = { let label = UILabel() - label.setContentCompressionResistancePriority(.required, for: .horizontal) addSubview(label) return label }() diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index f42862bb3..3b43cd31f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1045,7 +1045,6 @@ 8580BACA20BD983400239D9D /* MentionTransitionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAC520BD983400239D9D /* MentionTransitionProtocol.swift */; }; 8580BACC20BD984500239D9D /* MessageEditInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BACB20BD984400239D9D /* MessageEditInfo.swift */; }; 8580BACE20BD98CF00239D9D /* UpdateResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BACD20BD98CF00239D9D /* UpdateResult.swift */; }; - 8580BAD720BD98E700239D9D /* ChatListMessageAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */; }; 8580BAD820BD98E700239D9D /* CounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD220BD98E600239D9D /* CounterView.swift */; }; 8580BAD920BD98E700239D9D /* ChatListMessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */; }; 8580BADA20BD98E700239D9D /* ChatListMessageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */; }; @@ -1105,6 +1104,7 @@ 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */; }; 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */; }; 85CEFBC0218C5D9500760F9E /* TypingObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */; }; + 85CEFBC5218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */; }; 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */; }; 85D669E520BD956000FBD803 /* UIButtonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */; }; 85D669E620BD956000FBD803 /* UIView+Shadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E320BD956000FBD803 /* UIView+Shadow.swift */; }; @@ -3271,7 +3271,6 @@ 8580BAC520BD983400239D9D /* MentionTransitionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionTransitionProtocol.swift; sourceTree = ""; }; 8580BACB20BD984400239D9D /* MessageEditInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEditInfo.swift; sourceTree = ""; }; 8580BACD20BD98CF00239D9D /* UpdateResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateResult.swift; sourceTree = ""; }; - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageAccessoryView.swift; sourceTree = ""; }; 8580BAD220BD98E600239D9D /* CounterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CounterView.swift; sourceTree = ""; }; 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageTableViewCell.swift; sourceTree = ""; }; 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageContentView.swift; sourceTree = ""; }; @@ -3328,6 +3327,7 @@ 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackExtension.swift; sourceTree = ""; }; 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticSelectionFeedbackGenerator.swift; sourceTree = ""; }; 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingObservable.swift; sourceTree = ""; }; + 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageIndicatorsView.swift; sourceTree = ""; }; 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Int+AnyObject.swift"; sourceTree = ""; }; 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensions.swift; sourceTree = ""; }; 85D669E320BD956000FBD803 /* UIView+Shadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Shadow.swift"; sourceTree = ""; }; @@ -8805,8 +8805,8 @@ children = ( 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */, 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */, - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */, + 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */, 8580BAD220BD98E600239D9D /* CounterView.swift */, ); path = Cell; @@ -14950,6 +14950,7 @@ A45F112920B4218D00F45004 /* MessageContentAppearance.swift in Sources */, C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */, FE2D7CCD211C71AE00520D78 /* WalletService.swift in Sources */, + 85CEFBC5218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift in Sources */, 4BE2C5E22142EB0F00A73DD9 /* AudioManager.swift in Sources */, E7598F5B1FA1D5D90082FBE7 /* ProfileActionCellLayout.swift in Sources */, 85082DDD2045A873000AE4B2 /* UserSettingsService.swift in Sources */, @@ -15259,7 +15260,6 @@ A415132220DBD59B00C2C01F /* Link_Spec.swift in Sources */, 85433F25204D596D00B373A7 /* WebFullScreenInteractor.swift in Sources */, 8504DEA920693588006722AC /* MediaFullWheelItemModel.swift in Sources */, - 8580BAD720BD98E700239D9D /* ChatListMessageAccessoryView.swift in Sources */, 6F3F21025258D8071BCF95EF /* LoginWireframe.swift in Sources */, 26DCB2522064BA46001EF0AB /* ContactsInteractor.swift in Sources */, B767F48F215D1E0A00FA9B27 /* ComingSoonExtension.swift in Sources */, diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index 1ad7e579f..afbdb2a0a 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -12,6 +12,9 @@ import NynjaUIKit final class ChatListMessageContentView: BaseView { + private static let dateFormatter = DialogDateConverter() + + // MARK: - Views private(set) lazy var titleLabel: UILabel = { @@ -20,9 +23,30 @@ final class ChatListMessageContentView: BaseView { let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.medium.name) label.accessibilityIdentifier = "chat_name" + addSubview(label) label.snp.makeConstraints { maker in - maker.top.left.right.equalToSuperview() + maker.top.left.equalToSuperview() + maker.height.equalTo(height) + } + + return label + }() + + private(set) lazy var timeLabel: UILabel = { + let leftInfset = Constraints.timeLabel.leftInset.adjustedByWidth + + let height = Constraints.timeLabel.height.adjustedByWidth + let color = UIColor.nynja.manatee + + let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name, textAlignment: .right) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.setContentHuggingPriority(.required, for: .horizontal) + + addSubview(label) + label.snp.makeConstraints { maker in + maker.top.right.equalToSuperview() + maker.left.equalTo(titleLabel.snp.right).offset(leftInfset) maker.height.equalTo(height) } @@ -35,7 +59,7 @@ final class ChatListMessageContentView: BaseView { addSubview(textView) textView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.bottom.left.right.equalToSuperview() + maker.bottom.left.equalToSuperview() } return textView @@ -49,21 +73,36 @@ final class ChatListMessageContentView: BaseView { addSubview(typingView) typingView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.left.right.equalToSuperview() + maker.left.equalToSuperview() maker.height.equalTo(height) } return typingView }() + private(set) lazy var indicatorsView: ChatListMessageIndicatorsView = { + let leftInfset = Constraints.indicatorsView.leftInset.adjustedByWidth + let topInset = Constraints.indicatorsView.topInset.adjustedByWidth + + let indicatorsView = ChatListMessageIndicatorsView() + + addSubview(indicatorsView) + indicatorsView.snp.makeConstraints { maker in + maker.top.equalTo(timeLabel.snp.bottom).offset(topInset) + maker.right.equalToSuperview() + maker.left.equalTo(textView.snp.right).offset(leftInfset) + maker.left.equalTo(typingView.snp.right).offset(leftInfset) + } + + return indicatorsView + }() + // MARK: - Setup override func baseSetup() { super.baseSetup() - titleLabel.isHidden = false - textView.isHidden = false - typingView.isHidden = false + indicatorsView.isHidden = false } func setupTitle(_ title: String?) { @@ -75,6 +114,14 @@ final class ChatListMessageContentView: BaseView { textView.setup(sender: sender, image: image, text: text) } + func setup(date: Date) { + timeLabel.text = type(of: self).dateFormatter.toString(date) + } + + func setup(mentions: Bool, unreadCount: Int) { + indicatorsView.setup(mentions: mentions, unreadCount: unreadCount) + } + func showTyping(with appearance: TypingView.Appearance) { typingView.isHidden = false textView.isHidden = true @@ -98,8 +145,18 @@ final class ChatListMessageContentView: BaseView { static let height: CGFloat = 22 } + enum timeLabel { + static let leftInset: CGFloat = 8.0 + static let height: CGFloat = 17 + } + enum typingView { static let height: CGFloat = 20 } + + enum indicatorsView { + static let leftInset: CGFloat = 8.0 + static let topInset: CGFloat = 4.0 + } } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift similarity index 59% rename from Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift rename to Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift index 0faef603f..b697a079d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift @@ -1,65 +1,48 @@ // -// ChatListMessageAccessoryView.swift +// ChatListMessageIndicatorsView.swift // Nynja // -// Created by Anton Poltoratskyi on 15.05.2018. +// Created by Anton Poltoratskyi on 02.11.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // import UIKit import SnapKit -final class ChatListMessageAccessoryView: BaseView { - - private static let dateFormatter = DialogDateConverter() - +final class ChatListMessageIndicatorsView: BaseView { // MARK: - Views - private(set) lazy var timeLabel: UILabel = { - let height = Constraints.timeLabel.height.adjustedByWidth - let color = UIColor.nynja.manatee - - let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name, textAlignment: .right) - label.setContentCompressionResistancePriority(.required, for: .horizontal) - - addSubview(label) - label.snp.makeConstraints { maker in - maker.top.left.right.equalToSuperview() - maker.height.equalTo(height) - } - - return label - }() + private var counterSuperviewLeftConstraint: Constraint? private(set) lazy var counterView: CounterView = { - let height = Constraints.countView.height.adjustedByWidth - let topInset = Constraints.countView.topInset.adjustedByWidth - let fontHeight = Constraints.countView.fontHeight.adjustedByWidth let font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: fontHeight)! let textColor = UIColor.nynja.white let inset = Constraints.countView.horizontalContentInset.adjustedByWidth - let view = CounterView(font: font, - textColor: textColor, - horizontalInset: inset) - + let view = CounterView(font: font, textColor: textColor, horizontalInset: inset) + + view.setContentCompressionResistancePriority(.required, for: .horizontal) + view.setContentHuggingPriority(.required, for: .horizontal) + view.backgroundColor = UIColor.nynja.mainRed addSubview(view) view.snp.makeConstraints { maker in - maker.top.equalTo(timeLabel.snp.bottom).offset(topInset) - maker.right.equalToSuperview() - maker.height.equalTo(height) + maker.top.bottom.right.equalToSuperview() + counterSuperviewLeftConstraint = maker.left.equalToSuperview().constraint } return view }() + + private var mentionCounterConstraint: Constraint? + private var mentionSuperviewConstraint: Constraint? + private(set) lazy var mentionIndicatorView: UIImageView = { let size = Constraints.mentionIndicatorView.size.adjustedByWidth - let topInset = Constraints.mentionIndicatorView.topInset.adjustedByWidth let rightInset = Constraints.mentionIndicatorView.rightInset.adjustedByWidth let imageView = UIImageView() @@ -69,19 +52,18 @@ final class ChatListMessageAccessoryView: BaseView { addSubview(imageView) imageView.snp.makeConstraints { maker in - maker.top.equalTo(timeLabel.snp.bottom).offset(topInset) + maker.top.bottom.equalToSuperview() + mentionCounterConstraint = maker.right.equalTo(counterView.snp.left).offset(-rightInset).constraint mentionSuperviewConstraint = maker.right.equalToSuperview().constraint - maker.left.greaterThanOrEqualToSuperview() + + maker.left.equalToSuperview() maker.width.height.equalTo(size) } return imageView }() - private var mentionCounterConstraint: Constraint? - private var mentionSuperviewConstraint: Constraint? - // MARK: - Setup @@ -92,15 +74,13 @@ final class ChatListMessageAccessoryView: BaseView { mentionSuperviewConstraint?.deactivate() } - func setup(date: Date) { - timeLabel.text = type(of: self).dateFormatter.toString(date) - } - func setup(mentions: Bool, unreadCount: Int) { mentionIndicatorView.isHidden = !mentions counterView.count = unreadCount if mentions { + counterSuperviewLeftConstraint?.deactivate() + if unreadCount <= 0 { mentionCounterConstraint?.deactivate() mentionSuperviewConstraint?.activate() @@ -108,31 +88,25 @@ final class ChatListMessageAccessoryView: BaseView { mentionCounterConstraint?.activate() mentionSuperviewConstraint?.deactivate() } + } else { + counterSuperviewLeftConstraint?.activate() } } } // MARK: - Layout -extension ChatListMessageAccessoryView { +private extension ChatListMessageIndicatorsView { enum Constraints { - - enum timeLabel { - static let height: CGFloat = 17 - } - + enum countView { - static let topInset: CGFloat = 4.0 - static let height: CGFloat = 18.0 - static let fontHeight: CGFloat = 17.0 static let horizontalContentInset: CGFloat = 2.0 } enum mentionIndicatorView { static let size: CGFloat = 18.0 - static let topInset: CGFloat = 4.0 static let rightInset: CGFloat = 16.0 } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 4885b1c82..7e9e1b34d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -41,36 +41,14 @@ final class ChatListMessageTableViewCell: UITableViewCell { private(set) lazy var messageContentView: ChatListMessageContentView = { let view = ChatListMessageContentView() - let width = Constraints.messageContentView.width.adjustedByWidth let leftInset = Constraints.messageContentView.leftInset.adjustedByWidth - let rightInset = Constraints.messageAccessoryView.minLeftInset.adjustedByWidth - - view.setContentHuggingPriority(.required, for: .horizontal) + let rightInset = Constraints.messageContentView.rightInset.adjustedByWidth contentView.addSubview(view) view.snp.makeConstraints { maker in maker.left.equalTo(avatarImageView.snp.right).offset(leftInset) - maker.right.lessThanOrEqualTo(messageAccessoryView.snp.left).offset(-rightInset) - maker.centerY.equalToSuperview() - } - - view.titleLabel.snp.makeConstraints { maker in - maker.centerY.equalTo(messageAccessoryView.timeLabel) - } - - return view - }() - - private(set) lazy var messageAccessoryView: ChatListMessageAccessoryView = { - let view = ChatListMessageAccessoryView() - - let height = Constraints.messageAccessoryView.height.adjustedByWidth - let rightInset = Constraints.messageAccessoryView.rightInset.adjustedByWidth - - contentView.addSubview(view) - view.snp.makeConstraints { maker in maker.right.equalToSuperview().inset(rightInset) - maker.height.equalTo(height) + maker.centerY.equalToSuperview() } return view @@ -191,12 +169,6 @@ extension ChatListMessageTableViewCell { enum messageContentView { static let leftInset: CGFloat = 16.0 - static let width: CGFloat = 260 - } - - enum messageAccessoryView { - static let height: CGFloat = ChatListMessageAccessoryView.Constraints.timeLabel.height + ChatListMessageAccessoryView.Constraints.countView.height - static let minLeftInset: CGFloat = 8.0 static let rightInset: CGFloat = 16.0 } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift index 8616b9efe..ca3c23fa9 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift @@ -21,12 +21,16 @@ final class CounterView: UIView { isHidden = true countLabel.isHidden = true } + invalidateIntrinsicContentSize() + setNeedsLayout() } } var font: UIFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: CGFloat(17.0.adjustedByWidth))! { didSet { countLabel.font = font + invalidateIntrinsicContentSize() + setNeedsLayout() } } @@ -38,13 +42,20 @@ final class CounterView: UIView { var horizontalInset: CGFloat = 0 { didSet { - countLabel.snp.updateConstraints { maker in - maker.left.greaterThanOrEqualToSuperview().offset(horizontalInset) - maker.right.lessThanOrEqualToSuperview().offset(-horizontalInset) - } + invalidateIntrinsicContentSize() + setNeedsLayout() } } + override var intrinsicContentSize: CGSize { + let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) + + var size = countLabel.sizeThatFits(maxSize) + size.width += horizontalInset * 2 + + return size + } + // MARK: - Views @@ -59,8 +70,6 @@ final class CounterView: UIView { addSubview(label) label.snp.makeConstraints { maker in maker.center.equalToSuperview() - maker.left.greaterThanOrEqualToSuperview().offset(horizontalInset) - maker.right.lessThanOrEqualToSuperview().offset(-horizontalInset) } return label @@ -87,10 +96,6 @@ final class CounterView: UIView { private func setup() { countLabel.isHidden = false - snp.makeConstraints { maker in - maker.width.equalTo(self.snp.height).priority(.high) - maker.width.greaterThanOrEqualTo(self.snp.height) - } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 5cfa7a1b4..1f3f4919d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -59,9 +59,9 @@ final class ChatListMessageCellModel: CellViewModel { let unreadCount = min(Int(model.unreadMessagesCount), type(of: self).unreadCounterLimit) let shouldDisplayMention = model.hasMentions - cell.messageAccessoryView.setup(mentions: shouldDisplayMention, unreadCount: unreadCount) + cell.messageContentView.setup(mentions: shouldDisplayMention, unreadCount: unreadCount) if let createdDate = model.message?.createdDate { - cell.messageAccessoryView.setup(date: createdDate) + cell.messageContentView.setup(date: createdDate) } } -- GitLab From c17682144b9970aeed39bac24c3bd297c27c408a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 10:46:54 +0200 Subject: [PATCH 070/138] [NY-4699] Remove unused computed properties. --- .../Models/Statuses/Typing/ActionStatus.swift | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift index 13c65fdba..3ba25b8fa 100644 --- a/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift @@ -19,27 +19,6 @@ enum ActionStatus: Equatable { return false } - var isTyping: Bool { - if case .typing = self { - return true - } - return false - } - - var isSendingFile: Bool { - if case .sending = self { - return true - } - return false - } - - var isRecording: Bool { - if case .recording = self { - return true - } - return false - } - var title: String { switch self { case .done: -- GitLab From 069f5622aa13ebd83488f6e9b88daec2964ab9a1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 11:52:02 +0200 Subject: [PATCH 071/138] [NY-4853] Fixed email text field on login page. --- Nynja.xcodeproj/project.pbxproj | 5 ++- .../Material/Base/MaterialTextContainer.swift | 2 ++ .../Material/Config/NynjaMTIConfig.swift | 2 ++ .../Material/MaterialTextField.swift | 25 ++++++++++--- .../View/Subviews/EmailLoginView.swift | 36 ++++++++++++------- .../View/ViewsFactory/AuthViewsFactory.swift | 21 ++++++----- ...SeedVerificationWalletViewController.swift | 4 +-- 7 files changed, 63 insertions(+), 32 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 570d7860c..abb72e38a 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -7595,10 +7595,10 @@ 5EEB73BE216199DE00D8ECE6 /* AuthModule */ = { isa = PBXGroup; children = ( - 5EEB73BF216199DE00D8ECE6 /* Presenter */, - 5EEB73C0216199DE00D8ECE6 /* Wireframe */, 5EEB73C1216199DE00D8ECE6 /* View */, + 5EEB73BF216199DE00D8ECE6 /* Presenter */, 5EEB73C2216199DE00D8ECE6 /* Interactor */, + 5EEB73C0216199DE00D8ECE6 /* Wireframe */, 5EEB73C3216199DE00D8ECE6 /* Entities */, 5EEB73C4216199ED00D8ECE6 /* AuthProtocols.swift */, ); @@ -15727,7 +15727,6 @@ B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */, 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */, 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */, - 264312EC210DE4040057E8B0 /* LanguageSectionCoordinator.swift in Sources */, 85579882209322A8007050B8 /* StickerMenuDataSource.swift in Sources */, 8506F001206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift in Sources */, 85C16C3C20D261C000EDB77E /* MessageStickerView.swift in Sources */, diff --git a/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift index f7ba3eeca..917623cf6 100644 --- a/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift +++ b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift @@ -38,6 +38,8 @@ class MaterialTextContainer: BaseView, MaterialTextInput { var textColor: UIColor = defaultConfig.textColor var cursorColor: UIColor = defaultConfig.cursorColor var keyboardType: UIKeyboardType = defaultConfig.keyboardType + var keyboardAppearance: UIKeyboardAppearance = defaultConfig.keyboardAppearance + var returnKeyType: UIReturnKeyType = defaultConfig.returnKeyType var isSecureTextEntry: Bool = defaultConfig.isSecureTextEntry var activeSeparatorHeight: CGFloat = defaultConfig.activeSeparatorHeight { diff --git a/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift b/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift index ff8a5dcda..aac04a918 100644 --- a/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift +++ b/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift @@ -17,6 +17,8 @@ struct NynjaMTIConfig: MTIConfigProtocol { let cursorColor: UIColor = defaultColor let keyboardType: UIKeyboardType = .default + let keyboardAppearance: UIKeyboardAppearance = .default + let returnKeyType: UIReturnKeyType = .default let isSecureTextEntry = false let placeholderFont: UIFont = defaultFont diff --git a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift index b11f96c41..76b86dc89 100644 --- a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift +++ b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift @@ -32,6 +32,14 @@ class MaterialTextField: MaterialTextContainer { override var keyboardType: UIKeyboardType { didSet { textField.keyboardType = keyboardType } } + + override var keyboardAppearance: UIKeyboardAppearance { + didSet { textField.keyboardAppearance = keyboardAppearance } + } + + override var returnKeyType: UIReturnKeyType { + didSet { textField.returnKeyType = returnKeyType } + } override var isSecureTextEntry: Bool { didSet { textField.isSecureTextEntry = isSecureTextEntry } @@ -52,10 +60,9 @@ class MaterialTextField: MaterialTextContainer { var autocapitalizationType: UITextAutocapitalizationType = UITextAutocapitalizationType.sentences { didSet { textField.autocapitalizationType = autocapitalizationType } } - - func setTextFieldFirstResponder() { - textField.becomeFirstResponder() - } + + var returnHandler: ((MaterialTextField) -> Bool)? + // MARK: - Views @@ -87,9 +94,13 @@ class MaterialTextField: MaterialTextContainer { } override func becomeFirstResponder() -> Bool { - return self.textField.becomeFirstResponder() + return textField.becomeFirstResponder() } + override func resignFirstResponder() -> Bool { + return textField.resignFirstResponder() + } + // MARK: - Actions @@ -110,6 +121,10 @@ extension MaterialTextField: UITextFieldDelegate { endEditingText() } + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return returnHandler?(self) ?? false + } + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { return shouldTextChanged?(self, range, string) ?? true } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index e8fefa32d..13fc35fb5 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -10,10 +10,10 @@ import Foundation final class EmailLoginView: UIView, Configurable { - private lazy var inputFieldContainer: UIView = viewsFactory.makeInputFieldContainer(on: self) - private lazy var inputField: UITextField = viewsFactory.makeInputField(on: inputFieldContainer) - private lazy var detailsLabel: UILabel = viewsFactory.makeDetailsLabel(on: self, top: inputFieldContainer) - private lazy var nextButton: UIButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) + private lazy var inputFieldContainer = viewsFactory.makeInputFieldContainer(on: self) + private lazy var inputField = viewsFactory.makeInputField(on: inputFieldContainer) + private lazy var detailsLabel = viewsFactory.makeDetailsLabel(on: self, top: inputFieldContainer) + private lazy var nextButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) private var textFieldController: TextFieldController? private var nextAction: ((String) -> Void)? @@ -50,13 +50,23 @@ extension EmailLoginView { } nextAction = config.nextAction - inputField.delegate = textFieldController + + inputField.shouldTextChanged = { [weak self] textInput, range, string in + return self?.textFieldController? + .textInput(textInput, + shouldChangeCharactersIn: range, + replacementString: string) ?? true + } + + inputField.returnHandler = { [weak self] textInput in + return self?.textFieldController?.textInputShouldReturn(textInput) ?? false + } _ = [inputFieldContainer, inputField, detailsLabel, nextButton] } } -// MARK: - ACtions +// MARK: - Aсtions private extension EmailLoginView { @objc func next(sender: UIButton) { @@ -80,7 +90,8 @@ private extension EmailLoginView { // MARK: - TextFieldController private extension EmailLoginView { - final class TextFieldController: NSObject, UITextFieldDelegate { + + final class TextFieldController { private let validator: Validator private let validationAction: (Bool) -> Void @@ -89,8 +100,9 @@ private extension EmailLoginView { self.validationAction = validationAction } - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if let str = textField.text as NSString? { + func textInput(_ textInput: MaterialTextInput, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + + if let str = textInput.text as NSString? { let resultStr = str.replacingCharacters(in: range, with: string) validationAction(validator.isValid(email: resultStr)) } @@ -98,9 +110,9 @@ private extension EmailLoginView { return true } - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.becomeFirstResponder() - return true + func textInputShouldReturn(_ textInput: MaterialTextField) -> Bool { + textInput.resignFirstResponder() + return false } } } diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index 6d4130a21..e477d9039 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -34,7 +34,7 @@ protocol AuthViewsFactoryProtocol { // MARK: - Email Login View func makeInputFieldContainer(on view: UIView) -> UIView - func makeInputField(on view: UIView) -> UITextField + func makeInputField(on view: UIView) -> MaterialTextField func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel func makeEmailNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton @@ -300,19 +300,22 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { return container } - func makeInputField(on view: UIView) -> UITextField { - let textField = UITextField() + func makeInputField(on view: UIView) -> MaterialTextField { + let textField = MaterialTextField() view.addSubview(textField) - textField.attributedPlaceholder = NSAttributedString( - string: "Email".localized, - attributes: [NSAttributedStringKey.foregroundColor : UIColor.nynja.dustyGray]) + textField.placeholderColor = UIColor.nynja.dustyGray + textField.placeholder = "Email".localized + textField.textColor = UIColor.nynja.white textField.font = FontFamily.NotoSans.medium.font(size: 16) - textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + textField.separatorColor = UIColor.nynja.dustyGray - textField.snp.makeConstraints { (make) in + textField.keyboardType = .emailAddress + textField.returnKeyType = .done + + textField.snp.makeConstraints { make in make.centerY.equalToSuperview() make.left.equalToSuperview() make.right.equalToSuperview() @@ -325,7 +328,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { let label = UILabel() view.addSubview(label) - label.text = "Enter your email adsress to receive the login code.".localized + label.text = "Enter your email address to receive the login code.".localized label.font = FontFamily.NotoSans.regular.font(size: 14) label.textColor = UIColor.nynja.dustyGray diff --git a/Nynja/Modules/Wallet Flows/SeedVerification/View/SeedVerificationWalletViewController.swift b/Nynja/Modules/Wallet Flows/SeedVerification/View/SeedVerificationWalletViewController.swift index aa0144eee..d1ddcc821 100644 --- a/Nynja/Modules/Wallet Flows/SeedVerification/View/SeedVerificationWalletViewController.swift +++ b/Nynja/Modules/Wallet Flows/SeedVerification/View/SeedVerificationWalletViewController.swift @@ -138,9 +138,7 @@ TestableViewControllerProtocol, UICollectionViewDelegateFlowLayout { } private func setCurrentCellFrirstResponder() { - if let cell = currentCell { - cell.wordTextField.setTextFieldFirstResponder() - } + currentCell?.wordTextField.becomeFirstResponder() } // MARK: - Life Cycle -- GitLab From b30498cfb3051a1768f4fbb7aac53c10db5084e2 Mon Sep 17 00:00:00 2001 From: AshCenso Date: Mon, 5 Nov 2018 12:47:43 +0200 Subject: [PATCH 072/138] in progress --- Nynja.xcodeproj/project.pbxproj | 104 ++++++++++ Nynja/AppDelegate.swift | 3 +- Nynja/Generated/AssetsConstants.swift | 2 + .../AccountSettingsProtocols.swift | 45 ++++- .../Entities/AddContactCellModel.swift | 19 ++ .../Entities/ContactTVCellModel.swift | 26 +++ .../Entities/DescriptionCellModel.swift | 28 +++ .../Entities/MaterialTextFieldCellModel.swift | 29 +++ .../Entities/SettingsSectionHeader.swift | 20 ++ .../Entities/SettingsSelectorCellModel.swift | 27 +++ .../Entities/SettingsSetAvatarCellModel.swift | 23 +++ .../AccountSettings/Entities/Sizeble.swift | 20 ++ .../Entities/StatusTimeout.swift | 18 ++ .../Entities/UserContact.swift | 32 +++ .../Entities/UserContactAction.swift | 16 ++ .../Entities/UserProfile.swift | 25 +++ .../AccountSettings/Entities/UserStatus.swift | 17 ++ .../AccountSettingsInteractor.swift | 73 +++++++ .../Presenter/AccountSettingsPresenter.swift | 177 ++++++++++++++++ .../View/AccountSettingsViewController.swift | 189 +++++++++++++++++- .../View/Cells/AddContactCell.swift | 64 ++++++ .../View/Cells/ContactTVCell.swift | 98 +++++++++ .../View/Cells/DescriptionTVCell.swift | 41 ++++ .../View/Cells/MaterialTextFieldTVCell.swift | 45 +++++ .../View/Cells/SettingsSelectorTVCell.swift | 69 +++++++ .../View/Cells/SettingsSetAvatarTVCell.swift | 47 +++++ .../Header/SettingsSectionHeaderView.swift | 38 ++++ .../Wireframe/AccountSettingsWireframe.swift | 55 ++++- .../AccountSettingsCoordinator.swift | 140 +++++++++++++ .../CreateProfileProtocols.swift | 2 + .../Interactor/CreateProfileInteractor.swift | 1 + .../Subviews/CreateProfileContentView.swift | 1 + .../Contents.json | 13 ++ .../Disclosure_Indicator.pdf | Bin 0 -> 5676 bytes .../ic_add.imageset/Contents.json | 13 ++ .../ic_add.imageset/ic_add.pdf | Bin 0 -> 5763 bytes 36 files changed, 1510 insertions(+), 10 deletions(-) create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/AddContactCellModel.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/ContactTVCellModel.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/DescriptionCellModel.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/MaterialTextFieldCellModel.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSectionHeader.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSelectorCellModel.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSetAvatarCellModel.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/Sizeble.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/StatusTimeout.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContact.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContactAction.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/UserProfile.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Entities/UserStatus.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Interactor/AccountSettingsInteractor.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/Cells/AddContactCell.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/Cells/ContactTVCell.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/Cells/DescriptionTVCell.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSelectorTVCell.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift create mode 100644 Nynja/Modules/AccountSettings/AccountSettings/View/Header/SettingsSectionHeaderView.swift create mode 100644 Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Disclosure_Indicator.pdf create mode 100644 Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/ic_add.imageset/ic_add.pdf diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 645ec28a5..2662a8511 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -664,6 +664,28 @@ 5E0B9FF22170BCE600A95467 /* CreateProfileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */; }; 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */; }; 5E278E14F45F56BACB71271C /* VideoPreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5541C91FE7845F3E5C7EB2 /* VideoPreviewWireframe.swift */; }; + 5E7D5D38218C40B6009B5D8D /* AccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */; }; + 5E7D5D3A218C42D0009B5D8D /* AccountSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D39218C42D0009B5D8D /* AccountSettingsInteractor.swift */; }; + 5E7D5D3D218C59F1009B5D8D /* UserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D3C218C59F1009B5D8D /* UserStatus.swift */; }; + 5E7D5D3F218C5A12009B5D8D /* StatusTimeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */; }; + 5E7D5D41218C5A36009B5D8D /* UserContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D40218C5A36009B5D8D /* UserContact.swift */; }; + 5E7D5D43218C5A4C009B5D8D /* UserContactAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */; }; + 5E7D5D45218C5A5D009B5D8D /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D44218C5A5D009B5D8D /* UserProfile.swift */; }; + 5E7D5D47218C5D0A009B5D8D /* SettingsSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */; }; + 5E7D5D4A218C5D42009B5D8D /* SettingsSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */; }; + 5E7D5D4C218C6239009B5D8D /* SettingsSetAvatarTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */; }; + 5E7D5D4E218C645A009B5D8D /* SettingsSetAvatarCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D4D218C645A009B5D8D /* SettingsSetAvatarCellModel.swift */; }; + 5E7D5D50218C6588009B5D8D /* SettingsSelectorTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D4F218C6588009B5D8D /* SettingsSelectorTVCell.swift */; }; + 5E7D5D52218C68BA009B5D8D /* SettingsSelectorCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D51218C68BA009B5D8D /* SettingsSelectorCellModel.swift */; }; + 5E7D5D54218FDD81009B5D8D /* Sizeble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D53218FDD81009B5D8D /* Sizeble.swift */; }; + 5E7D5D56218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D55218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift */; }; + 5E7D5D58218FF473009B5D8D /* MaterialTextFieldCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D57218FF473009B5D8D /* MaterialTextFieldCellModel.swift */; }; + 5E7D5D5A21901BC6009B5D8D /* DescriptionTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5921901BC6009B5D8D /* DescriptionTVCell.swift */; }; + 5E7D5D5C21901D18009B5D8D /* DescriptionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5B21901D18009B5D8D /* DescriptionCellModel.swift */; }; + 5E7D5D5E2190415F009B5D8D /* AddContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5D2190415F009B5D8D /* AddContactCell.swift */; }; + 5E7D5D60219044CB009B5D8D /* AddContactCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */; }; + 5E7D5D6221904E25009B5D8D /* ContactTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */; }; + 5E7D5D6421905390009B5D8D /* ContactTVCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */; }; 5E7E9FB9215BA0BE004D306B /* CountrySelectorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */; }; 5E7E9FBC215BA19B004D306B /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FBB215BA19B004D306B /* Country.swift */; }; 5E7E9FBE215BA51C004D306B /* CountrySelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */; }; @@ -2826,6 +2848,28 @@ 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileWireframe.swift; sourceTree = ""; }; 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileContentView.swift; sourceTree = ""; }; 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; + 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsPresenter.swift; sourceTree = ""; }; + 5E7D5D39218C42D0009B5D8D /* AccountSettingsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsInteractor.swift; sourceTree = ""; }; + 5E7D5D3C218C59F1009B5D8D /* UserStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatus.swift; sourceTree = ""; }; + 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTimeout.swift; sourceTree = ""; }; + 5E7D5D40218C5A36009B5D8D /* UserContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContact.swift; sourceTree = ""; }; + 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContactAction.swift; sourceTree = ""; }; + 5E7D5D44218C5A5D009B5D8D /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; + 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = ""; }; + 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeaderView.swift; sourceTree = ""; }; + 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSetAvatarTVCell.swift; sourceTree = ""; }; + 5E7D5D4D218C645A009B5D8D /* SettingsSetAvatarCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSetAvatarCellModel.swift; sourceTree = ""; }; + 5E7D5D4F218C6588009B5D8D /* SettingsSelectorTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSelectorTVCell.swift; sourceTree = ""; }; + 5E7D5D51218C68BA009B5D8D /* SettingsSelectorCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSelectorCellModel.swift; sourceTree = ""; }; + 5E7D5D53218FDD81009B5D8D /* Sizeble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizeble.swift; sourceTree = ""; }; + 5E7D5D55218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaterialTextFieldTVCell.swift; sourceTree = ""; }; + 5E7D5D57218FF473009B5D8D /* MaterialTextFieldCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaterialTextFieldCellModel.swift; sourceTree = ""; }; + 5E7D5D5921901BC6009B5D8D /* DescriptionTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionTVCell.swift; sourceTree = ""; }; + 5E7D5D5B21901D18009B5D8D /* DescriptionCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionCellModel.swift; sourceTree = ""; }; + 5E7D5D5D2190415F009B5D8D /* AddContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactCell.swift; sourceTree = ""; }; + 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactCellModel.swift; sourceTree = ""; }; + 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTVCell.swift; sourceTree = ""; }; + 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTVCellModel.swift; sourceTree = ""; }; 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorProtocols.swift; sourceTree = ""; }; 5E7E9FBB215BA19B004D306B /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewController.swift; sourceTree = ""; }; @@ -6931,6 +6975,27 @@ path = Subviews; sourceTree = ""; }; + 5E7D5D3B218C44A7009B5D8D /* Cells */ = { + isa = PBXGroup; + children = ( + 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */, + 5E7D5D4F218C6588009B5D8D /* SettingsSelectorTVCell.swift */, + 5E7D5D55218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift */, + 5E7D5D5921901BC6009B5D8D /* DescriptionTVCell.swift */, + 5E7D5D5D2190415F009B5D8D /* AddContactCell.swift */, + 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 5E7D5D48218C5D3A009B5D8D /* Header */ = { + isa = PBXGroup; + children = ( + 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */, + ); + path = Header; + sourceTree = ""; + }; 5E7E9FB3215BA0AD004D306B /* CountrySelector */ = { isa = PBXGroup; children = ( @@ -7036,6 +7101,7 @@ 5EDD454921885EC400C50BC8 /* Presenter */ = { isa = PBXGroup; children = ( + 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */, ); path = Presenter; sourceTree = ""; @@ -7051,6 +7117,8 @@ 5EDD454B21885EC400C50BC8 /* View */ = { isa = PBXGroup; children = ( + 5E7D5D48218C5D3A009B5D8D /* Header */, + 5E7D5D3B218C44A7009B5D8D /* Cells */, 5EDD45562188617A00C50BC8 /* ViewsFactory */, 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */, ); @@ -7060,6 +7128,7 @@ 5EDD454C21885EC400C50BC8 /* Interactor */ = { isa = PBXGroup; children = ( + 5E7D5D39218C42D0009B5D8D /* AccountSettingsInteractor.swift */, ); path = Interactor; sourceTree = ""; @@ -7067,6 +7136,19 @@ 5EDD454D21885EC400C50BC8 /* Entities */ = { isa = PBXGroup; children = ( + 5E7D5D3C218C59F1009B5D8D /* UserStatus.swift */, + 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */, + 5E7D5D40218C5A36009B5D8D /* UserContact.swift */, + 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */, + 5E7D5D44218C5A5D009B5D8D /* UserProfile.swift */, + 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */, + 5E7D5D4D218C645A009B5D8D /* SettingsSetAvatarCellModel.swift */, + 5E7D5D51218C68BA009B5D8D /* SettingsSelectorCellModel.swift */, + 5E7D5D53218FDD81009B5D8D /* Sizeble.swift */, + 5E7D5D57218FF473009B5D8D /* MaterialTextFieldCellModel.swift */, + 5E7D5D5B21901D18009B5D8D /* DescriptionCellModel.swift */, + 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */, + 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */, ); path = Entities; sourceTree = ""; @@ -14370,6 +14452,7 @@ 26AD28371FFB0AE3009E4580 /* StorageSubscriber.swift in Sources */, A49E1BCD20A9A6970074DFD3 /* BaseChatModel.swift in Sources */, 3A62B7D81F4CB9D100F45B51 /* BaseMQTTModel.swift in Sources */, + 5E7D5D38218C40B6009B5D8D /* AccountSettingsPresenter.swift in Sources */, 3A1DC73F1EF15B65006A8E9F /* IoHandler.swift in Sources */, E77FBDDD1FFE828400BDB255 /* AVURLAsset+Duration.swift in Sources */, 85433F26204D596D00B373A7 /* WebFullScreenWireFrame.swift in Sources */, @@ -14384,6 +14467,7 @@ 3A3FD2831F39E0A000B6958F /* HistoryRequestModel.swift in Sources */, A42D51BC206A361400EEB952 /* error2.swift in Sources */, 8ED0F3BF1FBC5CB1004916AB /* DialogCellModel.swift in Sources */, + 5E7D5D6421905390009B5D8D /* ContactTVCellModel.swift in Sources */, 8502DB552061030100613C8C /* WheelPositionPickerInteractor.swift in Sources */, A4330A652109DFA00060BD93 /* UserInfo.swift in Sources */, F10B0E2120B4CF3800528E7A /* CameraCoordinator.swift in Sources */, @@ -14417,6 +14501,7 @@ F105C6AC20A0DCC70091786A /* UIImagePickerControllerCameraCaptureModeExtensions.swift in Sources */, A42D51A6206A361400EEB952 /* process.swift in Sources */, A42D51CC206A361400EEB952 /* ok.swift in Sources */, + 5E7D5D52218C68BA009B5D8D /* SettingsSelectorCellModel.swift in Sources */, 26131E02210399BA00BE94F9 /* TranscribeService.swift in Sources */, 853FB0752049B4FF000996C5 /* TextTableViewCell.swift in Sources */, A42D52DC206A53AB00EEB952 /* process_Spec.swift in Sources */, @@ -14649,6 +14734,7 @@ 268C34152107479600F1472A /* TranscribeLongResponseData.swift in Sources */, 6D36F8E71F0BBFC300FA1AC8 /* ContactManager.swift in Sources */, 32868DD51F31CADF0028B260 /* ChatsListProtocols.swift in Sources */, + 5E7D5D5E2190415F009B5D8D /* AddContactCell.swift in Sources */, A49CC1D220E4A9C000879D41 /* InputBar+DisplayMode.swift in Sources */, A42D51B3206A361400EEB952 /* Room.swift in Sources */, 85BDD2BA21467A9500695DE5 /* MessageFactoryProtocol.swift in Sources */, @@ -14664,6 +14750,7 @@ A408A0BC20C174040029F54B /* ChannelsListProtocols.swift in Sources */, 3A2374D91F262A1600701045 /* ContactHandler.swift in Sources */, 5EEB73AA215D406400D8ECE6 /* AuthCoordinator.swift in Sources */, + 5E7D5D41218C5A36009B5D8D /* UserContact.swift in Sources */, 6D5168A21F30430900DA3728 /* SpeakerView.swift in Sources */, 8503B529205046A6006F0593 /* NotificationSettingsWireFrame.swift in Sources */, FBD885782147F9640099B8C3 /* FontsConstants.swift in Sources */, @@ -14684,6 +14771,7 @@ A42D51AF206A361400EEB952 /* receiveTask.swift in Sources */, 857C070620DB8A3D00626EEB /* StickerInputState.swift in Sources */, A4B544EF20EFB4DF00EB7B0F /* BertTupleExtension.swift in Sources */, + 5E7D5D43218C5A4C009B5D8D /* UserContactAction.swift in Sources */, 8502DB512061030100613C8C /* WheelPositionPickerProtocols.swift in Sources */, 00F7B3402029DD6200E443E1 /* TextItemView.swift in Sources */, E77764BE1FBDA9B60042541D /* ImageWheelItemView.swift in Sources */, @@ -14727,6 +14815,7 @@ 4B5A714D204F069000A551F5 /* ChatService.swift in Sources */, 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */, 2603139420A0A4B9009AC66D /* LanguageSelectorPresenter.swift in Sources */, + 5E7D5D6221904E25009B5D8D /* ContactTVCell.swift in Sources */, 26E476591FFEE2D400C06C05 /* Modelka.swift in Sources */, 4B749F07214FEE4F002F3A33 /* VerifyNumberProtocols.swift in Sources */, 852003FE20D46680007C0036 /* StickerPack.swift in Sources */, @@ -14778,6 +14867,7 @@ A42D51C9206A361400EEB952 /* writer.swift in Sources */, 2648C3E62069B49000863614 /* UITextField+Extension.swift in Sources */, A432CF1520B4347D00993AFB /* FloatingPlaceholderContainer.swift in Sources */, + 5E7D5D56218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift in Sources */, E70F78BB1FD6CB5600385565 /* DBChatCheckpoint.swift in Sources */, 263C04E92132E2FF00B8F0BE /* WrappedTaskOperation.swift in Sources */, A42D52BE206A53AA00EEB952 /* cur_Spec.swift in Sources */, @@ -14987,6 +15077,7 @@ 267BE90C2069405200153FB8 /* StarMessageDAOProtocol.swift in Sources */, 8E55172E200D095B00C12B5D /* UserGroupRulesVC.swift in Sources */, A4B544EA20EFB1A800EB7B0F /* errors_Spec.swift in Sources */, + 5E7D5D45218C5A5D009B5D8D /* UserProfile.swift in Sources */, 4BDC7E63203494C000BCD381 /* ScheduleButton.swift in Sources */, 00E98250205C2668008BF03D /* SessionHeaderView.swift in Sources */, 95FE45E089AF69B08815EB9E /* ProfilePresenter.swift in Sources */, @@ -15123,6 +15214,7 @@ A42D51A2206A361400EEB952 /* Desc.swift in Sources */, FBCE83CF20E52352003B7558 /* PaymentWireFrame.swift in Sources */, C940514B204C7FAF00D72B04 /* DataAndStorageProtocols.swift in Sources */, + 5E7D5D5C21901D18009B5D8D /* DescriptionCellModel.swift in Sources */, F105C69C209F71BF0091786A /* CameraPresenter.swift in Sources */, 5BC1D37920D3B4A8002A44B3 /* GroupCollectionViewCell.swift in Sources */, 26610F5B2015476C00609F77 /* LocationFullWheelItemModel.swift in Sources */, @@ -15166,6 +15258,7 @@ 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */, A4679B8920B2DA550021FE9C /* Array+ChannelSubscriber.swift in Sources */, A4ED79AC20C7056C00A41F67 /* AllChannelsItemsFactory.swift in Sources */, + 5E7D5D4C218C6239009B5D8D /* SettingsSetAvatarTVCell.swift in Sources */, E707C4AF1FA0F6E700B86137 /* ProfileActionCell.swift in Sources */, 2648C4172069B52100863614 /* ChangeNumberStep2Wireframe.swift in Sources */, A45F111A20B4218D00F45004 /* MessageImageView.swift in Sources */, @@ -15178,6 +15271,7 @@ 2605311D21274116002E1CF1 /* LogOutputView.swift in Sources */, 263529152075729400DC6FBD /* Job+DB.swift in Sources */, 8520040B20D4FB06007C0036 /* ReplyInfoView.swift in Sources */, + 5E7D5D47218C5D0A009B5D8D /* SettingsSectionHeader.swift in Sources */, E77D58991F98B94E00FBE926 /* ProfileTablewViewDS.swift in Sources */, E72906E72011156B007C5C5B /* UITableViewExtensions.swift in Sources */, 2603139320A0A4B9009AC66D /* LanguageSelectorProtocols.swift in Sources */, @@ -15219,6 +15313,7 @@ A42CE5AD20692EDB000889CC /* StringAtom.swift in Sources */, A4CB1520210372DF00C3B68B /* JDMechanism.swift in Sources */, 8E23E0882006853000A59B8C /* GroupVideosListVC.swift in Sources */, + 5E7D5D60219044CB009B5D8D /* AddContactCellModel.swift in Sources */, A42D52C7206A53AA00EEB952 /* iter_Spec.swift in Sources */, E72AE64F1F8E3CCB006417D0 /* GradientButton.swift in Sources */, A43B25DE20AB1F5C00FF8107 /* RawRepresentable+Localized.swift in Sources */, @@ -15474,6 +15569,7 @@ 8566771E20C1579C00DD4204 /* StorageSubscriberReference.swift in Sources */, F117871420ACF018007A9A1B /* CameraSettingsWireframe.swift in Sources */, E7E6E3DE1FB2F37900401D9E /* ParticipantsDelegate.swift in Sources */, + 5E7D5D54218FDD81009B5D8D /* Sizeble.swift in Sources */, A42D51AA206A361400EEB952 /* error.swift in Sources */, 628E2C26BE0854DB1DF64990 /* SplashWireframe.swift in Sources */, B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */, @@ -15501,6 +15597,7 @@ 850C301C204DA87A00DB26C2 /* PrivacyListViewController.swift in Sources */, E791178E1F97A31D00462D68 /* ProfileViewControllerLayout.swift in Sources */, 85482846204E918000DCBEC8 /* PrivacyCellModel.swift in Sources */, + 5E7D5D3A218C42D0009B5D8D /* AccountSettingsInteractor.swift in Sources */, 26F47052201B7248005D3192 /* ReturnToCallView.swift in Sources */, A42D51C6206A361400EEB952 /* Tag.swift in Sources */, 8526187D20D05BF700824357 /* StickerGridPlaceholderCellModel.swift in Sources */, @@ -15519,6 +15616,7 @@ C940515B204C99C100D72B04 /* DataAndStorageTableDelegate.swift in Sources */, E09CECE79892CABEF8793389 /* ImagePreviewInteractor.swift in Sources */, 2603139820A0A4B9009AC66D /* LangCellViewModel.swift in Sources */, + 5E7D5D4E218C645A009B5D8D /* SettingsSetAvatarCellModel.swift in Sources */, A4BCEC6C20DBF2A40078B076 /* Link+DB.swift in Sources */, B79FA02B2107731400F286BF /* MarketplacePresenter.swift in Sources */, 0008E9282036F480003E316E /* ScheduledMessage.swift in Sources */, @@ -15540,6 +15638,7 @@ F105C6BC20A1347E0091786A /* PhotoPreviewWireframeProtocol.swift in Sources */, A43B25A620AB1DFA00FF8107 /* RecordingAudioWaveform.swift in Sources */, 26C1A3EB2031AAD20009F7F0 /* OtherUserInteractor.swift in Sources */, + 5E7D5D3F218C5A12009B5D8D /* StatusTimeout.swift in Sources */, 26D8317520EA65200067C5B4 /* TranslationInfo.swift in Sources */, A4868F3A2121E22C001F624E /* AntiDebuggingService.swift in Sources */, 85D66A0020BD963C00FBD803 /* InputTextMessage.swift in Sources */, @@ -15653,6 +15752,7 @@ 260313A220A0A4BA009AC66D /* ActionCell.swift in Sources */, 26E7D04A1FCB8973001C69B7 /* Amazon+FileSync.swift in Sources */, E70938411FBEE488006CCDC6 /* TableDefinitionExtension.swift in Sources */, + 5E7D5D58218FF473009B5D8D /* MaterialTextFieldCellModel.swift in Sources */, 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, @@ -15709,6 +15809,7 @@ 04EDA50C90C7EBD46AA1FCB2 /* AddParticipantsViewController.swift in Sources */, B750EF022046B24D00A99F9C /* TransferInfo.swift in Sources */, 859B862E204820DC003272B2 /* ThemePickerProtocols.swift in Sources */, + 5E7D5D4A218C5D42009B5D8D /* SettingsSectionHeaderView.swift in Sources */, 1D1D5634D125333796D14E10 /* AddParticipantsPresenter.swift in Sources */, 26EA201320BECDA600FBB9CA /* ConversationLanguageSettingServiceProtocol.swift in Sources */, A46C362F2121995800172773 /* DebuggingDetectorProtocol.swift in Sources */, @@ -15790,6 +15891,7 @@ 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */, 85D669E620BD956000FBD803 /* UIView+Shadow.swift in Sources */, 896D51F07E2F79C8B5502DBF /* EditGroupPhotoInteractor.swift in Sources */, + 5E7D5D50218C6588009B5D8D /* SettingsSelectorTVCell.swift in Sources */, A4ED79AA20C704F500A41F67 /* MyChannelsItemsFactory.swift in Sources */, 5BC1D38420D3B670002A44B3 /* CallCreatorMediator.swift in Sources */, B7EF8ED2210C502D00E0E981 /* InterpretationTypeTableDelegate.swift in Sources */, @@ -15854,12 +15956,14 @@ B3D0F59E1E7BDB7E485AE662 /* GroupStorageWireframe.swift in Sources */, A45F114120B4218D00F45004 /* MessageInteractor+StorageSubscriber.swift in Sources */, FEA59F90B93C7B49BAF99F9C /* SelectCountryProtocols.swift in Sources */, + 5E7D5D3D218C59F1009B5D8D /* UserStatus.swift in Sources */, 26ABCA3E21189DA400EA4782 /* Aps.swift in Sources */, 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */, 260313AB20A0A4BA009AC66D /* ChatLanguageSettingsInteractor.swift in Sources */, 7C51CDC1260CE191C07EE46C /* SelectCountryViewController.swift in Sources */, 8596CEF22048A763006FC65D /* ThemeCellModel.swift in Sources */, 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */, + 5E7D5D5A21901BC6009B5D8D /* DescriptionTVCell.swift in Sources */, A1AD6864F4F49D9FC8997D59 /* SelectCountryPresenter.swift in Sources */, 32E5A25AD25BF752EB3864AB /* SelectCountryInteractor.swift in Sources */, A42D52DB206A53AB00EEB952 /* messageEvent_Spec.swift in Sources */, diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 55735f709..38d249e2c 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -90,7 +90,8 @@ private extension AppDelegate { let navigation = UINavigationController() navigation.isNavigationBarHidden = true // SplashWireFrame().presentSplash(navigation: navigation) - let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) +// let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + let coordinator = AccountSettingsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) self.window?.rootViewController = navigation self.window?.makeKeyAndVisible() diff --git a/Nynja/Generated/AssetsConstants.swift b/Nynja/Generated/AssetsConstants.swift index 2bdcfba6d..fb11eb49f 100644 --- a/Nynja/Generated/AssetsConstants.swift +++ b/Nynja/Generated/AssetsConstants.swift @@ -47,6 +47,7 @@ internal enum Asset { internal static let avaPlaceholder = ImageAsset(name: "ava_placeholder") internal static let circleRed = ImageAsset(name: "circle_red") internal static let contactSeparatop = ImageAsset(name: "contact_separatop") + internal static let disclosureIndicator = ImageAsset(name: "Disclosure_Indicator") internal static let icForwardContacts = ImageAsset(name: "ic_forward_contacts") internal static let icForwardContactsSelected = ImageAsset(name: "ic_forward_contacts_selected") internal static let icForwardGroups = ImageAsset(name: "ic_forward_groups") @@ -292,6 +293,7 @@ internal enum Asset { internal static let editIcon = ImageAsset(name: "edit-icon") internal static let emojiWhite = ImageAsset(name: "emoji_white") internal static let frame = ImageAsset(name: "frame") + internal static let icAdd = ImageAsset(name: "ic_add") internal static let icAddParticipants = ImageAsset(name: "ic_add_participants") internal static let icAddPhotoPlaceholder = ImageAsset(name: "ic_add_photo_placeholder") internal static let icArrowDown = ImageAsset(name: "ic_arrow_down") diff --git a/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift index 61197db7d..0ae57fca3 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift @@ -9,21 +9,62 @@ import Foundation protocol AccountSettingsWireframeProtocol: WireframeProtocol { + func back() + func chooseAvatar(completion: @escaping (UIImage?) -> Void) + func chooseStatus(completion: @escaping (UserStatus) -> Void) + func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) + + func addContact(completion: @escaping (Result) -> Void) + func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) } protocol AccountSettingsViewProtocol: class where Self: UIViewController { - + func reloadData() } protocol AccountSettingsPresenterProtocol: NavigationProtocol { + func save() + + func countOfSections() -> Int + func rowsInSection(section: Int) -> Int + func item(for section: Int) -> SettingsSectionHeader? + func item(for section: Int, row: Int) -> AnyObject? + + func chooseAvatar(completion: @escaping (UIImage?) -> Void) + func chooseStatus(completion: @escaping (UserStatus) -> Void) + func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) + func setProfileMessage(message: String) + + func setFirstName(value: String) + func setLastName(value: String) + func setBirthday(value: Date) + + func setUserName(value: String) + func addContact(completion: @escaping (Result) -> Void) + func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) + func contacts() -> [UserContact] } protocol AccountSettingsInputInteractorProtocol { + func save(completion: (Result) -> Void) + var avatar: UIImage? { get set } + var status: UserStatus { get set } + var statusTimeout: StatusTimeout { get set } + var profileMessage: String? { get set } + + var firstName: String? { get set } + var lastName: String? { get set } + var birthday: Date? { get set } + + var userName: String? { get set } + + var contacts: [UserContact] { get } + + func update(contact: UserContact, action: UserContactActions) } protocol AccountSettingsOutputInteractorProtocol { - } diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/AddContactCellModel.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/AddContactCellModel.swift new file mode 100644 index 000000000..7aab2fe81 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/AddContactCellModel.swift @@ -0,0 +1,19 @@ +// +// AddContactCellModel.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AddContactCellModel: IdentityProtocol, VerticalSizeble { + static var identifier: String { + return "AddContactTVCell" + } + + var height: CGFloat { + return 44 + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/ContactTVCellModel.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/ContactTVCellModel.swift new file mode 100644 index 000000000..a7df9c868 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/ContactTVCellModel.swift @@ -0,0 +1,26 @@ +// +// ContactCellModel.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class ContactTVCellModel: IdentityProtocol, VerticalSizeble { + static var identifier: String { return "ContactTVCell" } + + var height: CGFloat { return 60 } + + let typeImage: UIImage + let title: String + let details: String + + init(typeImage: UIImage, title: String, details: String) { + self.typeImage = typeImage + self.title = title + self.details = details + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/DescriptionCellModel.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/DescriptionCellModel.swift new file mode 100644 index 000000000..96373e450 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/DescriptionCellModel.swift @@ -0,0 +1,28 @@ +// +// DescriptionCellModel.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct DescriptionCellModel: IdentityProtocol{//}, VerticalSizeble { + static var identifier: String { + return "DescriptionTVCell" + } + +// var height: CGFloat { +// let text = +// "You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.".localized +// + "\n" +// + "You can use a-z, 0-9 and underscores. Minimum lenght is 2 characters.".localized +// +// let font = FontFamily.NotoSans.regular.font(size: 14) +// +// let size = (text as NSString).size(withAttributes: [NSAttributedStringKey.font: font]) +// +// return size.height +// } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/MaterialTextFieldCellModel.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/MaterialTextFieldCellModel.swift new file mode 100644 index 000000000..703e86b93 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/MaterialTextFieldCellModel.swift @@ -0,0 +1,29 @@ +// +// MaterialTextFieldCellModel.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class MaterialTextFieldCellModel: IdentityProtocol, VerticalSizeble { + let profileField: ProfileField + let value: String + let action: (String) -> Void + + let height: CGFloat + + init(profileField: ProfileField, value: String, action: @escaping (String) -> Void, height: CGFloat = 65) { + self.profileField = profileField + self.value = value + self.action = action + self.height = height + } + + static var identifier: String { + return "MaterialTextFieldTVCell" + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSectionHeader.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSectionHeader.swift new file mode 100644 index 000000000..98bb0075f --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSectionHeader.swift @@ -0,0 +1,20 @@ +// +// SettingsSectionHeader.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +struct SettingsSectionHeader { + let text: String + let height: CGFloat + + init(text: String, height: CGFloat = 40) { + self.text = text + self.height = height + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSelectorCellModel.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSelectorCellModel.swift new file mode 100644 index 000000000..4ebf4ff18 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSelectorCellModel.swift @@ -0,0 +1,27 @@ +// +// SettingsSelectorCellModel.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +struct SettingsSelectorCellModel: IdentityProtocol, VerticalSizeble { + let title: String + let details: String + + let height: CGFloat + + init(title: String, details: String, height: CGFloat = 42) { + self.title = title + self.details = details + self.height = height + } + + static var identifier: String { + return "SettingsSelectorTVCell" + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSetAvatarCellModel.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSetAvatarCellModel.swift new file mode 100644 index 000000000..33dc2c081 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSetAvatarCellModel.swift @@ -0,0 +1,23 @@ +// +// SettingsSetAvatarCellModel.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct SettingsSetAvatarCellModel: IdentityProtocol, VerticalSizeble { + let image: UIImage + let height: CGFloat + + init(image: UIImage, height: CGFloat = 145) { + self.image = image + self.height = height + } + + static var identifier: String { + return "SettingsSetAvatarTVCell" + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/Sizeble.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/Sizeble.swift new file mode 100644 index 000000000..f68783fe6 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/Sizeble.swift @@ -0,0 +1,20 @@ +// +// Sizeble.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +protocol Sizeble: VerticalSizeble, HorizontalSizeble {} + +protocol VerticalSizeble { + var height: CGFloat { get } +} + +protocol HorizontalSizeble { + var width: CGFloat { get } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/StatusTimeout.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/StatusTimeout.swift new file mode 100644 index 000000000..d836ddffc --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/StatusTimeout.swift @@ -0,0 +1,18 @@ +// +// StatusTimeout.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +enum StatusTimeout: String { + case fiveMin = "5 min" + case fifteenMin = "15 min" + case thirtyMin = "30 min" + case oneHour = "60 min" + case never = "Never" +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContact.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContact.swift new file mode 100644 index 000000000..02f73d25e --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContact.swift @@ -0,0 +1,32 @@ +// +// UserContact.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +struct UserContact { + enum ContactType { + case email + case phone + case facebook + case google + } + + enum ContactDetailType { + case mobile + case work + case home + case custom(String) + + + } + + let type: ContactType + let value: String + let detailType: ContactDetailType +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContactAction.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContactAction.swift new file mode 100644 index 000000000..2d2c1b58a --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContactAction.swift @@ -0,0 +1,16 @@ +// +// UserContactAction.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +enum UserContactActions { + case create + case update + case delete +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserProfile.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserProfile.swift new file mode 100644 index 000000000..62398bd60 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserProfile.swift @@ -0,0 +1,25 @@ +// +// UserProfile.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +struct UserProfile { + let avatar: UIImage? + let status: UserStatus + let statusTimeout: StatusTimeout + let profileMessage: String + + let firstName: String + let lastName: String + let birthday: Date? + + let userName: String + + let contacts: [UserContact] +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserStatus.swift b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserStatus.swift new file mode 100644 index 000000000..f0a706e16 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserStatus.swift @@ -0,0 +1,17 @@ +// +// UserStatus.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +enum UserStatus: String { + case active + case inactive + case busy + case offline +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/AccountSettings/AccountSettings/Interactor/AccountSettingsInteractor.swift new file mode 100644 index 000000000..5b41e4cb1 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Interactor/AccountSettingsInteractor.swift @@ -0,0 +1,73 @@ +// +// AccountSettingsInteractor.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AccountSettingsInteractor: AccountSettingsInputInteractorProtocol, SetInjectable { + private var presenter: AccountSettingsOutputInteractorProtocol? + + var avatar: UIImage? + var status: UserStatus = .active + var statusTimeout: StatusTimeout = .fiveMin + var profileMessage: String? + + var firstName: String? + var lastName: String? + var birthday: Date? + + var userName: String? + + var contacts: [UserContact] = [] +} + +// MARK: - AccountSettingsInputInteractorProtocol + +extension AccountSettingsInteractor { + func save(completion: (Result) -> Void) { + let userProfile = UserProfile( + avatar: avatar, + status: status, + statusTimeout: statusTimeout, + profileMessage: profileMessage ?? "", + firstName: firstName ?? "", + lastName: lastName ?? "", + birthday: birthday, + userName: userName ?? "", + contacts: contacts) + + // should be saved in some service + + completion(.success(())) + } + + func update(contact: UserContact, action: UserContactActions) { + + } +} + +// MARK: - SetInjectable + +extension AccountSettingsInteractor { + struct Dependencies { + let presenter: AccountSettingsOutputInteractorProtocol + let userProfile: UserProfile + } + + func inject(dependencies: AccountSettingsInteractor.Dependencies) { + presenter = dependencies.presenter + avatar = dependencies.userProfile.avatar + status = dependencies.userProfile.status + statusTimeout = dependencies.userProfile.statusTimeout + profileMessage = dependencies.userProfile.profileMessage + firstName = dependencies.userProfile.firstName + lastName = dependencies.userProfile.lastName + birthday = dependencies.userProfile.birthday + userName = dependencies.userProfile.userName + contacts.append(contentsOf: dependencies.userProfile.contacts) + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift new file mode 100644 index 000000000..d6825d26d --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -0,0 +1,177 @@ +// +// AccountSettingsPresenter.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AccountSettingsPresenter: AccountSettingsPresenterProtocol, SetInjectable, AccountSettingsOutputInteractorProtocol { + private var view: AccountSettingsViewProtocol? + private var wireframe: AccountSettingsWireframe? + private var interactor: AccountSettingsInputInteractorProtocol? +} + +// MARK: - AccountSettingsPresenterProtocol + +extension AccountSettingsPresenter { + func save() { + interactor?.save { + $0.onSuccess { + wireframe?.back() + } + } + } + + func back() { + wireframe?.back() + } +} + +// MARK: - AccountSettingsOutputInteractorProtocol + +extension AccountSettingsPresenter { + func countOfSections() -> Int { + return 4 + } + + func rowsInSection(section: Int) -> Int { + switch section { + case 0: return 4 + case 1: return 2//3 + case 2: return 2 + case 3: return 1 + (interactor?.contacts.count ?? 0) + default: return 0 + } + } + + func item(for section: Int) -> SettingsSectionHeader? { + switch section { + case 1: return SettingsSectionHeader(text: "Personal Information".localized) + case 2: return SettingsSectionHeader(text: "Username".localized) + case 3: return SettingsSectionHeader(text: "Contact Information".localized) + default: return nil + } + } + + func item(for section: Int, row: Int) -> AnyObject? { + switch section { + case 0: + switch row { + case 0: return SettingsSetAvatarCellModel(image: interactor?.avatar ?? UIImage.avatarPlaceholder) as AnyObject + case 1: return SettingsSelectorCellModel(title: "Status".localized, details: (interactor?.status.rawValue.localized ?? "")) as AnyObject + case 2: return SettingsSelectorCellModel(title: "Idle Timeout".localized, details: (interactor?.statusTimeout.rawValue.localized ?? "")) as AnyObject + case 3: return MaterialTextFieldCellModel(profileField: .profileMessage, value: interactor?.profileMessage ?? "", action: setProfileMessage) + default: return nil + } + case 1: + switch row { + case 0: return MaterialTextFieldCellModel(profileField: .firstName, value: interactor?.firstName ?? "", action: setFirstName) + case 1: return MaterialTextFieldCellModel(profileField: .lastName, value: interactor?.lastName ?? "", action: setLastName) + default: return nil + } + case 2: + switch row { + case 0: return MaterialTextFieldCellModel(profileField: .userName, value: interactor?.userName ?? "", action: setUserName) + case 1: return DescriptionCellModel() as AnyObject + default: return nil + } + case 3: + switch row { + case 0: return AddContactCellModel() + default: + let contact = interactor?.contacts[row - 1] + return ContactTVCellModel(typeImage: UIImage(named: "arrow_up")!, title: contact?.value ?? "", details: "") + } + default: return nil + } + } + + func chooseAvatar(completion: @escaping (UIImage?) -> Void) { + wireframe?.chooseAvatar { + self.interactor?.avatar = $0 + completion($0) + } + } + + func chooseStatus(completion: @escaping (UserStatus) -> Void) { + wireframe?.chooseStatus { + self.interactor?.status = $0 + completion($0) + } + } + + func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { + wireframe?.chooseTimeout { + self.interactor?.statusTimeout = $0 + completion($0) + } + } + + func setProfileMessage(message: String) { + interactor?.profileMessage = message + } + + func setFirstName(value: String) { + interactor?.firstName = value + } + + func setLastName(value: String) { + interactor?.lastName = value + } + + func setBirthday(value: Date) { + interactor?.birthday = value + } + + func setUserName(value: String) { + interactor?.userName = value + } + + func addContact(completion: @escaping (Result) -> Void) { + wireframe?.addContact { + $0 + .onSuccess { + self.interactor?.update(contact: $0, action: .create) + completion(.success(())) + } + .onFailure { completion(.failure($0)) } + } + } + + func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) { + wireframe?.contactDetails(contact: contact) { + $0 + .onSuccess { + self.interactor?.update(contact: $0, action: .update) + completion(.success(())) + } + .onFailure { completion(.failure($0)) } + } + } + + func contacts() -> [UserContact] { + return interactor?.contacts ?? [] + } +} + +// MARK: - SetInjectable + +extension AccountSettingsPresenter { + struct Dependencies { + let view: AccountSettingsViewProtocol + let wireframe: AccountSettingsWireframe + let interactor: AccountSettingsInputInteractorProtocol + } + + func inject(dependencies: AccountSettingsPresenter.Dependencies) { + view = dependencies.view + wireframe = dependencies.wireframe + interactor = dependencies.interactor + } +} + + diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift index 8105451d8..0f2b5f5be 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift @@ -9,7 +9,7 @@ import Foundation -final class AccountSettingsViewController: UIViewController, AccountSettingsViewProtocol, InitializeInjectable, UITableViewDelegate, UITableViewDataSource { +final class AccountSettingsViewController: UIViewController, AccountSettingsViewProtocol, InitializeInjectable, UITableViewDelegate, UITableViewDataSource, KeyboardInteractive { private let viewsFactory: AccountSettingsViewsFactoryProtocol private let presenter: AccountSettingsPresenterProtocol @@ -35,7 +35,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView title: "Account settings".localized.uppercased(), navigationHandler: presenter, backButtonImage: UIImage.backButtonImage)) - + header.backgroundColor = UIColor.nynja.clear return header }() @@ -43,6 +43,20 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView let table = UITableView(frame: CGRect.zero, style: UITableViewStyle.grouped) view.addSubview(table) + table.delegate = self + table.dataSource = self + + table.register(SettingsSetAvatarTVCell.self, forCellReuseIdentifier: SettingsSetAvatarCellModel.identifier) + table.register(SettingsSelectorTVCell.self, forCellReuseIdentifier: SettingsSelectorCellModel.identifier) + table.register(MaterialTextFieldTVCell.self, forCellReuseIdentifier: MaterialTextFieldCellModel.identifier) + table.register(DescriptionTVCell.self, forCellReuseIdentifier: DescriptionCellModel.identifier) + table.register(AddContactTVCell.self, forCellReuseIdentifier: AddContactCellModel.identifier) + table.register(ContactTVCell.self, forCellReuseIdentifier: ContactTVCellModel.identifier) + + table.backgroundColor = UIColor.nynja.clear + + table.separatorStyle = .none + table.snp.makeConstraints{ (make) in make.top.equalTo(headerView.snp.bottom) make.left.right.equalToSuperview() @@ -65,6 +79,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView button.addTarget(self, action: #selector(saveAction(sender:)), for: .touchUpInside) button.layer.cornerRadius = 22 + button.clipsToBounds = true button.snp.makeConstraints { (make) in make.height.equalTo(44) @@ -75,6 +90,11 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView return button }() + struct Dependencies { + let viewsFactory: AccountSettingsViewsFactoryProtocol + let presenter: AccountSettingsPresenterProtocol + } + init(dependencies: AccountSettingsViewController.Dependencies) { viewsFactory = dependencies.viewsFactory presenter = dependencies.presenter @@ -86,12 +106,171 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView fatalError("init(coder:) has not been implemented") } - struct Dependencies { - let viewsFactory: AccountSettingsViewsFactoryProtocol - let presenter: AccountSettingsPresenterProtocol + override func viewDidLoad() { + super.viewDidLoad() + + _ = [topHeaderLayoutGuide, headerView, tableView, saveButton] + + view.backgroundColor = UIColor.nynja.contextMenuBackGray + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unregisterForKeyboardNotifications() + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent } + func reloadData() { + tableView.reloadData() + } +} + +// MARK: - Actions + +extension AccountSettingsViewController { @objc func saveAction(sender: UIButton) { + presenter.save() + } +} + +// MARK: - Table view + +extension AccountSettingsViewController { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let item = presenter.item(for: section) else { + return nil + } + + let header = SettingsSectionHeaderView() + header.configure(config: SettingsSectionHeaderView.Config(title: item.text)) + + return header + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + let item = presenter.item(for: section) + + return CGFloat(item?.height ?? 0) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + guard let item = presenter.item(for: indexPath.section, row: indexPath.row) as? VerticalSizeble else { + return UITableViewAutomaticDimension + } + return CGFloat(item.height) + } + + func numberOfSections(in tableView: UITableView) -> Int { + return presenter.countOfSections() + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return presenter.rowsInSection(section: section) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch (indexPath.section, indexPath.row) { + case (0, 1): presenter.chooseStatus { _ in + tableView.reloadRows(at: [indexPath], with: .automatic) + } + case (0, 2): presenter.chooseTimeout { _ in + tableView.reloadRows(at: [indexPath], with: .automatic) + } + case (3, 0): presenter.addContact { _ in tableView.reloadData() } + case (3, _): presenter.contactDetails(contact: presenter.contacts()[indexPath.row - 1]) { _ in tableView.reloadData() } + default: break + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let item = presenter.item(for: indexPath.section, row: indexPath.row) as? IdentityProtocol else { + return UITableViewCell() + } + + let cell = tableView.dequeueReusableCell(withIdentifier: type(of: item).identifier, for: indexPath) + switch cell { + case is SettingsSetAvatarTVCell: configureSetAvatarCell(cell: cell as? SettingsSetAvatarTVCell, item: item as? SettingsSetAvatarCellModel) + case is SettingsSelectorTVCell: configureSelector(cell: cell as? SettingsSelectorTVCell, item: item as? SettingsSelectorCellModel) + case is MaterialTextFieldTVCell: configureTextField(cell: cell as? MaterialTextFieldTVCell, item: item as? MaterialTextFieldCellModel) + case is DescriptionTVCell: break + case is ContactTVCell: configureContactCell(cell: cell as? ContactTVCell, item: item as? ContactTVCellModel) + default: break + } + + cell.selectionStyle = .none + + return cell + } + + private func configureContactCell(cell: ContactTVCell?, item: ContactTVCellModel?) { + guard let cell = cell, let item = item else { + return + } + + cell.configure(config: ContactTVCell.Config(typeImage: item.typeImage, title: item.title, details: item.details)) + } + + private func configureSetAvatarCell(cell: SettingsSetAvatarTVCell?, item: SettingsSetAvatarCellModel?) { + guard let cell = cell, let item = item else { + return + } + + cell.configure(config: SettingsSetAvatarTVCell.Config(image: item.image, chooseAvatarAction: { [weak self] in + self?.presenter.chooseAvatar(completion: { (image) in + cell.avatarButton.setImage(image, for: .normal) + }) + })) + } + + private func configureSelector(cell: SettingsSelectorTVCell?, item: SettingsSelectorCellModel?) { + guard let cell = cell, let item = item else { + return + } + + cell.configure(config: SettingsSelectorTVCell.Config(title: item.title, details: item.details)) + } + + private func configureTextField(cell: MaterialTextFieldTVCell?, item: MaterialTextFieldCellModel?) { + guard let cell = cell, let item = item else { + return + } + + cell.configure(config: MaterialTextFieldTVCell.Config( + fieldType: item.profileField, + textChangedHandler: { [weak self] (field, value) in + switch field { + case .firstName: self?.presenter.setFirstName(value: value) + case .lastName: self?.presenter.setLastName(value: value) + case .accountName: break + case .userName: self?.presenter.setUserName(value: value) + case .profileMessage: self?.presenter.setProfileMessage(message: value) + } + }, + shouldChangeTextHandler: nil)) + } +} + +extension AccountSettingsViewController { + func keyboardNotified(endFrame: CGRect) { + var bottomInset: CGFloat = 28 + + if endFrame.origin.y < UIScreen.main.bounds.size.height { + bottomInset += endFrame.height + } else { + bottomInset += UIWindow.safeAreaBottomPadding() + } + + saveButton.snp.updateConstraints { (make) in + make.bottom.equalToSuperview().inset(bottomInset) + } } } diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/AddContactCell.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/AddContactCell.swift new file mode 100644 index 000000000..66d069e76 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/AddContactCell.swift @@ -0,0 +1,64 @@ +// +// AddContactCell.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class AddContactTVCell: UITableViewCell { + lazy var titleLabel: UILabel = { + let label = UILabel() + contentView.addSubview(label) + + textLabel?.text = "Add Contact Info".localized + textLabel?.font = FontFamily.NotoSans.regular.font(size: 16) + textLabel?.textColor = UIColor.nynja.white + + label.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + make.left.equalToSuperview().inset(16) + } + + return label + }() + + lazy var plusImageView: UIImageView = { + let imageView = UIImageView() + contentView.addSubview(imageView) + + imageView.image = UIImage(named: "ic_add") + imageView.contentMode = .scaleAspectFill + + imageView.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + make.right.equalToSuperview().inset(16) + make.height.width.equalTo(24) + } + + return imageView + }() + + lazy var separatorView: UIView = { + let view = UIView() + contentView.addSubview(view) + view.backgroundColor = UIColor.nynja.gray + view.snp.makeConstraints { (make) in + make.height.equalTo(1) + make.bottom.equalToSuperview() + make.left.right.equalToSuperview().inset(16) + } + + return view + }() + + override func prepareForReuse() { + super.prepareForReuse() + backgroundColor = UIColor.nynja.clear + contentView.backgroundColor = UIColor.nynja.clear + _ = [titleLabel, plusImageView, separatorView] + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/ContactTVCell.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/ContactTVCell.swift new file mode 100644 index 000000000..11a777ad6 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/ContactTVCell.swift @@ -0,0 +1,98 @@ +// +// ContactTVCell.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class ContactTVCell: UITableViewCell, Configurable { + lazy var typeImageView: UIImageView = { + let imageView = UIImageView() + contentView.addSubview(imageView) + + imageView.contentMode = .scaleAspectFill + + imageView.snp.makeConstraints { (make) in + make.width.height.equalTo(24) + make.left.equalToSuperview().offset(16) + make.centerY.equalToSuperview() + } + + return imageView + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + contentView.addSubview(label) + + label.font = FontFamily.NotoSans.regular.font(size: 16) + label.textColor = UIColor.nynja.white + + label.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + make.left.equalTo(typeImageView.snp.right).offset(16) + } + + return label + }() + + lazy var detailedImageView: UIImageView = { + let imageView = UIImageView() + contentView.addSubview(imageView) + + imageView.image = UIImage(named: "disclosure_indicator") + + imageView.snp.makeConstraints { (make) in + make.right.equalToSuperview().offset(-16) + make.centerY.equalToSuperview() + } + + return imageView + }() + + lazy var detailsLabel: UILabel = { + let label = UILabel() + contentView.addSubview(label) + + label.font = FontFamily.NotoSans.regular.font(size: 16) + label.textColor = UIColor.nynja.dustyGray + + label.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + make.right.equalTo(detailedImageView.snp.left) + } + + return label + }() + + lazy var separatorView: UIView = { + let view = UIView() + contentView.addSubview(view) + view.backgroundColor = UIColor.nynja.gray + view.snp.makeConstraints { (make) in + make.height.equalTo(1) + make.bottom.equalToSuperview() + make.left.right.equalToSuperview().inset(16) + } + + return view + }() + + struct Config { + let typeImage: UIImage + let title: String + let details: String + } + + func configure(config: ContactTVCell.Config) { + _ = [typeImageView, titleLabel, detailsLabel, detailedImageView, separatorView] + + typeImageView.image = config.typeImage + titleLabel.text = config.title + detailsLabel.text = config.details + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/DescriptionTVCell.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/DescriptionTVCell.swift new file mode 100644 index 000000000..ef65e51f0 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/DescriptionTVCell.swift @@ -0,0 +1,41 @@ +// +// DescriptionTVCell.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class DescriptionTVCell: UITableViewCell { + lazy var descriptionLabel: UILabel = { + let label = UILabel() + contentView.addSubview(label) + + label.text = + "You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.".localized + + "\n" + + "You can use a-z, 0-9 and underscores. Minimum lenght is 2 characters.".localized + + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + + label.numberOfLines = 0 + + label.snp.makeConstraints { (make) in + make.left.right.equalToSuperview().inset(16) + make.top.bottom.equalToSuperview() + } + + return label + }() + + override func prepareForReuse() { + super.prepareForReuse() + contentView.backgroundColor = UIColor.nynja.clear + backgroundColor = UIColor.nynja.clear + _ = [descriptionLabel] + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift new file mode 100644 index 000000000..4682992cb --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift @@ -0,0 +1,45 @@ +// +// MaterialTextFieldTVCell.swift +// Nynja +// +// Created by Ash on 11/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class MaterialTextFieldTVCell: UITableViewCell, Configurable { + lazy var mainTextField: MaterialTextField = { + let textField = MaterialTextField() + contentView.addSubview(textField) + + textField.snp.makeConstraints { (make) in + make.left.right.equalToSuperview().inset(16) + make.centerY.equalToSuperview() + make.height.equalTo(65) + } + + return textField + }() + + struct Config { + let fieldType: ProfileField + let textChangedHandler: ((ProfileField, String) -> Void)? + let shouldChangeTextHandler: ((ProfileField, String) -> Bool)? + } + + func configure(config: MaterialTextFieldTVCell.Config) { + contentView.backgroundColor = UIColor.nynja.clear + backgroundColor = UIColor.nynja.clear + + mainTextField.placeholder = config.fieldType.placeholder.localized + (config.fieldType.isRequired ? "*" : "") + mainTextField.textChanged = { (input) in + config.textChangedHandler?(config.fieldType, input.text) + } + + mainTextField.shouldTextChanged = { (input, range, string) in + return config.shouldChangeTextHandler?(config.fieldType, input.text + string) ?? true + } + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSelectorTVCell.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSelectorTVCell.swift new file mode 100644 index 000000000..abfdfcb39 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSelectorTVCell.swift @@ -0,0 +1,69 @@ +// +// SettingsSelectorTVCell.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class SettingsSelectorTVCell: UITableViewCell, Configurable { + lazy var titleLabel: UILabel = { + let label = UILabel() + contentView.addSubview(label) + + label.textColor = UIColor.nynja.white + label.font = FontFamily.NotoSans.medium.font(size: 16) + + label.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(16) + } + + return label + }() + + lazy var detailsLabel: UILabel = { + let label = UILabel() + contentView.addSubview(label) + + label.textColor = UIColor.nynja.dustyGray + label.font = FontFamily.NotoSans.medium.font(size: 16) + + label.snp.makeConstraints { (make) in + make.centerY.equalToSuperview() + + make.right.equalToSuperview().offset(-16) + } + + return label + }() + + lazy var separatorView: UIView = { + let view = UIView() + contentView.addSubview(view) + view.backgroundColor = UIColor.nynja.gray + view.snp.makeConstraints { (make) in + make.height.equalTo(1) + make.bottom.equalToSuperview() + make.left.right.equalToSuperview().inset(16) + } + + return view + }() + + struct Config { + let title: String + let details: String + } + + func configure(config: SettingsSelectorTVCell.Config) { + backgroundColor = UIColor.nynja.clear + contentView.backgroundColor = UIColor.nynja.clear + _ = [separatorView] + titleLabel.text = config.title + detailsLabel.text = config.details + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift new file mode 100644 index 000000000..03ac9dec0 --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift @@ -0,0 +1,47 @@ +// +// SettingsSetAvatarTVCell.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class SettingsSetAvatarTVCell: UITableViewCell, Configurable { + private var chooseAvatarAction: (() -> Void)? + + lazy var avatarButton: UIButton = { + let button = UIButton() + contentView.addSubview(button) + + button.layer.cornerRadius = 47 + button.clipsToBounds = true + + button.addTarget(self, action: #selector(chooseAvatarAction(sender:)), for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.center.equalToSuperview() + make.height.width.equalTo(95) + } + + return button + }() + + struct Config { + let image: UIImage + let chooseAvatarAction: () -> Void + } + + func configure(config: SettingsSetAvatarTVCell.Config) { + avatarButton.setImage(config.image, for: .normal) + backgroundColor = UIColor.nynja.clear + contentView.backgroundColor = UIColor.nynja.clear + chooseAvatarAction = config.chooseAvatarAction + } + + @objc func chooseAvatarAction(sender: UIButton) { + chooseAvatarAction?() + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Header/SettingsSectionHeaderView.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/Header/SettingsSectionHeaderView.swift new file mode 100644 index 000000000..38af207bd --- /dev/null +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/Header/SettingsSectionHeaderView.swift @@ -0,0 +1,38 @@ +// +// SettingsSectionHeaderView.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import SnapKit + + +final class SettingsSectionHeaderView: UIView, Configurable { + lazy var label: UILabel = { + let label = UILabel() + addSubview(label) + + label.textColor = UIColor.nynja.dustyGray + label.textAlignment = .left + label.font = FontFamily.NotoSans.regular.font(size: 14) + + label.snp.makeConstraints { (make) in + make.left.right.equalToSuperview().inset(16) + make.centerY.equalToSuperview() + } + + return label + }() + + struct Config { + let title: String + } + + func configure(config: SettingsSectionHeaderView.Config) { + backgroundColor = UIColor.nynja.backgroundColor + label.text = config.title + } +} diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift index 9d2d1977c..52e068259 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -17,11 +17,62 @@ final class AccountSettingsWireframe: AccountSettingsWireframeProtocol { struct Dependencies {} - enum State {} + enum State { + case back + case chooseAvatar(completion: (UIImage?) -> Void) + case chooseStatus(completion: (UserStatus) -> Void) + case chooseTimeout(completion: (StatusTimeout) -> Void) + case addContact(completion: (Result) -> Void) + case contactDetails(contact: UserContact, completion: (Result) -> Void) + } + + private let coordinator: AccountSettingsCoordinatorProtocol + + init(coordinator: AccountSettingsCoordinatorProtocol) { + self.coordinator = coordinator + } func prepareModule(parameters: AccountSettingsWireframe.Parameters, dependencies: AccountSettingsWireframe.Dependencies) -> UIViewController { - let view = UIViewController() + let presenter = AccountSettingsPresenter() + let viewDep = AccountSettingsViewController.Dependencies(viewsFactory: AccountSettingsViewsFactory(), presenter: presenter) + let view = AccountSettingsViewController(dependencies: viewDep) + let interactor = AccountSettingsInteractor() + + let presenterDep = AccountSettingsPresenter.Dependencies(view: view, wireframe: self, interactor: interactor) + + let userContact = UserContact(type: .phone, value: "380678888888", detailType: .work) + + let userProfile = UserProfile(avatar: nil, status: .active, statusTimeout: .fiveMin, profileMessage: "Some", firstName: "Alan", lastName: "Po", birthday: nil, userName: "AlanPo", contacts: [userContact]) + + let interactorDep = AccountSettingsInteractor.Dependencies(presenter: presenter, userProfile: userProfile) + + presenter.inject(dependencies: presenterDep) + interactor.inject(dependencies: interactorDep) return view } + + func back() { + coordinator.wireframe(self, didEndWithState: .back) + } + + func chooseAvatar(completion: @escaping (UIImage?) -> Void) { + coordinator.wireframe(self, didEndWithState: .chooseAvatar(completion: completion)) + } + + func chooseStatus(completion: @escaping (UserStatus) -> Void) { + coordinator.wireframe(self, didEndWithState: .chooseStatus(completion: completion)) + } + + func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { + coordinator.wireframe(self, didEndWithState: .chooseTimeout(completion: completion)) + } + + func addContact(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .addContact(completion: completion)) + } + + func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .contactDetails(contact: contact, completion: completion) ) + } } diff --git a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift index 05412c444..5066cba1e 100644 --- a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift @@ -7,3 +7,143 @@ // import Foundation + +final class AccountSettingsCoordinator: CoordinatorProtocol, AccountSettingsCoordinatorProtocol { + private let navigation: UINavigationController + private let serviceFactory: ServiceFactoryProtocol + + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { + self.navigation = navigation + self.serviceFactory = serviceFactory + } + + func start() { + let wireframe = AccountSettingsWireframe.init(coordinator: self) + let view = wireframe.prepareModule(parameters: AccountSettingsWireframe.Parameters(), dependencies: AccountSettingsWireframe.Dependencies()) + + navigation.pushViewController(view, animated: true) + } + + func end() { + + } +} + +// MARK: - AccountSettingsCoordinatorProtocol + +extension AccountSettingsCoordinator { + func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) { + switch state { + case .back: navigation.popViewController(animated: true) + case .addContact(let completion): showAddContactPopup(completion: completion) + case .chooseAvatar(let completion): selectAvatar(with: completion) + case .chooseStatus(let completion): chooseStatus(completion: completion) + case .chooseTimeout(let completion): chooseTimeout(completion: completion) + case .contactDetails(let contact, let completion): break + } + } + + func showAddContactPopup(completion: (Result) -> Void) { + let view = UIAlertController.init(title: "Add Contact Info".localized, message: nil, preferredStyle: .actionSheet) + + let phoneNumberAction = UIAlertAction(title: "Phone Number".localized, style: .default) { _ in +// completion(.fiveMin) + } + + let emailAction = UIAlertAction(title: "Email".localized, style: .default) { _ in +// completion(.fifteenMin) + } + + let facebookAction = UIAlertAction(title: "Facebook".localized, style: .default) { _ in +// completion(.thirtyMin) + } + + let twitterAction = UIAlertAction(title: "Twitter".localized, style: .default) { _ in +// completion(.oneHour) + } + + let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) + + [phoneNumberAction, emailAction, facebookAction, twitterAction, cancelAction].forEach { view.addAction($0) } + + navigation.present(view, animated: true, completion: nil) + } + + func getAvatar(source: SelectAvatarFlowCoordinatorSource, completion: @escaping (UIImage?) -> Void) { + let coordinatorDependencies = SelectAvatarFlowCoordinator.Dependencies( + source: source, + rootViewController: navigation.viewControllers.last!, + serviceFactory: serviceFactory, + completion: { (url) in + completion(UIImage.sd_image(with: try? Data(contentsOf: url))) + }) + + let coordinator = SelectAvatarFlowCoordinator(dependencies: coordinatorDependencies) + + coordinator.start() + } + + func selectAvatar(with completion: @escaping (UIImage?) -> Void) { + let view = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + let cameraAction = UIAlertAction(title: "Take from Camera".localized, style: .default) { [weak self] _ in self?.getAvatar(source: .camera, completion: completion) } + let galleryAction = UIAlertAction(title: "Take from Gallery".localized, style: .default) { [weak self] _ in self?.getAvatar(source: .gallery, completion: completion) } + let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) + + [cameraAction, galleryAction, cancelAction].forEach { view.addAction($0) } + + navigation.present(view, animated: true, completion: nil) + } + + func chooseStatus(completion: @escaping (UserStatus) -> Void) { + let view = UIAlertController.init(title: "Status".localized, message: nil, preferredStyle: .actionSheet) + + let activeAction = UIAlertAction(title: "Active".localized, style: .default) { _ in + completion(.active) + } + + let inactiveAction = UIAlertAction(title: "Inactive".localized, style: .default) { _ in + completion(.inactive) + } + + let busyAction = UIAlertAction(title: "Busy".localized, style: .default) { _ in + completion(.busy) + } + + let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) + + [activeAction, inactiveAction, busyAction, cancelAction].forEach { view.addAction($0) } + + navigation.present(view, animated: true, completion: nil) + } + + func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { + let view = UIAlertController.init(title: "Idle Timeout".localized, message: nil, preferredStyle: .actionSheet) + + let timeout5MinAction = UIAlertAction(title: "5 min".localized, style: .default) { _ in + completion(.fiveMin) + } + + let timeout15MinAction = UIAlertAction(title: "15 min".localized, style: .default) { _ in + completion(.fifteenMin) + } + + let timeout30MinAction = UIAlertAction(title: "30 min".localized, style: .default) { _ in + completion(.thirtyMin) + } + + let timeout60MinAction = UIAlertAction(title: "60 min".localized, style: .default) { _ in + completion(.oneHour) + } + + let timeoutNeverAction = UIAlertAction(title: "Never".localized, style: .default) { _ in + completion(.never) + } + + let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) + + [timeout5MinAction, timeout15MinAction, timeout30MinAction, timeout60MinAction, timeoutNeverAction, cancelAction].forEach { view.addAction($0) } + + navigation.present(view, animated: true, completion: nil) + } +} diff --git a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift index 3b767f395..69091ae48 100644 --- a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift +++ b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift @@ -13,6 +13,7 @@ enum ProfileField { case lastName case accountName case userName + case profileMessage var isRequired: Bool { return self == .firstName @@ -28,6 +29,7 @@ enum ProfileField { case .lastName: return "Last Name" case .accountName: return "Account Name" case .userName: return "Username" + case .profileMessage: return "Profile Message" } } } diff --git a/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift index c91a40722..9339b99e3 100644 --- a/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift +++ b/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -69,6 +69,7 @@ extension CreateProfileInteractor { case .lastName: lastName = value case .accountName: accountName = value case .userName: userName = value + case .profileMessage: break } } diff --git a/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift b/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift index e23958853..102dd7e62 100644 --- a/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift +++ b/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift @@ -89,6 +89,7 @@ extension CreateProfileContentView { case .lastName: lastNameTextField.text = value case .accountName: accountNameTextField.text = value case .userName: usernameTextField.text = value + case .profileMessage: break } } diff --git a/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Contents.json new file mode 100644 index 000000000..fd198c98f --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "info" : { + "author" : "xcode", + "version" : "1" + }, + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "Disclosure_Indicator.pdf" + } + ] +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Disclosure_Indicator.pdf b/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Disclosure_Indicator.pdf new file mode 100644 index 0000000000000000000000000000000000000000..80e030d79e676c732bff5ef26b50500d23fa062a GIT binary patch literal 5676 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 Date: Mon, 5 Nov 2018 12:54:02 +0200 Subject: [PATCH 073/138] [NY-4851] Fixed retain cycles in Auth module. Updated flow for selecting country --- Nynja.xcodeproj/project.pbxproj | 7 +---- Nynja/CountriesProvider.swift | 4 +++ Nynja/CountriesProviding.swift | 1 + Nynja/Modules/Auth/AuthCoordinator.swift | 6 ++-- .../Auth/AuthModule/AuthProtocols.swift | 21 ++++++++----- .../Interactor/AuthInteractor.swift | 10 +++++- .../AuthModule/Presenter/AuthPresenter.swift | 31 +++++++++++++------ .../AuthModule/View/AuthViewController.swift | 13 +++++++- .../View/ViewsFactory/AuthViewsFactory.swift | 10 +++--- .../AuthModule/Wireframe/AuthWireframe.swift | 14 ++++++--- .../ServiceFactory/ServiceFactory.swift | 5 +++ Nynja/Services/StorageService.swift | 8 +++-- .../Wireframe/WireframeProtocol.swift | 1 - 13 files changed, 89 insertions(+), 42 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 570d7860c..e0a49ef08 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1507,9 +1507,7 @@ A4330A662109E04E0060BD93 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A642109DFA00060BD93 /* UserInfo.swift */; }; A4330A6A2109EA850060BD93 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A692109EA850060BD93 /* DatabaseManager.swift */; }; A4330A6E2109EBA70060BD93 /* CountriesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A6D2109EBA70060BD93 /* CountriesProvider.swift */; }; - A4330A6F2109EBA70060BD93 /* CountriesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A6D2109EBA70060BD93 /* CountriesProvider.swift */; }; A4330A712109EBB30060BD93 /* CountriesProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A702109EBB30060BD93 /* CountriesProviding.swift */; }; - A4330A722109EBB30060BD93 /* CountriesProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A702109EBB30060BD93 /* CountriesProviding.swift */; }; A4330A742109F0D40060BD93 /* StorageService+UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A732109F0D40060BD93 /* StorageService+UserInfo.swift */; }; A4330A752109F0D40060BD93 /* StorageService+UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4330A732109F0D40060BD93 /* StorageService+UserInfo.swift */; }; A433D9A120A5C18C00C946F9 /* ContactsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A433D9A020A5C18C00C946F9 /* ContactsProvider.swift */; }; @@ -7595,12 +7593,12 @@ 5EEB73BE216199DE00D8ECE6 /* AuthModule */ = { isa = PBXGroup; children = ( + 5EEB73C4216199ED00D8ECE6 /* AuthProtocols.swift */, 5EEB73BF216199DE00D8ECE6 /* Presenter */, 5EEB73C0216199DE00D8ECE6 /* Wireframe */, 5EEB73C1216199DE00D8ECE6 /* View */, 5EEB73C2216199DE00D8ECE6 /* Interactor */, 5EEB73C3216199DE00D8ECE6 /* Entities */, - 5EEB73C4216199ED00D8ECE6 /* AuthProtocols.swift */, ); path = AuthModule; sourceTree = ""; @@ -15095,7 +15093,6 @@ 359EB23D1F9A1BE600147437 /* Queue.swift in Sources */, 359EB23C1F9A1BD800147437 /* Reachability.swift in Sources */, A4B5450020EFC52100EB7B0F /* StatusCode.swift in Sources */, - A4330A722109EBB30060BD93 /* CountriesProviding.swift in Sources */, A42CE5FC20692EDB000889CC /* userTask_Spec.swift in Sources */, A42CE5CE20692EDB000889CC /* Vox_Spec.swift in Sources */, A42CE56620692EDB000889CC /* receiveTask.swift in Sources */, @@ -15112,7 +15109,6 @@ A45F114E20B4222F00F45004 /* RecordingStatus.swift in Sources */, A42CE5B220692EDB000889CC /* log_Spec.swift in Sources */, A42CE54620692EDB000889CC /* messageEvent.swift in Sources */, - A4330A6F2109EBA70060BD93 /* CountriesProvider.swift in Sources */, A42CE54820692EDB000889CC /* reader.swift in Sources */, A42CE60A20692EDB000889CC /* Index_Spec.swift in Sources */, 26352916207572AA00DC6FBD /* JobExtension.swift in Sources */, @@ -15727,7 +15723,6 @@ B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */, 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */, 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */, - 264312EC210DE4040057E8B0 /* LanguageSectionCoordinator.swift in Sources */, 85579882209322A8007050B8 /* StickerMenuDataSource.swift in Sources */, 8506F001206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift in Sources */, 85C16C3C20D261C000EDB77E /* MessageStickerView.swift in Sources */, diff --git a/Nynja/CountriesProvider.swift b/Nynja/CountriesProvider.swift index e00258d91..4cd8d21a8 100644 --- a/Nynja/CountriesProvider.swift +++ b/Nynja/CountriesProvider.swift @@ -22,4 +22,8 @@ final class CountriesProvider: CountriesProviding { .filter { !$0.name.isEmpty } .sorted { $0.name > $1.name } } + + func fetchDefaultCountry() -> Country { + return Country(ISO: "ARG", name: "Argentina", code: "54", numberTemplate: "XX XX XXX XX") + } } diff --git a/Nynja/CountriesProviding.swift b/Nynja/CountriesProviding.swift index 72e333803..42f966b20 100644 --- a/Nynja/CountriesProviding.swift +++ b/Nynja/CountriesProviding.swift @@ -8,4 +8,5 @@ protocol CountriesProviding { func fetchCountries() -> [CountryModel] + func fetchDefaultCountry() -> Country } diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 66298e0d4..4a4a94d24 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -22,8 +22,10 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt func start() { let wireframe = AuthWireframe(coordinator: self) - let view = wireframe.prepareModule(parameters: NSNull(), dependencies: AuthWireframe.Dependencies()) - + let view = wireframe.prepareModule( + parameters: NSNull(), + dependencies: AuthWireframe.Dependencies(countriesProvider: serviceFactory.makeCountriesProvider()) + ) navigation?.pushViewController(view, animated: true) } diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index 3d87a582f..13c315292 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -8,36 +8,41 @@ import Foundation -protocol AuthWireframeProtocol: WireframeProtocol { +protocol AuthWireframeProtocol: class { func selectCountry(completion: @escaping (Result) -> Void) func continueLogin(loginOption: LoginOption) } protocol AuthViewProtocol: class where Self: UIViewController { - + func update(country: Country) } -protocol AuthPresenterProtocol { +protocol AuthPresenterProtocol: class { var loginOption: LoginOption { get } - var country: Country { get } + + var selectedCountry: Country { get } func switchLoginOption() + func loginViaFacebook(completion: (Result) -> Void) func loginViaGoogle(completion: (Result) -> Void) func loginViaEmail(_ email: String, completion: (Result) -> Void) func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) - func selectCountry(completion: @escaping (Result) -> Void) + func selectCountry() } -protocol AuthInputInteractorProtocol { - associatedtype Code = String +protocol AuthInputInteractorProtocol: class { + typealias Code = String + func loginViaFacebook(completion: (Result) -> Void) func loginViaGoogle(completion: (Result) -> Void) func loginViaEmail(_ email: String, completion: (Result) -> Void) func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) + + func fetchDefaultCountry() -> Country } -protocol AuthOutputInteractorProtocol { +protocol AuthOutputInteractorProtocol: class { } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 7e435f3f4..c25f1687d 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -10,7 +10,8 @@ import Foundation final class AuthInteractor: AuthInputInteractorProtocol, SetInjectable { - private var presenter: AuthOutputInteractorProtocol? + private weak var presenter: AuthOutputInteractorProtocol? + private var countriesProvider: CountriesProviding! } // MARK: - SetInjectable @@ -18,16 +19,23 @@ final class AuthInteractor: AuthInputInteractorProtocol, SetInjectable { extension AuthInteractor { struct Dependencies { let presenter: AuthOutputInteractorProtocol + let countriesProvider: CountriesProviding } func inject(dependencies: AuthInteractor.Dependencies) { presenter = dependencies.presenter + countriesProvider = dependencies.countriesProvider } } // MARK: - AuthInputInteractorProtocol extension AuthInteractor { + + func fetchDefaultCountry() -> Country { + return countriesProvider.fetchDefaultCountry() + } + func loginViaFacebook(completion: (Result) -> Void) { completion(.success("Some code")) } diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 862e48d8c..6aee9706f 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -10,19 +10,24 @@ import Foundation final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, SetInjectable { - private var view: AuthViewProtocol? - private var interactor: AuthInteractor? - private var wireframe: AuthWireframe? + private weak var view: AuthViewProtocol? + private var interactor: AuthInputInteractorProtocol! + private var wireframe: AuthWireframeProtocol! - var loginOption: LoginOption = .phoneNumber(number: "") + private(set) var loginOption: LoginOption = .phoneNumber(number: "") - var country: Country = Country(ISO: "ARG", name: "Argentina", code: "54", numberTemplate: "XX XX XXX XX") + private(set) lazy var selectedCountry: Country = { + return interactor.fetchDefaultCountry() + }() func switchLoginOption() { switch loginOption { - case .email: loginOption = .phoneNumber(number: "") - case .phoneNumber: loginOption = .email(email: "") - default: break + case .email: + loginOption = .phoneNumber(number: "") + case .phoneNumber: + loginOption = .email(email: "") + default: + break } } @@ -70,9 +75,15 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } } - func selectCountry(completion: @escaping (Result) -> Void) { + func selectCountry() { wireframe?.selectCountry { result in - completion(result) + switch result { + case let .success(country): + self.selectedCountry = country + self.view?.update(country: country) + case .failure: + break + } } } } diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 09f9d62cb..5460303be 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -64,6 +64,15 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn } } +// MARK: - AuthViewProtocol + +extension AuthViewController { + + func update(country: Country) { + phoneNumberLoginView?.updateCountry(country) + } +} + // MARK: - KeyboardInteractive extension AuthViewController { @@ -84,7 +93,9 @@ extension AuthViewController { private extension AuthViewController { func showPhoneNumberLogin(animated: Bool) { - phoneNumberLoginView = viewsFactory.makePhoneNumberLoginView(on: scrollContentView, presenter: presenter, country: presenter.country ) + phoneNumberLoginView = viewsFactory.makePhoneNumberLoginView(on: scrollContentView, + presenter: presenter, + country: presenter.selectedCountry) if animated { animateChangingViews(first: emailLoginView, second: phoneNumberLoginView) diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index 6d4130a21..56e7afc2c 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -117,13 +117,11 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { loginView.configure(config: PhoneNumberLoginView.Config( country: country, - countrySelectorAction: { - presenter.selectCountry { (result) in - result.onSuccess { loginView.updateCountry($0) } - } + countrySelectorAction: { [weak presenter] in + presenter?.selectCountry() }, - nextAction: { - presenter.loginViaPhoneNumber($0) { (result) in + nextAction: { [weak presenter] in + presenter?.loginViaPhoneNumber($0) { (result) in print(#function) } })) diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 0dd258504..90cb9d0c1 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -12,7 +12,8 @@ protocol AuthCoordinatorProtocol { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) } -final class AuthWireframe: AuthWireframeProtocol { +final class AuthWireframe: WireframeProtocol, AuthWireframeProtocol { + private let coordinator: AuthCoordinatorProtocol init(coordinator: AuthCoordinatorProtocol) { @@ -21,20 +22,23 @@ final class AuthWireframe: AuthWireframeProtocol { typealias Parameters = NSNull - struct Dependencies {} + struct Dependencies { + let countriesProvider: CountriesProviding + } enum State { case continueLogin(loginOption: LoginOption) case getCountry(callback: (Result) -> Void) } - func prepareModule(parameters: NSNull, dependencies: AuthWireframe.Dependencies) -> UIViewController { + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = AuthPresenter() - let view = AuthViewController(dependencies: AuthViewController.Dependencies(presenter: presenter, viewsFactory: AuthViewsFactory())) + let view = AuthViewController(dependencies: AuthViewController.Dependencies(presenter: presenter, + viewsFactory: AuthViewsFactory())) let interactor = AuthInteractor() let presenterDep = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) - let interactorDep = AuthInteractor.Dependencies(presenter: presenter) + let interactorDep = AuthInteractor.Dependencies(presenter: presenter, countriesProvider: dependencies.countriesProvider) presenter.inject(dependencies: presenterDep) interactor.inject(dependencies: interactorDep) diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index f581ec61b..6196d552e 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -25,6 +25,7 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol { func makeContactsProvider() -> ContactsProviding func makeConversationsProvider() -> ConversationsProviding func makeStickersProvider() -> StickersProviding + func makeCountriesProvider() -> CountriesProviding func makeTextInputValidationService() -> TextInputValidationServiceProtocol func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol @@ -109,6 +110,10 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { ) } + func makeCountriesProvider() -> CountriesProviding { + return CountriesProvider() + } + func makePermissionManager() -> PermissionManager { return PermissionManager() } diff --git a/Nynja/Services/StorageService.swift b/Nynja/Services/StorageService.swift index 432c785e9..ea4cd33a3 100644 --- a/Nynja/Services/StorageService.swift +++ b/Nynja/Services/StorageService.swift @@ -15,7 +15,10 @@ import CryptoSwift protocol StorageServiceProtocol { var userDefaults: UserDefaults? { get } var keychain: KeychainService { get } + + #if !SHARE_EXTENSION var countries: [CountryModel] { get set } + #endif func setupDatabase(with name: String, application: UIApplication) func clearStorage() @@ -28,15 +31,14 @@ class StorageService: StorageServiceProtocol { let userDefaults = UserDefaults(suiteName: Bundle.main.appGroupName) let keychain = KeychainService.standard + #if !SHARE_EXTENSION private let countriesProvider = CountriesProvider() - #if !SHARE_EXTENSION private let databaseManager = DatabaseManager() var dbPool: DatabasePool? { return databaseManager.dbPool } - #endif // MARK: - Properties @@ -44,6 +46,8 @@ class StorageService: StorageServiceProtocol { return countriesProvider.fetchCountries() }() + #endif + /// It is used only for debug purposes. /// Note: You should make clear instalation (don't upgrade old version of app). private let shouldEncryptDB: Bool = true diff --git a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift b/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift index a109004a1..4ec04dad2 100644 --- a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift +++ b/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift @@ -8,7 +8,6 @@ import Foundation - protocol WireframeProtocol: class { associatedtype Parameters associatedtype Dependencies -- GitLab From 4829f49dd57cbdc6b73e2d22a3ebb1cd2b6e646f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 14:01:38 +0200 Subject: [PATCH 074/138] [NY-4851] Fetch default country. --- Nynja/CountriesProvider.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Nynja/CountriesProvider.swift b/Nynja/CountriesProvider.swift index 4cd8d21a8..061db9e77 100644 --- a/Nynja/CountriesProvider.swift +++ b/Nynja/CountriesProvider.swift @@ -10,6 +10,7 @@ import Foundation final class CountriesProvider: CountriesProviding { + // FIXME: return array of Country func fetchCountries() -> [CountryModel] { let path = Bundle.main.path(forResource: "countries", ofType: "txt")! guard let text = try? String(contentsOfFile: path, encoding: .utf8) else { @@ -24,6 +25,11 @@ final class CountriesProvider: CountriesProviding { } func fetchDefaultCountry() -> Country { - return Country(ISO: "ARG", name: "Argentina", code: "54", numberTemplate: "XX XX XXX XX") + let countries = fetchCountries().map { + // FIXME: + Country(ISO: $0.ISO, name: $0.name, code: $0.code, numberTemplate: $0.placeHolder ?? "") + } + let code = (NSLocale.current.regionCode ?? countries.last?.code)?.replacingOccurrences(of: "+", with: "") + return countries.first { $0.code == code || $0.ISO == code } ?? countries.last! } } -- GitLab From 9b43ee2c60806b9851553e99dc2c9d1ae40d235e Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 14:31:05 +0200 Subject: [PATCH 075/138] [NY-4699] Present old login UI. --- Nynja/AppDelegate.swift | 296 +--------------------------------------- 1 file changed, 3 insertions(+), 293 deletions(-) diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 63e768b9d..596fbbf2e 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -105,12 +105,12 @@ private extension AppDelegate { self.window = UIWindow(frame: UIScreen.main.bounds) let navigation = UINavigationController() navigation.isNavigationBarHidden = true -// SplashWireFrame().presentSplash(navigation: navigation) - let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + SplashWireFrame().presentSplash(navigation: navigation) +// let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) self.window?.rootViewController = navigation self.window?.makeKeyAndVisible() - coordinator.start() +// coordinator.start() } private func configureDependencies() { @@ -184,293 +184,3 @@ private extension AppDelegate { Intercom.setApiKey(intercomServiceConfig.apiKey, forAppId: intercomServiceConfig.appId) } } - - - - - - -//// MARK: - Service factory -// -//protocol ServiceFactoryProtocol { -// func makeService1(/*Some parameters*/) -> Service1 -// func makeService2(/*Some parameters*/) -> Service2 -// // Another services -//} -// -//final class ServiceFactory: ServiceFactoryProtocol { -// func makeService1(/*Some parameters*/) -> Service1 { -// return Service1.shared -// } -// -// func makeService2(/*Some parameters*/) -> Service2 { -// let service = Service2(/*Some parameters*/) -// return service2 -// } -// -// // Another services -//} -// -//// MARK: - Modules stack -// -//protocol ModulesStackProtocol { -// func present(view: UIViewController) -// func back() -// func close() -//} -// -//final class ModulesStack: ModulesStackProtocol { -// private weak var navigationController: UINavigationController? -// private var viewControllers: [UIViewController] -// -// init(navigationController: UINavigationController?) { -// self.navigationController = navigationController -// viewControllers = [] -// } -// -// func present(view: UIViewController) { -// // Some code for presenting -// } -// -// func back() { -// // Some code for popping -// } -// -// func close() { -// // Some code for closing all stack -// } -//} -// -//// MARK: - Coordinators factory -// -//protocol CoordinatorsFactoryProtocol { -// func makeCoordinator1() -> CoordinatorProtocol -// func makeCoordinator2(/*Some parameters*/) -> CoordinatorProtocol -// // Another coordinators -//} -// -//final class CoordinatorsFactory: CoordinatorsFactoryProtocol { -// func makeCoordinator1() -> CoordinatorProtocol { -// return Coordinator1() -// } -// -// func makeCoordinator2(/*Some parameters*/) -> CoordinatorProtocol { -// return Coordinator2(/*Some parameters*/) -// } -// -// // Another coordinators -//} -// -//// MARK: - Coordinators stack -// -//protocol CoordinatorsStackProtocol { -// -//} -// -//final class CoordinatorsStack: CoordinatorsStackProtocol { -// -//} -// -//// MARK: - AppCoordinator -// -//protocol AppCoordinatorProtocol { -// -//} -// -//final class AppCoordinator: AppCoordinatorProtocol { -// -//} -// -//// MARK: - Coordinator -// -//protocol CoordinatorProtocol { -// func start() -// func end() -//} -// -//final class Coordinator: CoordinatorProtocol, WireframeCoordinatorProtocol { -// private weak var stack: ModulesStackProtocol -// private let serviceFactory: ServiceFactoryProtocol -// // Some properties -// -// init(stack: ModulesStackProtocol, serviceFactory: ServiceFactoryProtocol/*, Some parameters*/) { -// self.stack = stack -// } -// -// func start() { -// let wireframe = Wireframe(coordinator: self) -// let parameters = Wireframe.Parameters(someParameter: someParameter) -// let dependencies = Wireframe.Dependencies(someService1: serviceFactory.makeSomeService1) -// -// let view = wireframe.prepareModule(parameters: parameters, dependencies: dependencies) -// stack.present(view: view) -// } -// -// func end() { -// stack.close() -// // Some code -// } -// -// func wireframe(_ wireframe: Wireframe, finishedWithState state: Wireframe.State) { -// switch state { -// case .stateForOpen1: // Some code -// break -// case .stateForOpen2(/*Some parameters*/): stack.back() -// break -// } -// } -//} -// -//// MARK: - Wireframe -// -//protocol WireframeCoordinatorProtocol { -// func wireframe(_ wireframe: Wireframe, finishedWithState state: Wireframe.State) -//} -// -//protocol WireframeProtocol { -// associatedtype Parameters -// associatedtype Dependencies -// associatedtype State -// -// func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController -// func open1(/*Some parameters*/) -// func open2(/*Some parameters*/) -//} -// -//final class Wireframe: WireframeProtocol { -// struct Parameters { -// let someParameter: SomeType -// // Another parameters -// } -// -// struct Dependencies { -// let someService1: SomeService1 -// // Another dependencies -// } -// -// enum State { -// case stateForOpen1 -// case stateForOpen2(/*Some parameters*/) -// } -// -// private let coordinator: WireframeCoordinatorProtocol -// -// init(coordinator: WireframeCoordinatorProtocol) { -// self.coordinator = coordinator -// } -// -// func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { -// let view = View() -// let presenter = Presenter() -// let interactor = Interactor() -// -// let viewDependencies = View.Dependencies(presenter: presenter, someService1: dependencies.makeService1) -// let interactorDependencies = Interactor.Dependencies(presenter: presenter, someService1: dependencies.makeService1) -// let presenterDependencies = Presenter.Dependencies(interactor: interactor, someService1: dependencies.makeService1) -// -// // set some parameters from Parameters structure -// -// view.inject(viewDependencies) -// presenter.inject(presenterDependencies) -// interactor.inject(interactorDependencies) -// -// return view -// } -// -// func open1(/*Some parameters*/) { -// coordinator.wireframe(self, finishedWithState: .stateForOpen1) -// } -// -// func open2(/*Some parameters*/) { -// coordinator.wireframe(self, finishedWithState: .stateForOpen2(/*Some parameters*/)) -// } -//} -// -//// MARK: - Presenter -// -//protocol PresenterProtocol: SetInjectable { -// func someMethod1() -//} -// -//final class Presenter: PresenterProtocol { -// private let interactor: InteractorProtocol -// private weak var view: ViewProtocol? -// private let someService: SomeService1 -// // Another properties -// -// struct Dependencies { -// let interactor: InteractorProtocol -// let view: ViewProtocol -// let someService1: SomeService1 -// // Another dependencies -// } -// -// func inject(dependencies: Presenter.Dependencies) { -// interactor = dependencies.interactor -// view = dependencies.view -// someService = dependencies.someService1 -// // Another dependencies -// } -// -// func someMethod1() { -// // Some code -// } -//} -// -//// MARK: - Interactor -// -//protocol InteractorProtocol: SetInjectable { -// func someMethod1() -//} -// -//final class Interactor: InteractorProtocol { -// private weak var presenter: PresenterProtocol? -// private let someService1: SomeService1 -// // Another properties -// -// struct Dependencies { -// let presenter: PresenterProtocol -// let someService1: SomeService1 -// // Another dependencies -// } -// -// func inject(dependencies: Interactor.Dependencies) { -// presenter = dependencies.presenter -// someService = dependencies.someService1 -// // Another dependencies -// } -// -// func someMethod1() { -// // Some code -// } -//} -// -//// MARK: - View -// -//protocol ViewProtocol: SetInjectable { -// func someMethod1() -//} -// -//final class View: ViewProtocol { -// private weak var presenter: PresenterProtocol? -// private let someService1: SomeService1 -// // Another properties -// -// struct Dependencies { -// let presenter: PresenterProtocol -// let someService1: SomeService1 -// // Another dependencies -// } -// -// func inject(dependencies: View.Dependencies) { -// presenter = dependencies.presenter -// someService = dependencies.someService1 -// // Another dependencies -// } -// -// func someMethod1() { -// // Some code -// } -//} - - -- GitLab From 391848bd529f51d895c520984e29279de2b0059a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 14:52:05 +0200 Subject: [PATCH 076/138] Fixed CounterView. --- .../Cell/ChatListMessageIndicatorsView.swift | 1 + .../Cells/ChatListMessageCell/Cell/CounterView.swift | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift index b697a079d..a4883c26e 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift @@ -23,6 +23,7 @@ final class ChatListMessageIndicatorsView: BaseView { let view = CounterView(font: font, textColor: textColor, horizontalInset: inset) + view.height = fontHeight view.setContentCompressionResistancePriority(.required, for: .horizontal) view.setContentHuggingPriority(.required, for: .horizontal) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift index ca3c23fa9..34ec7cdc4 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift @@ -47,11 +47,19 @@ final class CounterView: UIView { } } + var height: CGFloat = CGFloat(17.0).adjustedByWidth { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + override var intrinsicContentSize: CGSize { let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) var size = countLabel.sizeThatFits(maxSize) size.width += horizontalInset * 2 + size.width = max(size.width, height) return size } -- GitLab From 60ec689fd2136c1d81a0b4c2ee88134382571317 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 15:10:25 +0200 Subject: [PATCH 077/138] Added account settings and login options items to the wheel, --- Nynja/Generated/LocalizableConstants.swift | 10 ++-- Nynja/HomeItemsFactory.swift | 49 +++++++------------ Nynja/Modules/Main/MainProtocols.swift | 14 ++++-- .../Main/Presenter/MainPresenter.swift | 10 ++-- .../Main/View/MainNavigationItem.swift | 5 +- .../MainViewController+NavigateProtocol.swift | 13 ++--- .../Main/View/MainViewController.swift | 9 ---- .../Modules/Main/View/NavigateProtocol.swift | 6 +-- .../Main/WireFrame/MainWireframe.swift | 14 +++--- Nynja/Resources/en.lproj/Localizable.strings | 5 +- 10 files changed, 54 insertions(+), 81 deletions(-) diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 056de2385..2d32f364f 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1318,6 +1318,8 @@ internal extension String { static var wheelInviteFriends: String { return localizable.tr("Localizable", "wheel_invite_friends") } /// About static var wheelItemAbout: String { return localizable.tr("Localizable", "wheel_item_about") } + /// Account Settings + static var wheelItemAccountSettings: String { return localizable.tr("Localizable", "wheel_item_account_settings") } /// Actions static var wheelItemActions: String { return localizable.tr("Localizable", "wheel_item_actions") } /// All @@ -1390,6 +1392,8 @@ internal extension String { static var wheelItemLanguage: String { return localizable.tr("Localizable", "wheel_item_language") } /// Location static var wheelItemLocation: String { return localizable.tr("Localizable", "wheel_item_location") } + /// Login Options + static var wheelItemLoginOptions: String { return localizable.tr("Localizable", "wheel_item_login_options") } /// Log Out static var wheelItemLogOut: String { return localizable.tr("Localizable", "wheel_item_logOut") } /// Marketplace @@ -1404,8 +1408,6 @@ internal extension String { static var wheelItemMyAlias: String { return localizable.tr("Localizable", "wheel_item_myAlias") } /// Myself static var wheelItemMyself: String { return localizable.tr("Localizable", "wheel_item_myself") } - /// My Name - static var wheelItemName: String { return localizable.tr("Localizable", "wheel_item_name") } /// New static var wheelItemNew: String { return localizable.tr("Localizable", "wheel_item_new") } /// New Channel @@ -1420,8 +1422,6 @@ internal extension String { static var wheelItemNotifications: String { return localizable.tr("Localizable", "wheel_item_notifications") } /// Settings static var wheelItemOptions: String { return localizable.tr("Localizable", "wheel_item_options") } - /// My Photo - static var wheelItemPhoto: String { return localizable.tr("Localizable", "wheel_item_photo") } /// Recents static var wheelItemRecents: String { return localizable.tr("Localizable", "wheel_item_recents") } /// Search @@ -1432,8 +1432,6 @@ internal extension String { static var wheelItemStarred: String { return localizable.tr("Localizable", "wheel_item_starred") } /// Transfer static var wheelItemTransfer: String { return localizable.tr("Localizable", "wheel_item_transfer") } - /// My Username - static var wheelItemUsername: String { return localizable.tr("Localizable", "wheel_item_username") } /// Video Call static var wheelItemVideoCall: String { return localizable.tr("Localizable", "wheel_item_videoCall") } /// Video Group diff --git a/Nynja/HomeItemsFactory.swift b/Nynja/HomeItemsFactory.swift index c4c64d1b6..35e7ae54e 100644 --- a/Nynja/HomeItemsFactory.swift +++ b/Nynja/HomeItemsFactory.swift @@ -13,22 +13,22 @@ class HomeItemsFactory: WCBaseItemsFactory { } override var secondLevelItems: ItemModels { - let edit = ImageActionItemModel(nameImage: "ic_edit_profile", navItem: .editProfile, action: { [weak navigateDelegate] (item, indexPath) in + let edit = ImageActionItemModel(nameImage: "ic_edit_profile", navItem: .editProfile) { [weak navigateDelegate] (item, indexPath) in navigateDelegate?.editProfile(indexPath: indexPath) - }) - edit.subitems = [photo, name, username, phoneNumber] + } + edit.subitems = [loginOptions, accountSettings] - let myQR = ImageActionItemModel(nameImage: "ic_by_qr_code", navItem: .myQRCode, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in + let myQR = ImageActionItemModel(nameImage: "ic_by_qr_code", navItem: .myQRCode, isSelectable: false) { [weak navigateDelegate] (item, indexPath) in navigateDelegate?.myQR(indexPath: indexPath) - }) + } - let call = ImageActionItemModel(nameImage: "ic_calls", navItem: .call, action: { [weak navigateDelegate] (item, indexPath) in + let call = ImageActionItemModel(nameImage: "ic_calls", navItem: .call) { [weak navigateDelegate] (item, indexPath) in navigateDelegate?.call(indexPath: indexPath) - }) + } - let help = ImageActionItemModel(navItem: .help, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in + let help = ImageActionItemModel(navItem: .help, isSelectable: false) { [weak navigateDelegate] (item, indexPath) in navigateDelegate?.helpFeedBack(indexPath: indexPath) - }) + } call.subitems = [videoCall, voiceCall] @@ -51,30 +51,18 @@ class HomeItemsFactory: WCBaseItemsFactory { // MARK: - Edit Profile - var photo: ImageFilledItemModel { - return ImageFilledItemModel(navItem: .photo, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in - navigateDelegate?.photo(indexPath: indexPath) - }) - } - - var name: ImageFilledItemModel { - return ImageFilledItemModel(navItem: .name, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in - navigateDelegate?.name(indexPath: indexPath) - }) - } - - var username: ImageFilledItemModel { - return ImageFilledItemModel(navItem: .username, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in - navigateDelegate?.username(indexPath: indexPath) - }) + var accountSettings: ImageFilledItemModel { + return ImageFilledItemModel(navItem: .accountSettings, isSelectable: false) { [weak navigateDelegate] item, indexPath in + navigateDelegate?.accountSettings(indexPath: indexPath) + } } - var phoneNumber: ImageFilledItemModel { - return ImageFilledItemModel(navItem: .phoneNumber, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in - navigateDelegate?.unavailableFunctionality() - }) + var loginOptions: ImageFilledItemModel { + return ImageFilledItemModel(navItem: .loginOptions, isSelectable: false) { [weak navigateDelegate] item, indexPath in + navigateDelegate?.loginOptions(indexPath: indexPath) + } } - + // MARK: - Items @@ -89,5 +77,4 @@ class HomeItemsFactory: WCBaseItemsFactory { item.state = .selected return item } - } diff --git a/Nynja/Modules/Main/MainProtocols.swift b/Nynja/Modules/Main/MainProtocols.swift index 98d9743d6..7ab217766 100644 --- a/Nynja/Modules/Main/MainProtocols.swift +++ b/Nynja/Modules/Main/MainProtocols.swift @@ -84,14 +84,16 @@ protocol MainWireFrameProtocol: class { func showQRGenerator() func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) func showAddContactByUserName() - func showEditName() - func showEditUsername() func showWallet(for profile: Profile) func getRecentsLocation() -> [LocationType] func getStarredLocation() -> [LocationType] func getRecentsMedia() -> [Media] func openMarketplace() + // Edit Profile + func showAccountSettings() + func showLoginOptions() + // Group func showAddParticipants() func showGroupsList() @@ -127,7 +129,6 @@ protocol MainViewProtocol: WheelOutProtocol { func hidePartnerVideoView() func showQRReader() func loadFromCamera() - func showEditPhoto() func showUILocker() func hideUILocker() @@ -184,8 +185,11 @@ protocol MainPresenterProtocol: class { func showSettingsDataAndStorage(with usageMode: DataDownloadAndUsageMode) func showQRGenerator() - func showEditName() - func showEditUsername() + + // Edit Profile + func showAccountSettings() + func showLoginOptions() + func showAddContactByUserName() func conferenceVoiceCall() diff --git a/Nynja/Modules/Main/Presenter/MainPresenter.swift b/Nynja/Modules/Main/Presenter/MainPresenter.swift index 5292588ec..bfd662489 100644 --- a/Nynja/Modules/Main/Presenter/MainPresenter.swift +++ b/Nynja/Modules/Main/Presenter/MainPresenter.swift @@ -259,12 +259,12 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu self.wireFrame.showAddContactByUserName() } - func showEditName() { - self.wireFrame.showEditName() + func showAccountSettings() { + wireFrame.showAccountSettings() } - - func showEditUsername() { - self.wireFrame.showEditUsername() + + func showLoginOptions() { + wireFrame.showLoginOptions() } // MARK: Group diff --git a/Nynja/Modules/Main/View/MainNavigationItem.swift b/Nynja/Modules/Main/View/MainNavigationItem.swift index 7fd4c3064..209cf7424 100644 --- a/Nynja/Modules/Main/View/MainNavigationItem.swift +++ b/Nynja/Modules/Main/View/MainNavigationItem.swift @@ -40,12 +40,11 @@ enum MainNavigationItem: String { case event = "wheel_item_event" case editProfile = "wheel_item_editProfile" case myQRCode = "wheel_item_my_qr_code" - case photo = "wheel_item_photo" - case name = "wheel_item_name" - case username = "wheel_item_username" case phoneNumber = "wheel_item_changeNumber" case payment = "wheel_item_transfer" case help = "wheel_item_help" + case accountSettings = "wheel_item_account_settings" + case loginOptions = "wheel_item_login_options" // Chats section case starred = "wheel_item_starred" diff --git a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift index 23e4a8d39..fe98acf09 100644 --- a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift @@ -184,18 +184,13 @@ extension MainViewController: NavigateProtocol { expand(by: indexPath) } - func photo(indexPath: IndexPath?) { - self.showEditPhoto() + func accountSettings(indexPath: IndexPath?) { + presenter.showAccountSettings() closeWheel(indexPath: indexPath) } - func name(indexPath: IndexPath?) { - presenter.showEditName() - closeWheel(indexPath: indexPath) - } - - func username(indexPath: IndexPath?) { - presenter.showEditUsername() + func loginOptions(indexPath: IndexPath?) { + presenter.showLoginOptions() closeWheel(indexPath: indexPath) } diff --git a/Nynja/Modules/Main/View/MainViewController.swift b/Nynja/Modules/Main/View/MainViewController.swift index 36d8fc95f..cd02bf482 100644 --- a/Nynja/Modules/Main/View/MainViewController.swift +++ b/Nynja/Modules/Main/View/MainViewController.swift @@ -622,15 +622,6 @@ class MainViewController: BaseVC, MainViewProtocol, HitTestDelegate, UINavigatio } } - func showEditPhoto() { - ImageSelector().startSelection(selectHandler: { [weak self] (imageURL) in - guard let imageURL = imageURL else { - return - } - - self?.presenter.interactor.updateAvatar(url: imageURL) - }) - } // MARK: Gesture recognizer delegate func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { diff --git a/Nynja/Modules/Main/View/NavigateProtocol.swift b/Nynja/Modules/Main/View/NavigateProtocol.swift index 8b2368203..1655cf33e 100644 --- a/Nynja/Modules/Main/View/NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/NavigateProtocol.swift @@ -63,9 +63,9 @@ protocol SecondLevelNavigateProtocol: class { func myQR(indexPath: IndexPath?) func editProfile(indexPath: IndexPath?) - func photo(indexPath: IndexPath?) - func name(indexPath: IndexPath?) - func username(indexPath: IndexPath?) + func accountSettings(indexPath: IndexPath?) + func loginOptions(indexPath: IndexPath?) + func phonenumber(indexPath: IndexPath?) func conferenceVoiceCall(indexPath: IndexPath?) diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index cbb1e674e..ba4acbac4 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -451,15 +451,15 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { // MARK: Edit Profile - - func showEditName() { - EditProfileWireFrame().presentEditProfile(navigation: navigation!, main: self) + + func showAccountSettings() { + print(#function) } - - func showEditUsername() { - EditUsernameWireFrame().presentEditUsername(navigation: navigation!, main: self) + + func showLoginOptions() { + print(#function) } - + func updateAvatar(image: UIImage) { let editingDelegate = view?.presenter.interactor as? EditPhotoDelegate EditPhotoWireFrame().presentEditPhoto(navigation: navigation!, image: image, delegate:editingDelegate) diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index f0a3d16be..7f0020501 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -555,9 +555,6 @@ "wheel_item_event"="Event"; "wheel_item_editProfile"="Edit Profile"; "wheel_item_my_qr_code"="My QR Code"; -"wheel_item_photo"="My Photo"; -"wheel_item_name"="My Name"; -"wheel_item_username"="My Username"; "wheel_item_changeNumber"="Change Number"; "wheel_item_starred"="Starred"; "wheel_item_recents"="Recents"; @@ -601,6 +598,8 @@ "wheel_invite_friends"="Invite Friends"; "wheel_item_transfer" = "Transfer"; "wheel_item_help" = "Help & Feedback"; +"wheel_item_account_settings" = "Account Settings"; +"wheel_item_login_options" = "Login Options"; // MARK: Main "main_undefined"="Undefined"; -- GitLab From fa9fda389c324d11f1ba6766761417820675a5c7 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 15:51:18 +0200 Subject: [PATCH 078/138] Show account settings from the wheel. --- .../View/AccountSettingsViewController.swift | 4 ++-- Nynja/Modules/Main/WireFrame/MainWireframe.swift | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift index ae189bbb1..7159867d8 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift @@ -35,7 +35,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView title: "Account settings".localized.uppercased(), navigationHandler: presenter, backButtonImage: UIImage.nynja.icBackNavigation.image)) - header.backgroundColor = UIColor.nynja.clear + header.backgroundColor = UIColor.nynja.darkLight return header }() @@ -53,7 +53,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView table.register(AddContactTVCell.self, forCellReuseIdentifier: AddContactCellModel.identifier) table.register(ContactTVCell.self, forCellReuseIdentifier: ContactTVCellModel.identifier) - table.backgroundColor = UIColor.nynja.clear + table.backgroundColor = UIColor.nynja.darkLight table.separatorStyle = .none diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index ba4acbac4..acd7a8641 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -19,6 +19,8 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { weak var external: EditParticipantsDelegate? = nil + private var accountSettingsCoordinator: AccountSettingsCoordinator? + func presentMain(navigation: UINavigationController, isRegistered: Bool, checkSession: Bool = false) { let view = MainViewController() let presenter = MainPresenter() @@ -453,7 +455,9 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { // MARK: Edit Profile func showAccountSettings() { - print(#function) + guard let navigation = navigation else { return } + accountSettingsCoordinator = AccountSettingsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + accountSettingsCoordinator?.start() } func showLoginOptions() { -- GitLab From 798e9316be3a0b1fcd1dc68366b0cb4eeff9ba34 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 15:56:13 +0200 Subject: [PATCH 079/138] Hide contact info cell. --- .../AccountSettings/Presenter/AccountSettingsPresenter.swift | 2 +- .../AccountSettings/View/AccountSettingsViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift index a3b8d14e8..c8a83d544 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -43,7 +43,7 @@ extension AccountSettingsPresenter { case 0: return 4 case 1: return 2//3 case 2: return 2 - case 3: return 1 + (interactor?.contacts.count ?? 0) + case 3: return 0// 1 + (interactor?.contacts.count ?? 0) default: return 0 } } diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift index 7159867d8..4fa883749 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift @@ -111,7 +111,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView _ = [topHeaderLayoutGuide, headerView, tableView, saveButton] - view.backgroundColor = UIColor.nynja.contextMenuBackGray + view.backgroundColor = UIColor.nynja.darkLight } override func viewWillAppear(_ animated: Bool) { -- GitLab From 8cd1059b5aa0b792dca4307e4a483132d854ae3e Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 18:12:57 +0200 Subject: [PATCH 080/138] Pull review: make requested changes in AuthModule. --- Nynja.xcodeproj/project.pbxproj | 2 +- Nynja/AppDelegate.swift | 17 +++--- Nynja/Library/Result/Result.swift | 23 +++---- Nynja/Modules/Auth/AuthCoordinator.swift | 15 +++-- .../Auth/AuthModule/AuthProtocols.swift | 17 +++--- .../Interactor/AuthInteractor.swift | 33 +++++----- .../AuthModule/Presenter/AuthPresenter.swift | 60 +++++++++++-------- .../AuthModule/Wireframe/AuthWireframe.swift | 12 ++-- 8 files changed, 90 insertions(+), 89 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 8a636fb52..1bd36b7ab 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -7477,11 +7477,11 @@ 5E07BC45216F64DB000E4558 /* CreateProfile */ = { isa = PBXGroup; children = ( + 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */, 5E07BC46216F64DB000E4558 /* Presenter */, 5E07BC47216F64DB000E4558 /* Wireframe */, 5E07BC48216F64DB000E4558 /* View */, 5E07BC4A216F64DB000E4558 /* Interactor */, - 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */, ); path = CreateProfile; sourceTree = ""; diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index ef548c457..de20c5966 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -105,17 +105,16 @@ private extension AppDelegate { self.window = UIWindow(frame: UIScreen.main.bounds) let navigation = UINavigationController() navigation.isNavigationBarHidden = true - - SplashWireFrame().presentSplash(navigation: navigation) -// let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + + let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) self.window?.rootViewController = navigation self.window?.makeKeyAndVisible() -// coordinator.start() + coordinator.start() } - private func configureDependencies() { + func configureDependencies() { setupTestFairy() setupCrashlytics() setupGoogleMaps() @@ -129,7 +128,7 @@ private extension AppDelegate { FileManagerService.sharedInstance.createDirectory(dirName: Constants.Folders.downloads) } - private func wipeStorage() { + func wipeStorage() { MQTTService.sharedInstance.wasRunApp = storageService.wasRun if !storageService.wasRun { @@ -139,7 +138,7 @@ private extension AppDelegate { } } - private func observeGroupAppChanges() { + func observeGroupAppChanges() { self.appGroupObserver = AppGroupFlagObserver(fileManager: .default, appGroup: Bundle.main.appGroupName) do { try appGroupObserver?.prepare() @@ -160,9 +159,9 @@ private extension AppDelegate { // MARK: - Setup third party services private extension AppDelegate { + func setupTestFairy() { let key = ThirdPartyServicesFactory.testFairy.serviceConfig.key - TestFairy.begin(key) } @@ -181,7 +180,7 @@ private extension AppDelegate { ServiceFactory().makeAmazonInitializer().initialize() } - private func setupIntercom() { + func setupIntercom() { let intercomServiceConfig = ThirdPartyServicesFactory.intercom.serviceConfig Intercom.setApiKey(intercomServiceConfig.apiKey, forAppId: intercomServiceConfig.appId) } diff --git a/Nynja/Library/Result/Result.swift b/Nynja/Library/Result/Result.swift index e5d11afa5..f824f8d82 100644 --- a/Nynja/Library/Result/Result.swift +++ b/Nynja/Library/Result/Result.swift @@ -8,10 +8,6 @@ import Foundation - -import Foundation - - public enum Result { case success(Value) case failure(Error) @@ -50,7 +46,8 @@ public enum Result { // MARK: - CustomStringConvertible -extension Result: CustomStringConvertible { +extension Result: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { switch self { case .success: @@ -59,11 +56,7 @@ extension Result: CustomStringConvertible { return "FAILURE" } } -} - -// MARK: - CustomDebugStringConvertible - -extension Result: CustomDebugStringConvertible { + public var debugDescription: String { switch self { case .success(let value): @@ -140,15 +133,17 @@ extension Result { @discardableResult public func onSuccess(_ closure: (Value) -> Void) -> Result { - if case let .success(value) = self { closure(value) } - + if case let .success(value) = self { + closure(value) + } return self } @discardableResult public func onFailure(_ closure: (Error) -> Void) -> Result { - if case let .failure(error) = self { closure(error) } - + if case let .failure(error) = self { + closure(error) + } return self } } diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index eef172758..85bd52496 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -10,6 +10,7 @@ import Foundation import SDWebImage final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol, CreateProfileCoordinatorProtocol { + private weak var navigation: UINavigationController? private let serviceFactory: ServiceFactoryProtocol @@ -21,12 +22,14 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt } func start() { - let wireframe = AuthWireframe(coordinator: self) - let view = wireframe.prepareModule( - parameters: NSNull(), - dependencies: AuthWireframe.Dependencies(countriesProvider: serviceFactory.makeCountriesProvider()) - ) - navigation?.pushViewController(view, animated: true) + SplashWireFrame().presentSplash(navigation: navigation!) + +// let wireframe = AuthWireframe(coordinator: self) +// let view = wireframe.prepareModule( +// parameters: NSNull(), +// dependencies: AuthWireframe.Dependencies(countriesProvider: serviceFactory.makeCountriesProvider()) +// ) +// navigation?.pushViewController(view, animated: true) } func end() { diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index 13c315292..c02148984 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -24,10 +24,10 @@ protocol AuthPresenterProtocol: class { func switchLoginOption() - func loginViaFacebook(completion: (Result) -> Void) - func loginViaGoogle(completion: (Result) -> Void) - func loginViaEmail(_ email: String, completion: (Result) -> Void) - func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) + func loginViaFacebook(completion: @escaping (Result) -> Void) + func loginViaGoogle(completion: @escaping (Result) -> Void) + func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) + func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) func selectCountry() } @@ -35,14 +35,13 @@ protocol AuthPresenterProtocol: class { protocol AuthInputInteractorProtocol: class { typealias Code = String - func loginViaFacebook(completion: (Result) -> Void) - func loginViaGoogle(completion: (Result) -> Void) - func loginViaEmail(_ email: String, completion: (Result) -> Void) - func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) + func loginViaFacebook(completion: @escaping (Result) -> Void) + func loginViaGoogle(completion: @escaping (Result) -> Void) + func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) + func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) func fetchDefaultCountry() -> Country } protocol AuthOutputInteractorProtocol: class { - } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index c25f1687d..9f31cb000 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -9,46 +9,47 @@ import Foundation -final class AuthInteractor: AuthInputInteractorProtocol, SetInjectable { +final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { + private weak var presenter: AuthOutputInteractorProtocol? - private var countriesProvider: CountriesProviding! -} - -// MARK: - SetInjectable - -extension AuthInteractor { + + // MARK: - Services + + private let countriesProvider: CountriesProviding + struct Dependencies { let presenter: AuthOutputInteractorProtocol let countriesProvider: CountriesProviding } - func inject(dependencies: AuthInteractor.Dependencies) { + + // MARK: - Init + + init(dependencies: AuthInteractor.Dependencies) { presenter = dependencies.presenter countriesProvider = dependencies.countriesProvider } -} - -// MARK: - AuthInputInteractorProtocol + -extension AuthInteractor { + // MARK: - AuthInputInteractorProtocol func fetchDefaultCountry() -> Country { return countriesProvider.fetchDefaultCountry() } - func loginViaFacebook(completion: (Result) -> Void) { + func loginViaFacebook(completion: @escaping (Result) -> Void) { completion(.success("Some code")) } - func loginViaGoogle(completion: (Result) -> Void) { + func loginViaGoogle(completion: @escaping (Result) -> Void) { completion(.success("Some code")) } - func loginViaEmail(_ email: String, completion: (Result) -> Void) { + func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) { completion(.success(())) } - func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) { + func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) { completion(.success(())) } } diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 6aee9706f..183af7707 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -26,57 +26,65 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, loginOption = .phoneNumber(number: "") case .phoneNumber: loginOption = .email(email: "") - default: + case .facebook, .google: break } } - func loginViaFacebook(completion: (Result) -> Void) { - interactor?.loginViaFacebook { - $0.onSuccess { + func loginViaFacebook(completion: @escaping (Result) -> Void) { + interactor.loginViaFacebook { [weak self] result in + switch result { + case let .success(code): completion(.success(())) - wireframe?.continueLogin(loginOption: .facebook(code: $0)) - }.onFailure { - completion(.failure($0)) + self?.wireframe?.continueLogin(loginOption: .facebook(code: code)) + + case let .failure(error): + completion(.failure(error)) } } } - func loginViaGoogle(completion: (Result) -> Void) { - interactor?.loginViaGoogle { - $0.onSuccess { + func loginViaGoogle(completion: @escaping (Result) -> Void) { + interactor.loginViaGoogle { [weak self] result in + switch result { + case let .success(code): completion(.success(())) - wireframe?.continueLogin(loginOption: .google(code: $0)) - }.onFailure { - completion(.failure($0)) + self?.wireframe?.continueLogin(loginOption: .google(code: code)) + + case let .failure(error): + completion(.failure(error)) } } } - func loginViaEmail(_ email: String, completion: (Result) -> Void) { - interactor?.loginViaEmail(email) { - $0.onSuccess { + func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) { + interactor.loginViaEmail(email) { [weak self] result in + switch result { + case .success: completion(.success(())) - wireframe?.continueLogin(loginOption: .email(email: email)) - }.onFailure { - completion(.failure($0)) + self?.wireframe?.continueLogin(loginOption: .email(email: email)) + + case let .failure(error): + completion(.failure(error)) } } } - func loginViaPhoneNumber(_ phoneNumber: String, completion: (Result) -> Void) { - interactor?.loginViaPhoneNumber(phoneNumber) { - $0.onSuccess { + func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) { + interactor.loginViaPhoneNumber(phoneNumber) { [weak self] result in + switch result { + case .success: completion(.success(())) - wireframe?.continueLogin(loginOption: .phoneNumber(number: phoneNumber)) - }.onFailure { - completion(.failure($0)) + self?.wireframe?.continueLogin(loginOption: .phoneNumber(number: phoneNumber)) + + case let .failure(error): + completion(.failure(error)) } } } func selectCountry() { - wireframe?.selectCountry { result in + wireframe.selectCountry { result in switch result { case let .success(country): self.selectedCountry = country diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 90cb9d0c1..a4fac8ae3 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -8,7 +8,7 @@ import Foundation -protocol AuthCoordinatorProtocol { +protocol AuthCoordinatorProtocol: class { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) } @@ -33,15 +33,11 @@ final class AuthWireframe: WireframeProtocol, AuthWireframeProtocol { func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = AuthPresenter() - let view = AuthViewController(dependencies: AuthViewController.Dependencies(presenter: presenter, - viewsFactory: AuthViewsFactory())) - let interactor = AuthInteractor() - let presenterDep = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) - let interactorDep = AuthInteractor.Dependencies(presenter: presenter, countriesProvider: dependencies.countriesProvider) + let view = AuthViewController(dependencies: .init(presenter: presenter, viewsFactory: AuthViewsFactory())) + let interactor = AuthInteractor(dependencies: .init(presenter: presenter, countriesProvider: dependencies.countriesProvider)) - presenter.inject(dependencies: presenterDep) - interactor.inject(dependencies: interactorDep) + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) return view } -- GitLab From 63c1e7ff16c6f0bc935549c17377cb4a9496885a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 18:41:22 +0200 Subject: [PATCH 081/138] Pull review: make requested changes in CreateProfile module. --- .../Auth/CreateProfile/Entities/ProfileField.swift | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift diff --git a/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift b/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift new file mode 100644 index 000000000..ff0117475 --- /dev/null +++ b/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift @@ -0,0 +1,9 @@ +// +// ProfileField.swift +// Nynja +// +// Created by Anton Poltoratskyi on 05.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation -- GitLab From 9820424048f1149139784cfc7dfcba8ad288b50d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 18:58:39 +0200 Subject: [PATCH 082/138] Pull review: make requested changes in CodeConfirmation module. --- Nynja.xcodeproj/project.pbxproj | 28 ++++++++--- .../Interfaces/NavigationProtocol.swift | 3 -- .../Interactor/AuthInteractor.swift | 6 +-- .../AuthModule/Presenter/AuthPresenter.swift | 1 - .../AuthModule/Wireframe/AuthWireframe.swift | 10 ++-- .../CodeConfirmationProtocols.swift | 7 ++- .../Entities/AuthProviderType.swift | 14 ++++++ .../CodeConfirmation/Entities/AuthType.swift | 1 - .../CodeConfirmationInteractor.swift | 11 +++-- .../Presenter/CodeConfirmationPresenter.swift | 44 ++++++++--------- .../Wireframe/CodeConfirmationWireframe.swift | 30 ++++++------ .../CreateProfileProtocols.swift | 32 ++---------- .../CreateProfile/Entities/ProfileField.swift | 26 +++++++++- .../Interactor/CreateProfileInteractor.swift | 49 ++++++++++--------- .../Presenter/CreateProfilePresenter.swift | 38 +++++++------- .../View/CreateProfileViewController.swift | 1 - .../Wireframe/CreateProfileWireframe.swift | 19 +++---- 17 files changed, 175 insertions(+), 145 deletions(-) create mode 100644 Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 1bd36b7ab..ea25f6444 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1089,6 +1089,8 @@ 8572C3B92092364C00E4840C /* StickerPackageDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */; }; 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */; }; 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BD2092368600E4840C /* StickerDataSource.swift */; }; + 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */; }; + 85739FBD2190AAC3001C4EC8 /* AuthProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */; }; 85788C3C204422FB003600C9 /* BuildNumberProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */; }; 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C412044237B003600C9 /* BuildNumberViewController.swift */; }; 85788C4420442385003600C9 /* BuildNumberPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C4320442385003600C9 /* BuildNumberPresenter.swift */; }; @@ -3377,6 +3379,8 @@ 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackageDataSource.swift; sourceTree = ""; }; 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionDataSource.swift; sourceTree = ""; }; 8572C3BD2092368600E4840C /* StickerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDataSource.swift; sourceTree = ""; }; + 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileField.swift; sourceTree = ""; }; + 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderType.swift; sourceTree = ""; }; 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberProtocols.swift; sourceTree = ""; }; 85788C412044237B003600C9 /* BuildNumberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberViewController.swift; sourceTree = ""; }; 85788C4320442385003600C9 /* BuildNumberPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberPresenter.swift; sourceTree = ""; }; @@ -6955,8 +6959,8 @@ 4B749F0E214FEFC8002F3A33 /* Auth */ = { isa = PBXGroup; children = ( - 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, + 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, 5E7E9FB3215BA0AD004D306B /* CountrySelector */, 3AB452082A8DAEAD93F689D8 /* Login */, @@ -7478,10 +7482,11 @@ isa = PBXGroup; children = ( 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */, - 5E07BC46216F64DB000E4558 /* Presenter */, - 5E07BC47216F64DB000E4558 /* Wireframe */, 5E07BC48216F64DB000E4558 /* View */, + 5E07BC46216F64DB000E4558 /* Presenter */, 5E07BC4A216F64DB000E4558 /* Interactor */, + 5E07BC47216F64DB000E4558 /* Wireframe */, + 85739FB92190A3A5001C4EC8 /* Entities */, ); path = CreateProfile; sourceTree = ""; @@ -7725,12 +7730,12 @@ 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */ = { isa = PBXGroup; children = ( - 5EEB73AC216046EA00D8ECE6 /* Presenter */, - 5EEB73AD216046EA00D8ECE6 /* Wireframe */, + 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */, 5EEB73AE216046EA00D8ECE6 /* View */, + 5EEB73AC216046EA00D8ECE6 /* Presenter */, 5EEB73AF216046EA00D8ECE6 /* Interactor */, + 5EEB73AD216046EA00D8ECE6 /* Wireframe */, 5EEB73B0216046EA00D8ECE6 /* Entities */, - 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */, ); path = CodeConfirmation; sourceTree = ""; @@ -7772,6 +7777,7 @@ isa = PBXGroup; children = ( 5E07BC43216F56AF000E4558 /* AuthType.swift */, + 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */, ); path = Entities; sourceTree = ""; @@ -9249,6 +9255,14 @@ path = Cells; sourceTree = ""; }; + 85739FB92190A3A5001C4EC8 /* Entities */ = { + isa = PBXGroup; + children = ( + 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */, + ); + path = Entities; + sourceTree = ""; + }; 85788C3A20442263003600C9 /* BuildNumber */ = { isa = PBXGroup; children = ( @@ -15499,6 +15513,7 @@ 3A27B0A71EF307A900B4B3CB /* DeleteUserModel.swift in Sources */, 3A1F74FA1F5ED344009A11E4 /* PushService.swift in Sources */, FEA656042167777F00B44029 /* WalletBalancesInteractor.swift in Sources */, + 85739FBD2190AAC3001C4EC8 /* AuthProviderType.swift in Sources */, 261F2E2E200EB0AD007D0813 /* RepliesVC+CellDelegate.swift in Sources */, A45F110620B4218D00F45004 /* MessageConfiguration.swift in Sources */, 26F5C8BE206BD49B003A7FF5 /* DefaultActionItemModel.swift in Sources */, @@ -16531,6 +16546,7 @@ B7EF8EDD210CB0A200E0E981 /* InterpretationModel.swift in Sources */, E79117921F97A48900462D68 /* ProfileDetailsViewLayout.swift in Sources */, 8595E0DC204863DB00178171 /* CarouselPickerCellModel.swift in Sources */, + 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */, 2648C41C2069B52100863614 /* ChangeNumberStep2Interactor.swift in Sources */, 0DE4B40440737CF42D3E0204 /* HistoryWireframe.swift in Sources */, F117871820ACF018007A9A1B /* LabeledTableCell.swift in Sources */, diff --git a/Nynja/Library/Interfaces/NavigationProtocol.swift b/Nynja/Library/Interfaces/NavigationProtocol.swift index 81a48066a..f673afd9f 100644 --- a/Nynja/Library/Interfaces/NavigationProtocol.swift +++ b/Nynja/Library/Interfaces/NavigationProtocol.swift @@ -6,9 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - - protocol NavigationProtocol: class { func back() } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 9f31cb000..0393de6ef 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -17,14 +17,14 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { private let countriesProvider: CountriesProviding + + // MARK: - Init + struct Dependencies { let presenter: AuthOutputInteractorProtocol let countriesProvider: CountriesProviding } - - // MARK: - Init - init(dependencies: AuthInteractor.Dependencies) { presenter = dependencies.presenter countriesProvider = dependencies.countriesProvider diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 183af7707..69a9b65ff 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -8,7 +8,6 @@ import Foundation - final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, SetInjectable { private weak var view: AuthViewProtocol? private var interactor: AuthInputInteractorProtocol! diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index a4fac8ae3..8d19dd7b4 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -34,10 +34,14 @@ final class AuthWireframe: WireframeProtocol, AuthWireframeProtocol { func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = AuthPresenter() - let view = AuthViewController(dependencies: .init(presenter: presenter, viewsFactory: AuthViewsFactory())) - let interactor = AuthInteractor(dependencies: .init(presenter: presenter, countriesProvider: dependencies.countriesProvider)) + let viewDependencies = AuthViewController.Dependencies(presenter: presenter, viewsFactory: AuthViewsFactory()) + let view = AuthViewController(dependencies: viewDependencies) - presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + let interactorDependencies = AuthInteractor.Dependencies(presenter: presenter, countriesProvider: dependencies.countriesProvider) + let interactor = AuthInteractor(dependencies: interactorDependencies) + + let presenterDependencies = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) + presenter.inject(dependencies: presenterDependencies) return view } diff --git a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift index 96f9d2cd0..02eacc517 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift @@ -8,7 +8,7 @@ import Foundation -protocol CodeConfirmationWireframeProtocol: WireframeProtocol { +protocol CodeConfirmationWireframeProtocol: class { func codeValid(with type: AuthenticationType) func codeInvalid() func back() @@ -32,7 +32,7 @@ protocol CodeConfirmationPresenterProtocol: NavigationProtocol { func askForCall() } -protocol CodeConfirmationInputInteractorProtocol { +protocol CodeConfirmationInputInteractorProtocol: class { var address: String { get } var authProviderType: AuthProviderType { get } @@ -42,6 +42,5 @@ protocol CodeConfirmationInputInteractorProtocol { func askForCall() } -protocol CodeConfirmationOutputInteractorProtocol { - +protocol CodeConfirmationOutputInteractorProtocol: class { } diff --git a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift new file mode 100644 index 000000000..8128b1769 --- /dev/null +++ b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift @@ -0,0 +1,14 @@ +// +// AuthProviderType.swift +// Nynja +// +// Created by Anton Poltoratskyi on 05.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum AuthProviderType { + case email + case phoneNumber +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift index e0513a49b..0382d5f1d 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift @@ -8,7 +8,6 @@ import Foundation - enum AuthenticationType { case register case login diff --git a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index eaa401956..27702ce57 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -8,18 +8,21 @@ import Foundation - final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, SetInjectable { - private var presenter: CodeConfirmationOutputInteractorProtocol? - private(set) var address: String - private(set) var authProviderType: AuthProviderType + private weak var presenter: CodeConfirmationOutputInteractorProtocol? + + let address: String + let authProviderType: AuthProviderType init(address: String, authProviderType: AuthProviderType) { self.address = address self.authProviderType = authProviderType } + + // MARK: - CodeConfirmationInputInteractorProtocol + func sendConfirmationCode(code: String, completion: (Result) -> Void) { completion(.success(.register)) } diff --git a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index 8d8b43f8d..9457e5453 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -8,11 +8,12 @@ import Foundation - final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeConfirmationOutputInteractorProtocol, SetInjectable { - private var view: CodeConfirmationViewProtocol? - private var interactor: CodeConfirmationInputInteractorProtocol? - private var wireframe: CodeConfirmationWireframe? + + private weak var view: CodeConfirmationViewProtocol? + private var interactor: CodeConfirmationInputInteractorProtocol! + private var wireframe: CodeConfirmationWireframe! + private var timerValue = 0 { didSet { if timerValue > 60 { @@ -28,25 +29,21 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } } } + private var timer: Timer? var isCanAskForCall: Bool { guard let interactor = interactor else { return false } - return interactor.authProviderType == .phoneNumber } var address: String { - return interactor?.address ?? "" + return interactor.address } var descriptionText: String { - guard let interactor = interactor else { - return "" - } - if interactor.authProviderType == .phoneNumber { return "We've sent code to your phone".localized } else { @@ -56,24 +53,28 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } func viewDidLoad() { - switch interactor!.authProviderType { - case .email: timerValue = 15 * 60 - case .phoneNumber: timerValue = 60 + switch interactor.authProviderType { + case .email: + timerValue = 15 * 60 + case .phoneNumber: + timerValue = 60 } - - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let `self` = self else { return } self.timerValue = self.timerValue - 1 } } func sendConfirmationCode(code: String, completion: (Result) -> Void) { - interactor?.sendConfirmationCode(code: code) { - $0.onSuccess { + interactor.sendConfirmationCode(code: code) { [weak self] result in + switch result { + case let .success(authType): completion(.success(())) - wireframe?.codeValid(with: $0) - }.onFailure { - completion(.failure($0)) - wireframe?.codeInvalid() + wireframe?.codeValid(with: authType) + + case let .failure(error): + completion(.failure(error)) + wireframe?.codeInvalid() } } } @@ -86,7 +87,6 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo guard isCanAskForCall else { return } - interactor?.askForCall() } diff --git a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index b4adfec5a..36729d7cc 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -8,16 +8,12 @@ import Foundation -enum AuthProviderType { - case email - case phoneNumber -} - -protocol CodeConfirmationCoordinatorProtocol { +protocol CodeConfirmationCoordinatorProtocol: class { func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) } -final class CodeConfirmationWireframe: CodeConfirmationWireframeProtocol { +final class CodeConfirmationWireframe: WireframeProtocol, CodeConfirmationWireframeProtocol { + private let coordinator: CodeConfirmationCoordinatorProtocol init(coordinator: CodeConfirmationCoordinatorProtocol) { @@ -29,9 +25,7 @@ final class CodeConfirmationWireframe: CodeConfirmationWireframeProtocol { let authType: AuthProviderType } - struct Dependencies { - - } + struct Dependencies { } enum State { case validCode(type: AuthenticationType) @@ -41,15 +35,19 @@ final class CodeConfirmationWireframe: CodeConfirmationWireframeProtocol { func prepareModule(parameters: CodeConfirmationWireframe.Parameters, dependencies: CodeConfirmationWireframe.Dependencies) -> UIViewController { let presenter = CodeConfirmationPresenter() - let viewDep = CodeConfirmationViewController.Dependencies(presenter: presenter, viewsFactory: CodeConfirmationViewsFactory()) - let view = CodeConfirmationViewController(dependencies: viewDep) + + let view = CodeConfirmationViewController(dependencies: .init( + presenter: presenter, + viewsFactory: CodeConfirmationViewsFactory()) + ) + let interactor = CodeConfirmationInteractor(address: parameters.address, authProviderType: parameters.authType) - let presenterDep = CodeConfirmationPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) - let interactorDep = CodeConfirmationInteractor.Dependencies(presenter: presenter) + let interactorDependencies = CodeConfirmationInteractor.Dependencies(presenter: presenter) + interactor.inject(dependencies: interactorDependencies) - presenter.inject(dependencies: presenterDep) - interactor.inject(dependencies: interactorDep) + let presenterDependencies = CodeConfirmationPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) + presenter.inject(dependencies: presenterDependencies) return view } diff --git a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift index 69091ae48..1bde28915 100644 --- a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift +++ b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift @@ -8,33 +8,7 @@ import Foundation -enum ProfileField { - case firstName - case lastName - case accountName - case userName - case profileMessage - - var isRequired: Bool { - return self == .firstName - } - - var validationRule: String { - return "^([a-zA-Z]|[0-9]|_){2,}$" - } - - var placeholder: String { - switch self { - case .firstName: return "First Name" - case .lastName: return "Last Name" - case .accountName: return "Account Name" - case .userName: return "Username" - case .profileMessage: return "Profile Message" - } - } -} - -protocol CreateProfileWireframeProtocol: WireframeProtocol { +protocol CreateProfileWireframeProtocol: class { func back() func end() func chooseAvatar(completion: @escaping (UIImage?) -> Void) @@ -53,14 +27,14 @@ protocol CreateProfilePresenterProtocol: NavigationProtocol { func checkTermsOfUse() -> Bool } -protocol CreateProfileInputInteractorProtocol { +protocol CreateProfileInputInteractorProtocol: class { func setProfileField(_ field: ProfileField, value: String) func checkTermsOfUse() -> Bool func setAvatar(image: UIImage?) func isValidValue(_ value: String, for field: ProfileField) -> Result } -protocol CreateProfileOutputInteractorProtocol { +protocol CreateProfileOutputInteractorProtocol: class { func minimalRequirementsAreSatisfied(_ satisfied: Bool) func profileFieldUpdated(_ profileField: ProfileField, value: String) } diff --git a/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift b/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift index ff0117475..01a5e19b5 100644 --- a/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift +++ b/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift @@ -6,4 +6,28 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +enum ProfileField { + case firstName + case lastName + case accountName + case userName + case profileMessage + + var isRequired: Bool { + return self == .firstName + } + + var validationRule: String { + return "^([a-zA-Z]|[0-9]|_){2,}$" + } + + var placeholder: String { + switch self { + case .firstName: return "First Name" + case .lastName: return "Last Name" + case .accountName: return "Account Name" + case .userName: return "Username" + case .profileMessage: return "Profile Message" + } + } +} diff --git a/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift index 9339b99e3..92a713154 100644 --- a/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift +++ b/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -8,14 +8,11 @@ import Foundation -final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, SetInjectable { - private var presenter: CreateProfileOutputInteractorProtocol? +final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, InitializeInjectable { - private var checkTermsOfUsage: Bool = false { - didSet { - presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) - } - } + private weak var presenter: CreateProfileOutputInteractorProtocol? + + // MARK: - Fields private var avatar: UIImage? = nil @@ -48,11 +45,27 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, SetIn presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) } } -} - -// MARK: - CreateProfileInputInteractorProtocol - -extension CreateProfileInteractor { + + private var checkTermsOfUsage: Bool = false { + didSet { + presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) + } + } + + + // MARK: - Init + + struct Dependencies { + let presenter: CreateProfileOutputInteractorProtocol + } + + init(dependencies: CreateProfileInteractor.Dependencies) { + presenter = dependencies.presenter + } + + + // MARK: - CreateProfileInputInteractorProtocol + func checkTermsOfUse() -> Bool { checkTermsOfUsage = !checkTermsOfUsage @@ -86,18 +99,6 @@ extension CreateProfileInteractor { } } -// MARK: - SetInjectable - -extension CreateProfileInteractor { - struct Dependencies { - let presenter: CreateProfileOutputInteractorProtocol - } - - func inject(dependencies: CreateProfileInteractor.Dependencies) { - presenter = dependencies.presenter - } -} - // MARK: - Private private extension CreateProfileInteractor { diff --git a/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift index 492cb6ac0..05b6812a8 100644 --- a/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift +++ b/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -8,49 +8,51 @@ import Foundation - final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, CreateProfilePresenterProtocol, SetInjectable { - private var wireframe: CreateProfileWireframe? - private var interactor: CreateProfileInputInteractorProtocol? + private weak var view: CreateProfileViewProtocol? -} + private var interactor: CreateProfileInputInteractorProtocol! + private var wireframe: CreateProfileWireframeProtocol! -// MARK: - CreateProfileOutputInteractorProtocol & CreateProfilePresenterProtocol - -extension CreateProfilePresenter { - func isValidValue(_ value: String, for field: ProfileField) -> Result { - return interactor?.isValidValue(value, for: field) ?? .success(()) - } - - func profileFieldUpdated(_ profileField: ProfileField, value: String) { - view?.updateProfileField(profileField, value: value) - } + // MARK: - CreateProfilePresenterProtocol + func createAccount() { wireframe?.end() } + func isValidValue(_ value: String, for field: ProfileField) -> Result { + return interactor.isValidValue(value, for: field) + } + func setProfileField(value: String, field: ProfileField) { - interactor?.setProfileField(field, value: value) + interactor.setProfileField(field, value: value) } func chooseAvatar(completion: @escaping (UIImage?) -> Void) { - wireframe?.chooseAvatar(completion: { (image) in + wireframe.chooseAvatar(completion: { (image) in completion(image) self.interactor?.setAvatar(image: image) }) } + func profileFieldUpdated(_ profileField: ProfileField, value: String) { + view?.updateProfileField(profileField, value: value) + } + func checkTermsOfUse() -> Bool { - return interactor?.checkTermsOfUse() ?? false + return interactor.checkTermsOfUse() } + + // MARK: - CreateProfileOutputInteractorProtocol + func minimalRequirementsAreSatisfied(_ satisfied: Bool) { view?.setCreateEnabled(satisfied) } func back() { - wireframe?.back() + wireframe.back() } } diff --git a/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift index 96c9fcc7a..1b02b7f05 100644 --- a/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift +++ b/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift @@ -8,7 +8,6 @@ import Foundation - final class CreateProfileViewController: UIViewController, CreateProfileViewProtocol, InitializeInjectable, KeyboardInteractive { private let presenter: CreateProfilePresenterProtocol private let viewsFactory: CreateProfileViewsFactoryProtocol diff --git a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift index f7e25b651..d53701ce3 100644 --- a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -8,11 +8,11 @@ import Foundation -protocol CreateProfileCoordinatorProtocol { +protocol CreateProfileCoordinatorProtocol: class { func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) } -final class CreateProfileWireframe: CreateProfileWireframeProtocol { +final class CreateProfileWireframe: WireframeProtocol, CreateProfileWireframeProtocol { struct Parameters {} struct Dependencies {} @@ -29,16 +29,17 @@ final class CreateProfileWireframe: CreateProfileWireframeProtocol { self.coordinator = coordinator } - func prepareModule(parameters: CreateProfileWireframe.Parameters, dependencies: CreateProfileWireframe.Dependencies) -> UIViewController { + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = CreateProfilePresenter() - let view = CreateProfileViewController(dependencies: CreateProfileViewController.Dependencies(presenter: presenter, viewsFactory: CreateProfileViewsFactory())) - let interactor = CreateProfileInteractor() - let presenterDep = CreateProfilePresenter.Dependencies(wireframe: self, interactor: interactor, view: view) - let interactorDep = CreateProfileInteractor.Dependencies(presenter: presenter) + let viewDependencies = CreateProfileViewController.Dependencies(presenter: presenter, viewsFactory: CreateProfileViewsFactory()) + let view = CreateProfileViewController(dependencies: viewDependencies) - presenter.inject(dependencies: presenterDep) - interactor.inject(dependencies: interactorDep) + let interactorDependencies = CreateProfileInteractor.Dependencies(presenter: presenter) + let interactor = CreateProfileInteractor(dependencies: interactorDependencies) + + let presenterDependencies = CreateProfilePresenter.Dependencies(wireframe: self, interactor: interactor, view: view) + presenter.inject(dependencies: presenterDependencies) return view } -- GitLab From 8079327af7078c3e283f594ea4b5f5f1adb02382 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 5 Nov 2018 19:31:59 +0200 Subject: [PATCH 083/138] Pull review: remove StorageServiceProtocol and refactor WireframeProtocol to Wireframe and don't use in as generic constraint in module protocols. --- .../AccountSettingsProtocols.swift | 2 +- .../Wireframe/AccountSettingsWireframe.swift | 5 +++-- .../Auth/AuthModule/Wireframe/AuthWireframe.swift | 2 +- .../Wireframe/CodeConfirmationWireframe.swift | 4 ++-- .../CountrySelectorProtocols.swift | 7 +++---- .../Interactor/CountrySelectorInteractor.swift | 6 +++--- .../Presenter/CountrySelectorPresenter.swift | 2 +- .../Wireframe/CountrySelectorWireframe.swift | 10 +++++----- .../Wireframe/CreateProfileWireframe.swift | 2 +- .../Flows/CameraFlow/Camera/CameraProtocols.swift | 7 ++++--- .../Camera/Presenter/CameraPresenter.swift | 5 ++--- .../Camera/Wireframe/CameraWireframe.swift | 7 ++++--- .../PhotoPreview/PhotoPreviewProtocols.swift | 2 +- .../Presenter/PhotoPreviewPresenter.swift | 4 ++-- .../QRPreview/CameraQRPreviewProtocols.swift | 9 +++------ .../Presenter/CameraQRPreviewPresenter.swift | 4 ++-- .../Wireframe/CameraQRPreviewWireframe.swift | 7 ++++--- .../CameraVideoPreviewProtocols.swift | 4 ++-- .../Presenter/CameraVideoPreviewPresenter.swift | 4 ++-- .../Wireframe/CameraVideoPreviewWireframe.swift | 6 ++++-- .../CameraQualitySettingsProtocols.swift | 2 +- .../CameraQualitySettingsPresenter.swift | 2 +- .../CameraQualitySettingsWireframe.swift | 7 ++++--- .../CameraSettings/CameraSettingsProtocols.swift | 2 +- .../Presenter/CameraSettingsPresenter.swift | 2 +- .../GalleryFlow/Gallery/GalleryProtocols.swift | 2 +- .../Gallery/Presenter/GalleryPresenter.swift | 2 +- .../Gallery/Wireframe/GalleryWireframe.swift | 7 ++++--- .../MultiplePreviewProtocols.swift | 4 ++-- .../Presenter/MultiplePreviewPresenter.swift | 4 ++-- .../Wireframe/MultiplePreviewWireframe.swift | 15 ++++++--------- Nynja/Services/StorageService.swift | 14 +------------- .../BaseModule/Wireframe/WireframeProtocol.swift | 2 +- 33 files changed, 76 insertions(+), 88 deletions(-) diff --git a/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift index 0ae57fca3..0e5d9b8c1 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift @@ -8,7 +8,7 @@ import Foundation -protocol AccountSettingsWireframeProtocol: WireframeProtocol { +protocol AccountSettingsWireframeProtocol: class { func back() func chooseAvatar(completion: @escaping (UIImage?) -> Void) diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift index 52e068259..d81c26d09 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -8,11 +8,12 @@ import Foundation -protocol AccountSettingsCoordinatorProtocol { +protocol AccountSettingsCoordinatorProtocol: class { func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) } -final class AccountSettingsWireframe: AccountSettingsWireframeProtocol { +final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtocol { + struct Parameters {} struct Dependencies {} diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 8d19dd7b4..1e41c67dc 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -12,7 +12,7 @@ protocol AuthCoordinatorProtocol: class { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) } -final class AuthWireframe: WireframeProtocol, AuthWireframeProtocol { +final class AuthWireframe: Wireframe, AuthWireframeProtocol { private let coordinator: AuthCoordinatorProtocol diff --git a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index 36729d7cc..ff2c5415b 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -12,7 +12,7 @@ protocol CodeConfirmationCoordinatorProtocol: class { func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) } -final class CodeConfirmationWireframe: WireframeProtocol, CodeConfirmationWireframeProtocol { +final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProtocol { private let coordinator: CodeConfirmationCoordinatorProtocol @@ -33,7 +33,7 @@ final class CodeConfirmationWireframe: WireframeProtocol, CodeConfirmationWirefr case back } - func prepareModule(parameters: CodeConfirmationWireframe.Parameters, dependencies: CodeConfirmationWireframe.Dependencies) -> UIViewController { + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = CodeConfirmationPresenter() let view = CodeConfirmationViewController(dependencies: .init( diff --git a/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift b/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift index 9f85c4ed8..9df1d1f98 100644 --- a/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift +++ b/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift @@ -8,8 +8,7 @@ import Foundation - -protocol CountrySelectorWireframeProtocol: WireframeProtocol { +protocol CountrySelectorWireframeProtocol: NavigationProtocol { func selectCountry(_ country: Country) } @@ -26,11 +25,11 @@ protocol CountrySelectorPresenterProtocol: NavigationProtocol { func selectCountry(_ country: Country) } -protocol CountrySelectorInteractorOutputProtocol { +protocol CountrySelectorInteractorOutputProtocol: class { func filteredCountries(_ countries: [Country]) } -protocol CountrySelectorInteractorInputProtocol { +protocol CountrySelectorInteractorInputProtocol: class { var filter: String { get set } var filteredCountries: [Country] { get } } diff --git a/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift b/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift index 51a03faa2..4e24eb22a 100644 --- a/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift +++ b/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift @@ -9,8 +9,8 @@ import Foundation final class CountrySelectorInteractor: CountrySelectorInteractorInputProtocol, SetInjectable { - private var presenter: CountrySelectorInteractorOutputProtocol? - private var storageService: StorageServiceProtocol? + private weak var presenter: CountrySelectorInteractorOutputProtocol? + private var storageService: StorageService? var filter: String = "" { didSet { @@ -44,7 +44,7 @@ final class CountrySelectorInteractor: CountrySelectorInteractorInputProtocol, S extension CountrySelectorInteractor { struct Dependencies { let presenter: CountrySelectorInteractorOutputProtocol - let storageService: StorageServiceProtocol + let storageService: StorageService } func inject(dependencies: CountrySelectorInteractor.Dependencies) { diff --git a/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift b/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift index b66cc50b5..2559b854c 100644 --- a/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift +++ b/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift @@ -11,7 +11,7 @@ import Foundation final class CountrySelectorPresenter: CountrySelectorInteractorOutputProtocol, CountrySelectorPresenterProtocol, SetInjectable { private weak var view: CountrySelectorViewProtocol? private var interactor: CountrySelectorInteractorInputProtocol? - private var wireframe: CountrySelectorWireframe? + private var wireframe: CountrySelectorWireframeProtocol? private(set) var sections: [CountriesSection] = [] { didSet { diff --git a/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift b/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift index 222c5a2dd..704dce30c 100644 --- a/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift +++ b/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift @@ -12,11 +12,11 @@ protocol CountrySelectorCoordinatorProtocol { func wireframe(_ wireframe: CountrySelectorWireframe, endWithState state: CountrySelectorWireframe.State) } -final class CountrySelectorWireframe: CountrySelectorWireframeProtocol { +final class CountrySelectorWireframe: Wireframe, CountrySelectorWireframeProtocol { typealias Parameters = NSNull struct Dependencies { - let storageService: StorageServiceProtocol + let storageService: StorageService } enum State { @@ -24,7 +24,7 @@ final class CountrySelectorWireframe: CountrySelectorWireframeProtocol { case endWith(country: Country) } - private var coordinator: CountrySelectorCoordinatorProtocol? + private let coordinator: CountrySelectorCoordinatorProtocol init(coordinator: CountrySelectorCoordinatorProtocol) { self.coordinator = coordinator @@ -46,10 +46,10 @@ final class CountrySelectorWireframe: CountrySelectorWireframeProtocol { } func back() { - coordinator?.wireframe(self, endWithState: .back) + coordinator.wireframe(self, endWithState: .back) } func selectCountry(_ country: Country) { - coordinator?.wireframe(self, endWithState: .endWith(country: country)) + coordinator.wireframe(self, endWithState: .endWith(country: country)) } } diff --git a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift index d53701ce3..c6e45ab28 100644 --- a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -12,7 +12,7 @@ protocol CreateProfileCoordinatorProtocol: class { func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) } -final class CreateProfileWireframe: WireframeProtocol, CreateProfileWireframeProtocol { +final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { struct Parameters {} struct Dependencies {} diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift b/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift index 6c0ed90e7..40b98d3a8 100644 --- a/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift +++ b/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift @@ -8,15 +8,16 @@ import UIKit -protocol CameraWireFrameProtocol: WireframeProtocol { +protocol CameraWireFrameProtocol: class { func openSettings(on view: UIViewController) func back(from view: UIViewController) func openImagePreview(from view: UIViewController, image: UIImage) func openVideoPreview(from view: UIViewController, videoURL: URL) func openQRPreview(from view: UIViewController, text: String) + func end(from view: UIViewController) } -protocol CameraViewProtocol { +protocol CameraViewProtocol: class { func updateTime(seconds: Int) func switchFlashlight(mode: UIImagePickerControllerCameraFlashMode) func switchFlash(mode: UIImagePickerControllerCameraFlashMode) @@ -56,7 +57,7 @@ protocol CameraPresenterProtocol: NavigationProtocol { func qrCodeFullPreview() } -protocol CameraInteractorInputProtocol { +protocol CameraInteractorInputProtocol: class { func getAvailableCameraModes() -> [UIImagePickerControllerCameraCaptureMode] func getCurrentSourceType() -> UIImagePickerControllerSourceType func getCurrentCaptureMode() -> UIImagePickerControllerCameraCaptureMode diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/Presenter/CameraPresenter.swift b/Nynja/Modules/Flows/CameraFlow/Camera/Presenter/CameraPresenter.swift index 5a4214b57..f51a483ba 100644 --- a/Nynja/Modules/Flows/CameraFlow/Camera/Presenter/CameraPresenter.swift +++ b/Nynja/Modules/Flows/CameraFlow/Camera/Presenter/CameraPresenter.swift @@ -8,11 +8,10 @@ import UIKit - final class CameraPresenter: CameraPresenterProtocol, CameraInteractorOutputProtocol, SetInjectable { - private var view: CameraViewProtocol! + private weak var view: CameraViewProtocol! private var interactor: CameraInteractorInputProtocol! - private var wireFrame: CameraWireframe! + private var wireFrame: CameraWireFrameProtocol! } //MARK: - CameraPresenterProtocol diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/Wireframe/CameraWireframe.swift b/Nynja/Modules/Flows/CameraFlow/Camera/Wireframe/CameraWireframe.swift index 5a299a2e8..3bc40a0cc 100644 --- a/Nynja/Modules/Flows/CameraFlow/Camera/Wireframe/CameraWireframe.swift +++ b/Nynja/Modules/Flows/CameraFlow/Camera/Wireframe/CameraWireframe.swift @@ -8,12 +8,13 @@ import UIKit -protocol CameraCoordinatorProtocol { +protocol CameraCoordinatorProtocol: class { func wireframe(_ wireframe: CameraWireframe, with mainView: UIViewController, didEndWith state: CameraWireframe.State) } -final class CameraWireframe: CameraWireFrameProtocol { - private var coordinator: CameraCoordinatorProtocol +final class CameraWireframe: Wireframe, CameraWireFrameProtocol { + + private let coordinator: CameraCoordinatorProtocol init(coordinator: CameraCoordinatorProtocol) { self.coordinator = coordinator diff --git a/Nynja/Modules/Flows/CameraFlow/PhotoPreview/PhotoPreviewProtocols.swift b/Nynja/Modules/Flows/CameraFlow/PhotoPreview/PhotoPreviewProtocols.swift index d3dcf212c..9405976ee 100644 --- a/Nynja/Modules/Flows/CameraFlow/PhotoPreview/PhotoPreviewProtocols.swift +++ b/Nynja/Modules/Flows/CameraFlow/PhotoPreview/PhotoPreviewProtocols.swift @@ -8,7 +8,7 @@ import UIKit -protocol PhotoPreviewWireframeProtocol: class, WireframeProtocol { +protocol PhotoPreviewWireframeProtocol: class { func back(from view: PhotoPreviewViewProtocol) func end(from view: PhotoPreviewViewProtocol, imageURL: URL) func end(from view: PhotoPreviewViewProtocol, fileURL: URL) diff --git a/Nynja/Modules/Flows/CameraFlow/PhotoPreview/Presenter/PhotoPreviewPresenter.swift b/Nynja/Modules/Flows/CameraFlow/PhotoPreview/Presenter/PhotoPreviewPresenter.swift index d165d7b5e..48e71a029 100644 --- a/Nynja/Modules/Flows/CameraFlow/PhotoPreview/Presenter/PhotoPreviewPresenter.swift +++ b/Nynja/Modules/Flows/CameraFlow/PhotoPreview/Presenter/PhotoPreviewPresenter.swift @@ -10,9 +10,9 @@ import UIKit final class PhotoPreviewPresenter: PhotoPreviewPresenterProtocol, PhotoPreviewOutputInteractorProtocol, SetInjectable { - private var wireframe: PhotoPreviewWireframe! - private var interactor: PhotoPreviewInputInteractorProtocol! private weak var view: PhotoPreviewViewProtocol! + private var wireframe: PhotoPreviewWireframeProtocol! + private var interactor: PhotoPreviewInputInteractorProtocol! private var source: PhotoPreviewSource? } diff --git a/Nynja/Modules/Flows/CameraFlow/QRPreview/CameraQRPreviewProtocols.swift b/Nynja/Modules/Flows/CameraFlow/QRPreview/CameraQRPreviewProtocols.swift index 85856507f..a61199a9c 100644 --- a/Nynja/Modules/Flows/CameraFlow/QRPreview/CameraQRPreviewProtocols.swift +++ b/Nynja/Modules/Flows/CameraFlow/QRPreview/CameraQRPreviewProtocols.swift @@ -8,8 +8,7 @@ import Foundation - -protocol CameraQRPreviewWireframeProtocol: WireframeProtocol { +protocol CameraQRPreviewWireframeProtocol: class { func back(from view: CameraQRPreviewViewProtocol) func end(from view: CameraQRPreviewViewProtocol) } @@ -20,15 +19,13 @@ protocol CameraQRPreviewViewProtocol: class where Self: UIViewController { protocol CameraQRPreviewPresenterProtocol: NavigationProtocol { func getTitle() -> String func getText() -> String - func send() } -protocol CameraQRPreviewOutputInteractorProtocol { +protocol CameraQRPreviewOutputInteractorProtocol: class { } -protocol CameraQRPreviewInputInteractorProtocol { +protocol CameraQRPreviewInputInteractorProtocol: class { func getText() -> String - func send() } diff --git a/Nynja/Modules/Flows/CameraFlow/QRPreview/Presenter/CameraQRPreviewPresenter.swift b/Nynja/Modules/Flows/CameraFlow/QRPreview/Presenter/CameraQRPreviewPresenter.swift index d81a7f892..33f1f2721 100644 --- a/Nynja/Modules/Flows/CameraFlow/QRPreview/Presenter/CameraQRPreviewPresenter.swift +++ b/Nynja/Modules/Flows/CameraFlow/QRPreview/Presenter/CameraQRPreviewPresenter.swift @@ -10,9 +10,9 @@ import Foundation final class CameraQRPreviewPresenter: CameraQRPreviewPresenterProtocol, CameraQRPreviewOutputInteractorProtocol, SetInjectable { - private var wireframe: CameraQRPreviewWireframe! - private var interactor: CameraQRPreviewInputInteractorProtocol! private weak var view: CameraQRPreviewViewProtocol! + private var interactor: CameraQRPreviewInputInteractorProtocol! + private var wireframe: CameraQRPreviewWireframeProtocol! } // MARK: - CameraQRPreviewPresenterProtocol diff --git a/Nynja/Modules/Flows/CameraFlow/QRPreview/Wireframe/CameraQRPreviewWireframe.swift b/Nynja/Modules/Flows/CameraFlow/QRPreview/Wireframe/CameraQRPreviewWireframe.swift index 434863563..02d679977 100644 --- a/Nynja/Modules/Flows/CameraFlow/QRPreview/Wireframe/CameraQRPreviewWireframe.swift +++ b/Nynja/Modules/Flows/CameraFlow/QRPreview/Wireframe/CameraQRPreviewWireframe.swift @@ -8,12 +8,13 @@ import Foundation -protocol CameraQRPreviewCoordinatorProtocol { +protocol CameraQRPreviewCoordinatorProtocol: class { func wireframe(_ wireframe: CameraQRPreviewWireframe, with mainView: UIViewController, didEndWithState state: CameraQRPreviewWireframe.State) } -final class CameraQRPreviewWireframe: CameraQRPreviewWireframeProtocol { - private var coordinator: CameraQRPreviewCoordinatorProtocol +final class CameraQRPreviewWireframe: Wireframe, CameraQRPreviewWireframeProtocol { + + private let coordinator: CameraQRPreviewCoordinatorProtocol init(coordinator: CameraQRPreviewCoordinatorProtocol) { self.coordinator = coordinator diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift index a9dfe437b..28a86e335 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift @@ -8,7 +8,7 @@ import Foundation -protocol CameraVideoPreviewWireframeProtocol: WireframeProtocol { +protocol CameraVideoPreviewWireframeProtocol: class { func back(from view: CameraVideoPreviewViewProtocol) func end(from view: CameraVideoPreviewViewProtocol) } @@ -42,7 +42,7 @@ protocol CameraVideoPreviewOutputInteractorProtocol: class { func photoHasBeenSavedWithError(_ error: Error?) } -protocol CameraVideoPreviewInputInteractorProtocol { +protocol CameraVideoPreviewInputInteractorProtocol: class { var isCanSave: Bool { get } func getVideoUrl() -> URL diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift index 1e864adbb..71a23bc62 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift @@ -9,9 +9,9 @@ import Foundation final class CameraVideoPreviewPresenter: CameraVideoPreviewPresenterProtocol, CameraVideoPreviewOutputInteractorProtocol, SetInjectable { + private weak var view: CameraVideoPreviewViewProtocol! private var interactor: CameraVideoPreviewInputInteractorProtocol! - private var wireframe: CameraVideoPreviewWireframe! - private var view: CameraVideoPreviewViewProtocol! + private var wireframe: CameraVideoPreviewWireframeProtocol! } //MARK: - CameraVideoPreviewPresenterProtocol diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift index f82dbb9b0..125023e98 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift @@ -7,11 +7,13 @@ // import Foundation -protocol CameraVideoPreviewCoordinatorProtocol { + +protocol CameraVideoPreviewCoordinatorProtocol: class { func wireframe(_ wireframe: CameraVideoPreviewWireframe, with mainView: UIViewController, didEndWithState state: CameraVideoPreviewWireframe.State) } -final class CameraVideoPreviewWireframe: CameraVideoPreviewWireframeProtocol { +final class CameraVideoPreviewWireframe: Wireframe, CameraVideoPreviewWireframeProtocol { + private let coordinator: CameraVideoPreviewCoordinatorProtocol init(coordinator: CameraVideoPreviewCoordinatorProtocol) { diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/CameraQualitySettingsProtocols.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/CameraQualitySettingsProtocols.swift index 0046fd2eb..fce540a9c 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/CameraQualitySettingsProtocols.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/CameraQualitySettingsProtocols.swift @@ -10,7 +10,7 @@ import UIKit //MARK: - Wireframe -protocol CameraQualitySettingsWireFrameProtocol: WireframeProtocol { +protocol CameraQualitySettingsWireFrameProtocol: class { func back(from view: UIViewController) } diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Presenter/CameraQualitySettingsPresenter.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Presenter/CameraQualitySettingsPresenter.swift index 7f88297f1..c81ac5eb0 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Presenter/CameraQualitySettingsPresenter.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Presenter/CameraQualitySettingsPresenter.swift @@ -9,9 +9,9 @@ import UIKit final class CameraQualitySettingsPresenter: CameraQualitySettingsPresenterProtocol, Injectable { + private weak var view: CameraQualitySettingsViewProtocol! private var interactor: CameraQualitySettingsInputInteractorProtocol! private var wireFrame: CameraQualitySettingsWireframe! - private weak var view: CameraQualitySettingsViewProtocol! } //MARK: - Injectable diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Wireframe/CameraQualitySettingsWireframe.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Wireframe/CameraQualitySettingsWireframe.swift index 86a59480e..b4661c24d 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Wireframe/CameraQualitySettingsWireframe.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraQualitySettings/Wireframe/CameraQualitySettingsWireframe.swift @@ -8,14 +8,15 @@ import UIKit -protocol CameraQualitySettingsCoordinatorProtocol { +protocol CameraQualitySettingsCoordinatorProtocol: class { func wireframe(_ cameraQualitySettingsWireframe: CameraQualitySettingsWireframe, with mainView: UIViewController, didEndWithState state: CameraQualitySettingsWireframe.State) } -final class CameraQualitySettingsWireframe: CameraQualitySettingsWireFrameProtocol { - private var coordinator: CameraQualitySettingsCoordinatorProtocol +final class CameraQualitySettingsWireframe: Wireframe, CameraQualitySettingsWireFrameProtocol { + + private let coordinator: CameraQualitySettingsCoordinatorProtocol init(coordinator: CameraQualitySettingsCoordinatorProtocol) { self.coordinator = coordinator diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/CameraSettingsProtocols.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/CameraSettingsProtocols.swift index 0eebaec51..9f6c74278 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/CameraSettingsProtocols.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/CameraSettingsProtocols.swift @@ -10,7 +10,7 @@ import UIKit //MARK: - Wireframe -protocol CameraSettingsWireFrameProtocol: WireframeProtocol { +protocol CameraSettingsWireFrameProtocol: class { func back(from view: UIViewController) func detailSetting(setting: CameraSetting, from view: UIViewController) } diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Presenter/CameraSettingsPresenter.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Presenter/CameraSettingsPresenter.swift index 9d0dae5cb..47f83472c 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Presenter/CameraSettingsPresenter.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Presenter/CameraSettingsPresenter.swift @@ -10,9 +10,9 @@ import UIKit final class CameraSettingsPresenter: CameraSettingsPresenterProtocol, CameraSettingsOutputInteractorProtocol, SetInjectable { + private weak var view: CameraSettingsViewProtocol! private var interactor: CameraSettingsInputInteractorProtocol! private var wireFrame: CameraSettingsWireFrame! - private weak var view: CameraSettingsViewProtocol! } //MARK: - Injectable diff --git a/Nynja/Modules/Flows/GalleryFlow/Gallery/GalleryProtocols.swift b/Nynja/Modules/Flows/GalleryFlow/Gallery/GalleryProtocols.swift index 5517ae142..66baab09d 100644 --- a/Nynja/Modules/Flows/GalleryFlow/Gallery/GalleryProtocols.swift +++ b/Nynja/Modules/Flows/GalleryFlow/Gallery/GalleryProtocols.swift @@ -9,7 +9,7 @@ import UIKit -protocol GalleryWireframeProtocol: WireframeProtocol { +protocol GalleryWireframeProtocol: class { func back(from view: UIViewController) func openImagePreview(from view: UIViewController, image: UIImage) func openVideoPreview(from view: UIViewController, videoURL: URL) diff --git a/Nynja/Modules/Flows/GalleryFlow/Gallery/Presenter/GalleryPresenter.swift b/Nynja/Modules/Flows/GalleryFlow/Gallery/Presenter/GalleryPresenter.swift index 64c600cb5..7d21b4c5a 100644 --- a/Nynja/Modules/Flows/GalleryFlow/Gallery/Presenter/GalleryPresenter.swift +++ b/Nynja/Modules/Flows/GalleryFlow/Gallery/Presenter/GalleryPresenter.swift @@ -11,8 +11,8 @@ import Foundation final class GalleryPresenter: GalleryPresenterProtocol, GalleryOutputInteractorProtocol, SetInjectable { private weak var view: GalleryViewProtocol! - private var wireframe: GalleryWireframe! private var interactor: GalleryInputInteractorProtocol! + private var wireframe: GalleryWireframeProtocol! } // MARK: - GalleryPresenterProtocol diff --git a/Nynja/Modules/Flows/GalleryFlow/Gallery/Wireframe/GalleryWireframe.swift b/Nynja/Modules/Flows/GalleryFlow/Gallery/Wireframe/GalleryWireframe.swift index 8a3afe1c6..e7c515381 100644 --- a/Nynja/Modules/Flows/GalleryFlow/Gallery/Wireframe/GalleryWireframe.swift +++ b/Nynja/Modules/Flows/GalleryFlow/Gallery/Wireframe/GalleryWireframe.swift @@ -8,12 +8,13 @@ import Foundation -protocol GalleryCoordinatorProtocol { +protocol GalleryCoordinatorProtocol: class { func wireframe(_ wireframe: GalleryWireframe, with mainView: UIViewController, didEndWith state: GalleryWireframe.State) } -final class GalleryWireframe: GalleryWireframeProtocol { - private var coordinator: GalleryCoordinatorProtocol +final class GalleryWireframe: Wireframe, GalleryWireframeProtocol { + + private let coordinator: GalleryCoordinatorProtocol init(coordinator: GalleryCoordinatorProtocol) { self.coordinator = coordinator diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift index 30561ea9b..01292ac00 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift @@ -8,7 +8,7 @@ import UIKit -protocol MultiplePreviewWireframeProtocol: WireframeProtocol { +protocol MultiplePreviewWireframeProtocol: class { func back(from view: UIViewController) func end(from view: UIViewController, items: [(type: ResourceManagerMediaType, url: URL)]) func end(from view: UIViewController, itemsAsFile: [(type: ResourceManagerMediaType, url: URL)]) @@ -55,7 +55,7 @@ protocol MultiplePreviewOutputInteractorProtocol: class { func updateTime(seconds: Float) } -protocol MultiplePreviewInputInteractorProtocol { +protocol MultiplePreviewInputInteractorProtocol: class { var isVideoPlaying: Bool { get } var isMute: Bool { get set } diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift index 6f0ce8c4d..eb9047218 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift @@ -10,9 +10,9 @@ import Foundation final class MultiplePreviewPresenter: MultiplePreviewPresenterProtocol, MultiplePreviewOutputInteractorProtocol, SetInjectable { - private var wireframe: MultiplePreviewWireframe! private weak var view: MultiplePreviewViewProtocol! private var interactor: MultiplePreviewInputInteractorProtocol! + private var wireframe: MultiplePreviewWireframeProtocol! } // MARK: - MultiplePreviewPresenterProtocol @@ -150,7 +150,7 @@ extension MultiplePreviewPresenter { extension MultiplePreviewPresenter { struct Dependencies { - let wireframe: MultiplePreviewWireframe + let wireframe: MultiplePreviewWireframeProtocol let view: MultiplePreviewViewProtocol let interactor: MultiplePreviewInputInteractorProtocol } diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift index c81b267c8..c6519e2d3 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift @@ -8,21 +8,18 @@ import Foundation -protocol MultiplePreviewCoordinatorProtocol { +protocol MultiplePreviewCoordinatorProtocol: class { func wireframe(_ wireframe: MultiplePreviewWireframe, with mainView: UIViewController, didEndWithState state: MultiplePreviewWireframe.State) } -final class MultiplePreviewWireframe: MultiplePreviewWireframeProtocol { - private var coordinator: MultiplePreviewCoordinatorProtocol +final class MultiplePreviewWireframe: Wireframe, MultiplePreviewWireframeProtocol { + + private let coordinator: MultiplePreviewCoordinatorProtocol init(coordinator: MultiplePreviewCoordinatorProtocol) { self.coordinator = coordinator } -} - -// MARK: - MultiplePreviewWireframeProtocol - -extension MultiplePreviewWireframe { + struct Parameters { let activeItem: GalleryItem } @@ -41,7 +38,7 @@ extension MultiplePreviewWireframe { } - func prepareModule(parameters: MultiplePreviewWireframe.Parameters, dependencies: MultiplePreviewWireframe.Dependencies) -> UIViewController { + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let view = MultiplePreviewViewController() let presenter = MultiplePreviewPresenter() let interactor = MultiplePreviewInteractor() diff --git a/Nynja/Services/StorageService.swift b/Nynja/Services/StorageService.swift index ea4cd33a3..e9779215d 100644 --- a/Nynja/Services/StorageService.swift +++ b/Nynja/Services/StorageService.swift @@ -12,19 +12,7 @@ import CryptoSwift //MARK: - The Game is began -protocol StorageServiceProtocol { - var userDefaults: UserDefaults? { get } - var keychain: KeychainService { get } - - #if !SHARE_EXTENSION - var countries: [CountryModel] { get set } - #endif - - func setupDatabase(with name: String, application: UIApplication) - func clearStorage() -} - -class StorageService: StorageServiceProtocol { +final class StorageService { private let passphraseKey = KeychainService.Keys.dataBasePassphrase diff --git a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift b/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift index 4ec04dad2..28609eb32 100644 --- a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift +++ b/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift @@ -8,7 +8,7 @@ import Foundation -protocol WireframeProtocol: class { +protocol Wireframe: class { associatedtype Parameters associatedtype Dependencies associatedtype State -- GitLab From 5da587145e6baa0403f13cec58a7eb58a42e97df Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 6 Nov 2018 14:10:58 +0200 Subject: [PATCH 084/138] [NY-4907] Fixed minor UI issues in SelectCountry module. --- .../NynjaUIKit/Views/Utils/RoundView.swift | 6 +- Nynja.xcodeproj/project.pbxproj | 46 ++--- .../SwiftLibrary/Array/Array+Grouped.swift | 8 +- .../Interactor/SelectCountryInteractor.swift | 22 +-- .../Presenter/SelectCountryPresenter.swift | 14 +- .../SelectCountryProtocols.swift | 6 +- .../View/SelectCountryViewController.swift | 164 +++++++++++++----- .../SelectCountryViewControllerLayout.swift | 23 --- .../TableView/Cell/CountryCellModel.swift | 24 +++ .../CountryTableViewCell.swift} | 106 ++++++----- .../Header/SelectCountryHeaderView.swift | 95 ++++++++++ .../TableView/SelectCountryCellLayout.swift | 33 ---- .../TableView/SelectCountryHeaderView.swift | 59 ------- .../SelectCountryTableDataSource.swift | 42 ----- .../SelectCountryTableDelegate.swift | 60 ------- .../WireFrame/SelectCountryWireframe.swift | 6 +- 16 files changed, 358 insertions(+), 356 deletions(-) delete mode 100644 Nynja/Modules/SelectCountry/View/SelectCountryViewControllerLayout.swift create mode 100644 Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift rename Nynja/Modules/SelectCountry/View/TableView/{SelectCountryCell.swift => Cell/CountryTableViewCell.swift} (50%) create mode 100644 Nynja/Modules/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift delete mode 100644 Nynja/Modules/SelectCountry/View/TableView/SelectCountryCellLayout.swift delete mode 100644 Nynja/Modules/SelectCountry/View/TableView/SelectCountryHeaderView.swift delete mode 100644 Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDataSource.swift delete mode 100644 Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDelegate.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift index 9e8c2b633..ecab2ee22 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift @@ -8,14 +8,14 @@ import UIKit -class RoundView: BaseView { +public class RoundView: BaseView { - override func setup() { + public override func setup() { super.setup() layer.masksToBounds = true } - override func layoutSubviews() { + public override func layoutSubviews() { super.layoutSubviews() layer.cornerRadius = min(bounds.width, bounds.height) / 2 } diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index ea25f6444..5eecfd34c 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1091,6 +1091,8 @@ 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BD2092368600E4840C /* StickerDataSource.swift */; }; 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */; }; 85739FBD2190AAC3001C4EC8 /* AuthProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */; }; + 8575E5342191A9E70080DD4A /* CountryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8575E5322191A9E70080DD4A /* CountryTableViewCell.swift */; }; + 8575E5352191A9E70080DD4A /* CountryCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8575E5332191A9E70080DD4A /* CountryCellModel.swift */; }; 85788C3C204422FB003600C9 /* BuildNumberProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */; }; 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C412044237B003600C9 /* BuildNumberViewController.swift */; }; 85788C4420442385003600C9 /* BuildNumberPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C4320442385003600C9 /* BuildNumberPresenter.swift */; }; @@ -1933,13 +1935,8 @@ C9B8BEFE204DEBD00018748C /* DataDownloadAndUsageMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */; }; C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */; }; C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */; }; - C9C695032022306D00A57297 /* SelectCountryTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C695022022306D00A57297 /* SelectCountryTableDataSource.swift */; }; - C9C69505202230DD00A57297 /* SelectCountryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C69504202230DD00A57297 /* SelectCountryCell.swift */; }; C9C6952620232B0200A57297 /* SortableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C6952520232B0100A57297 /* SortableObject.swift */; }; C9C6952820232B7000A57297 /* Array+SortableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C6952720232B7000A57297 /* Array+SortableObject.swift */; }; - C9C6952E202349DA00A57297 /* SelectCountryCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C6952D202349DA00A57297 /* SelectCountryCellLayout.swift */; }; - C9C695302023639C00A57297 /* SelectCountryViewControllerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C6952F2023639C00A57297 /* SelectCountryViewControllerLayout.swift */; }; - C9DF574A2023A29A006B990A /* SelectCountryTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DF57492023A29A006B990A /* SelectCountryTableDelegate.swift */; }; C9DF574C2023BE92006B990A /* SelectCountryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */; }; CA6AA942773DEBE97BDCFDD6 /* DateTimePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */; }; CC59F623F661C99492F9F415 /* ImagePreviewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD33E34E2376166F3A132271 /* ImagePreviewProtocols.swift */; }; @@ -3381,6 +3378,8 @@ 8572C3BD2092368600E4840C /* StickerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDataSource.swift; sourceTree = ""; }; 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileField.swift; sourceTree = ""; }; 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderType.swift; sourceTree = ""; }; + 8575E5322191A9E70080DD4A /* CountryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTableViewCell.swift; sourceTree = ""; }; + 8575E5332191A9E70080DD4A /* CountryCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryCellModel.swift; sourceTree = ""; }; 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberProtocols.swift; sourceTree = ""; }; 85788C412044237B003600C9 /* BuildNumberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberViewController.swift; sourceTree = ""; }; 85788C4320442385003600C9 /* BuildNumberPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberPresenter.swift; sourceTree = ""; }; @@ -4068,13 +4067,8 @@ C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDownloadAndUsageMode.swift; sourceTree = ""; }; C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideAnimatedTransitioning.swift; sourceTree = ""; }; C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeBackHelper.swift; sourceTree = ""; }; - C9C695022022306D00A57297 /* SelectCountryTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryTableDataSource.swift; sourceTree = ""; }; - C9C69504202230DD00A57297 /* SelectCountryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryCell.swift; sourceTree = ""; }; C9C6952520232B0100A57297 /* SortableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortableObject.swift; sourceTree = ""; }; C9C6952720232B7000A57297 /* Array+SortableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SortableObject.swift"; sourceTree = ""; }; - C9C6952D202349DA00A57297 /* SelectCountryCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryCellLayout.swift; sourceTree = ""; }; - C9C6952F2023639C00A57297 /* SelectCountryViewControllerLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryViewControllerLayout.swift; sourceTree = ""; }; - C9DF57492023A29A006B990A /* SelectCountryTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryTableDelegate.swift; sourceTree = ""; }; C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryHeaderView.swift; sourceTree = ""; }; CA78C91DFDF5884E382D38FA /* EditGroupNamePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupNamePresenter.swift; sourceTree = ""; }; CA8C095C6950E26F3BC48A1C /* EditPhotoInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditPhotoInteractor.swift; sourceTree = ""; }; @@ -6470,7 +6464,6 @@ isa = PBXGroup; children = ( 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */, - C9C6952F2023639C00A57297 /* SelectCountryViewControllerLayout.swift */, C9C695062022318500A57297 /* TableView */, ); path = View; @@ -9263,6 +9256,23 @@ path = Entities; sourceTree = ""; }; + 8575E5362191B22E0080DD4A /* Cell */ = { + isa = PBXGroup; + children = ( + 8575E5322191A9E70080DD4A /* CountryTableViewCell.swift */, + 8575E5332191A9E70080DD4A /* CountryCellModel.swift */, + ); + path = Cell; + sourceTree = ""; + }; + 8575E5372191B5F90080DD4A /* Header */ = { + isa = PBXGroup; + children = ( + C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */, + ); + path = Header; + sourceTree = ""; + }; 85788C3A20442263003600C9 /* BuildNumber */ = { isa = PBXGroup; children = ( @@ -12163,11 +12173,8 @@ C9C695062022318500A57297 /* TableView */ = { isa = PBXGroup; children = ( - C9C695022022306D00A57297 /* SelectCountryTableDataSource.swift */, - C9DF57492023A29A006B990A /* SelectCountryTableDelegate.swift */, - C9C69504202230DD00A57297 /* SelectCountryCell.swift */, - C9C6952D202349DA00A57297 /* SelectCountryCellLayout.swift */, - C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */, + 8575E5372191B5F90080DD4A /* Header */, + 8575E5362191B22E0080DD4A /* Cell */, ); path = TableView; sourceTree = ""; @@ -15640,7 +15647,6 @@ E751E0051F73A70F00FEF533 /* MainItem.swift in Sources */, A4CB15252103751900C3B68B /* JDFilePermissionMechanism.swift in Sources */, 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */, - C9DF574A2023A29A006B990A /* SelectCountryTableDelegate.swift in Sources */, A42D51B5206A361400EEB952 /* chain.swift in Sources */, 8509452B206E684300B43C1C /* AddParticipantsContactCell.swift in Sources */, 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */, @@ -15972,6 +15978,7 @@ E785EF2A1FB9D99400F0C689 /* PinView.swift in Sources */, A42D51D0206A361400EEB952 /* Star.swift in Sources */, 85C16C3E20D2794500EDB77E /* BubbleImageSizeCalculatable.swift in Sources */, + 8575E5352191A9E70080DD4A /* CountryCellModel.swift in Sources */, A43B25D620AB1EE400FF8107 /* NewChannelPresenter.swift in Sources */, E7598F691FA1D8B90082FBE7 /* ProfileScheduledMesssageCellLayout.swift in Sources */, E7C1D3681F683A7D007D4E1E /* MainNavigationItem.swift in Sources */, @@ -16200,7 +16207,6 @@ F11786CB20A8E4FD007A9A1B /* CameraVideoPreviewWireframe.swift in Sources */, 9B0C32F12153CF1600094ECF /* HintView.swift in Sources */, 6DEEE1931F1F9CF6000FAF09 /* UIViewController+Child.swift in Sources */, - C9C695032022306D00A57297 /* SelectCountryTableDataSource.swift in Sources */, 8584C90F20920F3C001A0BBB /* StickerGridCellModel.swift in Sources */, A42D51A4206A361400EEB952 /* Feature.swift in Sources */, 26D6D227212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift in Sources */, @@ -16371,6 +16377,7 @@ 2657BE51201233E300F21935 /* ImageFilledItemModel.swift in Sources */, 85BA176120BEA7BD001EF8AC /* StickerPreviewContainerView.swift in Sources */, 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */, + 8575E5342191A9E70080DD4A /* CountryTableViewCell.swift in Sources */, 00E8513B2021E96E007DC792 /* GApiResponse.swift in Sources */, 850571252050B0AD00EDF794 /* NotificationAlertSoundsWireFrame.swift in Sources */, 85D66A0920BD963C00FBD803 /* MentionController.swift in Sources */, @@ -16390,7 +16397,6 @@ 68B66BDEEFD73CDC331AC840 /* EditProfilePresenter.swift in Sources */, 4B06D3082028A200003B275B /* WCItemsFactory.swift in Sources */, E7AE41681FCC596300C3ED5D /* DBRoomMember.swift in Sources */, - C9C69505202230DD00A57297 /* SelectCountryCell.swift in Sources */, 2648C4102069B52100863614 /* ChangeNumberStep3Wireframe.swift in Sources */, E785F1551FF3DDC8006C52D9 /* GroupRulesViewControllerConstants.swift in Sources */, 853FB0692049B193000996C5 /* SupportInteractor.swift in Sources */, @@ -16491,7 +16497,6 @@ 8504DEAD2069438D006722AC /* Media.swift in Sources */, 26B06C8020602643005BF9AF /* CarouselPickerCollectionViewCell.swift in Sources */, 2600CCC4216E419E00EDC9C3 /* MessageInteractor+AutoConversion.swift in Sources */, - C9C6952E202349DA00A57297 /* SelectCountryCellLayout.swift in Sources */, 3A237BCD1F30E5D400C42B6E /* RosterHandler.swift in Sources */, A4868F2F2121D349001F624E /* DetectorDebuggingPreventer.swift in Sources */, 26610F592015458D00609F77 /* LocationFullWheelItemView.swift in Sources */, @@ -16770,7 +16775,6 @@ 9B9670A02152356D0058E98F /* LeaveVoiceMessageProtocols.swift in Sources */, A481BD1F20EE73BD008FFED8 /* InfoInjectableConstants.swift in Sources */, 8528E50C2072724600A8644A /* StarDateConverter.swift in Sources */, - C9C695302023639C00A57297 /* SelectCountryViewControllerLayout.swift in Sources */, E791178C1F978ACF00462D68 /* ImagePreviewViewControllerLayout.swift in Sources */, E7302A971FC8642F002892F8 /* MucTable.swift in Sources */, A43B25D820AB1EE400FF8107 /* NewChannelViewController.swift in Sources */, diff --git a/Nynja/Extensions/SwiftLibrary/Array/Array+Grouped.swift b/Nynja/Extensions/SwiftLibrary/Array/Array+Grouped.swift index b7ed6e2c7..2f71029b0 100644 --- a/Nynja/Extensions/SwiftLibrary/Array/Array+Grouped.swift +++ b/Nynja/Extensions/SwiftLibrary/Array/Array+Grouped.swift @@ -18,17 +18,13 @@ extension Array { for item in self { titles.insert(transform(item)) } - let sortedTitles: [String] = titles.sorted() var grouped = GroupedElements() - sortedTitles.forEach { title in - // Construct contacts with the same 'firstLetter' - let temp = self + titles.forEach { title in + grouped[title] = self .filter { transform($0) == title } .sorted(by: comparator) - - grouped[title] = temp } return grouped diff --git a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift index 2713cf184..4515c8a3c 100644 --- a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift +++ b/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift @@ -6,25 +6,27 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class SelectCountryInteractor: SelectCountryInteractorInputProtocol { +final class SelectCountryInteractor: SelectCountryInteractorInputProtocol { weak var presenter: SelectCountryInteractorOutputProtocol! - var countriesData: [CountryModel] = [] + + private var countries: [CountryModel] = [] func getCountriesList() { - let countries = StorageService.sharedInstance.countries - self.countriesData = countries - self.prepareCountriesData(countries) + countries = StorageService.sharedInstance.countries + prepareCountriesData(countries) } func filterList(with text: String) { - let filtered = self.countriesData.filter { $0.name.contains(substring: text, options: .caseInsensitive) } - self.prepareCountriesData(filtered) + let filtered = countries.filter { $0.name.contains(substring: text, options: .caseInsensitive) } + prepareCountriesData(filtered) } - // MARK: 🔐 Private private func prepareCountriesData(_ countries: [CountryModel]) { - let preparedData: KeysListAndSectionsDictionaty = countries.sorted(withSortingMode: .ascending).splitBySections(withSortingMode: .ascending) - self.presenter.preparedCountriesList(preparedData) + let groupedCountries = countries.groupedDicrionary(transform: { String($0.name.first!) }, + comparator: { $0.name < $1.name }) + let keys = groupedCountries.keys.sorted() + + presenter.preparedCountriesList(titles: keys, countries: groupedCountries) } } diff --git a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift index 065e12253..9b4d357fd 100644 --- a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift +++ b/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift @@ -6,28 +6,28 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class SelectCountryPresenter: SelectCountryPresenterProtocol { +final class SelectCountryPresenter: SelectCountryPresenterProtocol { weak var view: SelectCountryViewProtocol! var interactor: SelectCountryInteractorInputProtocol! var wireFrame: SelectCountryWireFrameProtocol! func getCountries() { - self.interactor.getCountriesList() + interactor.getCountriesList() } - func filterList(withText text: String) { - self.interactor.filterList(with: text) + func filterList(with text: String) { + interactor.filterList(with: text) } func dismiss() { - self.wireFrame.dismiss() + wireFrame.dismiss() } } extension SelectCountryPresenter: SelectCountryInteractorOutputProtocol { - func preparedCountriesList(_ countries: KeysListAndSectionsDictionaty) { - self.view.updateCountriesList(with: countries) + func preparedCountriesList(titles: [String], countries: [String: [CountryModel]]) { + view.updateCountriesList(titles: titles, countries: countries) } } diff --git a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift b/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift index 478fff110..1ef6f9d48 100644 --- a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift +++ b/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift @@ -27,7 +27,7 @@ protocol SelectCountryViewProtocol: class { * Add here your methods for communication PRESENTER -> VIEW */ - func updateCountriesList(with data: KeysListAndSectionsDictionaty) + func updateCountriesList(titles: [String], countries: [String: [CountryModel]]) } protocol SelectCountryPresenterProtocol: class { @@ -40,7 +40,7 @@ protocol SelectCountryPresenterProtocol: class { * Add here your methods for communication VIEW -> PRESENTER */ func getCountries() - func filterList(withText text: String) + func filterList(with text: String) func dismiss() } @@ -50,7 +50,7 @@ protocol SelectCountryInteractorOutputProtocol: class { * Add here your methods for communication INTERACTOR -> PRESENTER */ - func preparedCountriesList(_ countries: KeysListAndSectionsDictionaty) + func preparedCountriesList(titles: [String], countries: [String: [CountryModel]]) } protocol SelectCountryInteractorInputProtocol: class { diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift index b1ec7e01d..91f57c877 100644 --- a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift +++ b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift @@ -7,38 +7,47 @@ // import UIKit +import NynjaUIKit -class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardInteractive { +final class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardInteractive { var presenter: SelectCountryPresenterProtocol! - private let topInset = Constraints.tableView.topInset.adjustedByWidth + private let topInset = CGFloat(Constraints.tableView.topInset.adjustedByWidth) private let bottomInset = Constraints.controlsContainerView.bottomInset.adjustedByWidth - var countriesTableDataSource: SelectCountryTableDataSource! - var countriesTableDelegate: SelectCountryTableDelegate! - var scrollBar: ScrollBar! + private var sections: [String] = [] + private var countriesDictionary: [String : [CountryModel]] = [:] + + private(set) var scrollBar: ScrollBar? + var scrollOffset: CGFloat = 0 weak var countryDelegate: SelectCountryDelegate? - // MARK: - 📑 Views + // MARK: - Views private lazy var tableView: UITableView = { let table = UITableView() table.keyboardDismissMode = .interactive table.backgroundColor = UIColor.nynja.clear table.separatorStyle = .none - table.tableFooterView = UIView() table.showsHorizontalScrollIndicator = false - table.rowHeight = SelectorCell.Constraints.height + table.tableFooterView = UIView() + + table.sectionHeaderHeight = SelectCountryHeaderView.Constraints.height.adjustedByWidth + table.estimatedSectionHeaderHeight = table.sectionHeaderHeight + + table.rowHeight = CountryCellModel.Cell.Constraints.height.adjustedByWidth table.estimatedRowHeight = table.rowHeight - self.view.addSubview(table) - table.snp.makeConstraints({ (make) in - make.top.equalTo(navigationView.snp.bottom).offset(topInset) + table.contentInset.top = topInset + + view.addSubview(table) + table.snp.makeConstraints { make in + make.top.equalTo(navigationView.snp.bottom) make.left.right.equalToSuperview() - }) + } return table }() @@ -66,36 +75,32 @@ class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardIn let searchField = NynjaSearchField() searchField.searchTextChangeHandler = { [weak self] searchQuery in let text = searchQuery ?? "" - self?.presenter.filterList(withText: text) + self?.presenter.filterList(with: text) } return searchField }() - // MARK: - 🌎 Lifecycle + // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() + override func initialize() { + super.initialize() screenTitle = Constants.LocalizableKeys.selectCountry.localized.uppercased() - self.setupData() - self.setupLayout() + setupData() + setupLayout() } private func setupData(){ - self.tableView.register(SelectCountryCell.self, forCellReuseIdentifier: SelectCountryCell.cellId) - - self.countriesTableDataSource = SelectCountryTableDataSource() - self.tableView.dataSource = self.countriesTableDataSource + tableView.register(viewModel: CountryCellModel.self) + tableView.register(headerFooter: SelectCountryHeaderView.self) - self.countriesTableDelegate = SelectCountryTableDelegate() - self.countriesTableDelegate.cellDelegate = self - self.tableView.delegate = self.countriesTableDelegate + tableView.dataSource = self + tableView.delegate = self - self.presenter.getCountries() + presenter.getCountries() - self.scrollBar = ScrollBar(scrollView: self.tableView) - self.countriesTableDelegate.scrollBar = self.scrollBar + scrollBar = ScrollBar(scrollView: tableView) } private func setupLayout() { @@ -106,22 +111,22 @@ class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardIn // MARK: - SelectCountryViewProtocol - func updateCountriesList(with data: KeysListAndSectionsDictionaty) { - self.countriesTableDataSource.sections = data.keysList - self.countriesTableDataSource.countriesDictionary = data.keySectionedDictionary - self.tableView.reloadData() + func updateCountriesList(titles: [String], countries: [String: [CountryModel]]){ + sections = titles + countriesDictionary = countries + tableView.reloadData() } - // MARK: - 🚀 Actions + // MARK: - Actions @objc private func onCloseButtonTapped() { - self.view.endEditing(true) - self.presenter.dismiss() + view.endEditing(true) + presenter.dismiss() } - // MARK: - ⌨️ Keyboard + // MARK: - Keyboard func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { @@ -132,10 +137,87 @@ class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardIn } } -// MARK: - SelectCountryCellDelegate -extension SelectCountryViewController: SelectCountryCellDelegate { - func didSelect(country: CountryModel, at indexPath: IndexPath) { - self.presenter.dismiss() - self.countryDelegate?.selected(country) + +// MARK: - TableView + +// MARK: UITableViewDataSource +extension SelectCountryViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let sectionKey = sections[section] + return countriesDictionary[sectionKey]?.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return tableView.dequeueReusableCell(withModel: cellModel(at: indexPath), for: indexPath) + } + + private func sectionTitle(for section: Int) -> String { + return sections[section] + } + + private func country(at indexPath: IndexPath) -> CountryModel { + let sectionKey = sectionTitle(for: indexPath.section) + let countries = countriesDictionary[sectionKey]! + return countries[indexPath.row] + } + + private func cellModel(at indexPath: IndexPath) -> AnyCellViewModel { + return CountryCellModel(country: country(at: indexPath)) + } +} + +// MARK: UITableViewDelegate +extension SelectCountryViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selectedCountry = country(at: indexPath) + countryDelegate?.selected(selectedCountry) + presenter.dismiss() + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = tableView.dequeueReusableHeaderFooterView(ofType: SelectCountryHeaderView.self) + headerView.titleLabel.text = sectionTitle(for: section) + return headerView + } + + + // MARK: UIScrollViewDelegate + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let tableView = scrollView as? UITableView else { return } + handleFastScroll(in: tableView) + } +} + + +// MARK: - FastScrollable + +extension SelectCountryViewController: FastScrollable { + + func fastScrollTitle(for section: Int, in tableView: UITableView) -> String? { + return sectionTitle(for: section) + } +} + + +// MARK: - Layout + +extension SelectCountryViewController { + + enum Constraints { + + enum tableView { + static let topInset = 16.0 + } + + enum controlsContainerView { + static let bottomInset: CGFloat = 28.0 + } } } diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewControllerLayout.swift b/Nynja/Modules/SelectCountry/View/SelectCountryViewControllerLayout.swift deleted file mode 100644 index 113a455db..000000000 --- a/Nynja/Modules/SelectCountry/View/SelectCountryViewControllerLayout.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SelectCountryViewControllerLayout.swift -// Nynja -// -// Created by Roma Chopovenko on 2/1/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -extension SelectCountryViewController { - - struct Constraints { - - struct tableView { - static let topInset = 16.0 - } - - struct controlsContainerView { - static let bottomInset: CGFloat = 28.0 - } - } -} diff --git a/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift new file mode 100644 index 000000000..2a6a9cc4d --- /dev/null +++ b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift @@ -0,0 +1,24 @@ +// +// CountryCellModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 06.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +struct CountryCellModel: CellViewModel { + + var accessibilityIdentifier: String { + return "select_country_cell" + } + + let country: CountryModel + + func setup(cell: CountryTableViewCell) { + cell.countryNameLabel.text = country.name + cell.countryCodeLabel.text = "+\(country.code)" + } +} diff --git a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryCell.swift b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift similarity index 50% rename from Nynja/Modules/SelectCountry/View/TableView/SelectCountryCell.swift rename to Nynja/Modules/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift index 4ec8b776d..b7d65995d 100644 --- a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryCell.swift +++ b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift @@ -1,25 +1,19 @@ // -// SelectCountryCell.swift +// CountryTableViewCell.swift // Nynja // -// Created by Roma Chopovenko on 1/31/18. +// Created by Anton Poltoratskyi on 06.11.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // import UIKit +import SnapKit -protocol SelectCountryCellDelegate: class { - func didSelect(country: CountryModel, at indexPath: IndexPath) -} - -class SelectCountryCell: UITableViewCell { +final class CountryTableViewCell: UITableViewCell { - var indexPath: IndexPath? - weak var cellDelegate: SelectCountryCellDelegate? + // MARK: - Views - //MARK: 📑 Views - lazy var countryNameLabel: UILabel = { - + private(set) lazy var countryNameLabel: UILabel = { let width = Constraints.countryNameLabel.width.adjustedByWidth let height = Constraints.countryNameLabel.height.adjustedByWidth let leftInset = Constraints.separatorView.leftInset.adjustedByWidth @@ -28,17 +22,16 @@ class SelectCountryCell: UITableViewCell { label.numberOfLines = 1 contentView.addSubview(label) - label.snp.makeConstraints({ (make) in + label.snp.makeConstraints { make in make.height.equalTo(height) - make.left.equalTo(self).offset(leftInset) + make.left.equalToSuperview().offset(leftInset) make.centerY.equalToSuperview() - }) + } return label }() - lazy var countryCodeLabel: UILabel = { - + private(set) lazy var countryCodeLabel: UILabel = { let width = Constraints.countryCodeLabel.width.adjustedByWidth let height = Constraints.countryCodeLabel.height.adjustedByWidth @@ -49,53 +42,78 @@ class SelectCountryCell: UITableViewCell { label.textAlignment = .right contentView.addSubview(label) - label.snp.makeConstraints({ (make) in + label.snp.makeConstraints { make in make.height.equalTo(height) make.width.equalTo(width) make.left.equalTo(self.countryNameLabel.snp.right).offset(16.adjustedByWidth) - make.right.equalTo(self).offset(-rightInset) + make.right.equalToSuperview().inset(rightInset) make.centerY.equalToSuperview() - }) + } + return label }() - //MARK: 🔐 Private private lazy var separatorView: UIView = { - let view = UIView() - view.backgroundColor = UIColor.nynja.backgroundGray + let view = SeparatorView() + view.color = UIColor.nynja.backgroundGray let leftInset = Constraints.separatorView.leftInset.adjustedByWidth let rightInset = Constraints.separatorView.rightInset.adjustedByWidth contentView.addSubview(view) - view.snp.makeConstraints({ (make) in - make.left.equalTo(self).offset(leftInset) - make.right.equalTo(self).offset(-rightInset) - make.bottom.equalTo(self) - make.height.equalTo(Constraints.separatorView.height) - }) + view.snp.makeConstraints { make in + make.left.equalToSuperview().offset(leftInset) + make.right.equalToSuperview().offset(-rightInset) + make.bottom.equalToSuperview() + } return view }() -} -extension CountryModel: CellModel {} - -extension SelectCountryCell: ConfigurableCell { - static let cellId: String = "SelectCountryCell" - static let accessibilityPrefix = "select_country_cell" - func setup(with model: CellModel) { - guard let model = model as? CountryModel else { return } - self.countryNameLabel.text = model.name - self.countryCodeLabel.text = "+\(model.code)" - self.baseSetup() + + // MARK: - Init + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() } - //MARK: 🔐 Private - private func baseSetup() { - self.backgroundColor = UIColor.nynja.clear - self.selectionStyle = .none + + // MARK: - Setup + + private func setup() { + backgroundColor = UIColor.nynja.clear + selectionStyle = .none separatorView.isHidden = false } + + + // MARK: - Layout + + enum Constraints { + + static let height: CGFloat = 54 + + fileprivate enum countryNameLabel { + static let width = 280.0 + static let height = 22.0 + } + + fileprivate enum countryCodeLabel { + static let width = 60.0 + static let height = 22.0 + } + + fileprivate enum separatorView { + static let height = 1.0 + static let leftInset = 68.0 + static let rightInset = 14.0 + } + } } diff --git a/Nynja/Modules/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift b/Nynja/Modules/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift new file mode 100644 index 000000000..90c9b1aa9 --- /dev/null +++ b/Nynja/Modules/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift @@ -0,0 +1,95 @@ +// +// SelectCountryHeaderView.swift +// Nynja +// +// Created by Roma Chopovenko on 2/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +final class SelectCountryHeaderView: UITableViewHeaderFooterView, Reusable { + + // MARK: - Views + + private(set) lazy var badgeView: RoundView = { + let view = RoundView() + + let size = Constraints.badgeView.size.adjustedByWidth + let leftInset = Constraints.badgeView.leftInset.adjustedByWidth + + view.backgroundColor = UIColor.nynja.mainRed + + contentView.addSubview(view) + view.snp.makeConstraints { make in + make.width.height.equalTo(size) + + make.centerY.equalToSuperview() + + make.left.equalTo(leftInset) + } + + return view + }() + + private(set) lazy var titleLabel: UILabel = { + let height = Constraints.titleLabel.height.adjustedByWidth + + let label = UILabel(size: CGSize(width: height, height: height), color: UIColor.nynja.white, fontName: FontFamily.NotoSans.bold.name, textAlignment: .center) + + badgeView.addSubview(label) + label.snp.makeConstraints { make in + make.center.equalToSuperview() + make.edges.equalToSuperview() + } + + return label + }() + + + // MARK: - Init + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + isUserInteractionEnabled = false + contentView.backgroundColor = UIColor.nynja.clear + + let view = UIView(frame: self.bounds) + view.backgroundColor = UIColor.nynja.clear + + backgroundView = view + + titleLabel.isHidden = false + badgeView.isHidden = false + } + + + // MARK: - Layout + + enum Constraints { + + static let height: CGFloat = badgeView.size + 4.0 + + fileprivate enum badgeView { + static let size: CGFloat = 28.0 + static let leftInset: CGFloat = 16.0 + } + + fileprivate enum titleLabel { + static let height: CGFloat = 25.0 + } + } +} diff --git a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryCellLayout.swift b/Nynja/Modules/SelectCountry/View/TableView/SelectCountryCellLayout.swift deleted file mode 100644 index 5f377a762..000000000 --- a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryCellLayout.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SelectCountryCellLayout.swift -// Nynja -// -// Created by Roma Chopovenko on 2/1/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -extension SelectCountryCell { - - struct Constraints { - - static let height: CGFloat = 54 - - struct countryNameLabel { - static let width = 280.0 - static let height = 22.0 - } - - struct countryCodeLabel { - static let width = 60.0 - static let height = 22.0 - } - - struct separatorView { - static let height = 1.0 - static let leftInset = 68.0 - static let rightInset = 14.0 - } - } -} diff --git a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryHeaderView.swift b/Nynja/Modules/SelectCountry/View/TableView/SelectCountryHeaderView.swift deleted file mode 100644 index 3c3109138..000000000 --- a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryHeaderView.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SelectCountryHeaderView.swift -// Nynja -// -// Created by Roma Chopovenko on 2/1/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -class SelectCountryHeaderView: UIView { - - struct Constraints { - - struct badgeView { - static let width: CGFloat = 28.0 - static let leftInset = 16.0 - } - - struct titleLabel { - static let height: CGFloat = 25.0 - } - } - - //MARK: 📑 Views - lazy var badgeView: UIView = { - let view = UIView() - - let width = Constraints.badgeView.width.adjustedByWidth - let leftInset = Constraints.badgeView.leftInset.adjustedByWidth - - view.backgroundColor = UIColor.nynja.mainRed - view.roundCorners(radius: width * 0.5) - - self.addSubview(view) - view.snp.makeConstraints({ (make) in - make.width.height.equalTo(width) - - make.top.equalToSuperview() - make.left.equalTo(leftInset) - }) - - return view - }() - - lazy var titleLabel: UILabel = { - let height = Constraints.titleLabel.height.adjustedByWidth - - let label = UILabel(size: CGSize(width: height, height: height), color: UIColor.nynja.white, fontName: FontFamily.NotoSans.bold.name, textAlignment: .center) - - self.badgeView.addSubview(label) - label.snp.makeConstraints({ (make) in - make.center.equalToSuperview() - make.edges.equalToSuperview() - }) - - return label - }() -} diff --git a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDataSource.swift b/Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDataSource.swift deleted file mode 100644 index 13e5b3e0c..000000000 --- a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDataSource.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SelectCountryTableDataSource.swift -// Nynja -// -// Created by Roma Chopovenko on 1/31/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class SelectCountryTableDataSource: NSObject, UITableViewDataSource { - - var countriesDictionary: [String : [SortableObject]] = [:] - var sections: [String] = [] - - func numberOfSections(in tableView: UITableView) -> Int { - return self.sections.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let sectionKey = self.sections[section] - guard let countries = self.countriesDictionary[sectionKey] else { return 0 } - return countries.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let sectionKey = self.sections[indexPath.section] - - let cell = tableView.dequeueReusableCell(withIdentifier: SelectCountryCell.cellId) - as! SelectCountryCell - - cell.indexPath = indexPath - cell.setupAccessibility(at: indexPath) - - guard let currentSectionOfCountries = self.countriesDictionary[sectionKey] else { return cell } - - guard let currentCountry = currentSectionOfCountries[indexPath.row] as? CountryModel else { return cell } - cell.setup(with: currentCountry) - - return cell - } -} diff --git a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDelegate.swift b/Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDelegate.swift deleted file mode 100644 index 89d87428d..000000000 --- a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryTableDelegate.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SelectCountryTableDelegate.swift -// Nynja -// -// Created by Roma Chopovenko on 2/1/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class SelectCountryTableDelegate: NSObject, UITableViewDelegate, UIScrollViewDelegate, FastScrollable { - - weak var cellDelegate: SelectCountryCellDelegate? - - weak var scrollBar: ScrollBar? - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let dataSource = tableView.dataSource as? SelectCountryTableDataSource else { return } - let keyCountry: String = dataSource.sections[indexPath.section] - guard let selectedItem: CountryModel = dataSource.countriesDictionary[keyCountry]?[indexPath.row] as? CountryModel else { return } - self.cellDelegate?.didSelect(country: selectedItem, at: indexPath) - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let dataSource = tableView.dataSource as? SelectCountryTableDataSource else { - return nil - } - let height: CGFloat = SelectCountryHeaderView.Constraints.badgeView.width - let frame = CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: height) - let headerView = SelectCountryHeaderView(frame: frame) - headerView.clipsToBounds = false - - headerView.titleLabel.text = dataSource.sections[section] - headerView.isUserInteractionEnabled = false - - return headerView - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 0.01 - } - - // MARK: UIScrollViewDelegate - var scrollOffset: CGFloat = 0 - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard let tableView = scrollView as? UITableView else { return } - handleFastScroll(in: tableView) - } - - // MARK: FastScrollable - - func fastScrollTitle(for section: Int, in tableView: UITableView) -> String? { - guard let dataSource = tableView.dataSource as? SelectCountryTableDataSource, - dataSource.sections.indices.contains(section) else { - return nil - } - return dataSource.sections[section] - } -} diff --git a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift b/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift index 812cb1e8f..fafa63298 100644 --- a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift +++ b/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift @@ -8,7 +8,7 @@ import UIKit -class SelectCountryWireFrame: SelectCountryWireFrameProtocol { +final class SelectCountryWireFrame: SelectCountryWireFrameProtocol { weak var navigation : UINavigationController? weak var main: MainWireFrame? @@ -29,9 +29,7 @@ class SelectCountryWireFrame: SelectCountryWireFrameProtocol { presenter.interactor = interactor interactor.presenter = presenter - view.modalTransitionStyle = .crossDissolve - view.modalPresentationStyle = .overCurrentContext - navigation.pushViewController(view as UIViewController, animated: true) + navigation.pushViewController(view, animated: true) } func dismiss() { -- GitLab From 701f2af5d5fc6ec5ece22cce7e43fc4d568d24cb Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 7 Nov 2018 17:06:10 +0200 Subject: [PATCH 085/138] [NY-4907] Fixed UI issues in SelectCountry module. Use it in auth flow. --- Nynja.xcodeproj/project.pbxproj | 128 +++----------- Nynja/CountriesProvider.swift | 8 +- Nynja/CountriesProviding.swift | 2 + Nynja/Extensions/CollectionsExtensions.swift | 9 + .../SwiftLibrary/Array/ArrayExtension.swift | 10 -- .../Wireframe/AccountSettingsWireframe.swift | 6 +- .../AccountSettingsCoordinator.swift | 2 +- .../AddContactViaPhoneProtocols.swift | 14 +- .../AddContactViaPhonePresenter.swift | 10 +- .../AddContactViaPhoneViewController.swift | 13 +- .../AddContactViaPhoneWireframe.swift | 29 +++- Nynja/Modules/Auth/AuthCoordinator.swift | 27 +-- .../View/Subviews/PhoneNumberLoginView.swift | 54 +++--- .../AuthModule/Wireframe/AuthWireframe.swift | 4 +- .../Wireframe/CodeConfirmationWireframe.swift | 2 - .../CountrySelectorProtocols.swift | 35 ---- .../CountrySelector/Entities/Country.swift | 21 --- .../CountrySelectorInteractor.swift | 54 ------ .../Presenter/CountrySelectorPresenter.swift | 69 -------- .../View/Cells/CountryTVCell.swift | 103 ----------- .../View/CountrySelectorViewController.swift | 160 ------------------ .../View/CountrySelectorViewsFactory.swift | 96 ----------- .../View/Headers/CountryTVHeader.swift | 74 -------- .../Wireframe/CountrySelectorWireframe.swift | 55 ------ .../Wireframe/CreateProfileWireframe.swift | 5 +- .../Wireframe/CameraSettingsWireframe.swift | 9 +- .../CameraSettingsCoordinator.swift | 2 +- Nynja/Modules/Main/MainProtocols.swift | 1 - .../Main/WireFrame/MainWireframe.swift | 6 - .../Entities/CountriesSection.swift | 12 ++ .../Interactor/SelectCountryInteractor.swift | 56 ++++-- .../Presenter/SelectCountryPresenter.swift | 37 +++- .../SelectCountry/SelctCountryDelegate.swift | 13 -- .../SelectCountryProtocols.swift | 58 ++----- .../View/SelectCountryViewController.swift | 84 +++++---- .../TableView/Cell/CountryCellModel.swift | 1 - .../WireFrame/SelectCountryWireframe.swift | 58 +++++-- ...swift => NavigableWireframeProtocol.swift} | 12 +- .../BaseModule/Wireframe/Wireframe.swift | 38 +++++ 39 files changed, 357 insertions(+), 1020 deletions(-) delete mode 100644 Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/Entities/Country.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift delete mode 100644 Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift create mode 100644 Nynja/Modules/SelectCountry/Entities/CountriesSection.swift delete mode 100644 Nynja/Modules/SelectCountry/SelctCountryDelegate.swift rename Nynja/Viper/BaseModule/Wireframe/{WireframeProtocol.swift => NavigableWireframeProtocol.swift} (65%) create mode 100644 Nynja/Viper/BaseModule/Wireframe/Wireframe.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 5eecfd34c..8a71304bb 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -738,7 +738,6 @@ 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */; }; 5E07BC3D216DFD08000E4558 /* AuthViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */; }; 5E07BC40216E09F0000E4558 /* CodeConfirmationViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */; }; - 5E07BC42216E30A8000E4558 /* CountrySelectorViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC41216E30A8000E4558 /* CountrySelectorViewsFactory.swift */; }; 5E07BC44216F56AF000E4558 /* AuthType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC43216F56AF000E4558 /* AuthType.swift */; }; 5E07BC4D216F64EC000E4558 /* CreateProfileProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */; }; 5E07BC4F216F659E000E4558 /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */; }; @@ -771,11 +770,6 @@ 5E7D5D60219044CB009B5D8D /* AddContactCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */; }; 5E7D5D6221904E25009B5D8D /* ContactTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */; }; 5E7D5D6421905390009B5D8D /* ContactTVCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */; }; - 5E7E9FB9215BA0BE004D306B /* CountrySelectorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */; }; - 5E7E9FBC215BA19B004D306B /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FBB215BA19B004D306B /* Country.swift */; }; - 5E7E9FBE215BA51C004D306B /* CountrySelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */; }; - 5E7E9FC2215BA681004D306B /* CountryTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */; }; - 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */; }; 5EB13FDBA6153EE67366115F /* ScheduleMessageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */; }; 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD49CF323041B47A752603E /* MapSearchInteractor.swift */; }; 5EDD454F21885ED200C50BC8 /* AccountSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */; }; @@ -783,9 +777,6 @@ 5EDD455321885F7800C50BC8 /* AccountSettingsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */; }; 5EDD45552188601400C50BC8 /* AccountSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */; }; 5EDD455821899BCE00C50BC8 /* AccountSettingsViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */; }; - 5EEB73A4215D00E300D8ECE6 /* CountrySelectorInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */; }; - 5EEB73A6215D00F100D8ECE6 /* CountrySelectorWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */; }; - 5EEB73A8215D00FD00D8ECE6 /* CountrySelectorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A7215D00FD00D8ECE6 /* CountrySelectorPresenter.swift */; }; 5EEB73AA215D406400D8ECE6 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */; }; 5EEB73B2216046FE00D8ECE6 /* CodeConfirmationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */; }; 5EEB73B4216047E000D8ECE6 /* CodeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */; }; @@ -1006,6 +997,8 @@ 85433F25204D596D00B373A7 /* WebFullScreenInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F20204D596D00B373A7 /* WebFullScreenInteractor.swift */; }; 85433F26204D596D00B373A7 /* WebFullScreenWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F21204D596D00B373A7 /* WebFullScreenWireFrame.swift */; }; 85433F2C204D5AA500B373A7 /* NynjaCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */; }; + 854574CA21931976001D43CF /* CountriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854574C921931976001D43CF /* CountriesSection.swift */; }; + 854574CC21933190001D43CF /* NavigableWireframeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854574CB21933190001D43CF /* NavigableWireframeProtocol.swift */; }; 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; 85458CDA212D6FFE00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; 85458CDB212D6FFE00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; @@ -1917,7 +1910,6 @@ C6B308C6734EFB77892832A0 /* SecurityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762BA232B5D027BD943DFA18 /* SecurityPresenter.swift */; }; C8C6310F83825D7385C3A6E4 /* MapProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2260535ED2762F80FA7A38 /* MapProtocols.swift */; }; C90E6A9B20558C0300D733E0 /* FileSizeFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90E6A9A20558C0300D733E0 /* FileSizeFormatter.swift */; }; - C90EE13E20246E2700FDB873 /* SelctCountryDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90EE13D20246E2700FDB873 /* SelctCountryDelegate.swift */; }; C921738220BADAFC00519A2D /* TextInputValidationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C921738120BADAFC00519A2D /* TextInputValidationService.swift */; }; C9405149204C7FAF00D72B04 /* DataAndStoragePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9405144204C7FAF00D72B04 /* DataAndStoragePresenter.swift */; }; C940514A204C7FAF00D72B04 /* DataAndStorageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9405145204C7FAF00D72B04 /* DataAndStorageViewController.swift */; }; @@ -2194,7 +2186,7 @@ F127F3BC20BF03BF007A6F87 /* WeakDays.swift in Sources */ = {isa = PBXBuildFile; fileRef = F127F3B920BF03BF007A6F87 /* WeakDays.swift */; }; F1313B0220888FE600E04092 /* ThirdPartyServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1313B0120888FE600E04092 /* ThirdPartyServices.swift */; }; F13EACD220B67EBE007104D6 /* GallerySectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F13EACD120B67EBE007104D6 /* GallerySectionHeader.swift */; }; - F13EACDB20B86B8C007104D6 /* WireframeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F13EACDA20B86B8C007104D6 /* WireframeProtocol.swift */; }; + F13EACDB20B86B8C007104D6 /* Wireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F13EACDA20B86B8C007104D6 /* Wireframe.swift */; }; F1607B1D20B20F7800BDF60A /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1607B1C20B20F7800BDF60A /* GridView.swift */; }; F1607B1F20B21A9D00BDF60A /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1607B1E20B21A9C00BDF60A /* CameraViewController.swift */; }; F1607B2620B2DE1300BDF60A /* CameraQRPreviewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1607B2520B2DE1300BDF60A /* CameraQRPreviewProtocols.swift */; }; @@ -3059,7 +3051,6 @@ 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryInteractor.swift; sourceTree = ""; }; 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewsFactory.swift; sourceTree = ""; }; 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationViewsFactory.swift; sourceTree = ""; }; - 5E07BC41216E30A8000E4558 /* CountrySelectorViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewsFactory.swift; sourceTree = ""; }; 5E07BC43216F56AF000E4558 /* AuthType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthType.swift; sourceTree = ""; }; 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileProtocols.swift; sourceTree = ""; }; 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = ""; }; @@ -3091,20 +3082,12 @@ 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactCellModel.swift; sourceTree = ""; }; 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTVCell.swift; sourceTree = ""; }; 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTVCellModel.swift; sourceTree = ""; }; - 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorProtocols.swift; sourceTree = ""; }; - 5E7E9FBB215BA19B004D306B /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; - 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorViewController.swift; sourceTree = ""; }; - 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVCell.swift; sourceTree = ""; }; - 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTVHeader.swift; sourceTree = ""; }; 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsCoordinator.swift; sourceTree = ""; }; 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsProtocols.swift; sourceTree = ""; }; 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsWireframe.swift; sourceTree = ""; }; 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewController.swift; sourceTree = ""; }; 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewsFactory.swift; sourceTree = ""; }; 5EEA3D18EFB98D7959F993E4 /* AddParticipantsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsProtocols.swift; sourceTree = ""; }; - 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorInteractor.swift; sourceTree = ""; }; - 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorWireframe.swift; sourceTree = ""; }; - 5EEB73A7215D00FD00D8ECE6 /* CountrySelectorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectorPresenter.swift; sourceTree = ""; }; 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationProtocols.swift; sourceTree = ""; }; 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationViewController.swift; sourceTree = ""; }; @@ -3317,6 +3300,8 @@ 85433F20204D596D00B373A7 /* WebFullScreenInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenInteractor.swift; sourceTree = ""; }; 85433F21204D596D00B373A7 /* WebFullScreenWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenWireFrame.swift; sourceTree = ""; }; 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaCloseButton.swift; sourceTree = ""; }; + 854574C921931976001D43CF /* CountriesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountriesSection.swift; sourceTree = ""; }; + 854574CB21933190001D43CF /* NavigableWireframeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableWireframeProtocol.swift; sourceTree = ""; }; 85458CD8212D6FED00BA8814 /* String+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = ""; }; 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIdentifiers.swift; sourceTree = ""; }; 85458CEC212D74B400BA8814 /* P2P+Opponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "P2P+Opponent.swift"; sourceTree = ""; }; @@ -4049,7 +4034,6 @@ C68A1D12FEF0CE24D6B3F6F5 /* SelectCountryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryInteractor.swift; sourceTree = ""; }; C8263B034B7C1F206A1C1A6C /* HistoryProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryProtocols.swift; sourceTree = ""; }; C90E6A9A20558C0300D733E0 /* FileSizeFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSizeFormatter.swift; sourceTree = ""; }; - C90EE13D20246E2700FDB873 /* SelctCountryDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelctCountryDelegate.swift; sourceTree = ""; }; C921738120BADAFC00519A2D /* TextInputValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputValidationService.swift; sourceTree = ""; }; C9405144204C7FAF00D72B04 /* DataAndStoragePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStoragePresenter.swift; sourceTree = ""; }; C9405145204C7FAF00D72B04 /* DataAndStorageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageViewController.swift; sourceTree = ""; }; @@ -4321,7 +4305,7 @@ F1313B0020888CC400E04092 /* ReleaseConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseConfig.xcconfig; sourceTree = ""; }; F1313B0120888FE600E04092 /* ThirdPartyServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyServices.swift; sourceTree = ""; }; F13EACD120B67EBE007104D6 /* GallerySectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GallerySectionHeader.swift; sourceTree = ""; }; - F13EACDA20B86B8C007104D6 /* WireframeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireframeProtocol.swift; sourceTree = ""; }; + F13EACDA20B86B8C007104D6 /* Wireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wireframe.swift; sourceTree = ""; }; F1607B1C20B20F7800BDF60A /* GridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = ""; }; F1607B1E20B21A9C00BDF60A /* CameraViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; F1607B2520B2DE1300BDF60A /* CameraQRPreviewProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraQRPreviewProtocols.swift; sourceTree = ""; }; @@ -4720,11 +4704,11 @@ isa = PBXGroup; children = ( 4F7C039B61A0663D43BE5AE5 /* SelectCountryProtocols.swift */, - C90EE13D20246E2700FDB873 /* SelctCountryDelegate.swift */, 43D5323E27F49A5C95BBB6D6 /* View */, 337A8E299DCF438AD28A7043 /* Presenter */, 0AD119947B4A6FA309A1060E /* Interactor */, CADE0A8BB5BE972F51CE1E2F /* WireFrame */, + 854574C821931945001D43CF /* Entities */, ); path = SelectCountry; sourceTree = ""; @@ -6955,7 +6939,6 @@ 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, - 5E7E9FB3215BA0AD004D306B /* CountrySelector */, 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, @@ -7555,78 +7538,6 @@ path = Header; sourceTree = ""; }; - 5E7E9FB3215BA0AD004D306B /* CountrySelector */ = { - isa = PBXGroup; - children = ( - 5E7E9FBA215BA186004D306B /* Entities */, - 5E7E9FB4215BA0AD004D306B /* Presenter */, - 5E7E9FB5215BA0AD004D306B /* Wireframe */, - 5E7E9FB6215BA0AD004D306B /* View */, - 5E7E9FB7215BA0AD004D306B /* Interactor */, - 5E7E9FB8215BA0BE004D306B /* CountrySelectorProtocols.swift */, - ); - path = CountrySelector; - sourceTree = ""; - }; - 5E7E9FB4215BA0AD004D306B /* Presenter */ = { - isa = PBXGroup; - children = ( - 5EEB73A7215D00FD00D8ECE6 /* CountrySelectorPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 5E7E9FB5215BA0AD004D306B /* Wireframe */ = { - isa = PBXGroup; - children = ( - 5EEB73A5215D00F100D8ECE6 /* CountrySelectorWireframe.swift */, - ); - path = Wireframe; - sourceTree = ""; - }; - 5E7E9FB6215BA0AD004D306B /* View */ = { - isa = PBXGroup; - children = ( - 5E7E9FBF215BA66E004D306B /* Cells */, - 5E7E9FC0215BA66E004D306B /* Headers */, - 5E7E9FBD215BA51C004D306B /* CountrySelectorViewController.swift */, - 5E07BC41216E30A8000E4558 /* CountrySelectorViewsFactory.swift */, - ); - path = View; - sourceTree = ""; - }; - 5E7E9FB7215BA0AD004D306B /* Interactor */ = { - isa = PBXGroup; - children = ( - 5EEB73A3215D00E300D8ECE6 /* CountrySelectorInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - 5E7E9FBA215BA186004D306B /* Entities */ = { - isa = PBXGroup; - children = ( - 5E7E9FBB215BA19B004D306B /* Country.swift */, - ); - path = Entities; - sourceTree = ""; - }; - 5E7E9FBF215BA66E004D306B /* Cells */ = { - isa = PBXGroup; - children = ( - 5E7E9FC1215BA681004D306B /* CountryTVCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; - 5E7E9FC0215BA66E004D306B /* Headers */ = { - isa = PBXGroup; - children = ( - 5E7E9FC3215BA68E004D306B /* CountryTVHeader.swift */, - ); - path = Headers; - sourceTree = ""; - }; 5EDD454621885EC400C50BC8 /* AccountSettings */ = { isa = PBXGroup; children = ( @@ -8994,6 +8905,14 @@ path = WireFrame; sourceTree = ""; }; + 854574C821931945001D43CF /* Entities */ = { + isa = PBXGroup; + children = ( + 854574C921931976001D43CF /* CountriesSection.swift */, + ); + path = Entities; + sourceTree = ""; + }; 85482841204E912600DCBEC8 /* ViewController */ = { isa = PBXGroup; children = ( @@ -13697,7 +13616,8 @@ F13EACD920B86B7F007104D6 /* Wireframe */ = { isa = PBXGroup; children = ( - F13EACDA20B86B8C007104D6 /* WireframeProtocol.swift */, + F13EACDA20B86B8C007104D6 /* Wireframe.swift */, + 854574CB21933190001D43CF /* NavigableWireframeProtocol.swift */, ); path = Wireframe; sourceTree = ""; @@ -15456,7 +15376,6 @@ A40F18BB20BFD9C60091B09E /* EmptyStateViewModel.swift in Sources */, A42D52B6206A53AA00EEB952 /* Service_Spec.swift in Sources */, 4B8996C8204ECE9B00DCB183 /* ContactDAO.swift in Sources */, - 5E07BC42216E30A8000E4558 /* CountrySelectorViewsFactory.swift in Sources */, 4B6D20EC2164D4AB003ADB29 /* DeliveryStatus.swift in Sources */, 4B7C73F3215A5509007924DB /* LogWriter.swift in Sources */, 262D43872033417F002F1E45 /* FriendExtansion+BERT.swift in Sources */, @@ -15497,6 +15416,7 @@ 852003F820D419E9007C0036 /* RecentStickerTable.swift in Sources */, A42D52C5206A53AA00EEB952 /* ExtendedStar_Spec.swift in Sources */, FEA655F52167777E00B44029 /* PaymentProtocols.swift in Sources */, + 854574CC21933190001D43CF /* NavigableWireframeProtocol.swift in Sources */, E76D132C1FA35CCF00B07F0E /* ProfilePlaceholderCell.swift in Sources */, A4BE4AB52068E98C00C041D1 /* ALTextInputBar+Trim.swift in Sources */, E7EED2341F740BF3005DAE20 /* ChatsItem.swift in Sources */, @@ -15740,7 +15660,6 @@ 26342CAB20ECBB0100D2196B /* TranscribeNetworkService.swift in Sources */, 5E07BC57216F6722000E4558 /* CreateProfileWireframe.swift in Sources */, A44B4D5820CE9BDF00CA700A /* AvatarCell.swift in Sources */, - 5E7E9FBC215BA19B004D306B /* Country.swift in Sources */, 4B8996F7204EF77100DCB183 /* FeedDAO.swift in Sources */, A418DA3820EE1AFD00FE780B /* CountAppearanceModel.swift in Sources */, B763DD9320AA1C3400A30B63 /* ContactCellLayout.swift in Sources */, @@ -15894,7 +15813,6 @@ A43B25AD20AB1DFA00FF8107 /* MyTextField.swift in Sources */, 267BE90D2069413A00153FB8 /* Handlers.swift in Sources */, F10AFEB820F7B1B000C7CE83 /* WheelChatItemPreview.swift in Sources */, - 5E7E9FC2215BA681004D306B /* CountryTVCell.swift in Sources */, FBCE841320E525A6003B7558 /* NetworkRouter.swift in Sources */, A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, @@ -16003,7 +15921,6 @@ 852003FE20D46680007C0036 /* StickerPack.swift in Sources */, B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */, 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */, - 5E7E9FC4215BA68E004D306B /* CountryTVHeader.swift in Sources */, 85579882209322A8007050B8 /* StickerMenuDataSource.swift in Sources */, 8506F001206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift in Sources */, 85C16C3C20D261C000EDB77E /* MessageStickerView.swift in Sources */, @@ -16221,7 +16138,6 @@ 859B863920486068003272B2 /* CarouselPickerViewControllerLayout.swift in Sources */, A43B25BF20AB1E9600FF8107 /* LengthInputValidator.swift in Sources */, 85D669E520BD956000FBD803 /* UIButtonExtensions.swift in Sources */, - 5EEB73A8215D00FD00D8ECE6 /* CountrySelectorPresenter.swift in Sources */, E74EC9ED1FC2DA6E007268E6 /* RoomTable.swift in Sources */, A42D52C8206A53AB00EEB952 /* Auth_Spec.swift in Sources */, 267BE90920693F4400153FB8 /* ProfileDAO.swift in Sources */, @@ -16302,7 +16218,6 @@ 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */, 0062D9432062EC4100B915AC /* ShareNynjaHeaderViewLayout.swift in Sources */, A42D52B9206A53AA00EEB952 /* Profile_Spec.swift in Sources */, - 5EEB73A4215D00E300D8ECE6 /* CountrySelectorInteractor.swift in Sources */, 26DCB25420692237001EF0AB /* Array+Feature.swift in Sources */, F1607B2E20B2DE8A00BDF60A /* CameraQRPreviewInteractor.swift in Sources */, B77C11EA2109254800CCB42E /* InterpretationTypeWireFrame.swift in Sources */, @@ -16464,7 +16379,6 @@ 8526187C20D05BF700824357 /* StickerGridPlaceholderCollectionViewCell.swift in Sources */, 26B32B601FE170FE00888A0A /* MigrationManager.swift in Sources */, 986BE2204D6D0813B13618B1 /* AddContactViaPhonePresenter.swift in Sources */, - 5E7E9FB9215BA0BE004D306B /* CountrySelectorProtocols.swift in Sources */, 265AEA171FE9AFD400AC4806 /* MemberModel.swift in Sources */, 2605311D21274116002E1CF1 /* LogOutputView.swift in Sources */, 263529152075729400DC6FBD /* Job+DB.swift in Sources */, @@ -16481,7 +16395,6 @@ A407348C20B712E9005762D5 /* UIView+Hierarchy.swift in Sources */, F117870F20ACF018007A9A1B /* CameraQualitySettingsViewController.swift in Sources */, E764919B1F7A5485001E741C /* MainWheelContainerDelegate.swift in Sources */, - 5EEB73A6215D00F100D8ECE6 /* CountrySelectorWireframe.swift in Sources */, A4330A6A2109EA850060BD93 /* DatabaseManager.swift in Sources */, 8E6C4BDE1FF40B97009C8374 /* GroupFilesCell.swift in Sources */, 26DCB24E2064B9DC001EF0AB /* ContactsTableDS.swift in Sources */, @@ -16720,7 +16633,6 @@ A432CF1620B4347D00993AFB /* FloatingPlaceholderProvider.swift in Sources */, E7302A931FC83477002892F8 /* Desc+DescMime.swift in Sources */, 8509FC7B2158CCA800734D93 /* MessageInteractor+Reply.swift in Sources */, - 5E7E9FBE215BA51C004D306B /* CountrySelectorViewController.swift in Sources */, A42D51CB206A361400EEB952 /* Job.swift in Sources */, E7F68D271FA22C45009C98D1 /* EditProfileVCStrings.swift in Sources */, 8ECC06801FC5C80C002CF225 /* MessagesProcessingManager.swift in Sources */, @@ -16857,7 +16769,6 @@ 859B8630204820DC003272B2 /* ThemePickerWireFrame.swift in Sources */, 263D66301FE8D20100A509F8 /* TypingExtension+BERT.swift in Sources */, F117872720ACF2DB007A9A1B /* CameraSourceFlow.swift in Sources */, - C90EE13E20246E2700FDB873 /* SelctCountryDelegate.swift in Sources */, 0062D9422062EC4100B915AC /* InviteFriendsSelectionViewModel.swift in Sources */, A4679BA520B2DD0F0021FE9C /* SubscribersSelectorPresenter.swift in Sources */, F105C6BC20A1347E0091786A /* PhotoPreviewWireframeProtocol.swift in Sources */, @@ -16888,6 +16799,7 @@ 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */, 3A8045DA1F60E18E00AED866 /* Queue.swift in Sources */, A42D51A3206A361400EEB952 /* Test.swift in Sources */, + 854574CA21931976001D43CF /* CountriesSection.swift in Sources */, A45F113220B4218D00F45004 /* BaseChatCellLayout.swift in Sources */, A411D95C20AC3A5A009D107C /* ConversationsProvider.swift in Sources */, 859F9B4C2035CB1E009D017A /* ForwardContent.swift in Sources */, @@ -17306,7 +17218,7 @@ 84BB63C68EA124AA7DD21B30 /* LanguageSettingsProtocols.swift in Sources */, FEA656022167777F00B44029 /* WalletBalancesViewController.swift in Sources */, 69CA7311E49F87A5CACC8A73 /* LanguageSettingsViewController.swift in Sources */, - F13EACDB20B86B8C007104D6 /* WireframeProtocol.swift in Sources */, + F13EACDB20B86B8C007104D6 /* Wireframe.swift in Sources */, E27620AE3F571711EE70C0C8 /* LanguageSettingsPresenter.swift in Sources */, BC1BA70218B40F3F64841848 /* LanguageSettingsInteractor.swift in Sources */, F922EF38E4C1662D54CE533D /* LanguageSettingsWireframe.swift in Sources */, diff --git a/Nynja/CountriesProvider.swift b/Nynja/CountriesProvider.swift index 061db9e77..380daaba6 100644 --- a/Nynja/CountriesProvider.swift +++ b/Nynja/CountriesProvider.swift @@ -10,7 +10,6 @@ import Foundation final class CountriesProvider: CountriesProviding { - // FIXME: return array of Country func fetchCountries() -> [CountryModel] { let path = Bundle.main.path(forResource: "countries", ofType: "txt")! guard let text = try? String(contentsOfFile: path, encoding: .utf8) else { @@ -24,11 +23,8 @@ final class CountriesProvider: CountriesProviding { .sorted { $0.name > $1.name } } - func fetchDefaultCountry() -> Country { - let countries = fetchCountries().map { - // FIXME: - Country(ISO: $0.ISO, name: $0.name, code: $0.code, numberTemplate: $0.placeHolder ?? "") - } + func fetchDefaultCountry() -> CountryModel { + let countries = fetchCountries() let code = (NSLocale.current.regionCode ?? countries.last?.code)?.replacingOccurrences(of: "+", with: "") return countries.first { $0.code == code || $0.ISO == code } ?? countries.last! } diff --git a/Nynja/CountriesProviding.swift b/Nynja/CountriesProviding.swift index 42f966b20..7c5756be3 100644 --- a/Nynja/CountriesProviding.swift +++ b/Nynja/CountriesProviding.swift @@ -6,6 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +typealias Country = CountryModel + protocol CountriesProviding { func fetchCountries() -> [CountryModel] func fetchDefaultCountry() -> Country diff --git a/Nynja/Extensions/CollectionsExtensions.swift b/Nynja/Extensions/CollectionsExtensions.swift index 038ceac38..3a6d3d016 100644 --- a/Nynja/Extensions/CollectionsExtensions.swift +++ b/Nynja/Extensions/CollectionsExtensions.swift @@ -9,9 +9,18 @@ import Foundation extension Collection { + subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } + + func count(where predicate: (Element) -> Bool) -> Int { + var count = 0 + for element in self where predicate(element) { + count += 1 + } + return count + } } extension Collection where Element: Hashable { diff --git a/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift index 4c3fd5ae1..7152cfba7 100644 --- a/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift +++ b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift @@ -27,16 +27,6 @@ extension Array { self = filter { !predicate($0) } } - func count(where predicate: (Element) -> Bool) -> Int { - var count = 0 - for element in self { - if predicate(element) { - count += 1 - } - } - return count - } - mutating func move(at index: Int, to newIndex: Int) { let element = remove(at: index) insert(element, at: newIndex) diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift index d81c26d09..111a3c393 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -14,10 +14,6 @@ protocol AccountSettingsCoordinatorProtocol: class { final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtocol { - struct Parameters {} - - struct Dependencies {} - enum State { case back case chooseAvatar(completion: (UIImage?) -> Void) @@ -33,7 +29,7 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco self.coordinator = coordinator } - func prepareModule(parameters: AccountSettingsWireframe.Parameters, dependencies: AccountSettingsWireframe.Dependencies) -> UIViewController { + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = AccountSettingsPresenter() let viewDep = AccountSettingsViewController.Dependencies(viewsFactory: AccountSettingsViewsFactory(), presenter: presenter) let view = AccountSettingsViewController(dependencies: viewDep) diff --git a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift index 5066cba1e..a9e84eb31 100644 --- a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift @@ -19,7 +19,7 @@ final class AccountSettingsCoordinator: CoordinatorProtocol, AccountSettingsCoor func start() { let wireframe = AccountSettingsWireframe.init(coordinator: self) - let view = wireframe.prepareModule(parameters: AccountSettingsWireframe.Parameters(), dependencies: AccountSettingsWireframe.Dependencies()) + let view = wireframe.prepareModule(dependencies: AccountSettingsWireframe.Dependencies()) navigation.pushViewController(view, animated: true) } diff --git a/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift b/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift index 44f91475b..ffe2ec8d4 100644 --- a/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift +++ b/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift @@ -18,7 +18,11 @@ protocol AddContactViaPhoneWireFrameProtocol: class { func presentAddContact(contact: Contact) func showMyProfile() - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) + func showSelectCountry() +} + +protocol AddContactViaPhoneWireFrameOutput: class { + func didSelectCountry(_ country: Country) } protocol AddContactViaPhoneViewProtocol: LoadingInteractiveView { @@ -28,6 +32,8 @@ protocol AddContactViaPhoneViewProtocol: LoadingInteractiveView { /** * Add here your methods for communication PRESENTER -> VIEW */ + + func setupSelectedCountry(_ country: Country) } protocol AddContactViaPhonePresenterProtocol: BasePresenterProtocol { @@ -40,7 +46,7 @@ protocol AddContactViaPhonePresenterProtocol: BasePresenterProtocol { * Add here your methods for communication VIEW -> PRESENTER */ func presentAddContact(with phoneNumber: String) - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) + func showSelectCountry() } protocol AddContactViaPhoneInteractorOutputProtocol: LoadingInteractive { @@ -56,9 +62,9 @@ protocol AddContactViaPhoneInteractorOutputProtocol: LoadingInteractive { protocol AddContactViaPhoneInteractorInputProtocol: class { var presenter: AddContactViaPhoneInteractorOutputProtocol! { get set } - - func getContactByPhone(number: String) + /** * Add here your methods for communication PRESENTER -> INTERACTOR */ + func getContactByPhone(number: String) } diff --git a/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift b/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift index 531074978..cf87b2270 100644 --- a/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift +++ b/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -final class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresenterProtocol, AddContactViaPhoneInteractorOutputProtocol { +final class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresenterProtocol, AddContactViaPhoneInteractorOutputProtocol, AddContactViaPhoneWireFrameOutput { override var itemsFactory: WCItemsFactory? { return ByNumberItemsFactory() @@ -32,8 +32,8 @@ final class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresen self.wireFrame.showMyProfile() } - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) { - self.wireFrame.showSelectCountry(selectCountryDelegate) + func showSelectCountry() { + self.wireFrame.showSelectCountry() } func showHUD() { @@ -43,4 +43,8 @@ final class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresen func hideHUD() { view.hideSpinner() } + + func didSelectCountry(_ country: Country) { + view.setupSelectedCountry(country) + } } diff --git a/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift b/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift index 8e7e3a02b..2057653c6 100644 --- a/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift +++ b/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift @@ -134,6 +134,11 @@ final class AddContactViaPhoneViewController: BaseVC, AddContactViaPhoneViewProt setupTestingKeysInSubviews() } + + func setupSelectedCountry(_ country: Country) { + setCountryRelatedTextFields(withData: country) + } + //MARK: - 🚀 Actions override func tapOnScreen(recognizer: UITapGestureRecognizer) { self.view.endEditing(true) @@ -155,7 +160,7 @@ final class AddContactViaPhoneViewController: BaseVC, AddContactViaPhoneViewProt @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { self.view.endEditing(true) - self.presenter.showSelectCountry(self) + self.presenter.showSelectCountry() } @objc func handleDeletePhoneNotification(notification: Notification) { @@ -264,12 +269,6 @@ extension AddContactViaPhoneViewController: PhoneFieldDelegate { } } -// MARK: - SelectCountryDelegate -extension AddContactViaPhoneViewController: SelectCountryDelegate { - func selected(_ country: CountryModel) { - self.setCountryRelatedTextFields(withData: country) - } -} // MARK: - LoginViewModifiable extension AddContactViaPhoneViewController: LoginViewModifiable { } diff --git a/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift b/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift index 1869f810b..8ea72ec89 100644 --- a/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift +++ b/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift @@ -13,6 +13,8 @@ final class AddContactViaPhoneWireFrame: AddContactViaPhoneWireFrameProtocol { weak var navigation : UINavigationController? weak var main: MainWireFrame? + weak var output: AddContactViaPhoneWireFrameOutput? + func presentAddContactViaPhone(navigation: UINavigationController, main: MainWireFrame?) { let view = AddContactViaPhoneViewController() let presenter = AddContactViaPhonePresenter() @@ -30,6 +32,8 @@ final class AddContactViaPhoneWireFrame: AddContactViaPhoneWireFrameProtocol { presenter.interactor = interactor interactor.inject(dependencies: interactorDependencies) + self.output = presenter + navigation.pushViewController(view as UIViewController, animated: true) } @@ -41,7 +45,28 @@ final class AddContactViaPhoneWireFrame: AddContactViaPhoneWireFrameProtocol { main?.showProfile() } - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) { - main?.showSelectCountry(selectCountryDelegate) + func showSelectCountry() { + let wireframe = SelectCountryWireFrame(coordinator: self) + let view = wireframe.prepareModule( + dependencies: SelectCountryWireFrame.Dependencies( + countriesProvider: ServiceFactory().makeCountriesProvider() + ) + ) + + main?.navigation?.pushViewController(view, animated: true) + } +} + +// FIXME: temp solution until new navigation login is not implemented. +extension AddContactViaPhoneWireFrame: CountrySelectorCoordinatorProtocol { + + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { + switch state { + case .dismiss: + break + case let .selected(country): + output?.didSelectCountry(country) + } + main?.navigation?.popViewController(animated: true) } } diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 85bd52496..8f9d85476 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -14,7 +14,7 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt private weak var navigation: UINavigationController? private let serviceFactory: ServiceFactoryProtocol - private var selectCountryCallback: ((Result) -> Void)? + private var selectCountryCallback: ((Result) -> Void)? init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { self.navigation = navigation @@ -40,12 +40,13 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt // MARK: - CountrySelectorCoordinatorProtocol extension AuthCoordinator { - func wireframe(_ wireframe: CountrySelectorWireframe, endWithState state: CountrySelectorWireframe.State) { + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { switch state { - case .endWith(let country): selectCountryCallback?(.success(country)) - case .back: break + case .selected(let country): + selectCountryCallback?(.success(country)) + case .dismiss: + break } - navigation?.popViewController(animated: true) } } @@ -62,7 +63,7 @@ extension AuthCoordinator { } private func handleType(_ type: AuthenticationType) { - let view = CreateProfileWireframe(coordinator: self).prepareModule(parameters: CreateProfileWireframe.Parameters(), dependencies: CreateProfileWireframe.Dependencies()) + let view = CreateProfileWireframe(coordinator: self).prepareModule(dependencies: CreateProfileWireframe.Dependencies()) navigation?.pushViewController(view, animated: true) @@ -78,14 +79,18 @@ extension AuthCoordinator { extension AuthCoordinator { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) { switch state { - case .continueLogin(let loginOption): continueLoginProcess(with: loginOption) + case .continueLogin(let loginOption): + continueLoginProcess(with: loginOption) + case .getCountry(let callback): selectCountryCallback = callback - let wireframe = CountrySelectorWireframe(coordinator: self) + + let wireframe = SelectCountryWireFrame(coordinator: self) let view = wireframe.prepareModule( - parameters: NSNull(), - dependencies: CountrySelectorWireframe.Dependencies( - storageService: serviceFactory.makeStorageService())) + dependencies: SelectCountryWireFrame.Dependencies( + countriesProvider: serviceFactory.makeCountriesProvider() + ) + ) navigation?.pushViewController(view, animated: true) } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index 2e7283dbf..c87d5b8e6 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -53,7 +53,7 @@ extension PhoneNumberLoginView { country = config.country countrySelectorAction = config.countrySelectorAction nextAction = config.nextAction - phoneNumberTextFieldController = TextFieldController(template: config.country.numberTemplate) { [weak self] result in + phoneNumberTextFieldController = TextFieldController(template: config.country.placeHolder) { [weak self] result in if result { self?.nextButton.backgroundColor = UIColor.nynja.mainRed } else { @@ -76,12 +76,12 @@ extension PhoneNumberLoginView { func updateCountry(_ country: Country) { self.country = country - phoneNumberTextFieldController?.template = country.numberTemplate + phoneNumberTextFieldController?.template = country.placeHolder countrySelector.setTitle(country.name, for: .normal) countryCodeLabel.text = "+" + country.code - phoneNumberTextField.text = "".updateWithMask(placeHolder: country.numberTemplate) + phoneNumberTextField.text = "".updateWithMask(placeHolder: country.placeHolder) } } @@ -102,10 +102,10 @@ private extension PhoneNumberLoginView { private extension PhoneNumberLoginView { final class TextFieldController: NSObject, UITextFieldDelegate { - var template: String + var template: String? var isFullFilelledAction: ((Bool) -> Void)? - init(template: String, isFullFilelledAction: ((Bool) -> Void)?) { + init(template: String?, isFullFilelledAction: ((Bool) -> Void)?) { self.template = template self.isFullFilelledAction = isFullFilelledAction } @@ -118,17 +118,17 @@ private extension PhoneNumberLoginView { cursorOffsetForNonEmptyString(textField: textField, range: range) : cursorOffsetForEmptyString(textField: textField, range: range) - updateCursorPosition(on: textField, position: range.location + offset) - + textField.cursorPosition = range.location + offset + isFullFilelledAction?(isFullfiled(textField: textField, template: template)) return false } func textFieldDidBeginEditing(_ textField: UITextField) { - let position = calculatedCursorPosition(on: textField) - updateCursorPosition(on: textField, position: position) + textField.cursorPosition = calculatedCursorPosition(on: textField) } + // MARK: - Private @@ -143,7 +143,7 @@ private extension PhoneNumberLoginView { } private func newRange(text: String, oldRange: NSRange,replacementString string: String) -> NSRange { - if string == "", Array(text)[oldRange.location] == " " { + if string == "", Array(text)[safe: oldRange.location] == " " { var range = oldRange range.location = range.location - 1 return range @@ -160,7 +160,7 @@ private extension PhoneNumberLoginView { let index = range.location + 1 let arr = Array(text) - if arr.count > index, arr[index] == " " { + if arr.count > index, arr[safe: index] == " " { return 2 } @@ -172,27 +172,27 @@ private extension PhoneNumberLoginView { return 0 } - return Array(text)[range.location] == " " ? -1 : 0 - } - - private func updateCursorPosition(on textField: UITextField, position: Int) { - guard let newPosition = textField.position(from: textField.beginningOfDocument, offset: position) else { - return - } - - textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + return Array(text)[safe: range.location] == " " ? -1 : 0 } private func calculatedCursorPosition(on textField: UITextField) -> Int { - return textField.text? - .enumerated() - .filter { $1 != " " && $1 != "\u{2013}" } - .last? - .offset ?? 0 + guard let text = textField.text else { + return 0 + } + let cursorIndex = text.lastIndex { $0 != " " && $0 != "\u{2013}" } ?? text.startIndex + return cursorIndex.encodedOffset } - private func isFullfiled(textField: UITextField, template: String) -> Bool { - return (textField.text ?? "").filter { $0 != "\u{2013}" }.count == template.count + private func isFullfiled(textField: UITextField, template: String?) -> Bool { + guard let text = textField.text else { + return false + } + guard let template = template else { + // If don't have number template, just check for non empty input. + return !text.isEmpty + } + + return text.count { $0 != "\u{2013}" } == template.count } } } diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 1e41c67dc..29cd1f608 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -28,7 +28,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { enum State { case continueLogin(loginOption: LoginOption) - case getCountry(callback: (Result) -> Void) + case getCountry(callback: (Result) -> Void) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -46,7 +46,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { return view } - func selectCountry(completion: @escaping (Result) -> Void) { + func selectCountry(completion: @escaping (Result) -> Void) { coordinator.wireframe(self, didEndWithState: .getCountry(callback: completion)) } diff --git a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index ff2c5415b..1dc3a59cf 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -25,8 +25,6 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto let authType: AuthProviderType } - struct Dependencies { } - enum State { case validCode(type: AuthenticationType) case invalidCode diff --git a/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift b/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift deleted file mode 100644 index 9df1d1f98..000000000 --- a/Nynja/Modules/Auth/CountrySelector/CountrySelectorProtocols.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// CountrySelectorProtocols.swift -// Nynja -// -// Created by Ash on 9/26/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol CountrySelectorWireframeProtocol: NavigationProtocol { - func selectCountry(_ country: Country) -} - -protocol CountrySelectorViewProtocol: class where Self: UIViewController { - func reloadData() -} - -protocol CountrySelectorPresenterProtocol: NavigationProtocol { - var sections: [CountriesSection] { get } - - func viewDidLoad() - - func applyFilter(_ filter: String) - func selectCountry(_ country: Country) -} - -protocol CountrySelectorInteractorOutputProtocol: class { - func filteredCountries(_ countries: [Country]) -} - -protocol CountrySelectorInteractorInputProtocol: class { - var filter: String { get set } - var filteredCountries: [Country] { get } -} diff --git a/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift b/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift deleted file mode 100644 index 870fd83da..000000000 --- a/Nynja/Modules/Auth/CountrySelector/Entities/Country.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Country.swift -// Nynja -// -// Created by Ash on 9/26/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -struct CountriesSection { - let symbol: String - var countries: [Country] -} - -struct Country: Equatable, Hashable { - let ISO: String - let name: String - let code: String - let numberTemplate: String -} diff --git a/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift b/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift deleted file mode 100644 index 4e24eb22a..000000000 --- a/Nynja/Modules/Auth/CountrySelector/Interactor/CountrySelectorInteractor.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// CountrySelectorInteractor.swift -// Nynja -// -// Created by Ash on 9/27/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -final class CountrySelectorInteractor: CountrySelectorInteractorInputProtocol, SetInjectable { - private weak var presenter: CountrySelectorInteractorOutputProtocol? - private var storageService: StorageService? - - var filter: String = "" { - didSet { - guard filter != "" else { - return filteredCountries = countries - } - - filteredCountries = countries.filter { - $0.name.contains(substring: filter, options: .caseInsensitive) - } - } - } - - private var countries: [Country] { - get { - return (storageService?.countries.map { - Country(ISO: $0.ISO, name: $0.name, code: $0.code, numberTemplate: $0.placeHolder ?? "") - } ?? []).sorted { $0.name < $1.name } - } - } - - private(set) var filteredCountries: [Country] = [] { - didSet { - presenter?.filteredCountries(filteredCountries) - } - } -} - -// MARK: - SetInjectable - -extension CountrySelectorInteractor { - struct Dependencies { - let presenter: CountrySelectorInteractorOutputProtocol - let storageService: StorageService - } - - func inject(dependencies: CountrySelectorInteractor.Dependencies) { - presenter = dependencies.presenter - storageService = dependencies.storageService - } -} diff --git a/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift b/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift deleted file mode 100644 index 2559b854c..000000000 --- a/Nynja/Modules/Auth/CountrySelector/Presenter/CountrySelectorPresenter.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// CountrySelectorPresenter.swift -// Nynja -// -// Created by Ash on 9/27/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -final class CountrySelectorPresenter: CountrySelectorInteractorOutputProtocol, CountrySelectorPresenterProtocol, SetInjectable { - private weak var view: CountrySelectorViewProtocol? - private var interactor: CountrySelectorInteractorInputProtocol? - private var wireframe: CountrySelectorWireframeProtocol? - - private(set) var sections: [CountriesSection] = [] { - didSet { - view?.reloadData() - } - } - - func viewDidLoad() { - interactor?.filter = "" - } - - func applyFilter(_ filter: String) { - interactor?.filter = filter - } - - func selectCountry(_ country: Country) { - wireframe?.selectCountry(country) - } - - func back() { - wireframe?.back() - } -} - -// MARK: - SetInjectable - -extension CountrySelectorPresenter { - struct Dependencies { - let view: CountrySelectorViewProtocol - let interactor: CountrySelectorInteractorInputProtocol - let wireframe: CountrySelectorWireframe - } - - func inject(dependencies: CountrySelectorPresenter.Dependencies) { - view = dependencies.view - interactor = dependencies.interactor - wireframe = dependencies.wireframe - } -} - -// MARK: - CountrySelectorInteractorOutputProtocol - -extension CountrySelectorPresenter { - func filteredCountries(_ countries: [Country]) { - let filterAction: (String, [Country]) -> [Country] = { symbol, countries in - countries.filter { $0.name.first == symbol.first } - } - - sections = countries - .compactMap { $0.name.first } - .map { String($0) } - .uniqueWithPreservedOrder() - .map { CountriesSection(symbol: $0, countries: filterAction($0, countries)) } - } -} diff --git a/Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift b/Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift deleted file mode 100644 index 433565986..000000000 --- a/Nynja/Modules/Auth/CountrySelector/View/Cells/CountryTVCell.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// CountryTVCell.swift -// Nynja -// -// Created by Ash on 9/26/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit -import SnapKit - -final class CountryTVCell: UITableViewCell, Configurable, IdentityProtocol { - static var identifier: String = "CountryTVCell" - - private var titleLabel: UILabel? - private var codeLabel: UILabel? -} - -// MARK: - Configurable - -extension CountryTVCell { - struct Config { - let name: String - let code: String - } - - func configure(config: CountryTVCell.Config) { - separatorInset = UIEdgeInsets( - top: separatorInset.top, - left: CGFloat(SelfLayout.separatorLeftInset), - bottom: separatorInset.bottom, - right: separatorInset.right) - - backgroundColor = UIColor.nynja.clear - contentView.backgroundColor = UIColor.nynja.clear - - titleLabel = titleLabel ?? makeTitleLabel(on: contentView) - codeLabel = codeLabel ?? makeCodeLabel(on: contentView) - - titleLabel?.text = config.name - codeLabel?.text = "+" + config.code - } -} - -// MARK: - ui factory methods - -private extension CountryTVCell { - func makeTitleLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.textAlignment = .left - label.textColor = UIColor.nynja.white - label.font = FontFamily.NotoSans.medium.font(size: CGFloat(TitleLabelLayout.fontSize)) - - label.snp.makeConstraints { (make) in - make.left.equalTo(TitleLabelLayout.left) - make.height.equalTo(TitleLabelLayout.height) - make.centerY.equalToSuperview() - } - - return label - } - - func makeCodeLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.textAlignment = .right - label.textColor = UIColor.nynja.dustyGray - label.font = FontFamily.NotoSans.medium.font(size: CGFloat(CodeLabelLayout.fontSize)) - - label.snp.makeConstraints { (make) in - make.right.equalTo(-CodeLabelLayout.right) - make.height.equalTo(CodeLabelLayout.height) - make.centerY.equalToSuperview() - } - - return label - } -} - -// MARK: - Layout - -private extension CountryTVCell { - enum TitleLabelLayout { - static let left = 68.0 - static let height = 22.0 - - static let fontSize = 16.0 - } - - enum CodeLabelLayout { - static let right = 16.0 - static let height = 20.0 - - static let fontSize = 14.0 - } - - enum SelfLayout { - static let separatorLeftInset = 68.0 - } -} diff --git a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift deleted file mode 100644 index e1759e46b..000000000 --- a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewController.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// CountrySelectorViewController.swift -// Nynja -// -// Created by Ash on 9/26/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class CountrySelectorViewController: UIViewController, CountrySelectorViewProtocol, InitializeInjectable, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate, KeyboardInteractive { - private let presenter: CountrySelectorPresenterProtocol - private let viewsFactory: CountrySelectorViewsFactoryProtocol - - private lazy var topHeaderLayoutGuide: UILayoutGuide = viewsFactory.makeTopLayoutGuide(on: view) - private lazy var headerView: NavigationView = viewsFactory.makeHeaderView(on: view, layoutGuide: topHeaderLayoutGuide, presenter: presenter) - private lazy var tableView: UITableView = viewsFactory.makeTableView(on: view, top: headerView, bottom: controlContainerView, delegate: self) - private lazy var controlContainerView: NynjaControlContainerView = viewsFactory.makeControlContainerView(on: view, searchField: searchField, verticalInsetCalculationAction: adjustVerticalInset) - - private lazy var searchField: NynjaSearchField = viewsFactory.makeSearchField(presenter: presenter) - - init(dependencies: Dependencies) { - presenter = dependencies.presenter - viewsFactory = dependencies.viewsFactory - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = UIColor.nynja.darkLight - - _ = [topHeaderLayoutGuide, headerView, tableView, controlContainerView, searchField] - - presenter.viewDidLoad() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - registerForKeyboardNotifications() - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - unregisterForKeyboardNotifications() - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } -} - -// MARK: - SetInjectable - -extension CountrySelectorViewController { - struct Dependencies { - let presenter: CountrySelectorPresenterProtocol - let viewsFactory: CountrySelectorViewsFactoryProtocol - } -} - -// MARK: - CountrySelectorViewProtocol - -extension CountrySelectorViewController { - func reloadData() { - tableView.reloadData() - } -} - -// MARK: - UITableViewDelegate - -extension CountrySelectorViewController { - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let view = CountryHeaderTVView() - let section = presenter.sections[section] - - view.configure(config: CountryHeaderTVView.Config(symbol: section.symbol)) - - return view - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 52 - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 36 - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - presenter.selectCountry(countryWithIndexPath(indexPath)) - } -} - -// MARK: - UITableViewDataSource - -extension CountrySelectorViewController { - func numberOfSections(in tableView: UITableView) -> Int { - return presenter.sections.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let sections = presenter.sections - let currentSection = sections[section] - let countriesInSection = currentSection.countries - - return countriesInSection.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: CountryTVCell.identifier, for: indexPath) as? CountryTVCell else { - fatalError() - } - - let country = countryWithIndexPath(indexPath) - cell.configure(config: CountryTVCell.Config(name: country.name, code: country.code)) - - return cell - } -} - -// MARK: - UIScrollViewDelegate - -extension CountrySelectorViewController { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let verticalIndicator = scrollView.subviews[scrollView.subviews.count - 1] - - verticalIndicator.backgroundColor = UIColor.nynja.mainRed - } -} - -// MARK: - KeyboardInteractive - -extension CountrySelectorViewController { - func keyboardNotified(endFrame: CGRect) { - if endFrame.origin.y >= UIScreen.main.bounds.size.height { - updateToHide(view: controlContainerView, offset: -28) - } else { - updateToShow(view: controlContainerView, offset: -28 - endFrame.height) - } - } -} - -// MARK: - Private - -private extension CountrySelectorViewController { - func sectionWithIndexPath(_ indexPath: IndexPath) -> CountriesSection { - return presenter.sections[indexPath.section] - } - - func countryWithIndexPath(_ indexPath: IndexPath) -> Country { - return sectionWithIndexPath(indexPath).countries[indexPath.row] - } -} diff --git a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift b/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift deleted file mode 100644 index af8ed7095..000000000 --- a/Nynja/Modules/Auth/CountrySelector/View/CountrySelectorViewsFactory.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// CountrySelectorViewsFactory.swift -// Nynja -// -// Created by Ash on 10/10/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation -import SnapKit - -typealias VerticalInsetCalculationAction = (_ inset: AdjustedInset, _ make: ConstraintMaker, _ offset: Int) -> Void - -protocol CountrySelectorViewsFactoryProtocol { - func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide - func makeHeaderView(on view: UIView, layoutGuide: UILayoutGuide, presenter: CountrySelectorPresenterProtocol) -> NavigationView - func makeTableView(on view: UIView, top: UIView, bottom: UIView, delegate: UITableViewDelegate & UITableViewDataSource) -> UITableView - func makeControlContainerView(on view: UIView, searchField: NynjaSearchField, verticalInsetCalculationAction: VerticalInsetCalculationAction) -> NynjaControlContainerView - func makeSearchField(presenter: CountrySelectorPresenterProtocol) -> NynjaSearchField -} - -final class CountrySelectorViewsFactory: CountrySelectorViewsFactoryProtocol { - func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide { - let layoutGuide = UILayoutGuide() - view.addLayoutGuide(layoutGuide) - - layoutGuide.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.height.equalTo(20 + UIWindow.safeAreaTopPadding()) - } - - return layoutGuide - } - - func makeHeaderView(on view: UIView, layoutGuide: UILayoutGuide, presenter: CountrySelectorPresenterProtocol) -> NavigationView { - let config = NavigationView.Config( - isVisibleSeparator: true, - isVisibleBackButton: true, - title: "select country".uppercased(), - navigationHandler: presenter, - backButtonImage: UIImage.nynja.icBackNavigation.image) - - return UIView.makeHeaderView(on: view, top: layoutGuide, config: config) - } - - func makeTableView(on view: UIView, top: UIView, bottom: UIView, delegate: UITableViewDelegate & UITableViewDataSource) -> UITableView { - let tableView = UITableView() - view.addSubview(tableView) - - tableView.keyboardDismissMode = .interactive - tableView.backgroundColor = UIColor.nynja.clear - tableView.separatorStyle = .singleLine - tableView.tableFooterView = UIView() - tableView.showsHorizontalScrollIndicator = false - tableView.rowHeight = 52 - tableView.estimatedRowHeight = tableView.rowHeight - - tableView.delegate = delegate - tableView.dataSource = delegate - - tableView.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom) - make.bottom.equalTo(bottom.snp.top) - make.left.right.equalToSuperview() - } - - tableView.register(CountryTVCell.self, - forCellReuseIdentifier: CountryTVCell.identifier) - - return tableView - } - - func makeControlContainerView(on view: UIView, searchField: NynjaSearchField, verticalInsetCalculationAction: VerticalInsetCalculationAction) -> NynjaControlContainerView { - let containerView = NynjaControlContainerView(contentView: searchField) - view.addSubview(containerView) - - containerView.snp.makeConstraints { make in - make.left.right.equalToSuperview() - verticalInsetCalculationAction(.bottom, make, -28) - } - - containerView.addGradientView() - - return containerView - } - - func makeSearchField(presenter: CountrySelectorPresenterProtocol) -> NynjaSearchField { - let searchField = NynjaSearchField() - - searchField.searchTextChangeHandler = { [weak self] searchQuery in - presenter.applyFilter(searchQuery ?? "") - } - - return searchField - } -} diff --git a/Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift b/Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift deleted file mode 100644 index b436967eb..000000000 --- a/Nynja/Modules/Auth/CountrySelector/View/Headers/CountryTVHeader.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// CountryTVHeader.swift -// Nynja -// -// Created by Ash on 9/26/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit -import SnapKit - - -final class CountryHeaderTVView: UIView, Configurable, IdentityProtocol { - static var identifier: String = "CountryHeaderTVView" - - private var title: UILabel? -} - -// MARK: - Configurable - -extension CountryHeaderTVView { - struct Config { - let symbol: String - } - - func configure(config: CountryHeaderTVView.Config) { - backgroundColor = UIColor.nynja.clear - - title = title ?? makeTitleLabel(on: self) - title?.text = config.symbol - } -} - -// MARK: - UI factory methods - -private extension CountryHeaderTVView { - func makeTitleLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.layer.cornerRadius = CGFloat(TitleLabelLayout.cornerRadius) - - label.backgroundColor = UIColor.nynja.mainRed - label.clipsToBounds = true - - label.textColor = UIColor.nynja.white - label.textAlignment = .center - label.font = FontFamily.NotoSans.medium.font(size: CGFloat(TitleLabelLayout.fontSize)) - - label.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - make.width.equalTo(TitleLabelLayout.width) - make.height.equalTo(TitleLabelLayout.height) - make.left.equalTo(TitleLabelLayout.left) - } - - return label - } -} - -// MARK: - Layout - -private extension CountryHeaderTVView { - enum TitleLabelLayout { - static let width = 28.0 - static let height = 28.0 - - static let cornerRadius = width / 2 - - static let left = 16.0 - - static let fontSize = 16.0 - } -} diff --git a/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift b/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift deleted file mode 100644 index 704dce30c..000000000 --- a/Nynja/Modules/Auth/CountrySelector/Wireframe/CountrySelectorWireframe.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// CountrySelectorWireframe.swift -// Nynja -// -// Created by Ash on 9/27/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol CountrySelectorCoordinatorProtocol { - func wireframe(_ wireframe: CountrySelectorWireframe, endWithState state: CountrySelectorWireframe.State) -} - -final class CountrySelectorWireframe: Wireframe, CountrySelectorWireframeProtocol { - typealias Parameters = NSNull - - struct Dependencies { - let storageService: StorageService - } - - enum State { - case back - case endWith(country: Country) - } - - private let coordinator: CountrySelectorCoordinatorProtocol - - init(coordinator: CountrySelectorCoordinatorProtocol) { - self.coordinator = coordinator - } - - func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { - let presenter = CountrySelectorPresenter() - let viewDep = CountrySelectorViewController.Dependencies(presenter: presenter, viewsFactory: CountrySelectorViewsFactory()) - let view = CountrySelectorViewController(dependencies: viewDep) - let interactor = CountrySelectorInteractor() - - let presenterDep = CountrySelectorPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) - let interactorDep = CountrySelectorInteractor.Dependencies(presenter: presenter, storageService: dependencies.storageService) - - presenter.inject(dependencies: presenterDep) - interactor.inject(dependencies: interactorDep) - - return view - } - - func back() { - coordinator.wireframe(self, endWithState: .back) - } - - func selectCountry(_ country: Country) { - coordinator.wireframe(self, endWithState: .endWith(country: country)) - } -} diff --git a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift index c6e45ab28..d53043eb5 100644 --- a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -13,10 +13,7 @@ protocol CreateProfileCoordinatorProtocol: class { } final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { - struct Parameters {} - - struct Dependencies {} - + enum State { case back case next diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Wireframe/CameraSettingsWireframe.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Wireframe/CameraSettingsWireframe.swift index ee1539e88..b4dd003e8 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Wireframe/CameraSettingsWireframe.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Wireframe/CameraSettingsWireframe.swift @@ -8,14 +8,14 @@ import UIKit -protocol CameraSettingsCoordinatorProtocol { +protocol CameraSettingsCoordinatorProtocol: class { func wireframe(_ wireframe: CameraSettingsWireFrame, with mainView: UIViewController, didEndWithState state: CameraSettingsWireFrame.State) } -final class CameraSettingsWireFrame: CameraSettingsWireFrameProtocol { - private var coordinator: CameraSettingsCoordinatorProtocol +final class CameraSettingsWireFrame: Wireframe, CameraSettingsWireFrameProtocol { + private let coordinator: CameraSettingsCoordinatorProtocol init(coordinator: CameraSettingsCoordinatorProtocol) { self.coordinator = coordinator @@ -25,7 +25,6 @@ final class CameraSettingsWireFrame: CameraSettingsWireFrameProtocol { //MARK: - CameraSettingsWireFrameProtocol extension CameraSettingsWireFrame { - typealias Parameters = NSNull struct Dependencies { let cameraSettingsService: CameraSettingsServiceProtocol @@ -36,7 +35,7 @@ extension CameraSettingsWireFrame { case detail(qualityType: CameraQualityType) } - func prepareModule(parameters: NSNull, dependencies: CameraSettingsWireFrame.Dependencies) -> UIViewController { + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let view = CameraSettingsViewController() let presenter = CameraSettingsPresenter() let interactor = CameraSettingsInteractor() diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift index 1f60adb9b..952845633 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift @@ -23,7 +23,7 @@ final class CameraSettingsFlowCoordinator: CameraSettingsFlowCoordinatorProtocol extension CameraSettingsFlowCoordinator { func start() { let wireframe = CameraSettingsWireFrame(coordinator: self) - let view = wireframe.prepareModule(parameters: NSNull(), dependencies: CameraSettingsWireFrame.Dependencies(cameraSettingsService: serviceFactory.makeCameraSettingsService(with: sourceFlow))) + let view = wireframe.prepareModule(dependencies: CameraSettingsWireFrame.Dependencies(cameraSettingsService: serviceFactory.makeCameraSettingsService(with: sourceFlow))) mainFlowVC = view diff --git a/Nynja/Modules/Main/MainProtocols.swift b/Nynja/Modules/Main/MainProtocols.swift index 7ab217766..5d759e17c 100644 --- a/Nynja/Modules/Main/MainProtocols.swift +++ b/Nynja/Modules/Main/MainProtocols.swift @@ -82,7 +82,6 @@ protocol MainWireFrameProtocol: class { func showMySelfChat(contact: Contact) func showFavorites() func showQRGenerator() - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) func showAddContactByUserName() func showWallet(for profile: Profile) func getRecentsLocation() -> [LocationType] diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index acd7a8641..3759a9040 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -112,12 +112,6 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { } } - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) { - if let navigation = self.navigation { - SelectCountryWireFrame().presentSelectCountry(navigation: navigation, main: self, selectCountryDelegate: selectCountryDelegate) - } - } - func openMarketplace() { if let navigation = self.navigation { MarketplaceWireFrame().presentMarketplace(navigation: navigation, main: self) diff --git a/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift b/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift new file mode 100644 index 000000000..e8dba2f38 --- /dev/null +++ b/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift @@ -0,0 +1,12 @@ +// +// CountriesSection.swift +// Nynja +// +// Created by Anton Poltoratskyi on 07.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct CountriesSection { + let symbol: String + let countries: [CountryModel] +} diff --git a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift index 4515c8a3c..f924d2de5 100644 --- a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift +++ b/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift @@ -6,27 +6,55 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -final class SelectCountryInteractor: SelectCountryInteractorInputProtocol { +final class SelectCountryInteractor: SelectCountryInteractorInputProtocol, InitializeInjectable { - weak var presenter: SelectCountryInteractorOutputProtocol! + private weak var presenter: SelectCountryInteractorOutputProtocol! - private var countries: [CountryModel] = [] + private(set) lazy var countries: [Country] = { + return countriesProvider.fetchCountries() + }() - func getCountriesList() { - countries = StorageService.sharedInstance.countries - prepareCountriesData(countries) + + // MARK: - Services + + private let countriesProvider: CountriesProviding + + + // MARK: - Init + + struct Dependencies { + let presenter: SelectCountryInteractorOutputProtocol + let countriesProvider: CountriesProviding + } + + init(dependencies: Dependencies) { + self.presenter = dependencies.presenter + self.countriesProvider = dependencies.countriesProvider + } + + + // MARK: - SelectCountryInteractorInputProtocol + + func getCountries() { + setup(countries) } - func filterList(with text: String) { - let filtered = countries.filter { $0.name.contains(substring: text, options: .caseInsensitive) } - prepareCountriesData(filtered) + func filter(with text: String) { + let filteredCountries = countries.filter { $0.name.contains(substring: text, options: .caseInsensitive) } + setup(filteredCountries) } - private func prepareCountriesData(_ countries: [CountryModel]) { - let groupedCountries = countries.groupedDicrionary(transform: { String($0.name.first!) }, - comparator: { $0.name < $1.name }) - let keys = groupedCountries.keys.sorted() + private func setup(_ countries: [Country]) { + let groupedCountries = countries.groupedDicrionary( + transform: { String($0.name.first!) }, + comparator: { $0.name < $1.name } + ) + + let sections: [CountriesSection] = groupedCountries.keys.sorted().map { + let countries = groupedCountries[$0] ?? [] + return CountriesSection(symbol: $0, countries: countries) + } - presenter.preparedCountriesList(titles: keys, countries: groupedCountries) + presenter.didFetch(sections: sections) } } diff --git a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift index 9b4d357fd..bf8bf373e 100644 --- a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift +++ b/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift @@ -8,26 +8,45 @@ final class SelectCountryPresenter: SelectCountryPresenterProtocol { - weak var view: SelectCountryViewProtocol! - var interactor: SelectCountryInteractorInputProtocol! - var wireFrame: SelectCountryWireFrameProtocol! + private weak var view: SelectCountryViewProtocol! + private var interactor: SelectCountryInteractorInputProtocol! + private var wireframe: SelectCountryWireFrameProtocol! func getCountries() { - interactor.getCountriesList() + interactor.getCountries() } - func filterList(with text: String) { - interactor.filterList(with: text) + func filter(with text: String) { + interactor.filter(with: text) + } + + func didSelect(country: CountryModel) { + wireframe.didSelect(country: country) } func dismiss() { - wireFrame.dismiss() + wireframe.dismiss() } } extension SelectCountryPresenter: SelectCountryInteractorOutputProtocol { - func preparedCountriesList(titles: [String], countries: [String: [CountryModel]]) { - view.updateCountriesList(titles: titles, countries: countries) + func didFetch(sections: [CountriesSection]) { + view.setup(sections: sections) + } +} + +extension SelectCountryPresenter: SetInjectable { + + struct Dependencies { + let view: SelectCountryViewProtocol + let interactor: SelectCountryInteractorInputProtocol + let wireframe: SelectCountryWireFrameProtocol + } + + func inject(dependencies: SelectCountryPresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireframe = dependencies.wireframe } } diff --git a/Nynja/Modules/SelectCountry/SelctCountryDelegate.swift b/Nynja/Modules/SelectCountry/SelctCountryDelegate.swift deleted file mode 100644 index ed9db6616..000000000 --- a/Nynja/Modules/SelectCountry/SelctCountryDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// SelctCountryDelegate.swift -// Nynja -// -// Created by Roma Chopovenko on 2/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol SelectCountryDelegate: class { - func selected(_ country: CountryModel) -} diff --git a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift b/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift index 1ef6f9d48..507860b2f 100644 --- a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift +++ b/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift @@ -8,58 +8,36 @@ import UIKit -protocol SelectCountryWireFrameProtocol: class { - - func presentSelectCountry(navigation: UINavigationController, main: MainWireFrame?, selectCountryDelegate: SelectCountryDelegate) - - /** - * Add here your methods for communication PRESENTER -> WIREFRAME - */ +// MARK: - Wireframe +protocol SelectCountryWireFrameProtocol: class { + func didSelect(country: CountryModel) func dismiss() } +// MARK: - View + protocol SelectCountryViewProtocol: class { - - var presenter: SelectCountryPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ - - func updateCountriesList(titles: [String], countries: [String: [CountryModel]]) + func setup(sections: [CountriesSection]) } -protocol SelectCountryPresenterProtocol: class { - - var view: SelectCountryViewProtocol! { get set } - var interactor: SelectCountryInteractorInputProtocol! { get set } - var wireFrame: SelectCountryWireFrameProtocol! { get set } +// MARK: - Presenter - /** - * Add here your methods for communication VIEW -> PRESENTER - */ +protocol SelectCountryPresenterProtocol: class { func getCountries() - func filterList(with text: String) + func filter(with text: String) + func didSelect(country: CountryModel) func dismiss() } -protocol SelectCountryInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ - - func preparedCountriesList(titles: [String], countries: [String: [CountryModel]]) -} +// MARK: - Interactor protocol SelectCountryInteractorInputProtocol: class { - - var presenter: SelectCountryInteractorOutputProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ - func getCountriesList() - func filterList(with text: String) + func getCountries() + func filter(with text: String) +} + + +protocol SelectCountryInteractorOutputProtocol: class { + func didFetch(sections: [CountriesSection]) } diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift index 91f57c877..aefc3ab57 100644 --- a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift +++ b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift @@ -9,47 +9,44 @@ import UIKit import NynjaUIKit -final class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardInteractive { +final class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardInteractive, NavigationProtocol { var presenter: SelectCountryPresenterProtocol! private let topInset = CGFloat(Constraints.tableView.topInset.adjustedByWidth) private let bottomInset = Constraints.controlsContainerView.bottomInset.adjustedByWidth - private var sections: [String] = [] - private var countriesDictionary: [String : [CountryModel]] = [:] + private var sections: [CountriesSection] = [] private(set) var scrollBar: ScrollBar? var scrollOffset: CGFloat = 0 - weak var countryDelegate: SelectCountryDelegate? - // MARK: - Views private lazy var tableView: UITableView = { - let table = UITableView() - table.keyboardDismissMode = .interactive - table.backgroundColor = UIColor.nynja.clear - table.separatorStyle = .none - table.showsHorizontalScrollIndicator = false - table.tableFooterView = UIView() + let tableView = UITableView() + tableView.keyboardDismissMode = .interactive + tableView.backgroundColor = UIColor.nynja.clear + tableView.separatorStyle = .none + tableView.showsHorizontalScrollIndicator = false + tableView.tableFooterView = UIView() - table.sectionHeaderHeight = SelectCountryHeaderView.Constraints.height.adjustedByWidth - table.estimatedSectionHeaderHeight = table.sectionHeaderHeight + tableView.sectionHeaderHeight = SelectCountryHeaderView.Constraints.height.adjustedByWidth + tableView.estimatedSectionHeaderHeight = tableView.sectionHeaderHeight - table.rowHeight = CountryCellModel.Cell.Constraints.height.adjustedByWidth - table.estimatedRowHeight = table.rowHeight + tableView.rowHeight = CountryCellModel.Cell.Constraints.height.adjustedByWidth + tableView.estimatedRowHeight = tableView.rowHeight - table.contentInset.top = topInset + tableView.contentInset.top = topInset - view.addSubview(table) - table.snp.makeConstraints { make in + view.addSubview(tableView) + tableView.snp.makeConstraints { make in make.top.equalTo(navigationView.snp.bottom) make.left.right.equalToSuperview() } - return table + return tableView }() private lazy var controlContainerView: NynjaControlContainerView = { @@ -63,11 +60,7 @@ final class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, Keyb } containerView.addGradientView() - - containerView.addCloseButton { [weak self] _ in - self?.onCloseButtonTapped() - } - + return containerView }() @@ -75,7 +68,7 @@ final class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, Keyb let searchField = NynjaSearchField() searchField.searchTextChangeHandler = { [weak self] searchQuery in let text = searchQuery ?? "" - self?.presenter.filterList(with: text) + self?.presenter.filter(with: text) } return searchField }() @@ -85,13 +78,23 @@ final class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, Keyb override func initialize() { super.initialize() + screenTitle = Constants.LocalizableKeys.selectCountry.localized.uppercased() + setupNavigationView() + setupTableView() + } + + private func setupNavigationView() { + let config = NavigationView.Config(isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: self, + backButtonImage: UIImage.nynja.icBackNavigation.image) - setupData() - setupLayout() + navigationView.configure(config: config) } - private func setupData(){ + private func setupTableView(){ tableView.register(viewModel: CountryCellModel.self) tableView.register(headerFooter: SelectCountryHeaderView.self) @@ -101,26 +104,22 @@ final class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, Keyb presenter.getCountries() scrollBar = ScrollBar(scrollView: tableView) - } - - private func setupLayout() { - navigationView.isSeparatorVisible = true + controlContainerView.isHidden = false } // MARK: - SelectCountryViewProtocol - func updateCountriesList(titles: [String], countries: [String: [CountryModel]]){ - sections = titles - countriesDictionary = countries + func setup(sections: [CountriesSection]){ + self.sections = sections tableView.reloadData() } // MARK: - Actions - @objc private func onCloseButtonTapped() { + func back() { view.endEditing(true) presenter.dismiss() } @@ -148,8 +147,7 @@ extension SelectCountryViewController: UITableViewDataSource { } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let sectionKey = sections[section] - return countriesDictionary[sectionKey]?.count ?? 0 + return sections[section].countries.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -157,13 +155,12 @@ extension SelectCountryViewController: UITableViewDataSource { } private func sectionTitle(for section: Int) -> String { - return sections[section] + return sections[section].symbol } private func country(at indexPath: IndexPath) -> CountryModel { - let sectionKey = sectionTitle(for: indexPath.section) - let countries = countriesDictionary[sectionKey]! - return countries[indexPath.row] + let section = sections[indexPath.section] + return section.countries[indexPath.row] } private func cellModel(at indexPath: IndexPath) -> AnyCellViewModel { @@ -176,8 +173,7 @@ extension SelectCountryViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectedCountry = country(at: indexPath) - countryDelegate?.selected(selectedCountry) - presenter.dismiss() + presenter.didSelect(country: selectedCountry) } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift index 2a6a9cc4d..11d55ce82 100644 --- a/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift +++ b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift @@ -6,7 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation import NynjaUIKit struct CountryCellModel: CellViewModel { diff --git a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift b/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift index fafa63298..facae3cd7 100644 --- a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift +++ b/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift @@ -8,31 +8,55 @@ import UIKit -final class SelectCountryWireFrame: SelectCountryWireFrameProtocol { +protocol CountrySelectorCoordinatorProtocol: class { + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) +} + +final class SelectCountryWireFrame: Wireframe, SelectCountryWireFrameProtocol { - weak var navigation : UINavigationController? - weak var main: MainWireFrame? + // MARK: - Init + + private let coordinator: CountrySelectorCoordinatorProtocol + + init(coordinator: CountrySelectorCoordinatorProtocol) { + self.coordinator = coordinator + } + + + // MARK: - Wireframe - func presentSelectCountry(navigation: UINavigationController, main: MainWireFrame?, selectCountryDelegate: SelectCountryDelegate) { - let view = SelectCountryViewController() + struct Dependencies { + let countriesProvider: CountriesProviding + } + + enum State { + case dismiss + case selected(country: CountryModel) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = SelectCountryPresenter() - let interactor = SelectCountryInteractor() - - self.navigation = navigation - self.main = main - // Connecting + let view = SelectCountryViewController() view.presenter = presenter - view.countryDelegate = selectCountryDelegate - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - navigation.pushViewController(view, animated: true) + let interactor = SelectCountryInteractor(dependencies: + .init(presenter: presenter, countriesProvider: dependencies.countriesProvider) + ) + + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + + return view + } + + + // MARK: - SelectCountryWireFrameProtocol + + func didSelect(country: CountryModel) { + coordinator.wireframe(self, endWithState: .selected(country: country)) } func dismiss() { - navigation?.popToRootViewController(animated: true) + coordinator.wireframe(self, endWithState: .dismiss) } } diff --git a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift b/Nynja/Viper/BaseModule/Wireframe/NavigableWireframeProtocol.swift similarity index 65% rename from Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift rename to Nynja/Viper/BaseModule/Wireframe/NavigableWireframeProtocol.swift index 28609eb32..71d42f8ce 100644 --- a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift +++ b/Nynja/Viper/BaseModule/Wireframe/NavigableWireframeProtocol.swift @@ -1,21 +1,13 @@ // -// WireframeProtocol.swift +// NavigableWireframeProtocol.swift // Nynja // -// Created by AshCenso on 5/25/18. +// Created by Anton Poltoratskyi on 07.11.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // import Foundation -protocol Wireframe: class { - associatedtype Parameters - associatedtype Dependencies - associatedtype State - - func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController -} - protocol NavigableWireframeProtocol: class { var navigation: UINavigationController? { get } diff --git a/Nynja/Viper/BaseModule/Wireframe/Wireframe.swift b/Nynja/Viper/BaseModule/Wireframe/Wireframe.swift new file mode 100644 index 000000000..e5f03fbf2 --- /dev/null +++ b/Nynja/Viper/BaseModule/Wireframe/Wireframe.swift @@ -0,0 +1,38 @@ +// +// Wireframe.swift +// Nynja +// +// Created by AshCenso on 5/25/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol Wireframe: class { + associatedtype Parameters = Void + associatedtype Dependencies = Void + associatedtype State + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController +} + +extension Wireframe where Parameters == Void { + + func prepareModule(dependencies: Dependencies) -> UIViewController { + return prepareModule(parameters: (), dependencies: dependencies) + } +} + +extension Wireframe where Dependencies == Void { + + func prepareModule(parameters: Parameters) -> UIViewController { + return prepareModule(parameters: parameters, dependencies: ()) + } +} + +extension Wireframe where Parameters == Void, Dependencies == Void { + + func prepareModule() -> UIViewController { + return prepareModule(parameters: (), dependencies: ()) + } +} -- GitLab From ac0d87f2008277669039bbfe9d56f52d226ebd3b Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 7 Nov 2018 17:32:46 +0200 Subject: [PATCH 086/138] [NY-4907] Remove countries fetch from StorageService. Rename CountryModel to Country. --- Nynja.xcodeproj/project.pbxproj | 30 ++------ Nynja/CountriesProvider.swift | 9 +-- Nynja/CountriesProviding.swift | 4 +- .../SortableObject/Array+SortableObject.swift | 69 ------------------- .../SortableObject/SortableObject.swift | 20 ------ .../TextInput/InputField/CountryField.swift | 4 +- .../CountryModel+SortableObject.swift | 15 ---- .../UI/TextInput/InputField/PhoneField.swift | 2 +- .../InputField/TextFieldWithPicker.swift | 4 +- .../Wheel/Factory/WheelItemViewFactory.swift | 2 +- .../ItemViews/CountryWheelItemView.swift | 2 +- .../AddContactViaPhoneViewController.swift | 6 +- Nynja/Modules/Auth/AuthCoordinator.swift | 2 +- .../AuthModule/Wireframe/AuthWireframe.swift | 4 +- Nynja/Modules/Auth/Login/LoginProtocols.swift | 2 +- .../Auth/Login/View/LoginViewController.swift | 6 +- .../View/LoginWheelContainerDataSource.swift | 6 +- .../View/LoginWheelContainerDelegate.swift | 2 +- .../View/MainWheelContainerDataSource.swift | 2 +- .../Entities/CountriesSection.swift | 2 +- .../SelectCountry/Entities/Country.swift} | 4 +- .../Presenter/SelectCountryPresenter.swift | 2 +- .../SelectCountryProtocols.swift | 5 +- .../View/SelectCountryViewController.swift | 2 +- .../TableView/Cell/CountryCellModel.swift | 2 +- .../WireFrame/SelectCountryWireframe.swift | 4 +- .../ChangeNumberStep2Protocols.swift | 4 +- .../ChangeNumberStep2Interactor.swift | 6 +- .../ChangeNumberStep2Presenter.swift | 2 +- .../ChangeNumberStep2ViewController.swift | 2 +- .../View/ChangeNumberView.swift | 2 +- Nynja/Services/StorageService.swift | 10 +-- 32 files changed, 53 insertions(+), 185 deletions(-) delete mode 100644 Nynja/Extensions/SortableObject/Array+SortableObject.swift delete mode 100644 Nynja/Extensions/SortableObject/SortableObject.swift delete mode 100644 Nynja/Library/UI/TextInput/InputField/CountryModel+SortableObject.swift rename Nynja/{Library/UI/TextInput/InputField/CountryModel.swift => Modules/SelectCountry/Entities/Country.swift} (90%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 8a71304bb..ec83fcae5 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1563,7 +1563,7 @@ A43B25A020AB1DFA00FF8107 /* TextFieldWithPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258320AB1DFA00FF8107 /* TextFieldWithPicker.swift */; }; A43B25A120AB1DFA00FF8107 /* EditFieldLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258420AB1DFA00FF8107 /* EditFieldLayout.swift */; }; A43B25A220AB1DFA00FF8107 /* EditField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258520AB1DFA00FF8107 /* EditField.swift */; }; - A43B25A320AB1DFA00FF8107 /* CountryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258620AB1DFA00FF8107 /* CountryModel.swift */; }; + A43B25A320AB1DFA00FF8107 /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258620AB1DFA00FF8107 /* Country.swift */; }; A43B25A420AB1DFA00FF8107 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258720AB1DFA00FF8107 /* TextField.swift */; }; A43B25A520AB1DFA00FF8107 /* DrawableAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */; }; A43B25A620AB1DFA00FF8107 /* RecordingAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258920AB1DFA00FF8107 /* RecordingAudioWaveform.swift */; }; @@ -1574,9 +1574,7 @@ A43B25AB20AB1DFA00FF8107 /* ALTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258F20AB1DFA00FF8107 /* ALTextView.swift */; }; A43B25AC20AB1DFA00FF8107 /* BaseInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B259020AB1DFA00FF8107 /* BaseInputView.swift */; }; A43B25AD20AB1DFA00FF8107 /* MyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B259120AB1DFA00FF8107 /* MyTextField.swift */; }; - A43B25AE20AB1DFA00FF8107 /* CountryModel+SortableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B259220AB1DFA00FF8107 /* CountryModel+SortableObject.swift */; }; A43B25AF20AB1DFA00FF8107 /* ImagePlaceholderField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B259320AB1DFA00FF8107 /* ImagePlaceholderField.swift */; }; - A43B25B020AB1E1000FF8107 /* CountryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258620AB1DFA00FF8107 /* CountryModel.swift */; }; A43B25B120AB1E1B00FF8107 /* NynjaSearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B257F20AB1DFA00FF8107 /* NynjaSearchField.swift */; }; A43B25B220AB1E2600FF8107 /* ImagePlaceholderField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B259320AB1DFA00FF8107 /* ImagePlaceholderField.swift */; }; A43B25B320AB1E2E00FF8107 /* DrawableAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */; }; @@ -1927,8 +1925,6 @@ C9B8BEFE204DEBD00018748C /* DataDownloadAndUsageMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */; }; C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */; }; C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */; }; - C9C6952620232B0200A57297 /* SortableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C6952520232B0100A57297 /* SortableObject.swift */; }; - C9C6952820232B7000A57297 /* Array+SortableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C6952720232B7000A57297 /* Array+SortableObject.swift */; }; C9DF574C2023BE92006B990A /* SelectCountryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */; }; CA6AA942773DEBE97BDCFDD6 /* DateTimePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */; }; CC59F623F661C99492F9F415 /* ImagePreviewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD33E34E2376166F3A132271 /* ImagePreviewProtocols.swift */; }; @@ -3722,7 +3718,7 @@ A43B258320AB1DFA00FF8107 /* TextFieldWithPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldWithPicker.swift; sourceTree = ""; }; A43B258420AB1DFA00FF8107 /* EditFieldLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditFieldLayout.swift; sourceTree = ""; }; A43B258520AB1DFA00FF8107 /* EditField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditField.swift; sourceTree = ""; }; - A43B258620AB1DFA00FF8107 /* CountryModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryModel.swift; sourceTree = ""; }; + A43B258620AB1DFA00FF8107 /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; A43B258720AB1DFA00FF8107 /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawableAudioWaveform.swift; sourceTree = ""; }; A43B258920AB1DFA00FF8107 /* RecordingAudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingAudioWaveform.swift; sourceTree = ""; }; @@ -3733,7 +3729,6 @@ A43B258F20AB1DFA00FF8107 /* ALTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ALTextView.swift; sourceTree = ""; }; A43B259020AB1DFA00FF8107 /* BaseInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseInputView.swift; sourceTree = ""; }; A43B259120AB1DFA00FF8107 /* MyTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyTextField.swift; sourceTree = ""; }; - A43B259220AB1DFA00FF8107 /* CountryModel+SortableObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CountryModel+SortableObject.swift"; sourceTree = ""; }; A43B259320AB1DFA00FF8107 /* ImagePlaceholderField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderField.swift; sourceTree = ""; }; A43B25B620AB1E7600FF8107 /* String+Range.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Range.swift"; sourceTree = ""; }; A43B25B720AB1E7600FF8107 /* String+Links.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Links.swift"; sourceTree = ""; }; @@ -4051,8 +4046,6 @@ C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDownloadAndUsageMode.swift; sourceTree = ""; }; C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideAnimatedTransitioning.swift; sourceTree = ""; }; C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeBackHelper.swift; sourceTree = ""; }; - C9C6952520232B0100A57297 /* SortableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortableObject.swift; sourceTree = ""; }; - C9C6952720232B7000A57297 /* Array+SortableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SortableObject.swift"; sourceTree = ""; }; C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryHeaderView.swift; sourceTree = ""; }; CA78C91DFDF5884E382D38FA /* EditGroupNamePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupNamePresenter.swift; sourceTree = ""; }; CA8C095C6950E26F3BC48A1C /* EditPhotoInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditPhotoInteractor.swift; sourceTree = ""; }; @@ -8908,6 +8901,7 @@ 854574C821931945001D43CF /* Entities */ = { isa = PBXGroup; children = ( + A43B258620AB1DFA00FF8107 /* Country.swift */, 854574C921931976001D43CF /* CountriesSection.swift */, ); path = Entities; @@ -10609,13 +10603,11 @@ A43B258320AB1DFA00FF8107 /* TextFieldWithPicker.swift */, A43B258420AB1DFA00FF8107 /* EditFieldLayout.swift */, A43B258520AB1DFA00FF8107 /* EditField.swift */, - A43B258620AB1DFA00FF8107 /* CountryModel.swift */, A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */, A43B258920AB1DFA00FF8107 /* RecordingAudioWaveform.swift */, A43B258A20AB1DFA00FF8107 /* TextInputBar */, A43B259020AB1DFA00FF8107 /* BaseInputView.swift */, A43B259120AB1DFA00FF8107 /* MyTextField.swift */, - A43B259220AB1DFA00FF8107 /* CountryModel+SortableObject.swift */, A43B259320AB1DFA00FF8107 /* ImagePlaceholderField.swift */, ); path = InputField; @@ -12098,15 +12090,6 @@ path = TableView; sourceTree = ""; }; - C9C6952920232BB900A57297 /* SortableObject */ = { - isa = PBXGroup; - children = ( - C9C6952520232B0100A57297 /* SortableObject.swift */, - C9C6952720232B7000A57297 /* Array+SortableObject.swift */, - ); - path = SortableObject; - sourceTree = ""; - }; CAB5F1F6E675E93CA821CC51 /* Presenter */ = { isa = PBXGroup; children = ( @@ -12828,7 +12811,6 @@ A416DA5E207533FB00FBF1BA /* CoreLocation */, 2648C3E52069B48F00863614 /* UITextField+Extension.swift */, 26B32B8B1FE20B0400888A0A /* MQTTModels */, - C9C6952920232BB900A57297 /* SortableObject */, E7A77FD91FACC375004AE609 /* Models */, E7A77FD71FACC360004AE609 /* UIDeviceExtension.swift */, E77FBDDC1FFE828400BDB255 /* AVURLAsset+Duration.swift */, @@ -15244,7 +15226,6 @@ E7A77FDB1FACC58A004AE609 /* KeychainService.swift in Sources */, 85D669E720BD959800FBD803 /* Int+AnyObject.swift in Sources */, A42CE58420692EDB000889CC /* Vox.swift in Sources */, - A43B25B020AB1E1000FF8107 /* CountryModel.swift in Sources */, A42CE59820692EDB000889CC /* iterator.swift in Sources */, A4F3DAAD2084940C00FF71C7 /* RecepientModel.swift in Sources */, 26A8562C2074C84700C642EA /* Collection+ViewLayout.swift in Sources */, @@ -15562,7 +15543,6 @@ F11DF06C20BEF43A00F3E005 /* ResourceManager.swift in Sources */, F117871920ACF018007A9A1B /* CameraSettingsInteractor.swift in Sources */, 26FA420A2017ADF000E6F6EC /* StarMessageCell.swift in Sources */, - C9C6952620232B0200A57297 /* SortableObject.swift in Sources */, A42D51B9206A361400EEB952 /* serviceTask.swift in Sources */, E751E0051F73A70F00FEF533 /* MainItem.swift in Sources */, A4CB15252103751900C3B68B /* JDFilePermissionMechanism.swift in Sources */, @@ -16888,7 +16868,7 @@ 9BB33F3E2146A14B009FB252 /* HoldToSpeakView.swift in Sources */, 850C0B5420E0369E003341D0 /* ChatListMessageCellModelDelegate.swift in Sources */, A432CF1220B4347D00993AFB /* InputInfoProvider.swift in Sources */, - A43B25A320AB1DFA00FF8107 /* CountryModel.swift in Sources */, + A43B25A320AB1DFA00FF8107 /* Country.swift in Sources */, A43B25A820AB1DFA00FF8107 /* ALTextInputBarDelegate.swift in Sources */, FEA655F92167777F00B44029 /* PaymentModel.swift in Sources */, A42D51C8206A361400EEB952 /* iterator.swift in Sources */, @@ -16946,7 +16926,6 @@ 2648C40D2069B52100863614 /* ChangeNumberStep1Protocols.swift in Sources */, A43B25B920AB1E7600FF8107 /* String+Range.swift in Sources */, E7C9CEC51FCC245F0090C2E0 /* P2pExtension.swift in Sources */, - C9C6952820232B7000A57297 /* Array+SortableObject.swift in Sources */, A42D52CC206A53AB00EEB952 /* ok2_Spec.swift in Sources */, 8503B53520504BA4006F0593 /* TextSwitchTableViewCell.swift in Sources */, 4B06D30520287060003B275B /* WCDataManager.swift in Sources */, @@ -16972,7 +16951,6 @@ 9B96705F214BE3FE0058E98F /* MultiPageCollectionView.swift in Sources */, EA7ABD1C9C761A1F58D89F8A /* AddParticipantsWireframe.swift in Sources */, E743A8161FB2081A0041073A /* AddPaticipantsViewControllerLayout.swift in Sources */, - A43B25AE20AB1DFA00FF8107 /* CountryModel+SortableObject.swift in Sources */, 8ED0F3C01FBC5CB1004916AB /* Room+DialogCellModel.swift in Sources */, 87DE79674FF430A52D2A0BB7 /* MyGroupAliasProtocols.swift in Sources */, A45F111E20B4218D00F45004 /* FileTransferInfoView.swift in Sources */, diff --git a/Nynja/CountriesProvider.swift b/Nynja/CountriesProvider.swift index 380daaba6..883af2c6f 100644 --- a/Nynja/CountriesProvider.swift +++ b/Nynja/CountriesProvider.swift @@ -10,7 +10,7 @@ import Foundation final class CountriesProvider: CountriesProviding { - func fetchCountries() -> [CountryModel] { + func fetchCountries() -> [Country] { let path = Bundle.main.path(forResource: "countries", ofType: "txt")! guard let text = try? String(contentsOfFile: path, encoding: .utf8) else { return [] @@ -18,14 +18,15 @@ final class CountriesProvider: CountriesProviding { return text .components(separatedBy: "\n") .lazy - .compactMap { CountryModel(input: $0) } + .compactMap { Country(input: $0) } .filter { !$0.name.isEmpty } .sorted { $0.name > $1.name } } - func fetchDefaultCountry() -> CountryModel { + func fetchDefaultCountry() -> Country { let countries = fetchCountries() - let code = (NSLocale.current.regionCode ?? countries.last?.code)?.replacingOccurrences(of: "+", with: "") + let regionCode = NSLocale.current.regionCode ?? countries.last?.code + let code = regionCode?.replacingOccurrences(of: "+", with: "") return countries.first { $0.code == code || $0.ISO == code } ?? countries.last! } } diff --git a/Nynja/CountriesProviding.swift b/Nynja/CountriesProviding.swift index 7c5756be3..b0e5837bb 100644 --- a/Nynja/CountriesProviding.swift +++ b/Nynja/CountriesProviding.swift @@ -6,9 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -typealias Country = CountryModel - protocol CountriesProviding { - func fetchCountries() -> [CountryModel] + func fetchCountries() -> [Country] func fetchDefaultCountry() -> Country } diff --git a/Nynja/Extensions/SortableObject/Array+SortableObject.swift b/Nynja/Extensions/SortableObject/Array+SortableObject.swift deleted file mode 100644 index c731576d0..000000000 --- a/Nynja/Extensions/SortableObject/Array+SortableObject.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Array+SortableObject.swift -// Nynja -// -// Created by Roma Chopovenko on 2/1/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -enum SortingMode { - case ascending - case descending -} - -typealias KeysListAndSectionsDictionaty = (keysList: [String], keySectionedDictionary: [String: [SortableObject]]) - -extension Array where Element: SortableObject { - - ///Returns sorted array according to passed sorting mode - func sorted(withSortingMode mode: SortingMode) -> [Element] { - switch mode { - case .ascending: - return self.sorted(by: { $0.keySortBy < $1.keySortBy }) - case .descending: - return self.sorted(by: { $0.keySortBy > $1.keySortBy }) - } - } - - ///Returns: Sorted array of Keys according to passed sorting mode and sectioned by Dictionary sectioned by respective Keys - func splitBySections(withSortingMode mode: SortingMode) -> (KeysListAndSectionsDictionaty) { - - var itemSectionTitles = [String]() - var valuesDictionary = [String : [Element]]() - - for item in self { - let itemKey = String(item.keySortBy.prefix(1)) - if var itemValues: [Element] = valuesDictionary[itemKey] { - itemValues.append(item) - valuesDictionary[itemKey] = itemValues - } else { - valuesDictionary[itemKey] = [item] - } - } - - itemSectionTitles = [String](valuesDictionary.keys) - - switch mode { - case .ascending: - itemSectionTitles = itemSectionTitles.sorted(by: { $0 < $1 }) - case .descending: - itemSectionTitles = itemSectionTitles.sorted(by: { $0 > $1 }) - } - - return (itemSectionTitles, valuesDictionary) - } - /* - NOTE: - - To get ascending Sections and ascending sorted values inside each section: - let newArray = array.sorted(withSortingMode: .ascending).splitBySections(withSortingMode: .ascending) - - As you can see you can combine these methods, - thus you can get ascending sorted sections with descending sorted values inside each section, - for example: - - let newArray = array.sorted(withSortingMode: .descending).splitBySections(withSortingMode: .ascending) - */ -} diff --git a/Nynja/Extensions/SortableObject/SortableObject.swift b/Nynja/Extensions/SortableObject/SortableObject.swift deleted file mode 100644 index aa9eb96ce..000000000 --- a/Nynja/Extensions/SortableObject/SortableObject.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SortableObject.swift -// Nynja -// -// Created by Roma Chopovenko on 2/1/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol SortableObject { - var keySortBy: String { get } -} -/* Usage: - extension CountryModel: SortableObject { - var keySortBy: String { - get { return self.name } - } - } - */ diff --git a/Nynja/Library/UI/TextInput/InputField/CountryField.swift b/Nynja/Library/UI/TextInput/InputField/CountryField.swift index 2c00010b1..3b81a10bd 100644 --- a/Nynja/Library/UI/TextInput/InputField/CountryField.swift +++ b/Nynja/Library/UI/TextInput/InputField/CountryField.swift @@ -10,7 +10,7 @@ import Foundation import UIKit protocol CountryFieldDelegate: class { - func updateCode(countryModel: CountryModel) + func updateCode(countryModel: Country) func ready() } @@ -68,7 +68,7 @@ class CountryField: BaseInputView, TextFieldWithPickerDelegate { // MARK: - - func selected(countryModel: CountryModel) { + func selected(countryModel: Country) { self.countryDelegate?.updateCode(countryModel: countryModel) } diff --git a/Nynja/Library/UI/TextInput/InputField/CountryModel+SortableObject.swift b/Nynja/Library/UI/TextInput/InputField/CountryModel+SortableObject.swift deleted file mode 100644 index f3db226a2..000000000 --- a/Nynja/Library/UI/TextInput/InputField/CountryModel+SortableObject.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CountryModel+SortableObject.swift -// Nynja -// -// Created by Roma Chopovenko on 2/1/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -extension CountryModel: SortableObject { - var keySortBy: String { - get { return self.name } - } -} diff --git a/Nynja/Library/UI/TextInput/InputField/PhoneField.swift b/Nynja/Library/UI/TextInput/InputField/PhoneField.swift index 0fd156bc7..4564c66ab 100644 --- a/Nynja/Library/UI/TextInput/InputField/PhoneField.swift +++ b/Nynja/Library/UI/TextInput/InputField/PhoneField.swift @@ -18,7 +18,7 @@ protocol PhoneFieldDelegate: class { class PhoneField: BaseInputView, UITextFieldDelegate { weak var delegate: PhoneFieldDelegate? - var countryModel: CountryModel? { + var countryModel: Country? { didSet { if let model = countryModel { //setupMask(countryModel: model) diff --git a/Nynja/Library/UI/TextInput/InputField/TextFieldWithPicker.swift b/Nynja/Library/UI/TextInput/InputField/TextFieldWithPicker.swift index d0e37e66c..2bb9966f3 100644 --- a/Nynja/Library/UI/TextInput/InputField/TextFieldWithPicker.swift +++ b/Nynja/Library/UI/TextInput/InputField/TextFieldWithPicker.swift @@ -10,12 +10,12 @@ import Foundation import UIKit protocol TextFieldWithPickerDelegate: class { - func selected(countryModel: CountryModel) + func selected(countryModel: Country) } class TextFieldWithPicker: UITextField, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource { - var model = [CountryModel]() + var model = [Country]() weak var textFieldDelegate: TextFieldWithPickerDelegate? init() { diff --git a/Nynja/Library/UI/WheelContainer/Wheel/Factory/WheelItemViewFactory.swift b/Nynja/Library/UI/WheelContainer/Wheel/Factory/WheelItemViewFactory.swift index f65fe5115..ee1f95c7d 100644 --- a/Nynja/Library/UI/WheelContainer/Wheel/Factory/WheelItemViewFactory.swift +++ b/Nynja/Library/UI/WheelContainer/Wheel/Factory/WheelItemViewFactory.swift @@ -25,7 +25,7 @@ class WheelItemViewFactory: ItemViewFactory { case is ImageFullWheelItemModel: return ImageFullWheelItemView(frame: frame) - case is CountryModel: + case is Country: return CountryWheelItemView(frame: frame) case is ChatWheelItemModel: diff --git a/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/CountryWheelItemView.swift b/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/CountryWheelItemView.swift index 385779fc6..5ab564521 100644 --- a/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/CountryWheelItemView.swift +++ b/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/CountryWheelItemView.swift @@ -64,7 +64,7 @@ class CountryWheelItemView: WheelItemView { override func update(model: WheelItemModel) { super.update(model: model) - if let c = model as? CountryModel { + if let c = model as? Country { label.text = c.name if model.isReversed { diff --git a/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift b/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift index 2057653c6..ae03099c4 100644 --- a/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift +++ b/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift @@ -20,7 +20,7 @@ final class AddContactViaPhoneViewController: BaseVC, AddContactViaPhoneViewProt private let util = NBPhoneNumberUtil.sharedInstance() var isRegionSelected = false fileprivate var region = "" - fileprivate var countryModels: [CountryModel] = [] + fileprivate var countryModels: [Country] = [] //MARK: - 📑 Views private lazy var topLabel: UILabel = { @@ -126,7 +126,7 @@ final class AddContactViaPhoneViewController: BaseVC, AddContactViaPhoneViewProt let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) self.countryField.addGestureRecognizer(tapGesture) - self.countryModels = StorageService.sharedInstance.countries + self.countryModels = CountriesProvider().fetchCountries() if let countryCode = NSLocale.current.regionCode ?? self.countryModels.last?.ISO { self.selectCountry(byISO: countryCode) } @@ -261,7 +261,7 @@ extension AddContactViaPhoneViewController: PhoneFieldDelegate { } //MARK: ⚙️🔐 Private Helper - private func setCountryRelatedTextFields(withData model: CountryModel) { + private func setCountryRelatedTextFields(withData model: Country) { self.phoneField.countryModel = model self.phoneField.code.text = "+\(model.code)" self.countryField.input.text = model.name.uppercased() diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 8f9d85476..f5b5691e9 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -14,7 +14,7 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt private weak var navigation: UINavigationController? private let serviceFactory: ServiceFactoryProtocol - private var selectCountryCallback: ((Result) -> Void)? + private var selectCountryCallback: ((Result) -> Void)? init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { self.navigation = navigation diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 29cd1f608..1e41c67dc 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -28,7 +28,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { enum State { case continueLogin(loginOption: LoginOption) - case getCountry(callback: (Result) -> Void) + case getCountry(callback: (Result) -> Void) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -46,7 +46,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { return view } - func selectCountry(completion: @escaping (Result) -> Void) { + func selectCountry(completion: @escaping (Result) -> Void) { coordinator.wireframe(self, didEndWithState: .getCountry(callback: completion)) } diff --git a/Nynja/Modules/Auth/Login/LoginProtocols.swift b/Nynja/Modules/Auth/Login/LoginProtocols.swift index 855591294..bfde50a79 100644 --- a/Nynja/Modules/Auth/Login/LoginProtocols.swift +++ b/Nynja/Modules/Auth/Login/LoginProtocols.swift @@ -13,7 +13,7 @@ protocol LoginWheelContainerViewProtocol: class { func getWheelContainerDS() -> LoginWheelContainerDataSource func getBorderView() -> BorderView func getWheelContainer() -> WheelContainer - func selectCountry(_ country: CountryModel, at index: Int) + func selectCountry(_ country: Country, at index: Int) } protocol LoginWireFrameProtocol: class { diff --git a/Nynja/Modules/Auth/Login/View/LoginViewController.swift b/Nynja/Modules/Auth/Login/View/LoginViewController.swift index 44903d7d4..273624e72 100644 --- a/Nynja/Modules/Auth/Login/View/LoginViewController.swift +++ b/Nynja/Modules/Auth/Login/View/LoginViewController.swift @@ -76,7 +76,7 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro override func initialize() { // Fetch country models LocationService.sharedInstance.getCountry() - let countryModels = StorageService.sharedInstance.countries + let countryModels = CountriesProvider().fetchCountries() configureLoginView() configureContainer(with: countryModels) @@ -100,7 +100,7 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro } // MARK: Configure views - func configureContainer(with countryModels: CountryModels) { + func configureContainer(with countryModels: [Country]) { wheelContainerDS = LoginWheelContainerDataSource(countryModels: countryModels) wheelContainerDelegate = LoginWheelContainerDelegate(view: self) @@ -350,7 +350,7 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro return container } - func selectCountry(_ country: CountryModel, at index: Int) { + func selectCountry(_ country: Country, at index: Int) { loginView.phoneField.countryModel = country loginView.phoneField.code.text = "+" + country.code loginView.countryField.input.text = country.name.uppercased() diff --git a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift b/Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift index ce1f7fdfb..25ff3edcd 100644 --- a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift +++ b/Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -typealias CountryModels = [CountryModel] +typealias CountryModels = [Country] class LoginWheelContainerDataSource { @@ -42,12 +42,12 @@ class LoginWheelContainerDataSource { var countryModels: CountryModels { get { if isReversed { - return _countryModels.reversed().map { (model: CountryModel) in + return _countryModels.reversed().map { (model: Country) in model.isReversed = true return model } } else { - return _countryModels.map { (model: CountryModel) in + return _countryModels.map { (model: Country) in model.isReversed = false return model } diff --git a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift b/Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift index 6aad13e12..f7dec8ca2 100644 --- a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift +++ b/Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift @@ -108,7 +108,7 @@ class LoginWheelContainerDelegate: WheelContainerDelegate { return itemIndex } - private func countryModel(at index: Int) -> CountryModel? { + private func countryModel(at index: Int) -> Country? { return view?.getWheelContainerDS().countryModels[index] } diff --git a/Nynja/Modules/Main/View/MainWheelContainerDataSource.swift b/Nynja/Modules/Main/View/MainWheelContainerDataSource.swift index f7c48354c..dc841f5a6 100644 --- a/Nynja/Modules/Main/View/MainWheelContainerDataSource.swift +++ b/Nynja/Modules/Main/View/MainWheelContainerDataSource.swift @@ -103,7 +103,7 @@ class MainWheelContainerDataSource: WheelContainerDataSource { itemView.accessibilityLabel?.append(model.navItem.rawValue) } else if let _ = itemModel as? ImageFullWheelItemModel { itemView.accessibilityLabel?.append("image") - } else if let model = itemModel as? CountryModel { + } else if let model = itemModel as? Country { itemView.accessibilityLabel?.append("country_\(model.name)") } else { itemView.accessibilityLabel?.append("\(indexPath.item)") diff --git a/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift b/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift index e8dba2f38..db504f4fd 100644 --- a/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift +++ b/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift @@ -8,5 +8,5 @@ struct CountriesSection { let symbol: String - let countries: [CountryModel] + let countries: [Country] } diff --git a/Nynja/Library/UI/TextInput/InputField/CountryModel.swift b/Nynja/Modules/SelectCountry/Entities/Country.swift similarity index 90% rename from Nynja/Library/UI/TextInput/InputField/CountryModel.swift rename to Nynja/Modules/SelectCountry/Entities/Country.swift index 215852f10..65994dd9c 100644 --- a/Nynja/Library/UI/TextInput/InputField/CountryModel.swift +++ b/Nynja/Modules/SelectCountry/Entities/Country.swift @@ -1,12 +1,12 @@ // -// CountryModel.swift +// Country.swift // Nynja // // Created by Anton Makarov on 04.07.2017. // Copyright © 2017 TecSynt Solutions. All rights reserved. // -final class CountryModel: WheelItemModel { +final class Country: WheelItemModel { let name: String let ISO: String diff --git a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift index bf8bf373e..a6063a9d2 100644 --- a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift +++ b/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift @@ -20,7 +20,7 @@ final class SelectCountryPresenter: SelectCountryPresenterProtocol { interactor.filter(with: text) } - func didSelect(country: CountryModel) { + func didSelect(country: Country) { wireframe.didSelect(country: country) } diff --git a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift b/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift index 507860b2f..22fb6f8e1 100644 --- a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift +++ b/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift @@ -11,7 +11,7 @@ import UIKit // MARK: - Wireframe protocol SelectCountryWireFrameProtocol: class { - func didSelect(country: CountryModel) + func didSelect(country: Country) func dismiss() } @@ -26,7 +26,7 @@ protocol SelectCountryViewProtocol: class { protocol SelectCountryPresenterProtocol: class { func getCountries() func filter(with text: String) - func didSelect(country: CountryModel) + func didSelect(country: Country) func dismiss() } @@ -37,7 +37,6 @@ protocol SelectCountryInteractorInputProtocol: class { func filter(with text: String) } - protocol SelectCountryInteractorOutputProtocol: class { func didFetch(sections: [CountriesSection]) } diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift index aefc3ab57..167e4a609 100644 --- a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift +++ b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift @@ -158,7 +158,7 @@ extension SelectCountryViewController: UITableViewDataSource { return sections[section].symbol } - private func country(at indexPath: IndexPath) -> CountryModel { + private func country(at indexPath: IndexPath) -> Country { let section = sections[indexPath.section] return section.countries[indexPath.row] } diff --git a/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift index 11d55ce82..90bc540ce 100644 --- a/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift +++ b/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift @@ -14,7 +14,7 @@ struct CountryCellModel: CellViewModel { return "select_country_cell" } - let country: CountryModel + let country: Country func setup(cell: CountryTableViewCell) { cell.countryNameLabel.text = country.name diff --git a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift b/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift index facae3cd7..7a6a4371d 100644 --- a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift +++ b/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift @@ -31,7 +31,7 @@ final class SelectCountryWireFrame: Wireframe, SelectCountryWireFrameProtocol { enum State { case dismiss - case selected(country: CountryModel) + case selected(country: Country) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -52,7 +52,7 @@ final class SelectCountryWireFrame: Wireframe, SelectCountryWireFrameProtocol { // MARK: - SelectCountryWireFrameProtocol - func didSelect(country: CountryModel) { + func didSelect(country: Country) { coordinator.wireframe(self, endWithState: .selected(country: country)) } diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/ChangeNumberStep2Protocols.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/ChangeNumberStep2Protocols.swift index e2be97c2a..5c20f6e08 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/ChangeNumberStep2Protocols.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/ChangeNumberStep2Protocols.swift @@ -39,7 +39,7 @@ protocol ChangeNumberStep2PresenterProtocol: BasePresenterProtocol { * Add here your methods for communication VIEW -> PRESENTER */ func goBack() - func getCountryModels() -> [CountryModel] + func getCountryModels() -> [Country] func setRegion(region: String) func changeNumber(number: String) } @@ -58,6 +58,6 @@ protocol ChangeNumberStep2InteractorInputProtocol: class { /** * Add here your methods for communication PRESENTER -> INTERACTOR */ - func getCountryModels() -> [CountryModel] + func getCountryModels() -> [Country] func changeNumber(number: String) } diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Interactor/ChangeNumberStep2Interactor.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Interactor/ChangeNumberStep2Interactor.swift index 85e22e98e..2829b3754 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Interactor/ChangeNumberStep2Interactor.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Interactor/ChangeNumberStep2Interactor.swift @@ -10,8 +10,10 @@ class ChangeNumberStep2Interactor: ChangeNumberStep2InteractorInputProtocol { weak var presenter: ChangeNumberStep2InteractorOutputProtocol! - func getCountryModels() -> [CountryModel] { - return StorageService.sharedInstance.countries + private let countriesProvider: CountriesProviding = CountriesProvider() + + func getCountryModels() -> [Country] { + return countriesProvider.fetchCountries() } func changeNumber(number: String) { diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Presenter/ChangeNumberStep2Presenter.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Presenter/ChangeNumberStep2Presenter.swift index d33c50bfa..913f15986 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Presenter/ChangeNumberStep2Presenter.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/Presenter/ChangeNumberStep2Presenter.swift @@ -20,7 +20,7 @@ class ChangeNumberStep2Presenter: BasePresenter, ChangeNumberStep2PresenterProto wireFrame.goBack() } - func getCountryModels() -> [CountryModel] { + func getCountryModels() -> [Country] { return interactor.getCountryModels() } diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift index 9b1d8153c..942869c26 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift @@ -202,7 +202,7 @@ class ChangeNumberStep2ViewController: BaseVC, LoginWheelContainerViewProtocol, return container } - func selectCountry(_ country: CountryModel, at index: Int) { + func selectCountry(_ country: Country, at index: Int) { changeNumberView.countryModel = country changeNumberView.phoneCountryCodeLabel.text = "+" + country.code changeNumberView.countryValueLabel.text = country.name.uppercased() diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift index e3768352b..dee6e46f8 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift @@ -9,7 +9,7 @@ import Foundation class ChangeNumberView: UIView, UserSettingsRespondable, UITextFieldDelegate, TestableViewProtocol { - var countryModel: CountryModel? { + var countryModel: Country? { didSet { if let model = countryModel { let text = "".updateWithMask(placeHolder: model.placeHolder) diff --git a/Nynja/Services/StorageService.swift b/Nynja/Services/StorageService.swift index e9779215d..08552d62d 100644 --- a/Nynja/Services/StorageService.swift +++ b/Nynja/Services/StorageService.swift @@ -19,21 +19,15 @@ final class StorageService { let userDefaults = UserDefaults(suiteName: Bundle.main.appGroupName) let keychain = KeychainService.standard + // MARK: - Properties + #if !SHARE_EXTENSION - private let countriesProvider = CountriesProvider() private let databaseManager = DatabaseManager() var dbPool: DatabasePool? { return databaseManager.dbPool } - - // MARK: - Properties - - lazy var countries: [CountryModel] = { - return countriesProvider.fetchCountries() - }() - #endif /// It is used only for debug purposes. -- GitLab From 5c68822a6e73e1d3757e7134876700ba0bdfbb71 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 7 Nov 2018 17:45:39 +0200 Subject: [PATCH 087/138] [NY-4907] Fixed Country init from file. --- Nynja.xcodeproj/project.pbxproj | 10 +++++++++- Nynja/Modules/SelectCountry/Entities/Country.swift | 7 ++++++- .../SelectCountryViewController.swift | 0 3 files changed, 15 insertions(+), 2 deletions(-) rename Nynja/Modules/SelectCountry/View/{ => ViewController}/SelectCountryViewController.swift (100%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index ec83fcae5..0559e8afc 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -6440,7 +6440,7 @@ 43D5323E27F49A5C95BBB6D6 /* View */ = { isa = PBXGroup; children = ( - 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */, + 854574CD21933D47001D43CF /* ViewController */, C9C695062022318500A57297 /* TableView */, ); path = View; @@ -8907,6 +8907,14 @@ path = Entities; sourceTree = ""; }; + 854574CD21933D47001D43CF /* ViewController */ = { + isa = PBXGroup; + children = ( + 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; 85482841204E912600DCBEC8 /* ViewController */ = { isa = PBXGroup; children = ( diff --git a/Nynja/Modules/SelectCountry/Entities/Country.swift b/Nynja/Modules/SelectCountry/Entities/Country.swift index 65994dd9c..d2cc4363c 100644 --- a/Nynja/Modules/SelectCountry/Entities/Country.swift +++ b/Nynja/Modules/SelectCountry/Entities/Country.swift @@ -23,9 +23,14 @@ final class Country: WheelItemModel { } code = data[0] ISO = data[1] - name = data[2] + name = Country.getName(iso: data[1]) ?? data[2] placeHolder = data[safe: 3] super.init() } + + private static func getName(iso: String) -> String? { + let locale = NSLocale(localeIdentifier: "en-US") + return locale.displayName(forKey: .countryCode, value: iso) + } } diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift b/Nynja/Modules/SelectCountry/View/ViewController/SelectCountryViewController.swift similarity index 100% rename from Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift rename to Nynja/Modules/SelectCountry/View/ViewController/SelectCountryViewController.swift -- GitLab From 5c4154ec25f5edc221c7c29d9379029cc725c613 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 7 Nov 2018 17:47:17 +0200 Subject: [PATCH 088/138] [NY-4907] Fixed project structure. --- .../{ => Auth}/SelectCountry/Entities/CountriesSection.swift | 0 Nynja/Modules/{ => Auth}/SelectCountry/Entities/Country.swift | 0 .../SelectCountry/Interactor/SelectCountryInteractor.swift | 0 .../SelectCountry/Presenter/SelectCountryPresenter.swift | 0 .../Modules/{ => Auth}/SelectCountry/SelectCountryProtocols.swift | 0 .../SelectCountry/View/TableView/Cell/CountryCellModel.swift | 0 .../SelectCountry/View/TableView/Cell/CountryTableViewCell.swift | 0 .../View/TableView/Header/SelectCountryHeaderView.swift | 0 .../View/ViewController/SelectCountryViewController.swift | 0 .../SelectCountry/WireFrame/SelectCountryWireframe.swift | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename Nynja/Modules/{ => Auth}/SelectCountry/Entities/CountriesSection.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/Entities/Country.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/Interactor/SelectCountryInteractor.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/Presenter/SelectCountryPresenter.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/SelectCountryProtocols.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/View/TableView/Cell/CountryCellModel.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/View/ViewController/SelectCountryViewController.swift (100%) rename Nynja/Modules/{ => Auth}/SelectCountry/WireFrame/SelectCountryWireframe.swift (100%) diff --git a/Nynja/Modules/SelectCountry/Entities/CountriesSection.swift b/Nynja/Modules/Auth/SelectCountry/Entities/CountriesSection.swift similarity index 100% rename from Nynja/Modules/SelectCountry/Entities/CountriesSection.swift rename to Nynja/Modules/Auth/SelectCountry/Entities/CountriesSection.swift diff --git a/Nynja/Modules/SelectCountry/Entities/Country.swift b/Nynja/Modules/Auth/SelectCountry/Entities/Country.swift similarity index 100% rename from Nynja/Modules/SelectCountry/Entities/Country.swift rename to Nynja/Modules/Auth/SelectCountry/Entities/Country.swift diff --git a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/Auth/SelectCountry/Interactor/SelectCountryInteractor.swift similarity index 100% rename from Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift rename to Nynja/Modules/Auth/SelectCountry/Interactor/SelectCountryInteractor.swift diff --git a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/Auth/SelectCountry/Presenter/SelectCountryPresenter.swift similarity index 100% rename from Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift rename to Nynja/Modules/Auth/SelectCountry/Presenter/SelectCountryPresenter.swift diff --git a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift b/Nynja/Modules/Auth/SelectCountry/SelectCountryProtocols.swift similarity index 100% rename from Nynja/Modules/SelectCountry/SelectCountryProtocols.swift rename to Nynja/Modules/Auth/SelectCountry/SelectCountryProtocols.swift diff --git a/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift b/Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryCellModel.swift similarity index 100% rename from Nynja/Modules/SelectCountry/View/TableView/Cell/CountryCellModel.swift rename to Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryCellModel.swift diff --git a/Nynja/Modules/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift b/Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift similarity index 100% rename from Nynja/Modules/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift rename to Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift diff --git a/Nynja/Modules/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift b/Nynja/Modules/Auth/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift similarity index 100% rename from Nynja/Modules/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift rename to Nynja/Modules/Auth/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift diff --git a/Nynja/Modules/SelectCountry/View/ViewController/SelectCountryViewController.swift b/Nynja/Modules/Auth/SelectCountry/View/ViewController/SelectCountryViewController.swift similarity index 100% rename from Nynja/Modules/SelectCountry/View/ViewController/SelectCountryViewController.swift rename to Nynja/Modules/Auth/SelectCountry/View/ViewController/SelectCountryViewController.swift diff --git a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift b/Nynja/Modules/Auth/SelectCountry/WireFrame/SelectCountryWireframe.swift similarity index 100% rename from Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift rename to Nynja/Modules/Auth/SelectCountry/WireFrame/SelectCountryWireframe.swift -- GitLab From 326cbcb076e14e1c1743d760dab36c58dfb77615 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 7 Nov 2018 17:47:36 +0200 Subject: [PATCH 089/138] [NY-4907] FIxed project structure. --- Nynja.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 0559e8afc..559db319e 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -6519,7 +6519,6 @@ 8ED0F3C21FBC5CF1004916AB /* GroupsList */, D25E638A8B9B267255AD766A /* GroupRules */, 91732B7DCE35ABC02702095D /* GroupStorage */, - 115A968821FB24FA3C58A6D5 /* SelectCountry */, 80CA53AB5B009455E0ECDC30 /* AddContactByUsername */, E61C394BD0E94E3DCF853D4F /* ScheduleMessage */, 975DB2471671357A9EEBF65B /* TimeZoneSelector */, @@ -6932,6 +6931,7 @@ 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, + 115A968821FB24FA3C58A6D5 /* SelectCountry */, 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, -- GitLab From 4265260bdcc1f81de7630425a89d7449ca356cdb Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 7 Nov 2018 17:50:51 +0200 Subject: [PATCH 090/138] [NY-4699] In-chat action based statuses (typing, sending, recording). (#1468) --- .../NynjaUIKit.xcodeproj/project.pbxproj | 60 ++++ .../CoreAnimation/CALayer+Animation.swift | 16 + .../Views/Avatar/AvatarStatusView.swift | 141 +++++++++ .../NynjaUIKit/Views/BaseView.swift | 32 ++ .../ContextMenu/View/NynjaContextMenu.swift | 18 +- .../Views/Typing/RecordingIndicatorView.swift | 58 ++++ .../Views/Typing/TypingIndicatorView.swift | 112 +++++++ .../NynjaUIKit/Views/Typing/TypingView.swift | 149 +++++++++ .../NynjaUIKit/Views/Utils/RoundView.swift | 22 ++ .../Handlers/Base/HandlerFactory.swift | 15 +- .../Services/Handlers/ContactHandler.swift | 10 +- .../Services/Handlers/MessageHandler.swift | 11 +- .../Services/Handlers/ProfileHandler.swift | 12 +- .../ForwardSelectorInteractor.swift | 8 +- Nynja.xcodeproj/project.pbxproj | 108 ++++++- Nynja/AppDelegate.swift | 296 +----------------- Nynja/AuthHandler.swift | 12 +- Nynja/ChatService/SenderService.swift | 4 +- Nynja/DB/Models/DBMember.swift | 46 ++- Nynja/ExtendedStarHandler.swift | 12 +- Nynja/Extensions/Models/StarExtension.swift | 4 + Nynja/Generated/LocalizableConstants.swift | 12 +- Nynja/HandlerFactory.swift | 34 +- Nynja/JobHandler.swift | 14 +- .../Cell/ChatListMessageContentView.swift | 163 +++++----- .../Cell/ChatListMessageDetailsView.swift | 178 +++++++++++ ...ft => ChatListMessageIndicatorsView.swift} | 82 ++--- .../Cell/ChatListMessageTableViewCell.swift | 124 ++++---- .../Cell/ChatListMessageTextView.swift | 178 +++++++++++ .../Cell/CounterView.swift | 25 +- .../Model/ChatListMessageCellModel.swift | 72 +++-- Nynja/LinkHandler.swift | 19 +- Nynja/MQTTModels/TypingExtension+BERT.swift | 18 +- Nynja/MemberHandler.swift | 15 +- .../Interactor/AddContactInteractor.swift | 2 +- .../AddContactByUsernameInteractor.swift | 2 +- .../AddContactViaPhoneInteractor.swift | 2 +- .../Login/Interactor/LoginInteractor.swift | 2 +- .../Interactor/VerifyNumberInteractor.swift | 2 +- .../Interactor/NewChannelInteractor.swift | 2 +- .../ChatsList/ChatsListProtocols.swift | 4 +- .../Interactor/ChatsListInteractor.swift | 24 ++ .../Presenter/ChatsListPresenter.swift | 8 + .../ChatsList/View/ChatListTableDS.swift | 11 +- .../View/ChatsListViewController.swift | 8 +- .../WireFrame/ChatsListWireframe.swift | 8 +- .../Interactor/ContactsInteractor.swift | 2 +- .../Interactor/EditUsernameInteractor.swift | 2 +- .../GroupsList/GroupsListProtocols.swift | 4 +- .../Interactor/GroupsListInteractor.swift | 25 ++ .../Presenter/GroupsListPresenter.swift | 8 + .../GroupsList/View/GroupsListTableDS.swift | 15 +- .../View/GroupsListViewController.swift | 4 +- .../WireFrame/GroupsListWireframe.swift | 9 +- .../Interactor/InviteFriendsInteractor.swift | 2 +- .../Interactor/MessageInteractor.swift | 48 +-- .../Interactor/PresenceStatusProvider.swift | 57 ++-- .../Message/Models/Statuses/ChatStatus.swift | 13 + .../Statuses/ChatStatusDisplayInfo.swift | 12 + .../{ => Internet}/InternetStatus.swift | 0 .../{ => Presence}/PresenceStatus.swift | 0 .../Statuses/{ => Typing}/ActionStatus.swift | 10 +- .../{ => Typing}/RecordingStatus.swift | 2 +- .../Statuses/{ => Typing}/SendingStatus.swift | 2 +- .../Message/Presenter/MessagePresenter.swift | 85 ++--- .../Message/Protocols/MessageProtocols.swift | 6 +- Nynja/Modules/Message/View/MessageVC.swift | 10 +- .../Message/View/MessageVCLayout.swift | 5 - .../View/Views/AvatarView/AvatarView.swift | 143 ++++++--- .../Views/AvatarView/AvatarViewLayout.swift | 25 +- .../Interactor/ParticipantsInteractor.swift | 2 +- .../Interactor/ProfileInteractor.swift | 39 ++- .../Presenter/Contact+DialogCellModel.swift | 5 +- .../Profile/Presenter/DialogCellModel.swift | 3 +- .../Profile/Presenter/ProfilePresenter.swift | 10 + .../Presenter/Room+DialogCellModel.swift | 5 +- Nynja/Modules/Profile/ProfileProtocols.swift | 4 +- .../View/DetailsView/ProfileDetailsView.swift | 22 +- .../ProfileDetailsViewLayout.swift | 24 +- .../Profile/View/ProfileViewController.swift | 19 +- .../View/TableView/ProfileTablewViewDS.swift | 5 + .../Profile/WireFrame/ProfileWireframe.swift | 11 +- .../Interactor/QRCodeReaderInteractor.swift | 2 +- .../ScheduleMessageInteractor.swift | 2 +- .../Interactor/SecurityInteractor.swift | 4 +- Nynja/Observable/KeyedObservable.swift | 44 +++ .../Observable/KeyedObservableContainer.swift | 72 +++++ Nynja/Observable/Observable.swift | 31 ++ Nynja/Observable/ObservableContainer.swift | 41 +++ Nynja/Resources/en.lproj/Localizable.strings | 8 +- Nynja/ServerModel/Model/Typing.swift | 6 +- Nynja/ServerModel/Source/Decoder.swift | 9 +- Nynja/ServerModel/Spec/Typing_Spec.swift | 6 + .../HandleServices/ContactHandler.swift | 25 +- .../HandleServices/HistoryHandler.swift | 37 ++- .../HandleServices/MessageHandler.swift | 41 ++- .../HandleServices/ProfileHandler.swift | 34 +- .../Services/HandleServices/RoomHandler.swift | 42 +-- .../HandleServices/RosterHandler.swift | 14 +- .../HandleServices/SearchHandler.swift | 13 +- .../Services/HandleServices/StarHandler.swift | 13 +- .../HandleServices/TypingHandler.swift | 26 +- Nynja/Services/MQTT/MQTTService.swift | 2 +- Nynja/Services/Member/MemberDAO.swift | 6 + Nynja/Services/Member/MemberDAOProtocol.swift | 3 +- Nynja/Services/Models/TypingModel.swift | 3 +- .../MQTTHandlerFactoryProtocol.swift | 13 + .../ServiceFactory/ServiceFactory.swift | 55 +--- .../ServiceFactoryProtocol.swift | 50 +++ Nynja/Statuses/TypingDisplayModel.swift | 66 ++++ Nynja/Statuses/TypingObservable.swift | 12 + Nynja/Statuses/TypingProvider.swift | 229 ++++++++++++++ .../Services/Handlers/Base/BaseHandler.swift | 18 +- Shared/Services/Handlers/ErrorsHandler.swift | 7 +- Shared/Services/Handlers/IoHandler.swift | 16 +- .../Messaging/TypingSenderService.swift | 47 +-- 116 files changed, 2751 insertions(+), 1039 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift create mode 100644 Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageDetailsView.swift rename Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/{ChatListMessageAccessoryView.swift => ChatListMessageIndicatorsView.swift} (57%) create mode 100644 Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTextView.swift create mode 100644 Nynja/Modules/Message/Models/Statuses/ChatStatus.swift create mode 100644 Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift rename Nynja/Modules/Message/Models/Statuses/{ => Internet}/InternetStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Presence}/PresenceStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/ActionStatus.swift (89%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/RecordingStatus.swift (89%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/SendingStatus.swift (91%) create mode 100644 Nynja/Observable/KeyedObservable.swift create mode 100644 Nynja/Observable/KeyedObservableContainer.swift create mode 100644 Nynja/Observable/Observable.swift create mode 100644 Nynja/Observable/ObservableContainer.swift create mode 100644 Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift create mode 100644 Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift create mode 100644 Nynja/Statuses/TypingDisplayModel.swift create mode 100644 Nynja/Statuses/TypingObservable.swift create mode 100644 Nynja/Statuses/TypingProvider.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index f64833620..215806154 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -32,6 +32,13 @@ 8514D51C20EE41E90002378A /* UIWindowExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51B20EE41E90002378A /* UIWindowExtensions.swift */; }; 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */; }; 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */; }; + 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */; }; + 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */; }; + 8540A00A2181EB87003A010F /* TypingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0092181EB87003A010F /* TypingView.swift */; }; + 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00B2181EBD2003A010F /* BaseView.swift */; }; + 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */; }; + 8540A019218213E2003A010F /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A018218213E2003A010F /* RoundView.swift */; }; + 85EB37F621832D41003A2D6F /* RecordingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F521832D41003A2D6F /* RecordingIndicatorView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -63,6 +70,13 @@ 8514D51B20EE41E90002378A /* UIWindowExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIWindowExtensions.swift; sourceTree = ""; }; 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+HitTestDelegate.swift"; sourceTree = ""; }; 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaContextMenuUserInfo.swift; sourceTree = ""; }; + 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStatusView.swift; sourceTree = ""; }; + 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; + 8540A0092181EB87003A010F /* TypingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingView.swift; sourceTree = ""; }; + 8540A00B2181EBD2003A010F /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; + 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Animation.swift"; sourceTree = ""; }; + 8540A018218213E2003A010F /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; + 85EB37F521832D41003A2D6F /* RecordingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingIndicatorView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C90742AD8E6E2E817F7DB1E9 /* Pods-NynjaUIKit.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.channels.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.channels.xcconfig"; sourceTree = ""; }; @@ -265,6 +279,7 @@ 8514D51920EE41AC0002378A /* Extensions */ = { isa = PBXGroup; children = ( + 8540A00D2181ED10003A010F /* CoreAnimation */, 8514D51F20EE47350002378A /* UIWindow */, ); path = Extensions; @@ -273,6 +288,10 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( + 8540A00B2181EBD2003A010F /* BaseView.swift */, + 8540A01A218213E8003A010F /* Utils */, + 85409FFD2181C8AF003A010F /* Avatar */, + 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, ); path = Views; @@ -287,6 +306,40 @@ path = UIWindow; sourceTree = ""; }; + 85409FFD2181C8AF003A010F /* Avatar */ = { + isa = PBXGroup; + children = ( + 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */, + ); + path = Avatar; + sourceTree = ""; + }; + 8540A0062181EA0D003A010F /* Typing */ = { + isa = PBXGroup; + children = ( + 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */, + 85EB37F521832D41003A2D6F /* RecordingIndicatorView.swift */, + 8540A0092181EB87003A010F /* TypingView.swift */, + ); + path = Typing; + sourceTree = ""; + }; + 8540A00D2181ED10003A010F /* CoreAnimation */ = { + isa = PBXGroup; + children = ( + 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */, + ); + path = CoreAnimation; + sourceTree = ""; + }; + 8540A01A218213E8003A010F /* Utils */ = { + isa = PBXGroup; + children = ( + 8540A018218213E2003A010F /* RoundView.swift */, + ); + path = Utils; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -411,12 +464,16 @@ 8514D4E820EE2D970002378A /* UITableView+ViewModels.swift in Sources */, 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */, 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, + 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, + 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, + 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, 8514D51420EE40540002378A /* NynjaContextMenuItemCellModel.swift in Sources */, 8514D51220EE40540002378A /* ContextMenuRow.swift in Sources */, + 8540A019218213E2003A010F /* RoundView.swift in Sources */, 8514D51620EE40540002378A /* NynjaContextMenuArrowView.swift in Sources */, 8514D4E620EE2D970002378A /* Reusable.swift in Sources */, 8514D4E320EE2D970002378A /* SupplementaryViewModel.swift in Sources */, @@ -426,7 +483,10 @@ 8514D4E920EE2D970002378A /* UICollectionView+ViewModel.swift in Sources */, 8514D51120EE40540002378A /* ContextMenuItem.swift in Sources */, 8514D51820EE40540002378A /* NynjaContextMenuItemsFactory.swift in Sources */, + 8540A00A2181EB87003A010F /* TypingView.swift in Sources */, + 85EB37F621832D41003A2D6F /* RecordingIndicatorView.swift in Sources */, 8514D51520EE40540002378A /* NynjaContextMenuItemCollectionViewCell.swift in Sources */, + 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */, 8514D4E120EE2D970002378A /* SelectableCellViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift new file mode 100644 index 000000000..df70d9566 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift @@ -0,0 +1,16 @@ +// +// CAAnimationExtensions.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +extension CALayer { + + public func hasAnimation(forKey key: String) -> Bool { + return animation(forKey: key) != nil + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift new file mode 100644 index 000000000..33ae6778e --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -0,0 +1,141 @@ +// +// AvatarStatusView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class AvatarStatusView: BaseView { + + public var angle: CGFloat = .pi / 4 { + didSet { + setNeedsLayout() + } + } + + public var statusIconSize: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + public var statusIconPadding: CGFloat = 2 { + didSet { + setNeedsLayout() + } + } + + public var imageRadius: CGFloat { + return bounds.height / 2 + } + + + // MARK: - Views + + public let imageView = UIImageView() + + private let statusView = UIImageView() + + private let maskLayer = CAShapeLayer() + + private var isMaskActive: Bool { + return !statusView.isHidden + } + + + // MARK: - Setup + + public override func setup() { + super.setup() + + backgroundColor = .clear + + imageView.layer.masksToBounds = true + addSubview(imageView) + + statusView.layer.masksToBounds = true + addSubview(statusView) + + update(.none) + } + + + // MARK: - Layout + + public override func layoutSubviews() { + super.layoutSubviews() + + imageView.frame = bounds + imageView.layer.cornerRadius = imageRadius + + if isMaskActive { + let clipCircleCenter = statusPosition(in: bounds) + let clipCircleSize = statusIconSize + statusIconPadding * 2 + let clipCircleFrame = CGRect( + x: clipCircleCenter.x - clipCircleSize / 2, + y: clipCircleCenter.y - clipCircleSize / 2, + width: clipCircleSize, + height: clipCircleSize + ) + updateClipMask(with: clipCircleFrame) + updateStatusView(with: clipCircleFrame.origin) + } else { + imageView.layer.mask = nil + } + } + + private func updateClipMask(with frame: CGRect) { + let statusCirclePath = UIBezierPath(ovalIn: frame) + + let path = UIBezierPath(rect: bounds) + path.append(statusCirclePath.reversing()) + + maskLayer.path = path.cgPath + + imageView.layer.mask = maskLayer + } + + private func updateStatusView(with origin: CGPoint) { + statusView.frame = CGRect(x: origin.x + statusIconPadding, + y: origin.y + statusIconPadding, + width: statusIconSize, + height: statusIconSize) + + statusView.layer.cornerRadius = statusIconSize / 2 + } + + private func statusPosition(in bounds: CGRect) -> CGPoint { + return CGPoint(x: imageRadius * cos(angle) + bounds.width / 2, + y: imageRadius * sin(angle) + bounds.height / 2) + } +} + +extension AvatarStatusView { + + public enum StatusAppearance { + case color(UIColor) + case image(UIImage) + case none + } + + public func update(_ statusAppearance: StatusAppearance) { + switch statusAppearance { + case let .color(color): + statusView.isHidden = false + statusView.backgroundColor = color + statusView.image = nil + case let .image(image): + statusView.isHidden = false + statusView.backgroundColor = nil + statusView.image = image + case .none: + statusView.isHidden = true + statusView.backgroundColor = nil + statusView.image = nil + } + setNeedsLayout() + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift new file mode 100644 index 000000000..dc6523cbb --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift @@ -0,0 +1,32 @@ +// +// BaseView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public class BaseView: UIView { + + // MARK: - Init + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + public func setup() { + // should be implemented in childs + } +} + diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift index ecea64cfb..23e4a01d3 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift @@ -18,7 +18,7 @@ public protocol NynjaContextMenuDelegate: class { userInfo: NynjaContextMenuUserInfo?) } -public final class NynjaContextMenu: UIView { +public final class NynjaContextMenu: BaseView { // MARK: - Properties @@ -99,22 +99,10 @@ public final class NynjaContextMenu: UIView { }() - // MARK: - Init - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - // MARK: - Setup - private func setup() { + public override func setup() { + super.setup() clipsToBounds = true contentView.layer.cornerRadius = cornerRadius contentView.clipsToBounds = true diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift new file mode 100644 index 000000000..30aadaa93 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift @@ -0,0 +1,58 @@ +// +// RecordingIndicatorView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +public final class RecordingIndicatorView: BaseView { + + public var horizontalInset: CGFloat = 4 { + didSet { + indicatorView.snp.updateConstraints { maker in + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + } + } + } + + public var circleSize: CGFloat = 8 { + didSet { + indicatorView.snp.updateConstraints { maker in + maker.width.height.equalTo(circleSize) + } + } + } + + public var circleColor: UIColor = .lightGray { + didSet { + indicatorView.backgroundColor = circleColor + } + } + + + // MARK: - Views + + private let indicatorView = RoundView() + + + // MARK: - Setup + + public override func setup() { + super.setup() + + backgroundColor = .clear + indicatorView.backgroundColor = backgroundColor + + addSubview(indicatorView) + indicatorView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + maker.width.height.equalTo(circleSize) + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift new file mode 100644 index 000000000..a3946fc2d --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift @@ -0,0 +1,112 @@ +// +// TypingIndicatorView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class TypingIndicatorView: BaseView { + + public var itemsCount: Int = 3 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemPadding: CGFloat = 4 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemSize: CGFloat = 4 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemColor: UIColor = .lightGray { + didSet { + setupColor() + } + } + + public override var intrinsicContentSize: CGSize { + let width = itemSize * CGFloat(itemsCount) + itemPadding * CGFloat(itemsCount - 1) + return CGSize(width: width, height: itemSize) + } + + + // MARK: - Layers + + public override class var layerClass: AnyClass { + return CAReplicatorLayer.self + } + + private var animationLayer: CAReplicatorLayer { + return layer as! CAReplicatorLayer + } + + private let itemLayer = CAShapeLayer() + + + // MARK: - Setup + + public override func setup() { + super.setup() + animationLayer.addSublayer(itemLayer) + animationLayer.masksToBounds = true + setupColor() + } + + private func setupColor() { + itemLayer.backgroundColor = itemColor.cgColor + } + + + // MARK: - Layout + + public override func layoutSubviews() { + super.layoutSubviews() + + itemLayer.frame.size = CGSize(width: itemSize, height: itemSize) + itemLayer.cornerRadius = itemSize / 2 + + animationLayer.instanceCount = itemsCount + animationLayer.instanceTransform = CATransform3DMakeTranslation(itemSize + itemPadding, 0, 0) + animationLayer.instanceAlphaOffset = Float(Animation.toValue - Animation.fromValue) / Float(itemsCount - 1) + animationLayer.instanceDelay = Animation.duration / Double(itemsCount) + + addAnimation() + } + + + // MARK: - Animation + + private func addAnimation() { + guard !itemLayer.hasAnimation(forKey: Animation.key) else { + return + } + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = Animation.fromValue + animation.toValue = Animation.toValue + animation.duration = Animation.duration + animation.autoreverses = true + animation.repeatCount = .infinity + + itemLayer.add(animation, forKey: Animation.key) + } + + private enum Animation { + static let key = "typing" + static let duration = 0.5 + static let fromValue = 0.5 + static let toValue = 1.0 + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift new file mode 100644 index 000000000..01ec22cda --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -0,0 +1,149 @@ +// +// TypingView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +public final class TypingView: BaseView { + + // MARK: - Appearance + + public struct Appearance { + public enum Indicator { + case dots(UIColor) + case circle(UIColor) + } + public let indicator: Indicator + public let textColor: UIColor + public let textFont: UIFont + public let senderInfo: String? + public let typingInfo: String + + public init(indicator: Indicator, + textColor: UIColor, + textFont: UIFont, + senderInfo: String?, + typingInfo: String) { + self.indicator = indicator + self.textColor = textColor + self.textFont = textFont + self.senderInfo = senderInfo + self.typingInfo = typingInfo + } + } + + + // MARK: - Views + + private lazy var indicatorContainer: UIView = { + let view = UIView() + view.setContentHuggingPriority(.required, for: .horizontal) + addSubview(view) + return view + }() + + private lazy var senderInfoLabel: UILabel = { + let label = UILabel() + addSubview(label) + return label + }() + + + // MARK: - Setup + + public override func setup() { + super.setup() + + indicatorContainer.snp.makeConstraints { maker in + maker.top.bottom.left.equalToSuperview() + } + + senderInfoLabel.snp.makeConstraints { maker in + maker.top.bottom.right.equalToSuperview() + maker.left.equalTo(indicatorContainer.snp.right).offset(Constraints.senderInfo.leftOffset.adjustedByWidth) + } + } + + + // MARK: - Layout + + public func update(_ appearance: Appearance) { + + + switch appearance.indicator { + case let .dots(color): + setupDotsIndicator(color: color) + case let .circle(color): + setupCircleIndicator(color: color) + } + + senderInfoLabel.font = appearance.textFont + senderInfoLabel.textColor = appearance.textColor + senderInfoLabel.text = appearance.senderInfo + .flatMap { "\($0) \(appearance.typingInfo)" } ?? appearance.typingInfo + } + + private func setupDotsIndicator(color: UIColor) { + guard !indicatorContainer.subviews.contains(where: { $0 is TypingIndicatorView }) else { return } + + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + + let indicatorView = TypingIndicatorView() + indicatorView.itemColor = color + indicatorView.itemSize = Constraints.indicator.dotsSize.adjustedByWidth + indicatorView.itemPadding = Constraints.indicator.dotsPadding.adjustedByWidth + + indicatorView.setContentCompressionResistancePriority(.required, for: .horizontal) + indicatorView.setContentHuggingPriority(.required, for: .horizontal) + + indicatorContainer.addSubview(indicatorView) + + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) + } + + private func setupCircleIndicator(color: UIColor) { + guard !indicatorContainer.subviews.contains(where: { $0 is RecordingIndicatorView }) else { return } + + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + + let indicatorView = RecordingIndicatorView() + indicatorContainer.addSubview(indicatorView) + + indicatorView.circleColor = color + indicatorView.circleSize = Constraints.indicator.circleSize.adjustedByWidth + indicatorView.horizontalInset = Constraints.indicator.horizontalInset.adjustedByWidth + + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) + } + + private func makeIndicatorViewConstraints() -> (ConstraintMaker) -> Void { + return { maker in + maker.centerY.equalToSuperview().offset(Constraints.indicator.centerVerticalOffset.adjustedByWidth) + maker.left.right.equalToSuperview() + } + } + + + // MARK: - Constraints + + private enum Constraints { + + enum indicator { + static let circleSize: CGFloat = 8 + static let dotsSize: CGFloat = 3 + static let dotsPadding: CGFloat = 4 + + static let horizontalInset: CGFloat = 4 + static let centerVerticalOffset: CGFloat = 1 + } + + enum senderInfo { + static let leftOffset: CGFloat = 4 + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift new file mode 100644 index 000000000..9e8c2b633 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift @@ -0,0 +1,22 @@ +// +// RoundView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class RoundView: BaseView { + + override func setup() { + super.setup() + layer.masksToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = min(bounds.width, bounds.height) / 2 + } +} diff --git a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift index 200518760..f4a7d5195 100644 --- a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift +++ b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift @@ -8,21 +8,20 @@ final class HandlerFactory { - static func handler(for handlerType: Handlers) -> BaseHandler.Type { + static func handler(for handlerType: Handlers) -> BaseHandler { switch handlerType { case .profile: - return ProfileHandler.self + return ProfileHandler.shared case .contact: - return ContactHandler.self + return ContactHandler.shared case .message: - return MessageHandler.self + return MessageHandler.shared case .io: - return IoHandler.self + return IoHandler.shared case .errors: - return ErrorsHandler.self + return ErrorsHandler.shared case .auth: - return AuthHandler.self + return AuthHandler.shared } } - } diff --git a/Nynja-Share/Services/Handlers/ContactHandler.swift b/Nynja-Share/Services/Handlers/ContactHandler.swift index a2041fabf..bdf7650af 100644 --- a/Nynja-Share/Services/Handlers/ContactHandler.swift +++ b/Nynja-Share/Services/Handlers/ContactHandler.swift @@ -16,11 +16,15 @@ extension ContactHandlerDelegate { } // TODO: need to think about this. It is share extension. -class ContactHandler: BaseHandler { +final class ContactHandler: BaseHandler { - static weak var delegate: ContactHandlerDelegate? + static let shared = ContactHandler() - static func executeHandle(data: BertTuple) { + private init() {} + + weak var delegate: ContactHandlerDelegate? + + func executeHandle(data: BertTuple) { guard let contact = get_Contact().parse(bert: data) as? Contact, contact.originalStatus != nil else { return } diff --git a/Nynja-Share/Services/Handlers/MessageHandler.swift b/Nynja-Share/Services/Handlers/MessageHandler.swift index b0daa836e..3c2b31d72 100644 --- a/Nynja-Share/Services/Handlers/MessageHandler.swift +++ b/Nynja-Share/Services/Handlers/MessageHandler.swift @@ -16,10 +16,15 @@ extension MessageHandlerDelegate { func getMessageSuccess(message: Message) {} } -class MessageHandler:BaseHandler { - static weak var delegate : MessageHandlerDelegate? +final class MessageHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = MessageHandler() + + private init() {} + + weak var delegate: MessageHandlerDelegate? + + func executeHandle(data: BertTuple) { if let message = get_Message().parse(bert: data) as? Message { delegate?.getMessageSuccess(message: message) } diff --git a/Nynja-Share/Services/Handlers/ProfileHandler.swift b/Nynja-Share/Services/Handlers/ProfileHandler.swift index 200095636..fbda22977 100644 --- a/Nynja-Share/Services/Handlers/ProfileHandler.swift +++ b/Nynja-Share/Services/Handlers/ProfileHandler.swift @@ -18,10 +18,15 @@ extension ProfileHandlerDelegate { func removeProfileSuccess() {} } -class ProfileHandler:BaseHandler { - static weak var delegate :ProfileHandlerDelegate? +final class ProfileHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = ProfileHandler() + + private init() {} + + weak var delegate: ProfileHandlerDelegate? + + func executeHandle(data: BertTuple) { if let profile = get_Profile().parse(bert: data) as? Profile { if let status = profile.status?.string { switch status { @@ -38,5 +43,4 @@ class ProfileHandler:BaseHandler { } } } - } diff --git a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift index 7acf73a6e..9c45854f1 100644 --- a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift +++ b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift @@ -64,10 +64,10 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P } private func initialize() { - ProfileHandler.delegate = self - MessageHandler.delegate = self - ContactHandler.delegate = self - IoHandler.delegate = self + ProfileHandler.shared.delegate = self + MessageHandler.shared.delegate = self + ContactHandler.shared.delegate = self + IoHandler.shared.delegate = self amazonInitializer.initialize() setupImageCache() notifyHostApplication() diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b69589c62..46bd49c3b 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -905,6 +905,8 @@ 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */; }; 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872BE20CD457F007CD6CA /* StickersProviding.swift */; }; 851872C120CD45B3007CD6CA /* StickersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872C020CD45B3007CD6CA /* StickersProvider.swift */; }; + 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */; }; + 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */; }; 851EBD7F20B418890065C644 /* StickersInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EBD7E20B418890065C644 /* StickersInputView.swift */; }; 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F520D4194A007C0036 /* DBRecentSticker.swift */; }; 852003F820D419E9007C0036 /* RecentStickerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F720D419E9007C0036 /* RecentStickerTable.swift */; }; @@ -970,6 +972,7 @@ 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */; }; 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; + 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B811218879B100A286E5 /* TypingDisplayModel.swift */; }; 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */; }; 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */; }; 85433F24204D596D00B373A7 /* WebFullScreenProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */; }; @@ -1008,6 +1011,9 @@ 85482848204EA56600DCBEC8 /* PrivacyListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */; }; 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */; }; 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548340D207769E800604051 /* DocumentInteractionInput.swift */; }; + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingProvider.swift */; }; + 8548341B2187449F002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* ObservableContainer.swift */; }; + 8548341D218744AC002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* Observable.swift */; }; 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */; }; 854A4B2D2080D68200759152 /* CellWithArrowCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */; }; 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */; }; @@ -1039,6 +1045,8 @@ 855EF421202CC6F800541BE3 /* GetExtendedStarsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */; }; 855EF423202CC85300541BE3 /* MQTTServiceStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */; }; 855EF425202CCADB00541BE3 /* ExtendedStarHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */; }; + 8560C4C6218997DD006635AE /* ChatStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8560C4C5218997DD006635AE /* ChatStatus.swift */; }; + 8560C4C8218999E3006635AE /* ChatStatusDisplayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */; }; 8562853220D140FC000C9739 /* InputBar+ButtonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853120D140FC000C9739 /* InputBar+ButtonType.swift */; }; 8562853420D16242000C9739 /* StickerPreviewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853320D16242000C9739 /* StickerPreviewing.swift */; }; 8562853620D164B5000C9739 /* ScaleAnimatableGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */; }; @@ -1074,7 +1082,6 @@ 8580BACA20BD983400239D9D /* MentionTransitionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAC520BD983400239D9D /* MentionTransitionProtocol.swift */; }; 8580BACC20BD984500239D9D /* MessageEditInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BACB20BD984400239D9D /* MessageEditInfo.swift */; }; 8580BACE20BD98CF00239D9D /* UpdateResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BACD20BD98CF00239D9D /* UpdateResult.swift */; }; - 8580BAD720BD98E700239D9D /* ChatListMessageAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */; }; 8580BAD820BD98E700239D9D /* CounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD220BD98E600239D9D /* CounterView.swift */; }; 8580BAD920BD98E700239D9D /* ChatListMessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */; }; 8580BADA20BD98E700239D9D /* ChatListMessageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */; }; @@ -1133,6 +1140,8 @@ 85CB25DC20D723D300D5E565 /* StickerPackDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CB25DB20D723D300D5E565 /* StickerPackDAOProtocol.swift */; }; 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */; }; 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */; }; + 85CEFBC0218C5D9500760F9E /* TypingObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */; }; + 85CEFBC5218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */; }; 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */; }; 85D669E520BD956000FBD803 /* UIButtonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */; }; 85D669E620BD956000FBD803 /* UIView+Shadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E320BD956000FBD803 /* UIView+Shadow.swift */; }; @@ -1163,6 +1172,9 @@ 85E1DD2520BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */; }; 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2620BEE961008AD211 /* ScalableCell.swift */; }; 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; + 85EB37F321831094003A2D6F /* ChatListMessageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageDetailsView.swift */; }; + 85EB37FB21837235003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservable.swift */; }; + 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3189,6 +3201,8 @@ 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDetailsPreviewView.swift; sourceTree = ""; }; 851872BE20CD457F007CD6CA /* StickersProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProviding.swift; path = Services/StickersProvider/StickersProviding.swift; sourceTree = ""; }; 851872C020CD45B3007CD6CA /* StickersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProvider.swift; path = Services/StickersProvider/StickersProvider.swift; sourceTree = ""; }; + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceFactoryProtocol.swift; sourceTree = ""; }; + 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTHandlerFactoryProtocol.swift; sourceTree = ""; }; 851EBD7E20B418890065C644 /* StickersInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputView.swift; sourceTree = ""; }; 852003F520D4194A007C0036 /* DBRecentSticker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRecentSticker.swift; sourceTree = ""; }; 852003F720D419E9007C0036 /* RecentStickerTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentStickerTable.swift; sourceTree = ""; }; @@ -3243,6 +3257,7 @@ 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDelegate.swift; sourceTree = ""; }; 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; + 8542B811218879B100A286E5 /* TypingDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingDisplayModel.swift; sourceTree = ""; }; 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenPresenter.swift; sourceTree = ""; }; 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenViewController.swift; sourceTree = ""; }; 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenProtocols.swift; sourceTree = ""; }; @@ -3262,6 +3277,9 @@ 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyListDataSource.swift; sourceTree = ""; }; 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastScrollable.swift; sourceTree = ""; }; 8548340D207769E800604051 /* DocumentInteractionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentInteractionInput.swift; sourceTree = ""; }; + 854834172186FADB002064E1 /* TypingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingProvider.swift; sourceTree = ""; }; + 8548341A2187449F002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; + 8548341C218744AC002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowCellModel.swift; sourceTree = ""; }; 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; @@ -3290,6 +3308,8 @@ 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExtendedStarsModel.swift; sourceTree = ""; }; 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTServiceStars.swift; sourceTree = ""; }; 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedStarHandler.swift; sourceTree = ""; }; + 8560C4C5218997DD006635AE /* ChatStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStatus.swift; sourceTree = ""; }; + 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStatusDisplayInfo.swift; sourceTree = ""; }; 8562853120D140FC000C9739 /* InputBar+ButtonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InputBar+ButtonType.swift"; sourceTree = ""; }; 8562853320D16242000C9739 /* StickerPreviewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewing.swift; sourceTree = ""; }; 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleAnimatableGrid.swift; sourceTree = ""; }; @@ -3323,7 +3343,6 @@ 8580BAC520BD983400239D9D /* MentionTransitionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionTransitionProtocol.swift; sourceTree = ""; }; 8580BACB20BD984400239D9D /* MessageEditInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEditInfo.swift; sourceTree = ""; }; 8580BACD20BD98CF00239D9D /* UpdateResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateResult.swift; sourceTree = ""; }; - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageAccessoryView.swift; sourceTree = ""; }; 8580BAD220BD98E600239D9D /* CounterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CounterView.swift; sourceTree = ""; }; 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageTableViewCell.swift; sourceTree = ""; }; 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageContentView.swift; sourceTree = ""; }; @@ -3379,6 +3398,8 @@ 85CB25DB20D723D300D5E565 /* StickerPackDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackDAOProtocol.swift; sourceTree = ""; }; 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackExtension.swift; sourceTree = ""; }; 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticSelectionFeedbackGenerator.swift; sourceTree = ""; }; + 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingObservable.swift; sourceTree = ""; }; + 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageIndicatorsView.swift; sourceTree = ""; }; 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Int+AnyObject.swift"; sourceTree = ""; }; 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensions.swift; sourceTree = ""; }; 85D669E320BD956000FBD803 /* UIView+Shadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Shadow.swift"; sourceTree = ""; }; @@ -3406,6 +3427,9 @@ 85D77806211D9B980044E72F /* ScrollPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollPosition.swift; sourceTree = ""; }; 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageVC+StickerInputModuleDelegate.swift"; sourceTree = ""; }; 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; + 85EB37F221831094003A2D6F /* ChatListMessageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageDetailsView.swift; sourceTree = ""; }; + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -6096,6 +6120,8 @@ 8509AC61206A54420089089B /* ResponseResult.swift */, B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, + 8548341921874434002064E1 /* Observable */, + 85EB37F9218365A6003A2D6F /* Statuses */, ); name = Services; sourceTree = ""; @@ -8826,6 +8852,17 @@ path = Documents; sourceTree = ""; }; + 8548341921874434002064E1 /* Observable */ = { + isa = PBXGroup; + children = ( + 8548341C218744AC002064E1 /* Observable.swift */, + 8548341A2187449F002064E1 /* ObservableContainer.swift */, + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */, + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */, + ); + path = Observable; + sourceTree = ""; + }; 854A4B392080E5D500759152 /* TableView */ = { isa = PBXGroup; children = ( @@ -8978,6 +9015,32 @@ path = CollectionView; sourceTree = ""; }; + 8560C4C221899793006635AE /* Typing */ = { + isa = PBXGroup; + children = ( + A45F10BE20B4218D00F45004 /* ActionStatus.swift */, + A45F10BC20B4218D00F45004 /* RecordingStatus.swift */, + A45F10BF20B4218D00F45004 /* SendingStatus.swift */, + ); + path = Typing; + sourceTree = ""; + }; + 8560C4C32189979B006635AE /* Internet */ = { + isa = PBXGroup; + children = ( + A45F10BD20B4218D00F45004 /* InternetStatus.swift */, + ); + path = Internet; + sourceTree = ""; + }; + 8560C4C4218997A4006635AE /* Presence */ = { + isa = PBXGroup; + children = ( + A45F10BB20B4218D00F45004 /* PresenceStatus.swift */, + ); + path = Presence; + sourceTree = ""; + }; 8562853720D164BE000C9739 /* ScaleAnimation */ = { isa = PBXGroup; children = ( @@ -9106,9 +9169,10 @@ 8580BAD020BD98E600239D9D /* Cell */ = { isa = PBXGroup; children = ( - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */, 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */, + 85EB37F221831094003A2D6F /* ChatListMessageDetailsView.swift */, + 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */, 8580BAD220BD98E600239D9D /* CounterView.swift */, ); path = Cell; @@ -9117,8 +9181,8 @@ 8580BAD520BD98E600239D9D /* Model */ = { isa = PBXGroup; children = ( - 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, 8580BAD620BD98E600239D9D /* ChatListMessageCellModel.swift */, + 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, ); path = Model; sourceTree = ""; @@ -9538,6 +9602,16 @@ path = BBCode; sourceTree = ""; }; + 85EB37F9218365A6003A2D6F /* Statuses */ = { + isa = PBXGroup; + children = ( + 854834172186FADB002064E1 /* TypingProvider.swift */, + 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */, + 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, + ); + path = Statuses; + sourceTree = ""; + }; 85EF7C342090DEFF0090C418 /* Models */ = { isa = PBXGroup; children = ( @@ -10684,11 +10758,11 @@ A45F10BA20B4218D00F45004 /* Statuses */ = { isa = PBXGroup; children = ( - A45F10BB20B4218D00F45004 /* PresenceStatus.swift */, - A45F10BC20B4218D00F45004 /* RecordingStatus.swift */, - A45F10BD20B4218D00F45004 /* InternetStatus.swift */, - A45F10BE20B4218D00F45004 /* ActionStatus.swift */, - A45F10BF20B4218D00F45004 /* SendingStatus.swift */, + 8560C4C5218997DD006635AE /* ChatStatus.swift */, + 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */, + 8560C4C4218997A4006635AE /* Presence */, + 8560C4C32189979B006635AE /* Internet */, + 8560C4C221899793006635AE /* Typing */, ); path = Statuses; sourceTree = ""; @@ -13200,6 +13274,8 @@ isa = PBXGroup; children = ( F11786F020AC5482007A9A1B /* ServiceFactory.swift */, + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */, + 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */, ); name = ServiceFactory; path = Services/ServiceFactory; @@ -15181,6 +15257,7 @@ F119E66E20D24BBF0043A532 /* MultiplePreviewWireframe.swift in Sources */, 2648C40F2069B52100863614 /* ChangeNumberStep3Presenter.swift in Sources */, A42D51A0206A361400EEB952 /* reader.swift in Sources */, + 8560C4C8218999E3006635AE /* ChatStatusDisplayInfo.swift in Sources */, A42D52BB206A53AA00EEB952 /* Vox_Spec.swift in Sources */, A418DA3420ED0D1300FE780B /* CountView.swift in Sources */, 4B8FC3082163ABC300602D6B /* Desc+Sticker.swift in Sources */, @@ -15238,6 +15315,7 @@ A45F112920B4218D00F45004 /* MessageContentAppearance.swift in Sources */, C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */, FE2D7CCD211C71AE00520D78 /* WalletService.swift in Sources */, + 85CEFBC5218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift in Sources */, 4BE2C5E22142EB0F00A73DD9 /* AudioManager.swift in Sources */, E7598F5B1FA1D5D90082FBE7 /* ProfileActionCellLayout.swift in Sources */, 85082DDD2045A873000AE4B2 /* UserSettingsService.swift in Sources */, @@ -15427,6 +15505,7 @@ 26342CA920ECBAEF00D2196B /* TranscribeNetworkClient.swift in Sources */, 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */, 267BE2831FDE905D00C47E18 /* SettingsProtocols.swift in Sources */, + 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */, 264638231FFFE269002590E6 /* RepliesHeaderView.swift in Sources */, 263D66331FE8D95100A509F8 /* TypingHandler.swift in Sources */, 4B8996F5204EF75500DCB183 /* FeedDAOProtocol.swift in Sources */, @@ -15553,7 +15632,6 @@ A415132220DBD59B00C2C01F /* Link_Spec.swift in Sources */, 85433F25204D596D00B373A7 /* WebFullScreenInteractor.swift in Sources */, 8504DEA920693588006722AC /* MediaFullWheelItemModel.swift in Sources */, - 8580BAD720BD98E700239D9D /* ChatListMessageAccessoryView.swift in Sources */, 6F3F21025258D8071BCF95EF /* LoginWireframe.swift in Sources */, 26DCB2522064BA46001EF0AB /* ContactsInteractor.swift in Sources */, B767F48F215D1E0A00FA9B27 /* ComingSoonExtension.swift in Sources */, @@ -15627,6 +15705,8 @@ 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 5E0B9FF22170BCE600A95467 /* CreateProfileContentView.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, + 85EB37F321831094003A2D6F /* ChatListMessageDetailsView.swift in Sources */, + 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15784,6 +15864,7 @@ E7E06C681F792B0200BFC8FA /* LoginWheelContainerDataSource.swift in Sources */, 854751492093BDD300F8D5F8 /* CollectionViewScrollProxy.swift in Sources */, 850C301B204DA87A00DB26C2 /* PrivacyListPresenter.swift in Sources */, + 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */, 267BE28E1FDE9FCC00C47E18 /* SettingsGroupWireFrame.swift in Sources */, 85BDD2B821465EFA00695DE5 /* ScrollDirection.swift in Sources */, A4679BA620B2DD0F0021FE9C /* SubscribersSelectorWireFrame.swift in Sources */, @@ -15793,6 +15874,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, + 85EB37FB21837235003A2D6F /* KeyedObservable.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, @@ -15802,6 +15884,7 @@ 8580BADA20BD98E700239D9D /* ChatListMessageContentView.swift in Sources */, E757B53D1FE9225C00467BA2 /* TypingExtension.swift in Sources */, C940514C204C7FAF00D72B04 /* DataAndStorageInteractor.swift in Sources */, + 8560C4C6218997DD006635AE /* ChatStatus.swift in Sources */, F1A9FA3590CC1F834B727955 /* AddContactPresenter.swift in Sources */, 6DD72F601F1547AC008CFF83 /* GCD.swift in Sources */, 5E07BC3D216DFD08000E4558 /* AuthViewsFactory.swift in Sources */, @@ -16010,6 +16093,7 @@ A42D52C4206A53AA00EEB952 /* error2_Spec.swift in Sources */, 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */, 00E9824E205C2604008BF03D /* SessionItemView.swift in Sources */, + 85CEFBC0218C5D9500760F9E /* TypingObservable.swift in Sources */, 5EEB73D02161CE2700D8ECE6 /* LoginOptionsView.swift in Sources */, 8520040920D4F9B4007C0036 /* MessageStickerRepliedView.swift in Sources */, 00102F40202C8E5300A877A9 /* NynjaCalendarView.swift in Sources */, @@ -16024,6 +16108,7 @@ 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, + 8548341D218744AC002064E1 /* Observable.swift in Sources */, 4B1D7DFE2029C41C00703228 /* AboutItemsFactory.swift in Sources */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */, @@ -16291,6 +16376,7 @@ E70938371FBEDA2B006CCDC6 /* ProfileTable.swift in Sources */, A416DA602075341C00FBF1BA /* CLLocationCoordinate2D+Payload.swift in Sources */, A4679BAE20B2DD100021FE9C /* SubscribersSelectorInteractor.swift in Sources */, + 8548341B2187449F002064E1 /* ObservableContainer.swift in Sources */, FEA655FD2167777F00B44029 /* TransferDetailsInteractor.swift in Sources */, E70F78B91FD6C64E00385565 /* ChatCheckpointTable.swift in Sources */, 4B06D30620287060003B275B /* WCDataManagerProtocol.swift in Sources */, @@ -16695,6 +16781,7 @@ 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */, F11DF06520BD96D000F3E005 /* GalleryFilterType.swift in Sources */, FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, @@ -16885,6 +16972,7 @@ 8505445720627C7C00E0F2B3 /* HistoryCellModel.swift in Sources */, 2F2A5C12A7202E7834F923DC /* GroupRulesWireframe.swift in Sources */, 2625DBF820EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift in Sources */, + 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */, D3A30AF05BD7C46A9A8C1FC1 /* GroupStorageProtocols.swift in Sources */, 8520040720D4F436007C0036 /* StickerPreviewConfig.swift in Sources */, F1607B1D20B20F7800BDF60A /* GridView.swift in Sources */, diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 63e768b9d..596fbbf2e 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -105,12 +105,12 @@ private extension AppDelegate { self.window = UIWindow(frame: UIScreen.main.bounds) let navigation = UINavigationController() navigation.isNavigationBarHidden = true -// SplashWireFrame().presentSplash(navigation: navigation) - let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + SplashWireFrame().presentSplash(navigation: navigation) +// let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) self.window?.rootViewController = navigation self.window?.makeKeyAndVisible() - coordinator.start() +// coordinator.start() } private func configureDependencies() { @@ -184,293 +184,3 @@ private extension AppDelegate { Intercom.setApiKey(intercomServiceConfig.apiKey, forAppId: intercomServiceConfig.appId) } } - - - - - - -//// MARK: - Service factory -// -//protocol ServiceFactoryProtocol { -// func makeService1(/*Some parameters*/) -> Service1 -// func makeService2(/*Some parameters*/) -> Service2 -// // Another services -//} -// -//final class ServiceFactory: ServiceFactoryProtocol { -// func makeService1(/*Some parameters*/) -> Service1 { -// return Service1.shared -// } -// -// func makeService2(/*Some parameters*/) -> Service2 { -// let service = Service2(/*Some parameters*/) -// return service2 -// } -// -// // Another services -//} -// -//// MARK: - Modules stack -// -//protocol ModulesStackProtocol { -// func present(view: UIViewController) -// func back() -// func close() -//} -// -//final class ModulesStack: ModulesStackProtocol { -// private weak var navigationController: UINavigationController? -// private var viewControllers: [UIViewController] -// -// init(navigationController: UINavigationController?) { -// self.navigationController = navigationController -// viewControllers = [] -// } -// -// func present(view: UIViewController) { -// // Some code for presenting -// } -// -// func back() { -// // Some code for popping -// } -// -// func close() { -// // Some code for closing all stack -// } -//} -// -//// MARK: - Coordinators factory -// -//protocol CoordinatorsFactoryProtocol { -// func makeCoordinator1() -> CoordinatorProtocol -// func makeCoordinator2(/*Some parameters*/) -> CoordinatorProtocol -// // Another coordinators -//} -// -//final class CoordinatorsFactory: CoordinatorsFactoryProtocol { -// func makeCoordinator1() -> CoordinatorProtocol { -// return Coordinator1() -// } -// -// func makeCoordinator2(/*Some parameters*/) -> CoordinatorProtocol { -// return Coordinator2(/*Some parameters*/) -// } -// -// // Another coordinators -//} -// -//// MARK: - Coordinators stack -// -//protocol CoordinatorsStackProtocol { -// -//} -// -//final class CoordinatorsStack: CoordinatorsStackProtocol { -// -//} -// -//// MARK: - AppCoordinator -// -//protocol AppCoordinatorProtocol { -// -//} -// -//final class AppCoordinator: AppCoordinatorProtocol { -// -//} -// -//// MARK: - Coordinator -// -//protocol CoordinatorProtocol { -// func start() -// func end() -//} -// -//final class Coordinator: CoordinatorProtocol, WireframeCoordinatorProtocol { -// private weak var stack: ModulesStackProtocol -// private let serviceFactory: ServiceFactoryProtocol -// // Some properties -// -// init(stack: ModulesStackProtocol, serviceFactory: ServiceFactoryProtocol/*, Some parameters*/) { -// self.stack = stack -// } -// -// func start() { -// let wireframe = Wireframe(coordinator: self) -// let parameters = Wireframe.Parameters(someParameter: someParameter) -// let dependencies = Wireframe.Dependencies(someService1: serviceFactory.makeSomeService1) -// -// let view = wireframe.prepareModule(parameters: parameters, dependencies: dependencies) -// stack.present(view: view) -// } -// -// func end() { -// stack.close() -// // Some code -// } -// -// func wireframe(_ wireframe: Wireframe, finishedWithState state: Wireframe.State) { -// switch state { -// case .stateForOpen1: // Some code -// break -// case .stateForOpen2(/*Some parameters*/): stack.back() -// break -// } -// } -//} -// -//// MARK: - Wireframe -// -//protocol WireframeCoordinatorProtocol { -// func wireframe(_ wireframe: Wireframe, finishedWithState state: Wireframe.State) -//} -// -//protocol WireframeProtocol { -// associatedtype Parameters -// associatedtype Dependencies -// associatedtype State -// -// func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController -// func open1(/*Some parameters*/) -// func open2(/*Some parameters*/) -//} -// -//final class Wireframe: WireframeProtocol { -// struct Parameters { -// let someParameter: SomeType -// // Another parameters -// } -// -// struct Dependencies { -// let someService1: SomeService1 -// // Another dependencies -// } -// -// enum State { -// case stateForOpen1 -// case stateForOpen2(/*Some parameters*/) -// } -// -// private let coordinator: WireframeCoordinatorProtocol -// -// init(coordinator: WireframeCoordinatorProtocol) { -// self.coordinator = coordinator -// } -// -// func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { -// let view = View() -// let presenter = Presenter() -// let interactor = Interactor() -// -// let viewDependencies = View.Dependencies(presenter: presenter, someService1: dependencies.makeService1) -// let interactorDependencies = Interactor.Dependencies(presenter: presenter, someService1: dependencies.makeService1) -// let presenterDependencies = Presenter.Dependencies(interactor: interactor, someService1: dependencies.makeService1) -// -// // set some parameters from Parameters structure -// -// view.inject(viewDependencies) -// presenter.inject(presenterDependencies) -// interactor.inject(interactorDependencies) -// -// return view -// } -// -// func open1(/*Some parameters*/) { -// coordinator.wireframe(self, finishedWithState: .stateForOpen1) -// } -// -// func open2(/*Some parameters*/) { -// coordinator.wireframe(self, finishedWithState: .stateForOpen2(/*Some parameters*/)) -// } -//} -// -//// MARK: - Presenter -// -//protocol PresenterProtocol: SetInjectable { -// func someMethod1() -//} -// -//final class Presenter: PresenterProtocol { -// private let interactor: InteractorProtocol -// private weak var view: ViewProtocol? -// private let someService: SomeService1 -// // Another properties -// -// struct Dependencies { -// let interactor: InteractorProtocol -// let view: ViewProtocol -// let someService1: SomeService1 -// // Another dependencies -// } -// -// func inject(dependencies: Presenter.Dependencies) { -// interactor = dependencies.interactor -// view = dependencies.view -// someService = dependencies.someService1 -// // Another dependencies -// } -// -// func someMethod1() { -// // Some code -// } -//} -// -//// MARK: - Interactor -// -//protocol InteractorProtocol: SetInjectable { -// func someMethod1() -//} -// -//final class Interactor: InteractorProtocol { -// private weak var presenter: PresenterProtocol? -// private let someService1: SomeService1 -// // Another properties -// -// struct Dependencies { -// let presenter: PresenterProtocol -// let someService1: SomeService1 -// // Another dependencies -// } -// -// func inject(dependencies: Interactor.Dependencies) { -// presenter = dependencies.presenter -// someService = dependencies.someService1 -// // Another dependencies -// } -// -// func someMethod1() { -// // Some code -// } -//} -// -//// MARK: - View -// -//protocol ViewProtocol: SetInjectable { -// func someMethod1() -//} -// -//final class View: ViewProtocol { -// private weak var presenter: PresenterProtocol? -// private let someService1: SomeService1 -// // Another properties -// -// struct Dependencies { -// let presenter: PresenterProtocol -// let someService1: SomeService1 -// // Another dependencies -// } -// -// func inject(dependencies: View.Dependencies) { -// presenter = dependencies.presenter -// someService = dependencies.someService1 -// // Another dependencies -// } -// -// func someMethod1() { -// // Some code -// } -//} - - diff --git a/Nynja/AuthHandler.swift b/Nynja/AuthHandler.swift index c5ec8f5f2..ca8b88591 100644 --- a/Nynja/AuthHandler.swift +++ b/Nynja/AuthHandler.swift @@ -18,11 +18,15 @@ extension AuthHandlerDelegate { func processDelete(auth: Auth) {} } -class AuthHandler: BaseHandler { +final class AuthHandler: BaseHandler { - static weak var delegate: AuthHandlerDelegate? + static let shared = AuthHandler() - static func executeHandle(data: BertTuple) { + private init() {} + + weak var delegate: AuthHandlerDelegate? + + func executeHandle(data: BertTuple) { guard let auth = get_Auth().parse(bert: data) as? Auth else {return} guard let type = StringAtom.string(auth.type) else {return} if type == "deleted" { @@ -36,7 +40,7 @@ class AuthHandler: BaseHandler { } } - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { let auths = data.elements.compactMap { get_Auth().parse(bert: $0) as? Auth } delegate?.processGetAll(auths: auths) } diff --git a/Nynja/ChatService/SenderService.swift b/Nynja/ChatService/SenderService.swift index c82826118..7c1d01954 100644 --- a/Nynja/ChatService/SenderService.swift +++ b/Nynja/ChatService/SenderService.swift @@ -16,7 +16,7 @@ final class SenderService: InitializeInjectable { init(dependencies: Dependencies) { mqttService = dependencies.mqttService - IoHandler.delegate = self + IoHandler.shared.delegate = self } @@ -35,7 +35,7 @@ final class SenderService: InitializeInjectable { } func updateSubscribes() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } } diff --git a/Nynja/DB/Models/DBMember.swift b/Nynja/DB/Models/DBMember.swift index 2b456cbcb..1ef475ada 100644 --- a/Nynja/DB/Models/DBMember.swift +++ b/Nynja/DB/Models/DBMember.swift @@ -176,6 +176,10 @@ class DBMember: Record, DBModelProtocol { return member } + static func memberAlias(from db: Database, roomId: String, phoneId: String) throws -> String? { + return try requestAlias(roomId: roomId, phoneId: phoneId).fetchOne(db) + } + private func construct(_ db: Database) throws { let memberId = "\(self.id)" self.features = (try? DBMember.requestFeature(targetId: memberId).fetchAll(db)) ?? [] @@ -202,16 +206,35 @@ class DBMember: Record, DBModelProtocol { } // MARK: - Requests + static private func requestFeature(targetId: String) -> QueryInterfaceRequest { return DBFeature.request(targetId: targetId, targetType: DBFeature.TargetType.member) } + static private func requestAlias(roomId: String, phoneId: String) -> AnyTypedRequest { + let sql = sqlMember(roomId: roomId, phoneId: phoneId, selection: MemberTable.Column.alias.title) + return SQLRequest(sql).asRequest(of: String.self) + } + static private func requestMember(roomId: String, phoneId: String) -> AnyTypedRequest { + let sql = sqlMember(roomId: roomId, phoneId: phoneId) + return SQLRequest(sql).asRequest(of: DBMember.self) + } + + static private func requestMember(roomId: String, isAdmin: Bool) -> AnyTypedRequest { + let sql = sqlAdmin(roomId: roomId, isAdmin: isAdmin) + return SQLRequest(sql).asRequest(of: DBMember.self) + } + + + // MARK: SQL + + static private func sqlMember(roomId: String, phoneId: String, selection: String = "*") -> String { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name let sql = """ - SELECT \(memberTable).* + SELECT \(memberTable).\(selection) FROM \(roomMemberTable) LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = \(memberTable).\(MemberTable.Column.id.title) @@ -219,23 +242,22 @@ class DBMember: Record, DBModelProtocol { AND \(MemberTable.Column.phoneId.title) = '\(phoneId)' """ - return SQLRequest(sql).asRequest(of: DBMember.self) + return sql } - static private func requestMember(roomId: String, isAdmin: Bool) -> AnyTypedRequest { + static private func sqlAdmin(roomId: String, isAdmin: Bool) -> String { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name let sql = """ - SELECT \(memberTable).* - FROM \(roomMemberTable) - LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = - \(memberTable).\(MemberTable.Column.id.title) - WHERE \(RoomMemberTable.Column.roomId.title) = '\(roomId)' - AND \(RoomMemberTable.Column.isAdmin.title) = \(isAdmin ? 1 : 0) - """ + SELECT \(memberTable).* + FROM \(roomMemberTable) + LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = + \(memberTable).\(MemberTable.Column.id.title) + WHERE \(RoomMemberTable.Column.roomId.title) = '\(roomId)' + AND \(RoomMemberTable.Column.isAdmin.title) = \(isAdmin ? 1 : 0) + """ - return SQLRequest(sql).asRequest(of: DBMember.self) + return sql } - } diff --git a/Nynja/ExtendedStarHandler.swift b/Nynja/ExtendedStarHandler.swift index 0322062f2..f0dc2beb4 100644 --- a/Nynja/ExtendedStarHandler.swift +++ b/Nynja/ExtendedStarHandler.swift @@ -10,7 +10,16 @@ import Foundation final class ExtendedStarHandler: BaseHandler { - static func executeHandle(data: BertList) { + // MARK: - Singleton + + static let shared = ExtendedStarHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertList) { let extendedStars = data.elements.compactMap { get_ExtendedStar().parse(bert: $0) as? ExtendedStar } let stars = extendedStars.compactMap { extendedStar -> DBStar? in @@ -24,5 +33,4 @@ final class ExtendedStarHandler: BaseHandler { try? StorageService.sharedInstance.perform(action: .save, with: stars) } - } diff --git a/Nynja/Extensions/Models/StarExtension.swift b/Nynja/Extensions/Models/StarExtension.swift index 95cbaf53a..8f5f4d171 100644 --- a/Nynja/Extensions/Models/StarExtension.swift +++ b/Nynja/Extensions/Models/StarExtension.swift @@ -12,6 +12,10 @@ extension Star: DialogCellModel { private static let deletedAccountTitle = MessageSender.deleted.fullname + var feedId: String? { + return message?.chatId + } + var title: String! { if let feedName = self.message?.feedName { let sender = self.message?.senderName ?? Star.deletedAccountTitle.localized diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 361ff5bfb..056de2385 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -678,12 +678,18 @@ internal extension String { static var messageDelay: String { return localizable.tr("Localizable", "message_delay") } /// New messages static var messageNewMessages: String { return localizable.tr("Localizable", "message_new_messages") } - /// ...sending a + /// sending static var messageSending: String { return localizable.tr("Localizable", "message_sending") } /// edited static var messageStatusEdited: String { return localizable.tr("Localizable", "message_status_edited") } - /// ...typing + /// typing static var messageStatusTyping: String { return localizable.tr("Localizable", "message_status_typing") } + /// %@ people + static func messageTypingStatusPeople(_ p1: String) -> String { + return localizable.tr("Localizable", "message_typing_status_people", p1) + } + /// ... + static var messageTypingStatusUndefined: String { return localizable.tr("Localizable", "message_typing_status_undefined") } /// meters static var meters: String { return localizable.tr("Localizable", "meters") } /// microphone @@ -864,7 +870,7 @@ internal extension String { static var questionEndCall: String { return localizable.tr("Localizable", "question_end_call") } /// Are you sure you want to leave the call? static var questionEndCallP2p: String { return localizable.tr("Localizable", "question_end_call_p2p") } - /// ...recording a + /// recording static var recording: String { return localizable.tr("Localizable", "recording") } /// Remove static var remove: String { return localizable.tr("Localizable", "remove") } diff --git a/Nynja/HandlerFactory.swift b/Nynja/HandlerFactory.swift index c94fbcbee..4f855ddb8 100644 --- a/Nynja/HandlerFactory.swift +++ b/Nynja/HandlerFactory.swift @@ -8,40 +8,40 @@ final class HandlerFactory { - static func handler(for handlerType: Handlers) -> BaseHandler.Type { + static func handler(for handlerType: Handlers) -> BaseHandler { switch handlerType { case .io: - return IoHandler.self + return IoHandler.shared case .profile: - return ProfileHandler.self + return ProfileHandler.shared case .roster: - return RosterHandler.self + return RosterHandler.shared case .contact: - return ContactHandler.self + return ContactHandler.shared case .history: - return HistoryHandler.self + return HistoryHandler.shared case .message: - return MessageHandler.self + return MessageHandler.shared case .search: - return SearchHandler.self + return SearchHandler.shared case .room: - return RoomHandler.self + return RoomHandler.shared case .member: - return MemberHandler.self + return MemberHandler.shared case .typing: - return TypingHandler.self + return TypingHandler.shared case .star: - return StarHandler.self + return StarHandler.shared case .job: - return JobHandler.self + return JobHandler.shared case .extendedStar: - return ExtendedStarHandler.self + return ExtendedStarHandler.shared case .auth: - return AuthHandler.self + return AuthHandler.shared case .link: - return LinkHandler.self + return LinkHandler.shared case .errors: - return ErrorsHandler.self + return ErrorsHandler.shared } } diff --git a/Nynja/JobHandler.swift b/Nynja/JobHandler.swift index cbf72bb79..c9b40eaf4 100644 --- a/Nynja/JobHandler.swift +++ b/Nynja/JobHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class JobHandler: BaseHandler { +final class JobHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = JobHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let job = get_Job().parse(bert: data) as? Job, let status = StringAtom.string(job.status) else { return @@ -23,6 +32,5 @@ class JobHandler: BaseHandler { break } } - } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index c0c44cc22..53e85b008 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -8,19 +8,14 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageContentView: BaseView { - private static let contentFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, - height: Constraints.contentLabel.height.adjustedByWidth)! + private static let dateFormatter = DialogDateConverter() - private static let contentBoldFont = UIFont.makeFont(with: FontFamily.NotoSans.bold.name, - height: Constraints.contentLabel.height.adjustedByWidth)! - - // MARK: - Views - private var contentLeftSuperviewConstraint: Constraint? - private var contentLeftContentTypeConstraint: Constraint? + // MARK: - Views private(set) lazy var titleLabel: UILabel = { let height = Constraints.titleLabel.height.adjustedByWidth @@ -28,50 +23,78 @@ final class ChatListMessageContentView: BaseView { let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.medium.name) label.accessibilityIdentifier = "chat_name" + addSubview(label) label.snp.makeConstraints { maker in - maker.top.left.right.equalToSuperview() + maker.top.left.equalToSuperview() maker.height.equalTo(height) } return label }() - private(set) lazy var contentLabel: AlignableLabel = { - let height = Constraints.contentLabel.height.adjustedByWidth - let contentIconInset = Constraints.contentLabel.contentTypeIconInset.adjustedByWidth + private(set) lazy var timeLabel: UILabel = { + let leftInfset = Constraints.timeLabel.leftInset.adjustedByWidth + + let height = Constraints.timeLabel.height.adjustedByWidth let color = UIColor.nynja.manatee - let label = AlignableLabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name) - label.verticalAlignement = .top - label.numberOfLines = 1 + let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name, textAlignment: .right) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.setContentHuggingPriority(.required, for: .horizontal) addSubview(label) label.snp.makeConstraints { maker in - maker.top.equalTo(titleLabel.snp.bottom) - contentLeftSuperviewConstraint = maker.left.equalToSuperview() - .constraint - contentLeftContentTypeConstraint = maker.left.equalTo(contentTypeImageView.snp.right).offset(contentIconInset) - .constraint - maker.bottom.right.equalToSuperview() + maker.top.right.equalToSuperview() + maker.left.equalTo(titleLabel.snp.right).offset(leftInfset) maker.height.equalTo(height) } return label }() + + private(set) lazy var detailsView: ChatListMessageDetailsView = { + let detailsView = ChatListMessageDetailsView() + + addSubview(detailsView) + detailsView.snp.makeConstraints { maker in + maker.top.equalTo(titleLabel.snp.bottom) + maker.bottom.left.equalToSuperview() + } + + return detailsView + }() - private(set) lazy var contentTypeImageView: UIImageView = { - let size = Constraints.contentTypeImageView.size.adjustedByWidth - let imageView = UIImageView() + private(set) lazy var typingView: TypingView = { + let height = Constraints.typingView.height.adjustedByWidth - addSubview(imageView) - imageView.snp.makeConstraints { maker in - maker.left.equalToSuperview() + let typingView = TypingView() + + addSubview(typingView) + typingView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.width.height.equalTo(size) + maker.left.equalToSuperview() + maker.height.equalTo(height) } - return imageView + return typingView + }() + + private(set) lazy var indicatorsView: ChatListMessageIndicatorsView = { + let leftInfset = Constraints.indicatorsView.leftInset.adjustedByWidth + let topInset = Constraints.indicatorsView.topInset.adjustedByWidth + + let indicatorsView = ChatListMessageIndicatorsView() + + addSubview(indicatorsView) + indicatorsView.snp.makeConstraints { maker in + maker.top.equalTo(timeLabel.snp.bottom).offset(topInset) + maker.right.equalToSuperview() + maker.left.equalTo(detailsView.snp.right).offset(leftInfset) + maker.left.equalTo(typingView.snp.right).offset(leftInfset) + } + + return indicatorsView }() @@ -79,20 +102,7 @@ final class ChatListMessageContentView: BaseView { override func baseSetup() { super.baseSetup() - contentLabel.isHidden = false - hideImageView() - } - - func showImageView() { - contentTypeImageView.isHidden = false - contentLeftSuperviewConstraint?.deactivate() - contentLeftContentTypeConstraint?.activate() - } - - func hideImageView() { - contentTypeImageView.isHidden = true - contentLeftSuperviewConstraint?.activate() - contentLeftContentTypeConstraint?.deactivate() + indicatorsView.isHidden = false } func setupTitle(_ title: String?) { @@ -100,60 +110,53 @@ final class ChatListMessageContentView: BaseView { titleLabel.accessibilityValue = title } - func setupContentTypeImage(_ image: UIImage?) { - contentTypeImageView.image = image + func setup(sender: String?, image: UIImage?, text: String?) { + detailsView.setup(sender: sender, image: image, text: text) } - func setupContent(_ text: String?) { - contentLabel.text = text + func setup(date: Date) { + timeLabel.text = type(of: self).dateFormatter.toString(date) } - func setupContent(sender: String, text: String) { - let defaultAttributes: [NSAttributedStringKey: Any] = [ - .foregroundColor: UIColor.nynja.manatee, - .font: type(of: self).contentFont - ] - let boldAttributes: [NSAttributedStringKey: Any] = [ - .foregroundColor: UIColor.nynja.manatee, - .font: type(of: self).contentBoldFont - ] - - let boldText = "\(sender):" - let resultText = "\(boldText) \(text)" - - let attributedText = NSMutableAttributedString(string: resultText, attributes: defaultAttributes) - - let boldRange = (boldText.startIndex.. BertObject { let topic = BertAtom(fromString: "Typing") - let _phone_id = Bert.getBin(self.phone_id) - var _comments: BertObject = BertNil() - if let com = self.comments as? String { - _comments = Bert.getBin(com) + + let feedId = Bert.getBin(self.feed_id) + let senderId = Bert.getBin(self.sender_id) + let senderAlias = Bert.getBin(self.sender_alias) + + let comments: BertObject + if let _comments = self.comments as? String { + comments = Bert.getBin(_comments) + } else { + comments = BertNil() } - return BertTuple(fromElements: [topic, _phone_id, _comments]) + return BertTuple(fromElements: [topic, feedId, senderId, senderAlias, comments]) } } - diff --git a/Nynja/MemberHandler.swift b/Nynja/MemberHandler.swift index 361d24a8b..f81c1b922 100644 --- a/Nynja/MemberHandler.swift +++ b/Nynja/MemberHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class MemberHandler: BaseHandler { +final class MemberHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = MemberHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let member = get_Member().parse(bert: data) as? Member, let status = (member.status as? StringAtom)?.string else { return @@ -24,8 +33,6 @@ class MemberHandler: BaseHandler { default: return } - } - } diff --git a/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift b/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift index 170fa07f9..f21673f79 100644 --- a/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift +++ b/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift @@ -11,7 +11,7 @@ class AddContactInteractor: AddContactInteractorInputProtocol, IoHandlerDelegate weak var presenter: AddContactInteractorOutputProtocol! init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } func addContact(contact: Contact) { diff --git a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift index 21d47a38e..2639da682 100644 --- a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift +++ b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift @@ -20,7 +20,7 @@ final class AddContactByUsernameInteractor: AddContactByUsernameInteractorInputP init() { mqttService = MQTTService.sharedInstance mqttService.addSubscriber(self) - IoHandler.delegate = self + IoHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift index b3e89801c..4a9c81ff8 100644 --- a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift +++ b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift @@ -20,7 +20,7 @@ final class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProto // MARK: - Init init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift index c0e1a555b..3c107f5cd 100644 --- a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift +++ b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift @@ -22,7 +22,7 @@ class LoginInteractor: BaseInteractor, LoginInteractorInputProtocol, IoHandlerDe // MARK: - Configure func configure() { - IoHandler.delegate = self + IoHandler.shared.delegate = self mqttService.addSubscriber(self) mqttService.tryReconnect() } diff --git a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift index c525eddce..a3d25180d 100644 --- a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift +++ b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift @@ -37,7 +37,7 @@ final class VerifyNumberInteractor: BaseInteractor, VerifyNumberInteractorInputP // MARK: - Config func configure() { - IoHandler.delegate = self + IoHandler.shared.delegate = self mqttService.addSubscriber(self) setupObservers() } diff --git a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift index 943525191..c00bf609c 100644 --- a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift +++ b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift @@ -33,7 +33,7 @@ final class NewChannelInteractor: BaseInteractor, NewChannelInteractorInputProto override init() { super.init() - LinkHandler.delegate = self + LinkHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index c5aafbdb5..e9ee87810 100644 --- a/Nynja/Modules/ChatsList/ChatsListProtocols.swift +++ b/Nynja/Modules/ChatsList/ChatsListProtocols.swift @@ -28,7 +28,7 @@ protocol ChatsListViewProtocol: class { func setup(with state: CollectionState<[Contact]>, displayMode: CollectionDisplayMode) } -protocol ChatsListPresenterProtocol: BasePresenterProtocol { +protocol ChatsListPresenterProtocol: BasePresenterProtocol, TypingObservable { var view: ChatsListViewProtocol! { get set } var interactor: ChatsListInteractorInputProtocol! { get set } @@ -50,7 +50,7 @@ protocol ChatsListInteractorOutputProtocol: class { func didFilter(chats: [Contact]) } -protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol { +protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var presenter: ChatsListInteractorOutputProtocol! { get set } diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 02aa17311..1dc1e96a3 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -12,21 +12,32 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingProvider private var chats: [Contact] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + typingProvider = dependencies.typingProvider + + super.init() + } + + deinit { + typingProvider.removeObserver(self) } @@ -38,6 +49,10 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchChats() } @@ -49,6 +64,15 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini applyFilter(with: searchText) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + // MARK: - StorageSubscriber diff --git a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift index 6456463eb..2db4c11d7 100644 --- a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift +++ b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift @@ -33,6 +33,14 @@ class ChatsListPresenter: BasePresenter, ChatsListPresenterProtocol, ChatsListIn wireFrame.showChatWith(contact: contact) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + // MARK: - ChatsListInteractorOutputProtocol diff --git a/Nynja/Modules/ChatsList/View/ChatListTableDS.swift b/Nynja/Modules/ChatsList/View/ChatListTableDS.swift index 174497229..1a8c94bc1 100644 --- a/Nynja/Modules/ChatsList/View/ChatListTableDS.swift +++ b/Nynja/Modules/ChatsList/View/ChatListTableDS.swift @@ -14,10 +14,14 @@ final class ChatListTableDS: NSObject, UITableViewDataSource { var chatList = [Contact]() private let payloadParser: MessagePayloadParserInput + + private let typingObservable: TypingObservable + private weak var delegate: ChatListMessageCellModelDelegate? - init(payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate) { + init(payloadParser: MessagePayloadParserInput, typingObservable: TypingObservable, delegate: ChatListMessageCellModelDelegate) { self.payloadParser = payloadParser + self.typingObservable = typingObservable self.delegate = delegate } @@ -30,6 +34,9 @@ final class ChatListTableDS: NSObject, UITableViewDataSource { } private func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { - return ChatListMessageCellModel(model: chatList[indexPath.row], payloadParser: payloadParser, delegate: delegate) + return ChatListMessageCellModel(model: chatList[indexPath.row], + observable: typingObservable, + payloadParser: payloadParser, + delegate: delegate) } } diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index 7f512269c..15702f53d 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -78,6 +78,12 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable setupUI() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // FIXME: update visile cells in other way + tableView.reloadData() + } + override func prepareForDissappear() { super.prepareForDissappear() if !swipeBackHelper.isSwipeActive { @@ -120,7 +126,7 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height tableView.estimatedRowHeight = tableView.rowHeight - dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), typingObservable: presenter, delegate: self) emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) tableView.dataSource = emptyStateDS diff --git a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift index 608c9433c..a1bbd89b0 100644 --- a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift +++ b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift @@ -14,13 +14,17 @@ class ChatsListWireFrame: ChatsListWireFrameProtocol { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) { + let serviceFactory = ServiceFactory() + // Componenets let view = ChatsListViewController() let presenter = ChatsListPresenter() let interactor = ChatsListInteractor( - dependencies: .init(storageService: StorageService.sharedInstance, - conversationsProvider: ConversationsProvider())) + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) self.main = main diff --git a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift index 5b0a313da..f6839b618 100644 --- a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift +++ b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift @@ -19,7 +19,7 @@ class ContactsInteractor: BaseInteractor, ContactsInteractorInputProtocol, IoHan init(mode: ContactViewMode) { contactViewMode = mode super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } //MARK: - BaseInteractor diff --git a/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift b/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift index cb9d7bd25..f7319c2cc 100644 --- a/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift +++ b/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift @@ -25,7 +25,7 @@ class EditUsernameInteractor: BaseInteractor, EditUsernameInteractorInputProtoco override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } func save(username: String) { diff --git a/Nynja/Modules/GroupsList/GroupsListProtocols.swift b/Nynja/Modules/GroupsList/GroupsListProtocols.swift index 6f973d065..f10ba5bd2 100644 --- a/Nynja/Modules/GroupsList/GroupsListProtocols.swift +++ b/Nynja/Modules/GroupsList/GroupsListProtocols.swift @@ -30,7 +30,7 @@ protocol GroupsListViewProtocol: class { func setup(with state: CollectionState<[Room]>, displayMode: CollectionDisplayMode) } -protocol GroupsListPresenterProtocol: BasePresenterProtocol { +protocol GroupsListPresenterProtocol: BasePresenterProtocol, TypingObservable { var view: GroupsListViewProtocol! { get set } var wireFrame: GroupsListWireFrameProtocol! { get set } @@ -55,7 +55,7 @@ protocol GroupsListInteractorOutputProtocol: class { func didFilter(groups: [Room]) } -protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol { +protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var presenter: GroupsListInteractorOutputProtocol! { get set } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index c16ee18b5..030bc26d8 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -12,23 +12,35 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingProvider private var chats: [Room] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + typingProvider = dependencies.typingProvider + + super.init() + } + + deinit { + typingProvider.removeObserver(self) } + // MARK: - BaseInteractor override var subscribes: [SubscribeType]? { @@ -41,6 +53,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchGroups() } @@ -52,6 +68,15 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I applyFilter(with: searchText) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + // MARK: - StorageSubscriber diff --git a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift index 4cd181f2e..8600c93ae 100644 --- a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift +++ b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift @@ -43,6 +43,14 @@ class GroupsListPresenter: BasePresenter, GroupsListPresenterProtocol, GroupsLis self.interactor.filter(with: text) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + // MARK: - GroupsListInteractorOutputProtocol diff --git a/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift b/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift index c7a9dbc10..49470a986 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift @@ -14,10 +14,17 @@ class GroupsListTableDS: NSObject, UITableViewDataSource { var rooms = [Room]() private let payloadParser: MessagePayloadParserInput + + private let typingObservable: TypingObservable? + private weak var delegate: ChatListMessageCellModelDelegate? - init(payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate? = nil) { + init(payloadParser: MessagePayloadParserInput, + typingObservable: TypingObservable? = nil, + delegate: ChatListMessageCellModelDelegate? = nil) { + self.payloadParser = payloadParser + self.typingObservable = typingObservable self.delegate = delegate } @@ -30,7 +37,9 @@ class GroupsListTableDS: NSObject, UITableViewDataSource { } private func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { - return ChatListMessageCellModel(model: rooms[indexPath.row], payloadParser: payloadParser, delegate: delegate) + return ChatListMessageCellModel(model: rooms[indexPath.row], + observable: typingObservable, + payloadParser: payloadParser, + delegate: delegate) } - } diff --git a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift index 780533112..1c779d577 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift @@ -88,6 +88,8 @@ final class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipab override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) presenter.viewWillAppear() + // FIXME: update visile cells in other way + tableView.reloadData() } @@ -107,7 +109,7 @@ final class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipab tableView.register(viewModel: ChatListMessageCellModel.self) tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height - dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), typingObservable: presenter, delegate: self) emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) tableView.dataSource = emptyStateDS diff --git a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift index 3db6d4df5..bb52986b2 100644 --- a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift +++ b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift @@ -18,15 +18,16 @@ class GroupsListWireFrame: GroupsListWireFrameProtocol { self.main = main // Dependencies - let conversationsProvider = ConversationsProvider() - + let serviceFactory = ServiceFactory() // Compomentes let view = GroupsListViewController() let presenter = GroupsListPresenter() let interactor = GroupsListInteractor( - dependencies: .init(storageService: StorageService.sharedInstance, - conversationsProvider: ConversationsProvider())) + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) // Connecting view.presenter = presenter diff --git a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift index 0c3cf1677..6608c5d16 100644 --- a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift +++ b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift @@ -24,7 +24,7 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } //MARK: - BaseInteractor diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 2b624dc6e..431beb157 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -10,7 +10,7 @@ import UIKit import CoreLocation -final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, TypingHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { +final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { private var callService = NynjaCommunicatorService.sharedInstance @@ -98,6 +98,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let stickersProvider: StickersProviding private var presenceProvider: PresenceStatusProvider! + + private let typingProvider: TypingProvider private let historyRequestFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() @@ -176,6 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) + typingProvider = TypingProviderImpl.shared super.init() @@ -184,20 +187,26 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } mqttService.addSubscriber(self) + + if let chatId = chat.id { + typingProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in + self?.presenter?.didReceiveTyping(typingInfo) + } + } ConnectionService.shared.addSubscriber(self) - MessageHandler.addSubscriber(self) - HistoryHandler.addSubscriber(self) + MessageHandler.shared.addSubscriber(self) + HistoryHandler.shared.addSubscriber(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() } - - + deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) - MessageHandler.removeSubscriber(self) - HistoryHandler.removeSubscriber(self) + typingProvider.removeObserver(self) + MessageHandler.shared.removeSubscriber(self) + HistoryHandler.shared.removeSubscriber(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -222,8 +231,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H processingManager.delegate = self - TypingHandler.delegate = self - isAfterConnectionAppeared = false prepareInitialValues() @@ -436,6 +443,13 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H presenter?.internetStatusChanged(.waiting) } } + + func askForTypingStatus() { + guard let feedId = chat.id, let typing = typingProvider.typingStatus(for: feedId) else { + return + } + presenter?.didReceiveTyping(typing) + } // MARK: - Send Message func sendMessage(_ message: InputTextMessage) { @@ -940,21 +954,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H self.performAutoConversion(for: self.configuration.messages) } } - - // MARK: - TypingHandlerDelegate - func getTyping(typing: Typing) { - guard self.contact?.phone_id == typing.phone_id, let typingModelType = typing.type else { - return - } - - let actionStatus = ActionStatus(typingModelType: typingModelType) - presenter?.actionStatusChanged(actionStatus) - if typingModelType != .done { - dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in - self?.presenter?.restoreStatus() - } - } - } func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { if service == .networking { @@ -982,6 +981,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private func chatUpdated(from oldChat: ChatModel, to newChat: ChatModel) { presenter?.chatUpdated(newChat) if let status = presenceProvider.presence(for: newChat) { + presenceProvider.refreshTimer(for: newChat) presenter?.presenceStatusChanged(status) } diff --git a/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift b/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift index c53f3c856..1c207c69c 100644 --- a/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift +++ b/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift @@ -46,42 +46,23 @@ class PresenceStatusProvider { } private func presence(contact: Contact) -> PresenceStatus { - invalidatePresenceTimer() - guard contact.presenceStatus != "online" else { return .active } - let minutesDiff = minutesAfterOffline(contact.updated) - if minutesDiff >= minutes { - return .inactive - } else { - let interval = timeInterval - minutesDiff * 60 - presenceTimer = TimerHandler(interval: interval, repeats: false) { [weak self] _ in - self?.didContactBecomeInactive() - } - - return .active - } + return minutesDiff >= minutes ? .inactive : .active } private func presence(group: Room) -> PresenceStatus { - invalidatePresenceTimer() - let allMembers = group.allMembers ?? [] - let activeCount = allMembers.reduce(0) { (temp, member) in + let activeCount = allMembers.reduce(0) { count, member in let minutesDiff = minutesAfterOffline(member.updated) if StringAtom.string(member.presence) == "online" || minutesDiff < minutes { - return temp + 1 + return count + 1 } - - return temp - } - - presenceTimer = TimerHandler(interval: timeInterval, repeats: false) { [weak self] _ in - self?.checkCountOfActiveMembers() + return count } return .room(allMembers.count, activeCount) @@ -102,15 +83,38 @@ class PresenceStatusProvider { // MARK: - Timer - @objc private func didContactBecomeInactive() { + func refreshTimer(for chat: ChatModel) { invalidatePresenceTimer() - handler(.inactive) + + switch chat { + case let chat as Contact: + let minutesDiff = minutesAfterOffline(chat.updated) + if minutesDiff < minutes { + let interval = timeInterval - minutesDiff * 60 + presenceTimer = TimerHandler(interval: interval, repeats: false) { [weak self] _ in + self?.didContactBecomeInactive() + } + } + case let chat as Room where chat.kind == .group: + presenceTimer = TimerHandler(interval: timeInterval, repeats: false) { [weak self] _ in + self?.checkCountOfActiveMembers() + } + default: + break + } } - @objc private func checkCountOfActiveMembers() { + private func didContactBecomeInactive() { invalidatePresenceTimer() + handler(.inactive) + } + + private func checkCountOfActiveMembers() { if let chat = self.chat, let status = presence(for: chat) { + refreshTimer(for: chat) handler(status) + } else { + invalidatePresenceTimer() } } @@ -118,5 +122,4 @@ class PresenceStatusProvider { presenceTimer?.invalidate() presenceTimer = nil } - } diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift new file mode 100644 index 000000000..1cedf26b2 --- /dev/null +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift @@ -0,0 +1,13 @@ +// +// ChatStatus.swift +// Nynja +// +// Created by Anton Poltoratskyi on 31.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct ChatStatus { + var presence: PresenceStatus? + var internet: InternetStatus + var typing: TypingDisplayModel +} diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift new file mode 100644 index 000000000..dfd9c1d6f --- /dev/null +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift @@ -0,0 +1,12 @@ +// +// ChatStatusDisplayInfo.swift +// Nynja +// +// Created by Anton Poltoratskyi on 31.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum ChatStatusDisplayInfo { + case text(String) + case typing(TypingDisplayModel.SenderInfo?, TypingDisplayModel.Status) +} diff --git a/Nynja/Modules/Message/Models/Statuses/InternetStatus.swift b/Nynja/Modules/Message/Models/Statuses/Internet/InternetStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/InternetStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Internet/InternetStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/PresenceStatus.swift b/Nynja/Modules/Message/Models/Statuses/Presence/PresenceStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/PresenceStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Presence/PresenceStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift similarity index 89% rename from Nynja/Modules/Message/Models/Statuses/ActionStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift index 5c3c25c45..3ba25b8fa 100644 --- a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift @@ -6,12 +6,19 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum ActionStatus { +enum ActionStatus: Equatable { case done case typing case sending(SendingStatus) case recording(RecordingStatus) + var isDone: Bool { + if case .done = self { + return true + } + return false + } + var title: String { switch self { case .done: @@ -47,5 +54,4 @@ enum ActionStatus { self = .recording(.voice) } } - } diff --git a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift similarity index 89% rename from Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift index bfb75367e..a65d33387 100644 --- a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum RecordingStatus: String { +enum RecordingStatus: String, Equatable { case video = "video" case voice = "voice_message" diff --git a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift similarity index 91% rename from Nynja/Modules/Message/Models/Statuses/SendingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift index 7ab436017..3a5ad85bc 100644 --- a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum SendingStatus: String { +enum SendingStatus: String, Equatable { case file = "file" case video = "video" case voice = "voice_message" diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index d2fa97a0d..d7f0d6d4f 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -10,23 +10,19 @@ import UIKit import CoreLocation.CLLocation -class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol, ForwardSelectorDelegate { +final class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol, ForwardSelectorDelegate { + + // MARK: - Properties - //MARK: - Properties var isMyselfChat: Bool { return interactor.isMyselfChat } - private var lastStatus: String = "" { - didSet { updateStatus(lastStatus) } - } - - private var internetStatus: InternetStatus = .connected private var isActionsEnabled: Bool = true private var wasViewDisappeared: Bool = false private let chatScreenAlertFactory: ChatScreenAlertFactoryProtocol = ChatScreenAlertFactory() - // -- unread mention counter + // MARK: Unread mention counter var uniqueUnreadMentionIds: Set = [] var unreadMentionIds: [MessageServerId] = [] @@ -39,10 +35,44 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } } } + + // MARK: Header Status + + private var headerStatus = ChatStatus(presence: nil, internet: .connected, typing: .none) { + didSet { + let internetStatus = headerStatus.internet + let presenceStatus = headerStatus.presence + let typing = headerStatus.typing + + switch internetStatus { + case .waiting, .connecting: + // Always show internet status when not connected + view?.updateHeaderStatus(.text(internetStatus.rawValue.localized)) + + case .connected: + // Show typing if exists, otherwise - show presence + + switch typing { + case let .typing(sender, status): + if status.isDone { + fallthrough + } + view.updateHeaderStatus(.typing(sender, status)) + + case .none: + if let presence = presenceStatus { + view?.updateHeaderStatus(.text(presence.title)) + } else { + view.updateHeaderStatus(.text(internetStatus.rawValue.localized)) + } + } + } + } + } + - // -- - - //MARK: - BasePresenter + // MARK: - BasePresenter + override var itemsFactory: WCItemsFactory? { if isMyselfChat { return MySelfItemsFactory(isActionsEnabled: true) @@ -73,11 +103,13 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract super.screenLoaded() interactor.askForInternetStatus() + interactor.askForTypingStatus() interactor.room.map { fetchMentionedMessages(in: $0) } } - //MARK: - MessagePresenterProtocol + // MARK: - MessagePresenterProtocol + weak var view: MessageViewProtocol! var wireFrame: MessageWireframeProtocol! var interactor: MessageInteractorInputProtocol! { @@ -951,29 +983,15 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func internetStatusChanged(_ status: InternetStatus) { - internetStatus = status - switch status { - case .waiting, .connecting: - view?.updateHeaderStatus(status.rawValue.localized) - case .connected: - restoreStatus() - } + headerStatus.internet = status } func presenceStatusChanged(_ status: PresenceStatus) { - lastStatus = status.title - } - - func actionStatusChanged(_ status: ActionStatus) { - if case .done = status { - restoreStatus() - } else { - view.updateHeaderStatus(status.title) - } + headerStatus.presence = status } - func restoreStatus() { - view.updateHeaderStatus(lastStatus) + func didReceiveTyping(_ typing: TypingDisplayModel) { + headerStatus.typing = typing } func messageSent(_ localId: String) { @@ -1015,12 +1033,9 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract self?.openProfileScreen(contact: contact) } } + + // MARK: - Utils - private func updateStatus(_ status: String) { - if internetStatus == .connected { - view.updateHeaderStatus(status) - } - } func getPreviousMessages(id: MessageServerId) { self.interactor.fetchMessages(from: id, isNew: true) diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index a63aa0a4a..d8999361d 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -157,8 +157,7 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func internetStatusChanged(_ status: InternetStatus) func presenceStatusChanged(_ status: PresenceStatus) - func actionStatusChanged(_ status: ActionStatus) - func restoreStatus() + func didReceiveTyping(_ typing: TypingDisplayModel) func messageSent(_ localId: MessageLocalId) func messageRead(_ localId: MessageLocalId) @@ -251,6 +250,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func sender(for message: Message) -> MessageSender func askForInternetStatus() + func askForTypingStatus() func editMessage(_ message: InputTextMessage) func clearEditMessageObject() @@ -302,7 +302,7 @@ protocol MessageViewProtocol: class { func scrollToBottomIfNeeded() func scrollToBottom() - func updateHeaderStatus(_ status: String) + func updateHeaderStatus(_ status: ChatStatusDisplayInfo) func updateDeliveryStatus(_ status: DeliveryStatus, messageId: String) func removeMessage(_ messageId: MessageLocalId, isForAllUsers: Bool) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index c04f374aa..e43591926 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -88,12 +88,10 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw av.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed))) self.view.addSubview(av) - av.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.avatarView.height.adjustedByWidth) - - self.adjustVerticalInset(.top, make: make, offset: Constraints.avatarView.topInset.adjustedByWidth) + av.snp.makeConstraints { make in + adjustVerticalInset(.top, make: make) make.left.right.equalToSuperview() - }) + } return av }() @@ -1162,7 +1160,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw avatarView.setup(with: viewModel) } - func updateHeaderStatus(_ status: String) { + func updateHeaderStatus(_ status: ChatStatusDisplayInfo) { avatarView.status = status } diff --git a/Nynja/Modules/Message/View/MessageVCLayout.swift b/Nynja/Modules/Message/View/MessageVCLayout.swift index b39e1d1f8..fae8c7252 100644 --- a/Nynja/Modules/Message/View/MessageVCLayout.swift +++ b/Nynja/Modules/Message/View/MessageVCLayout.swift @@ -10,11 +10,6 @@ extension MessageVC { enum Constraints { - enum avatarView { - static let topInset = 8.0 - static let height = 48.0 - } - enum tableView { static let defaultVerticalInset = CGFloat(4.adjustedByWidth) static let bottomInset = CGFloat(gradientView.height) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index c8c2d2ce6..44212f7e3 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -7,49 +7,87 @@ // import SnapKit +import NynjaUIKit -class AvatarView: BaseView { +final class AvatarView: BaseView { override var activatedViews: [UIView] { return [statusLabel, separatorView] } - var status: String? { - didSet { statusLabel.text = status - statusLabel.accessibilityValue = status + var status: ChatStatusDisplayInfo? { + didSet { + guard let status = status else { + return + } + + switch status { + case let .text(statusString): + statusLabel.text = statusString + statusLabel.accessibilityValue = statusString + + hideTyping() + + case let .typing(sender, status): + let indicator: TypingView.Appearance.Indicator + switch status.action { + case .recording?: + indicator = .circle(UIColor.nynja.white) + default: + indicator = .dots(UIColor.nynja.white) + } + + let appearance = TypingView.Appearance(indicator: indicator, + textColor: titleLabel.textColor, + textFont: statusLabel.font, + senderInfo: sender?.displayName, + typingInfo: status.title + ) + typingView.update(appearance) + + showTyping() + } } } private var muteWidthConstraint: Constraint? private var muteLeftConstraint: Constraint? + // MARK: - Views - private lazy var imageView: UIImageView = { - let img = UIImageView() - img.contentMode = .scaleAspectFill + + private lazy var avatarView: AvatarStatusView = { + let statusIconPadding = Constraints.imageView.statusIconPadding.adjustedByWidth + let statusIconSize = Constraints.imageView.statusIconSize.adjustedByWidth + + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconPadding = statusIconPadding + avatarView.statusIconSize = statusIconSize let width = Constraints.imageView.width.adjustedByWidth - img.roundCorners(radius: width / 2) + let verticalInset = Constraints.imageView.vertivalInset.adjustedByWidth - self.addSubview(img) - img.snp.makeConstraints({ (make) in + addSubview(avatarView) + avatarView.snp.makeConstraints { make in make.width.height.equalTo(width) + make.top.equalToSuperview().offset(verticalInset) + make.bottom.equalToSuperview().offset(-verticalInset) make.left.equalTo(Constraints.imageView.leftInset.adjustedByWidth) - make.centerY.equalToSuperview() - }) + } - return img + return avatarView }() - private lazy var labelsView: UIView = { + private lazy var titleContainerView: UIView = { let view = UIView() - let horizontalInset = Constraints.labelsView.horizontalInset.adjustedByWidth + let horizontalInset = Constraints.titleContainerView.horizontalInset.adjustedByWidth - self.addSubview(view) - view.snp.makeConstraints { (make) in + addSubview(view) + view.snp.makeConstraints { make in make.centerY.equalToSuperview() - make.left.equalTo(imageView.snp.right).offset(horizontalInset) + make.left.equalTo(avatarView.snp.right).offset(horizontalInset) make.right.equalTo(-horizontalInset) } @@ -58,15 +96,17 @@ class AvatarView: BaseView { private lazy var titleLabel: UILabel = { let height = Constraints.titleLabel.height.adjustedByWidth - let us = UILabel(height: height, color: UIColor.nynja.white, fontName: FontFamily.NotoSans.medium.name) - us.accessibilityIdentifier = "chat_title" - self.labelsView.addSubview(us) - us.snp.makeConstraints({ (make) in + + let label = UILabel(height: height, color: UIColor.nynja.white, fontName: FontFamily.NotoSans.medium.name) + label.accessibilityIdentifier = "chat_title" + + titleContainerView.addSubview(label) + label.snp.makeConstraints { make in make.height.equalTo(height) make.top.left.equalToSuperview() - }) + } - return us + return label }() private lazy var muteImageView: UIImageView = { @@ -74,7 +114,7 @@ class AvatarView: BaseView { let side = Constraints.muteImageView.side.adjustedByWidth - self.labelsView.addSubview(imageView) + titleContainerView.addSubview(imageView) imageView.snp.makeConstraints { make in make.height.equalTo(side) muteWidthConstraint = make.width.equalTo(0).constraint @@ -89,28 +129,39 @@ class AvatarView: BaseView { private lazy var statusLabel: UILabel = { let height = Constraints.statusLabel.height.adjustedByWidth - let sl = UILabel(height: height, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.regular.name) - sl.accessibilityIdentifier = "chat_status" - self.labelsView.addSubview(sl) - sl.snp.makeConstraints({ (make) in + + let label = UILabel(height: height, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.regular.name) + label.accessibilityIdentifier = "chat_status" + + titleContainerView.addSubview(label) + label.snp.makeConstraints { make in make.height.equalTo(height) - make.top.equalTo(muteImageView.snp.bottom) + make.top.equalTo(titleLabel.snp.bottom) make.left.right.bottom.equalToSuperview() - }) + } - return sl + return label }() - private lazy var separatorView: UIView = { - let view = UIView() + private lazy var typingView: TypingView = { + let typingView = TypingView() + + titleContainerView.addSubview(typingView) + typingView.snp.makeConstraints { maker in + maker.top.bottom.equalTo(statusLabel) + maker.left.right.equalToSuperview() + } - view.backgroundColor = UIColor.nynja.backgroundGray + return typingView + }() + + private lazy var separatorView: SeparatorView = { + let view = SeparatorView() - self.addSubview(view) - view.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.separatorView.height) + addSubview(view) + view.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() - }) + } return view }() @@ -121,6 +172,8 @@ class AvatarView: BaseView { func setup(with viewModel: AvatarViewModel) { titleLabel.text = viewModel.title titleLabel.accessibilityValue = viewModel.title + + let imageView = avatarView.imageView let placeholder = imageView.image ?? UIImage.nynja.Contacts.avaPlaceholder.image imageView.setImage(url: viewModel.avatarUrl, placeHolder: placeholder, options: .delayPlaceholder) @@ -137,4 +190,16 @@ class AvatarView: BaseView { } } + + // MARK: - Layout + + private func showTyping() { + statusLabel.isHidden = true + typingView.isHidden = false + } + + private func hideTyping() { + statusLabel.isHidden = false + typingView.isHidden = true + } } diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift index 7ee65abf2..7a5ca994f 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift @@ -8,35 +8,32 @@ extension AvatarView { - struct Constraints { + enum Constraints { - struct imageView { - static let width: CGFloat = 32.0 - + enum imageView { + static let width: CGFloat = 40.0 + static let vertivalInset: CGFloat = 8.0 static let leftInset = 16.0 + + static let statusIconPadding: CGFloat = 2.0 + static let statusIconSize: CGFloat = 8.0 } - struct labelsView { + enum titleContainerView { static let horizontalInset = 16.0 } - struct titleLabel { + enum titleLabel { static let height: CGFloat = 22.0 } - struct muteImageView { + enum muteImageView { static let side = 12.0 static let leftInset = 8.0 } - struct statusLabel { + enum statusLabel { static let height: CGFloat = 20.0 } - - struct separatorView { - static let height = 1.0 - } - } - } diff --git a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift index babd3b0c4..0097dde47 100644 --- a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift +++ b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift @@ -17,7 +17,7 @@ class ParticipantsInteractor: BaseInteractor, ParticipantsInteractorInputProtoco override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } diff --git a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift index 0b73d2bbf..aa441303d 100644 --- a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift +++ b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift @@ -20,17 +20,30 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { var starred: CellModels = [] var scheduled: CellModels = [] + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + private var contactsProvider: ContactsProviding! private var conversationsProvider: ConversationsProviding! + private var typingProvider: TypingProvider! private var mqttService: MQTTService! - //MARK: - BaseInteractor + deinit { + typingProvider.removeObserver(self) + } + + + // MARK: - BaseInteractor + override var subscribes: [SubscribeType]? { return [.contact(StorageService.sharedInstance.phoneId!), .contact(nil), .room(nil), .star(nil), .job(nil), .profile] } override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchContact() fetchRooms() fetchChats() @@ -40,6 +53,9 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { fetchLastEvents() } + + // MARK: - ProfileInteractorInputProtocol + func acceptContact(with phoneId: String) { mqttService.confirmFriend(friendPhoneId: phoneId) @@ -64,6 +80,16 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { } } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + + // MARK: - StorageSubscriber override func update(with changes: [StorageChange], type: SubscribeType) { @@ -88,8 +114,10 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { } } -//MARK: Private methods -fileprivate extension ProfileInteractor { + +// MARK: - Private methods + +private extension ProfileInteractor { func fetchContact() { guard let phoneId = StorageService.sharedInstance.phoneId, @@ -156,11 +184,15 @@ fileprivate extension ProfileInteractor { } + +// MARK: - SetInjectable + extension ProfileInteractor: SetInjectable { func inject(dependencies: ProfileInteractor.Dependencies) { presenter = dependencies.presenter contactsProvider = dependencies.contactsProvider conversationsProvider = dependencies.conversationsProvider + typingProvider = dependencies.typingProvider mqttService = dependencies.mqttService } @@ -168,6 +200,7 @@ extension ProfileInteractor: SetInjectable { let presenter: ProfileInteractorOutputProtocol let contactsProvider: ContactsProviding let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider let mqttService: MQTTService } } diff --git a/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift index 1001ca59a..cc1dd1d37 100644 --- a/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift @@ -10,7 +10,10 @@ import Foundation extension Contact : DialogCellModel { - // MARK: DialogCellModel methods + var feedId: String? { + return phone_id + } + var title: String! { return fullName } diff --git a/Nynja/Modules/Profile/Presenter/DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/DialogCellModel.swift index c247a3172..608d2b279 100644 --- a/Nynja/Modules/Profile/Presenter/DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/DialogCellModel.swift @@ -8,7 +8,8 @@ import Foundation -protocol DialogCellModel : CellModel { +protocol DialogCellModel: CellModel { + var feedId: String? { get } var title: String! { get } var message: Message? { get } var hasMentions: Bool { get } diff --git a/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift b/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift index 4728e19ed..806d35cd7 100644 --- a/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift +++ b/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift @@ -115,6 +115,16 @@ class ProfilePresenter: BasePresenter, ProfilePresenterProtocol, ProfileInteract return nil } } + + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + + // MARK: - ProfileInteractorOutputProtocol diff --git a/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift index 1e8cd4a00..7934b0313 100644 --- a/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift @@ -10,7 +10,10 @@ import Foundation extension Room: DialogCellModel { - // MARK: DialogCellModel methods + var feedId: String? { + return id + } + var title: String! { return name != nil ? name! : "" } diff --git a/Nynja/Modules/Profile/ProfileProtocols.swift b/Nynja/Modules/Profile/ProfileProtocols.swift index 045570511..83626aca3 100644 --- a/Nynja/Modules/Profile/ProfileProtocols.swift +++ b/Nynja/Modules/Profile/ProfileProtocols.swift @@ -28,7 +28,7 @@ protocol ProfileWireFrameProtocol: class { func showFavorites() } -protocol ProfilePresenterProtocol: BasePresenterProtocol { +protocol ProfilePresenterProtocol: BasePresenterProtocol, TypingObservable { func showImagePreview(from imageView: UIImageView) func showQRGenerator() @@ -66,7 +66,7 @@ protocol ProfileInteractorOutputProtocol: class { func showWallet(with balance: NYNMoney?) } -protocol ProfileInteractorInputProtocol: BaseInteractorProtocol { +protocol ProfileInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var myContact: Contact! { get set } diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift index f956cc96b..3a44d47a9 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift @@ -6,25 +6,27 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class ProfileDetailsView: UIView { // MARK: Views - lazy var avatarImageView: UIImageView = { + lazy var avatarImageView: AvatarStatusView = { let width = Constraints.avatarImageView.width.adjustedByWidth - let imageView = UIImageView(frame: CGRect(x: 0, y:0, width: width, height: width)) - - imageView.contentMode = .scaleAspectFill - imageView.roundImageView() - - self.addSubview(imageView) - imageView.snp.makeConstraints({ (make) in + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconSize = CGFloat(Constraints.avatarImageView.statusIconSize.adjustedByWidth) + avatarView.statusIconPadding = CGFloat(Constraints.avatarImageView.statusIconPadding.adjustedByWidth) + + addSubview(avatarView) + avatarView.snp.makeConstraints { make in make.width.height.equalTo(width) make.top.equalToSuperview().offset(Constraints.avatarImageView.topInset.adjustedByHeight) make.left.equalToSuperview().offset(Constraints.avatarImageView.leftInset.adjustedByWidth) - }) + } - return imageView + return avatarView }() lazy var infoView: UIView = { diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift index 38591f0c8..33c9b7617 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift @@ -8,21 +8,23 @@ extension ProfileDetailsView { - struct Constraints { + enum Constraints { - struct avatarImageView { + enum avatarImageView { static let width = 95.0 + static let statusIconSize = 16.0 + static let statusIconPadding = 4.0 static let topInset = 30.0 static let leftInset = 16.0 } // MARK: Info View - struct infoView { + enum infoView { static let leftInset = 16.0 } - struct nameLabel { + enum nameLabel { static let width = 220.0 static let height = 33.0 @@ -32,7 +34,7 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct surnameLabel { + enum surnameLabel { static let width = 197.0 static let height = 33.0 @@ -41,14 +43,14 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct usernameLabel { + enum usernameLabel { static let width = 223.0 static let height = 22.0 static let heightProportion = height / width } - struct phoneLabel { + enum phoneLabel { static let width = 223.0 static let height = 22.0 @@ -57,14 +59,14 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct balanceLabel { + enum balanceLabel { static let width = 44.0 static let height = 17.0 static let topInset = 4.0 static let heightProportion = height / width } - struct fundsLabel { + enum fundsLabel { static let width = 66.0 static let height = 17.0 @@ -72,13 +74,13 @@ extension ProfileDetailsView { } // MARK: QR Button - struct qrButton { + enum qrButton { static let width: CGFloat = 40.0 static let rightInset = 16.0 } - struct addWalletButton { + enum addWalletButton { static let width = 50 static let height = 36 } diff --git a/Nynja/Modules/Profile/View/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index d5459dd4d..81be85f01 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -58,6 +58,12 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { super.initialize() setupUI() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // FIXME: update visile cells in other way + tableView.reloadData() + } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -85,7 +91,10 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { tableView.register(StarMessageCell.self, forCellReuseIdentifier: StarMessageCell.cellId) tableViewDelegate = ProfileTableViewDelegate(sectionDelegate: self) - tableViewDS = ProfileTablewViewDS(payloadParser: payloadParser, contactCellDelegate: self, chatCellDelegate: self) + tableViewDS = ProfileTablewViewDS(payloadParser: payloadParser, + typingObservable: presenter, + contactCellDelegate: self, + chatCellDelegate: self) tableView.delegate = tableViewDelegate tableView.dataSource = tableViewDS @@ -108,9 +117,9 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { //MARK: - ProfileViewProtocol func setup(contact: Contact) { - detailsView.avatarImageView.setImage(url: contact.avatarUrl, - placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image, - accessibilityPrefix: "profile_details_view") + detailsView.avatarImageView.imageView.setImage(url: contact.avatarUrl, + placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image, + accessibilityPrefix: "profile_details_view") detailsView.nameLabel.text = contact.names detailsView.surnameLabel.text = contact.surnames @@ -150,7 +159,7 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { } @objc private func avatarTapped() { - presenter.showImagePreview(from: detailsView.avatarImageView) + presenter.showImagePreview(from: detailsView.avatarImageView.imageView) } @objc private func walletButtonAction() { diff --git a/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift b/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift index f5b2e40fa..1c7cf6a6b 100644 --- a/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift +++ b/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift @@ -21,11 +21,15 @@ class ProfileTablewViewDS: NSObject, UITableViewDataSource { private let payloadParser: MessagePayloadParserInput + private let typingObservable: TypingObservable + init(payloadParser: MessagePayloadParserInput, + typingObservable: TypingObservable, contactCellDelegate: ProfileContactCellDelegate, chatCellDelegate: ChatListMessageCellModelDelegate) { self.payloadParser = payloadParser + self.typingObservable = typingObservable self.contactCellDelegate = contactCellDelegate self.chatCellDelegate = chatCellDelegate } @@ -68,6 +72,7 @@ class ProfileTablewViewDS: NSObject, UITableViewDataSource { if let cellModel = cellModel as? DialogCellModel { let model = ChatListMessageCellModel( model: cellModel, + observable: typingObservable, payloadParser: payloadParser, delegate: chatCellDelegate ) diff --git a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift index 119210b33..e64262d1e 100644 --- a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift +++ b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift @@ -16,9 +16,7 @@ class ProfileWireFrame: ProfileWireFrameProtocol { main: MainWireFrame?) { // Dependencies - let contactsProvider = ContactsProvider() - let conversationsProvider = ConversationsProvider() - let mqttService = MQTTService.sharedInstance + let serviceFactory = ServiceFactory() // Components let view = ProfileViewController() @@ -26,9 +24,10 @@ class ProfileWireFrame: ProfileWireFrameProtocol { let interactor = ProfileInteractor() let interactorDependencies = ProfileInteractor.Dependencies(presenter: presenter, - contactsProvider: contactsProvider, - conversationsProvider: conversationsProvider, - mqttService: mqttService) + contactsProvider: serviceFactory.makeContactsProvider(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider(), + mqttService: serviceFactory.makeMQTTService()) let presenterDependencies = ProfilePresenter.Dependencies(view: view, wireFrame: self, interactor: interactor) let viewDependencies = ProfileViewController.Dependencies(presenter: presenter) diff --git a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift index 60cf171cd..b5fb466d4 100644 --- a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift +++ b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift @@ -14,7 +14,7 @@ class QRCodeReaderInteractor: QRCodeReaderInteractorInputProtocol, IoHandlerDele var status = "" init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } func getContactByPhone(number: String) { diff --git a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift index dec068752..788fc8f48 100644 --- a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift +++ b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift @@ -58,7 +58,7 @@ final class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractor required init(mode: ScheduledMessageMode) { self.mode = mode super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } func fetchInfo() { diff --git a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift index a1b34aa39..99602f669 100644 --- a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift +++ b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift @@ -15,8 +15,8 @@ class SecurityInteractor: SecurityInteractorInputProtocol, AuthHandlerDelegate, private var timer : Timer? init() { - AuthHandler.delegate = self - IoHandler.delegate = self + AuthHandler.shared.delegate = self + IoHandler.shared.delegate = self } //MARK: - SecurityInteractorInputProtocol diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift new file mode 100644 index 000000000..69bba3973 --- /dev/null +++ b/Nynja/Observable/KeyedObservable.swift @@ -0,0 +1,44 @@ +// +// KeyedObservable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol KeyedObservable: class { + associatedtype Key: Hashable + associatedtype Value + typealias Callback = (Key, Value) -> Void + + var observableContainer: KeyedObservableContainer { get } + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: Key) + func notify(_ key: Key, with value: Value) +} + +extension KeyedObservable { + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) { + observableContainer.addObserver(observer, callback: callback) + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) { + observableContainer.addObserver(observer, for: key, callback: callback) + } + + func removeObserver(_ observer: AnyObject) { + observableContainer.removeObserver(observer) + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + observableContainer.removeObserver(observer, for: key) + } + + func notify(_ key: Key, with value: Value) { + observableContainer.notify(key, with: value) + } +} diff --git a/Nynja/Observable/KeyedObservableContainer.swift b/Nynja/Observable/KeyedObservableContainer.swift new file mode 100644 index 000000000..de9a2c174 --- /dev/null +++ b/Nynja/Observable/KeyedObservableContainer.swift @@ -0,0 +1,72 @@ +// +// KeyedObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class KeyedObservableContainer { + + private typealias Observers = [AnyWeakSubscriber] + + private struct Handler { + var callback: (Key, Value) -> Void + } + + private let lock = NSLock() + + private var allObservers: Observers = [] + + private var groupedObservers: [Key: Observers] = [:] + + func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + allObservers.append(container) + + lock.unlock() + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + var newObservers = groupedObservers[key] ?? [] + newObservers.append(container) + groupedObservers[key] = newObservers + + lock.unlock() + } + + func removeObserver(_ observer: AnyObject) { + lock.lock() + allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + for (key, _) in groupedObservers { + groupedObservers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + } + lock.unlock() + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + lock.lock() + groupedObservers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() + } + + func notify(_ key: Key, with value: Value) { + lock.lock() + for observer in allObservers { + observer.handler.callback(key, value) + } + groupedObservers[key]?.forEach { $0.handler.callback(key, value) } + lock.unlock() + } +} diff --git a/Nynja/Observable/Observable.swift b/Nynja/Observable/Observable.swift new file mode 100644 index 000000000..a3d4c8021 --- /dev/null +++ b/Nynja/Observable/Observable.swift @@ -0,0 +1,31 @@ +// +// Observable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol Observable: class { + associatedtype Observer + var observableContainer: ObservableContainer { get } + + func addObserver(_ observer: Observer) + func removeObserver(_ observer: Observer) + func notify(_ block: (Observer) -> Void) +} + +extension Observable { + + func addObserver(_ observer: Observer) { + observableContainer.addObserver(observer) + } + + func removeObserver(_ observer: Observer) { + observableContainer.removeObserver(observer) + } + + func notify(_ block: (Observer) -> Void) { + observableContainer.notify(block) + } +} diff --git a/Nynja/Observable/ObservableContainer.swift b/Nynja/Observable/ObservableContainer.swift new file mode 100644 index 000000000..183ee0dd5 --- /dev/null +++ b/Nynja/Observable/ObservableContainer.swift @@ -0,0 +1,41 @@ +// +// ObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class ObservableContainer { + + private typealias Observers = [WeakRef] + + private let lock = NSLock() + + private var observers: Observers = [] + + func addObserver(_ observer: T) { + let container = WeakRef(value: observer as AnyObject) + + lock.lock() + + observers.append(container) + + lock.unlock() + } + + func removeObserver(_ observer: T) { + let observer = observer as AnyObject + lock.lock() + observers.removeAll { $0.value === observer || $0.value == nil } + lock.unlock() + } + + func notify(_ block: (T) -> Void) { + lock.lock() + observers.forEach { ($0.value as? T).flatMap(block) } + lock.unlock() + } +} diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 081cc02f6..f0a3d16be 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -492,9 +492,11 @@ "deleted_message_replied_preview" = "Deleted message"; // MARK: Message -"message_status_typing"="...typing"; +"message_status_typing"="typing"; "message_new_messages"="New messages"; -"message_sending"="...sending a"; +"message_sending"="sending"; +"message_typing_status_people"="%@ people"; +"message_typing_status_undefined"="..."; // MARK: Sending Status "file"="file"; @@ -502,7 +504,7 @@ // MARK: Recording Status "video"="video"; -"recording"="...recording a"; +"recording"="recording"; // MARK: Presence status "active"="active"; diff --git a/Nynja/ServerModel/Model/Typing.swift b/Nynja/ServerModel/Model/Typing.swift index f37b18267..e807bc87c 100644 --- a/Nynja/ServerModel/Model/Typing.swift +++ b/Nynja/ServerModel/Model/Typing.swift @@ -1,5 +1,7 @@ class Typing { - var phone_id: String? + var feed_id: String? + var sender_id: String? + var sender_alias: String? var comments: AnyObject? -} \ No newline at end of file +} diff --git a/Nynja/ServerModel/Source/Decoder.swift b/Nynja/ServerModel/Source/Decoder.swift index 0af94781c..d6116cd2e 100644 --- a/Nynja/ServerModel/Source/Decoder.swift +++ b/Nynja/ServerModel/Source/Decoder.swift @@ -304,10 +304,12 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? a_History.status = body[5].parse(bert: tuple.elements[6]) as? AnyObject return a_History case "Typing": - if body.count != 2 { return nil } + if body.count != 4 { return nil } let a_Typing = Typing() - a_Typing.phone_id = body[0].parse(bert: tuple.elements[1]) as? String - a_Typing.comments = body[1].parse(bert: tuple.elements[2]) as? AnyObject + a_Typing.feed_id = body[0].parse(bert: tuple.elements[1]) as? String + a_Typing.sender_id = body[1].parse(bert: tuple.elements[2]) as? String + a_Typing.sender_alias = body[2].parse(bert: tuple.elements[3]) as? String + a_Typing.comments = body[3].parse(bert: tuple.elements[4]) as? AnyObject return a_Typing case "Contact": if body.count != 14 || tuple.elements.count != 15 { return nil } @@ -540,3 +542,4 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? default: return nil } } + diff --git a/Nynja/ServerModel/Spec/Typing_Spec.swift b/Nynja/ServerModel/Spec/Typing_Spec.swift index 41ceda8a5..bdc43d54c 100644 --- a/Nynja/ServerModel/Spec/Typing_Spec.swift +++ b/Nynja/ServerModel/Spec/Typing_Spec.swift @@ -1,4 +1,10 @@ func get_Typing() -> Model { return Model(value:Tuple(name:"Typing",body:[ Model(value:Binary()), + Model(value:Chain(types:[ + Model(value:List(constant:"")), + Model(value:Binary())])), + Model(value:Chain(types:[ + Model(value:List(constant:"")), + Model(value:Binary())])), Model(value:Chain(types:[Model(value:Tuple()),Model(value:Atom()),Model(value:Binary()),Model(value:Number()),Model(value:List(constant:""))]))]))} diff --git a/Nynja/Services/HandleServices/ContactHandler.swift b/Nynja/Services/HandleServices/ContactHandler.swift index 8a6aa1cfe..74b1f09b4 100644 --- a/Nynja/Services/HandleServices/ContactHandler.swift +++ b/Nynja/Services/HandleServices/ContactHandler.swift @@ -8,18 +8,25 @@ import Foundation -class ContactHandler: BaseHandler { +final class ContactHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = ContactHandler() + + private init() {} + // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return .sharedInstance } // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let contact = get_Contact().parse(bert: data) as? Contact, let status = contact.originalStatus else { return @@ -46,11 +53,11 @@ class ContactHandler: BaseHandler { // MARK: - Statuses - private static func handleDeleted(_ contact: Contact) { + private func handleDeleted(_ contact: Contact) { try? storageService.perform(action: .delete, with: contact) } - private static func handleInternal(_ contact: Contact) { + private func handleInternal(_ contact: Contact) { var columns: Set = [.presence] if contact.updated != 0 { columns.insert(.update) @@ -58,11 +65,11 @@ class ContactHandler: BaseHandler { ContactDAO.updateColumns(columns, contact: contact) } - private static func handleLastMessage(_ contact: Contact) { + private func handleLastMessage(_ contact: Contact) { ContactDAO.updateColumns([.unread], contact: contact) } - private static func handleFriend(_ contact: Contact, data: BertTuple) { + private func handleFriend(_ contact: Contact, data: BertTuple) { guard let phoneId = contact.phone_id, let prevContact = ContactDAO.findContactBy(phoneId: phoneId), @@ -87,7 +94,7 @@ class ContactHandler: BaseHandler { } } - private static func handleAuthorization(_ contact: Contact, data: BertTuple) { + private func handleAuthorization(_ contact: Contact, data: BertTuple) { do { try storageService.perform(action: .save, with: contact) NotificationManager.shared.handle(bert: data, type: .request) @@ -96,7 +103,7 @@ class ContactHandler: BaseHandler { } } - private static func handleBan(_ contact: Contact) { + private func handleBan(_ contact: Contact) { guard let phoneId = contact.phone_id, let prevContact = ContactDAO.findContactBy(phoneId: phoneId) else { diff --git a/Nynja/Services/HandleServices/HistoryHandler.swift b/Nynja/Services/HandleServices/HistoryHandler.swift index 11d6bf824..9a405174e 100644 --- a/Nynja/Services/HandleServices/HistoryHandler.swift +++ b/Nynja/Services/HandleServices/HistoryHandler.swift @@ -22,19 +22,26 @@ extension HistoryHandlerDelegate { final class HistoryHandler: BaseHandler { + // MARK: - Singleton + + static let shared = HistoryHandler() + + private init() {} + + // MARK: - Subscribers - private static let subscribersLock = NSLock() + private let subscribersLock = NSLock() - private static var subscribers = [WeakRef]() + private var subscribers = [WeakRef]() - private static func notify(block: (HistoryHandlerDelegate) -> Void) { + private func notify(block: (HistoryHandlerDelegate) -> Void) { subscribersLock.lock() subscribers.forEach { ($0.value as? HistoryHandlerDelegate).map { block($0) } } subscribersLock.unlock() } - static func addSubscriber(_ subscriber: HistoryHandlerDelegate) { + func addSubscriber(_ subscriber: HistoryHandlerDelegate) { subscribersLock.lock() defer { subscribersLock.unlock() } @@ -45,7 +52,7 @@ final class HistoryHandler: BaseHandler { subscribers.append(ref) } - static func removeSubscriber(_ subscriber: HistoryHandlerDelegate) { + func removeSubscriber(_ subscriber: HistoryHandlerDelegate) { subscribersLock.lock() subscribers = subscribers.filter { $0.value != nil && $0.value !== subscriber } subscribersLock.unlock() @@ -54,22 +61,22 @@ final class HistoryHandler: BaseHandler { // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return StorageService.sharedInstance } - static var messageEditService: MessageEditServiceProtocol { + var messageEditService: MessageEditServiceProtocol { return MessageEditService(dependencies: .init(storageService: storageService)) } - static let stickersDownloadingService: StickersDownloadingService = { + let stickersDownloadingService: StickersDownloadingService = { return StickersDownloadingService() }() // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let history = get_History().parse(bert: data) as? History else { return } @@ -102,7 +109,7 @@ final class HistoryHandler: BaseHandler { // MARK: -- Messages /// The first message is new, the last is old. - private static func updateMessageHistory(_ messages: [Message]) { + private func updateMessageHistory(_ messages: [Message]) { var stackForSave = [Message](reserveCapacity: messages.count) var stackForDelete = [Message]() @@ -178,7 +185,7 @@ final class HistoryHandler: BaseHandler { systemClearMessage: systemClearMessage) } - private static func saveMessageHistory(stackForSave: [Message], + private func saveMessageHistory(stackForSave: [Message], stackForDelete: [Message], repliedMessages: [MessageServerId: [Message]], visibleRepliedMessages: Set, @@ -213,7 +220,7 @@ final class HistoryHandler: BaseHandler { try? MessageActionDAO.delete(deletedActions) } - private static func fetchType(from feed: AnyObject?) -> FetchType? { + private func fetchType(from feed: AnyObject?) -> FetchType? { switch feed { case let feed as muc: guard let name = feed.name else { @@ -231,14 +238,14 @@ final class HistoryHandler: BaseHandler { } /// Mark messages with 'serverId' <= id as trusted - private static func markHistoryAsTrusted(before id: MessageServerId, in fetchType: FetchType) { + private func markHistoryAsTrusted(before id: MessageServerId, in fetchType: FetchType) { try? MessageDAO.trustMessages(before: id, in: fetchType) } // MARK: -- Jobs - private static func updateJobsHistory(_ jobs: [Job]) { + private func updateJobsHistory(_ jobs: [Job]) { let stackForSave = jobs.filter { StringAtom.string($0.status) == "pending" } let deleteStatuses = ["delete", "complete"] @@ -253,7 +260,7 @@ final class HistoryHandler: BaseHandler { // MARK: -- Stickers - private static func updateStickerPacks(_ stickerPacks: [StickerPack]) { + private func updateStickerPacks(_ stickerPacks: [StickerPack]) { for package in stickerPacks { try? storageService.perform(action: .save, with: package) } diff --git a/Nynja/Services/HandleServices/MessageHandler.swift b/Nynja/Services/HandleServices/MessageHandler.swift index 7fbeebcc3..45bff6b4a 100644 --- a/Nynja/Services/HandleServices/MessageHandler.swift +++ b/Nynja/Services/HandleServices/MessageHandler.swift @@ -10,26 +10,33 @@ import Foundation final class MessageHandler: BaseHandler { + // MARK: - Singleton + + static let shared = MessageHandler() + + private init() {} + + // MARK: - Dependencies - private static var storageService: StorageService { + private var storageService: StorageService { return StorageService.sharedInstance } - private static var notificationManager: NotificationManager { + private var notificationManager: NotificationManager { return NotificationManager.shared } - private static var systemSoundManager: SystemSoundManager { + private var systemSoundManager: SystemSoundManager { return SystemSoundManager.sharedInstance } // MARK: - Subscribers - static var subscribers: [MessageHandlerSubscriberReference] = [] + private var subscribers: [MessageHandlerSubscriberReference] = [] - static func addSubscriber(_ subscriber: MessageHandlerSubscriber) { + func addSubscriber(_ subscriber: MessageHandlerSubscriber) { guard !subscribers.contains(where: { $0.subscriber === subscriber }) else { return } @@ -37,14 +44,14 @@ final class MessageHandler: BaseHandler { subscribers.append(ref) } - static func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { + func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { subscribers = subscribers.filter { $0.subscriber != nil && $0.subscriber !== subscriber } } // MARK: - Execute - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let message = get_Message().parse(bert: data) as? Message else { return } let types = message.types @@ -66,11 +73,11 @@ final class MessageHandler: BaseHandler { } } - private static func clearHistory(_ message: Message) { + private func clearHistory(_ message: Message) { ChatService.clearHistory(message) } - private static func updateReader(from message: Message) { + private func updateReader(from message: Message) { let shouldUpdateOwnReader = self.shouldUpdateOwnReader(from: message) let shouldUpdateOtherReader = !shouldUpdateOwnReader || message.isInOwnChat @@ -83,11 +90,11 @@ final class MessageHandler: BaseHandler { } } - private static func shouldUpdateOwnReader(from message: Message) -> Bool { + private func shouldUpdateOwnReader(from message: Message) -> Bool { return message.isOwn } - private static func deleteMessage(_ message: Message) { + private func deleteMessage(_ message: Message) { do { try save(message) ChatService.removeMessage(message) @@ -96,7 +103,7 @@ final class MessageHandler: BaseHandler { } } - private static func editMessage(_ message: Message) { + private func editMessage(_ message: Message) { do { try save(message) try ChatService.editMessage(message) @@ -105,7 +112,7 @@ final class MessageHandler: BaseHandler { } } - private static func updateMessage(_ message: Message) { + private func updateMessage(_ message: Message) { do { try save(message) try ChatService.updateMessage(message) @@ -115,7 +122,7 @@ final class MessageHandler: BaseHandler { } } - private static func saveMessage(_ message: Message, data: BertTuple) { + private func saveMessage(_ message: Message, data: BertTuple) { guard !shouldSkipMessage(message) else { return } @@ -159,7 +166,7 @@ final class MessageHandler: BaseHandler { } } - private static func shouldSkipMessage(_ message: Message) -> Bool { + private func shouldSkipMessage(_ message: Message) -> Bool { if let desc = message.files?.first, desc.mime == SendMessageType.audioCall.rawValue, desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, @@ -172,7 +179,7 @@ final class MessageHandler: BaseHandler { return false } - private static func save(_ message: Message) throws { + private func save(_ message: Message) throws { if let repliedMessage = message.repliedMessage { repliedMessage.localStatus = try MessageDAO.localStatusForRepliedMessage(repliedMessage) } @@ -185,7 +192,7 @@ final class MessageHandler: BaseHandler { /// Play sound for incoming messages if chat isn't muted. /// Play outcoming message only if chat screen is open. - private static func playSoundIfNeeded(for message: Message) { + private func playSoundIfNeeded(for message: Message) { guard message.isDelivered, let currentPhoneId = storageService.phoneId else { return } diff --git a/Nynja/Services/HandleServices/ProfileHandler.swift b/Nynja/Services/HandleServices/ProfileHandler.swift index d2d68a05c..8b99fe60e 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -6,34 +6,40 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class ProfileHandler: BaseHandler { +final class ProfileHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = ProfileHandler() + + private init() {} // MARK: - Dependencies - static var mqttService: MQTTService { + var mqttService: MQTTService { return MQTTService.sharedInstance } - static var historyFactory: HistoryRequestModelFactoryProtocol { + var historyFactory: HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } - static var storageService: StorageService { + var storageService: StorageService { return StorageService.sharedInstance } - static var messageBackgroundTaskHandler: BackgroundTaskHandler { + var messageBackgroundTaskHandler: BackgroundTaskHandler { return MessageBackgroundTaskHandler() } - static var alertManager: AlertManager { + var alertManager: AlertManager { return AlertManager.sharedInstance } // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let profile = get_Profile().parse(bert: data) as? Profile, let status = profile.status?.string else { return @@ -60,7 +66,7 @@ class ProfileHandler: BaseHandler { // MARK: Get & Init - private static func handleGetInit(_ profile: Profile) { + private func handleGetInit(_ profile: Profile) { do { guard let roster = (profile.rosters as? [Roster])?.first else { return @@ -82,7 +88,7 @@ class ProfileHandler: BaseHandler { } catch { } } - private static func prepareForReceived(_ newRoster: Roster) { + private func prepareForReceived(_ newRoster: Roster) { let currentRoster = RosterDAO.currentRoster func shouldSave(_ message: Message?) -> Bool { @@ -146,11 +152,11 @@ class ProfileHandler: BaseHandler { } } - private static func configureTestFairy(with roster: Roster) { + private func configureTestFairy(with roster: Roster) { TestFairy.setUserId("\(roster.myContact?.phone_id ?? "")_\(roster.myContact?.fullName ?? "")") } - private static func configureNynjaCommunicatorService(_ profile: Profile) { + private func configureNynjaCommunicatorService(_ profile: Profile) { guard let rosterId = (profile.rosters?.first as? Roster)?.myContact?.phone_id else { return } @@ -158,7 +164,7 @@ class ProfileHandler: BaseHandler { NynjaCommunicatorService.sharedInstance.initialize() } - private static func requestJobs(with phoneId: String) { + private func requestJobs(with phoneId: String) { do { let historyModel = try historyFactory.makeAllJobsRequest(rosterId: phoneId) mqttService.sendHistoryRequest(with: historyModel) @@ -167,7 +173,7 @@ class ProfileHandler: BaseHandler { } } - private static func requestStickerPacks(with rosterId: String) { + private func requestStickerPacks(with rosterId: String) { do { let historyModel = try historyFactory.makeStickerPackagesRequest(rosterId: rosterId) mqttService.sendHistoryRequest(with: historyModel) @@ -179,7 +185,7 @@ class ProfileHandler: BaseHandler { // MARK: Remove - private static func handleRemove(_ profile: Profile) { + private func handleRemove(_ profile: Profile) { try? storageService.perform(action: .delete, with: profile) storageService.phone = nil alertManager.showAlertOk(message: String.localizable.authAttemptsRemoved) diff --git a/Nynja/Services/HandleServices/RoomHandler.swift b/Nynja/Services/HandleServices/RoomHandler.swift index 6193bb931..a1ca2f862 100644 --- a/Nynja/Services/HandleServices/RoomHandler.swift +++ b/Nynja/Services/HandleServices/RoomHandler.swift @@ -6,22 +6,29 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class RoomHandler: BaseHandler { +final class RoomHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = RoomHandler() + + private init() {} + // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return .sharedInstance } - static var notificationManager: NotificationManager { + var notificationManager: NotificationManager { return .shared } // MARK: - Handler - static func executeHandle(data: BertTuple, codes: StatusCodes) { + func executeHandle(data: BertTuple, codes: StatusCodes) { guard let room = get_Room().parse(bert: data) as? Room else { return } @@ -32,7 +39,7 @@ class RoomHandler: BaseHandler { } } - private static func handle(room: Room, data: BertTuple) { + private func handle(room: Room, data: BertTuple) { guard let status = room.originalStatus else { return } @@ -62,7 +69,7 @@ class RoomHandler: BaseHandler { } } - private static func handle(codes: StatusCodes, room: Room) { + private func handle(codes: StatusCodes, room: Room) { let statusCodeManager = StatusCodeManager.shared codes.forEach { statusCodeManager.notify(model: room, code: $0) } } @@ -71,7 +78,7 @@ class RoomHandler: BaseHandler { // MARK: - Statuses // MARK: - Add Member - private static func handleAddMember(_ room: Room) { + private func handleAddMember(_ room: Room) { trustLastMessageIfNeeded(for: room) guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { @@ -91,7 +98,7 @@ class RoomHandler: BaseHandler { // MARK: - Add Member Channel - private static func handleChannelAddMember(_ room: Room, oldRoom: Room) { + private func handleChannelAddMember(_ room: Room, oldRoom: Room) { if let features = room.settings { oldRoom.settings = features } @@ -102,7 +109,7 @@ class RoomHandler: BaseHandler { // MARK: - Add Member Room - private static func handleGroupAddMember(_ room: Room, oldRoom: Room) { + private func handleGroupAddMember(_ room: Room, oldRoom: Room) { addNotExistedMembers(from: room, to: oldRoom) filterAdmins(using: room, in: oldRoom) @@ -117,7 +124,7 @@ class RoomHandler: BaseHandler { try? storageService.perform(action: .save, with: oldRoom) } - private static func addNotExistedMembers(from room: Room, to oldRoom: Room) { + private func addNotExistedMembers(from room: Room, to oldRoom: Room) { var notExistedMembers: [Member] = [] room.members?.forEach { member in @@ -133,7 +140,7 @@ class RoomHandler: BaseHandler { oldRoom.members = members } - private static func filterAdmins(using room: Room, in oldRoom: Room) { + private func filterAdmins(using room: Room, in oldRoom: Room) { oldRoom.admins = oldRoom.admins?.filter { admin in guard let members = room.members else { return true @@ -142,7 +149,7 @@ class RoomHandler: BaseHandler { } } - private static func addNotExistedAdmins(from room: Room, to oldRoom: Room) { + private func addNotExistedAdmins(from room: Room, to oldRoom: Room) { var notExistsAdmins: [Member] = [] room.admins?.forEach { member in @@ -158,7 +165,7 @@ class RoomHandler: BaseHandler { oldRoom.admins = admins } - private static func filterMembers(using room: Room, in oldRoom: Room) { + private func filterMembers(using room: Room, in oldRoom: Room) { oldRoom.members = oldRoom.members?.filter { member in guard let admins = room.admins else { return true @@ -170,7 +177,7 @@ class RoomHandler: BaseHandler { // MARK: - Remove Member - private static func handleRemoveMember(_ room: Room) { + private func handleRemoveMember(_ room: Room) { trustLastMessageIfNeeded(for: room) if let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) { @@ -187,7 +194,7 @@ class RoomHandler: BaseHandler { // MARK: - Leave - private static func handleLeave(_ room: Room) { + private func handleLeave(_ room: Room) { trustLastMessageIfNeeded(for: room) guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { @@ -213,7 +220,7 @@ class RoomHandler: BaseHandler { // MARK: - Last Message - private static func trustLastMessageIfNeeded(for room: Room) { + private func trustLastMessageIfNeeded(for room: Room) { guard let lastMessage = room.last_msg else { return } @@ -223,10 +230,9 @@ class RoomHandler: BaseHandler { // MARK: - Update - private static func updateReadersUnreadAndStatus(from room: Room, oldRoom: Room) { + private func updateReadersUnreadAndStatus(from room: Room, oldRoom: Room) { oldRoom.unread = room.unread oldRoom.readers = room.readers oldRoom.status = room.status } - } diff --git a/Nynja/Services/HandleServices/RosterHandler.swift b/Nynja/Services/HandleServices/RosterHandler.swift index 419d89e79..bab717eca 100644 --- a/Nynja/Services/HandleServices/RosterHandler.swift +++ b/Nynja/Services/HandleServices/RosterHandler.swift @@ -8,9 +8,18 @@ import Foundation -class RosterHandler: BaseHandler { +final class RosterHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = RosterHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let roster = get_Roster().parse(bert: data) as? Roster, let status = (roster.status as? StringAtom)?.string else { return @@ -24,5 +33,4 @@ class RosterHandler: BaseHandler { try? StorageService.sharedInstance.perform(action: .save, with: roster) } } - } diff --git a/Nynja/Services/HandleServices/SearchHandler.swift b/Nynja/Services/HandleServices/SearchHandler.swift index 9d8570b15..be743eb61 100644 --- a/Nynja/Services/HandleServices/SearchHandler.swift +++ b/Nynja/Services/HandleServices/SearchHandler.swift @@ -8,9 +8,18 @@ import Foundation -class SearchHandler: BaseHandler { +final class SearchHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = SearchHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let search = get_Search().parse(bert: data) as? Search, let ref = search.ref, let refType = SearchModelReference(rawValue: ref) else { diff --git a/Nynja/Services/HandleServices/StarHandler.swift b/Nynja/Services/HandleServices/StarHandler.swift index 416651962..c555a00d5 100644 --- a/Nynja/Services/HandleServices/StarHandler.swift +++ b/Nynja/Services/HandleServices/StarHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class StarHandler: BaseHandler { +final class StarHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = StarHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let star = get_Star().parse(bert: data) as? Star, let status = star.starStatus else { return } diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index e9023cba8..6e21a8e87 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -9,17 +9,29 @@ import Foundation protocol TypingHandlerDelegate: class { - func getTyping(typing: Typing) + func didReceiveTyping(_ typing: Typing) } -class TypingHandler: BaseHandler { +final class TypingHandler: BaseHandler, Observable { - static weak var delegate: TypingHandlerDelegate? + // MARK: - Singleton - static func executeHandle(data: BertTuple) { - if let typing = get_Typing().parse(bert: data) as? Typing { - delegate?.getTyping(typing: typing) + static let shared = TypingHandler() + + private init() {} + + + // MARK: - ObservableContainer + + let observableContainer = ObservableContainer() + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { + guard let typing = get_Typing().parse(bert: data) as? Typing else { + return } + notify { $0.didReceiveTyping(typing) } } - } diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index b3639a0f7..8794b9d0b 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -172,7 +172,7 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ConnectionServiceDelegate self.state = .notAuthenticated(isLoggedOutFromServer: true) - IoHandler.delegate?.sessionNotFound() + IoHandler.shared.delegate?.sessionNotFound() notifySubscribers { (delegate) in delegate.mqttServiceDidReceiveAuthenticationFailure(self) } diff --git a/Nynja/Services/Member/MemberDAO.swift b/Nynja/Services/Member/MemberDAO.swift index 9cf7ed62a..4375b187a 100644 --- a/Nynja/Services/Member/MemberDAO.swift +++ b/Nynja/Services/Member/MemberDAO.swift @@ -52,6 +52,12 @@ class MemberDAO: MemberDAOProtocol { return Member(member: member) } + static func fetchMemberAlias(roomId: String, phoneId: String) -> String? { + return dbManager.fetch { db in + return try DBMember.memberAlias(from: db, roomId: roomId, phoneId: phoneId) + } + } + // MARK: - Update diff --git a/Nynja/Services/Member/MemberDAOProtocol.swift b/Nynja/Services/Member/MemberDAOProtocol.swift index a8d461120..bb5b49b1a 100644 --- a/Nynja/Services/Member/MemberDAOProtocol.swift +++ b/Nynja/Services/Member/MemberDAOProtocol.swift @@ -15,10 +15,11 @@ protocol MemberDAOProtocol: DAOProtocol { static func findMemberBy(id: Int64) -> Member? static func findMemberBy(roomId: String, phoneId: String) -> Member? + static func fetchMemberAlias(roomId: String, phoneId: String) -> String? + // MARK: - Update static func updateColumns(_ columns: Set, member: Member) static func updateReader(_ reader: Int64, roomId: String, phoneId: String) - } diff --git a/Nynja/Services/Models/TypingModel.swift b/Nynja/Services/Models/TypingModel.swift index 1306e2159..fb04c7010 100644 --- a/Nynja/Services/Models/TypingModel.swift +++ b/Nynja/Services/Models/TypingModel.swift @@ -86,7 +86,8 @@ enum TypingModelType: String { final class TypingModel: BaseMQTTModel { enum Topic { - case p2p(phone: String) + /// phoneId without '_{roster_id}' + case p2p(phoneNumber: String) case room(id: String) fileprivate var path: String { diff --git a/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift new file mode 100644 index 000000000..48fa93797 --- /dev/null +++ b/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift @@ -0,0 +1,13 @@ +// +// MQTTHandlerFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol MQTTHandlerFactoryProtocol: class { + func makeTypingHandler() -> TypingHandler +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 6196d552e..69dc443ff 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -8,48 +8,6 @@ import Foundation -protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol { - func makeMessageSendingService() -> MessageSendingServiceProtocol - func makeResourceManager() -> ResourceManagerProtocol - func makeMessageFactory() -> MessageFactoryProtocol - - func makeMessagePayloadBuilder() -> MessagePayloadBuilderInput - func makeMessagePayloadParser() -> MessagePayloadParserInput - - func makeCameraSettingsService(with flow: CameraSourceFlow) -> CameraSettingsServiceProtocol - - func makeMesageProcessingManager() -> MessageProcessingManagerInterface - - func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol - - func makeContactsProvider() -> ContactsProviding - func makeConversationsProvider() -> ConversationsProviding - func makeStickersProvider() -> StickersProviding - func makeCountriesProvider() -> CountriesProviding - - func makeTextInputValidationService() -> TextInputValidationServiceProtocol - func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol - func makeWalletOpeningTextInputValidationService() -> WalletOpeningTextInputValidationServiceProtocol - func makeWalletFundingNetworkService() -> WalletFundingNetworkService - func makePermissionManager() -> PermissionManager - - func makeWalletService() -> WalletService - func makeSyncFileManager() -> SyncFileManager - - func makeMuteChatService() -> MuteChatServiceProtocol - - func makeConnectionService() -> ConnectionService - - func makeAlertManager() -> AlertManager - - func makeStatusCodeManager() -> StatusCodeManager - - func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol - func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol - - func makeAudioSessionManager() -> AudioSessionManager -} - final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { func makeMessageSendingService() -> MessageSendingServiceProtocol { @@ -95,6 +53,10 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } + + func makeTypingProvider() -> TypingProvider { + return TypingProviderImpl.shared + } func makeContactsProvider() -> ContactsProviding { return ContactsProvider() @@ -175,3 +137,12 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return AudioSessionManager.shared } } + +// MARK: - MQTT Handlers + +extension ServiceFactory { + + func makeTypingHandler() -> TypingHandler { + return TypingHandler.shared + } +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift new file mode 100644 index 000000000..9247df943 --- /dev/null +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -0,0 +1,50 @@ +// +// ServiceFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTHandlerFactoryProtocol { + func makeMessageSendingService() -> MessageSendingServiceProtocol + func makeResourceManager() -> ResourceManagerProtocol + func makeMessageFactory() -> MessageFactoryProtocol + + func makeMessagePayloadBuilder() -> MessagePayloadBuilderInput + func makeMessagePayloadParser() -> MessagePayloadParserInput + + func makeCameraSettingsService(with flow: CameraSourceFlow) -> CameraSettingsServiceProtocol + + func makeMesageProcessingManager() -> MessageProcessingManagerInterface + + func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol + + func makeTypingProvider() -> TypingProvider + func makeContactsProvider() -> ContactsProviding + func makeConversationsProvider() -> ConversationsProviding + func makeStickersProvider() -> StickersProviding + func makeCountriesProvider() -> CountriesProviding + + func makeTextInputValidationService() -> TextInputValidationServiceProtocol + func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol + func makeWalletOpeningTextInputValidationService() -> WalletOpeningTextInputValidationServiceProtocol + func makeWalletFundingNetworkService() -> WalletFundingNetworkService + func makePermissionManager() -> PermissionManager + + func makeWalletService() -> WalletService + func makeSyncFileManager() -> SyncFileManager + + func makeMuteChatService() -> MuteChatServiceProtocol + + func makeConnectionService() -> ConnectionService + + func makeAlertManager() -> AlertManager + + func makeStatusCodeManager() -> StatusCodeManager + + func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol + func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol + + func makeAudioSessionManager() -> AudioSessionManager +} diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift new file mode 100644 index 000000000..99ce063bb --- /dev/null +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -0,0 +1,66 @@ +// +// TypingDisplayModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum TypingDisplayModel { + case typing(SenderInfo?, Status) + case none + + var displayName: String? { + guard case let .typing(senderInfo, _) = self else { + return nil + } + return senderInfo?.displayName + } + + var status: Status? { + guard case let .typing(_, status) = self else { + return nil + } + return status + } +} + +extension TypingDisplayModel { + + struct SenderInfo { + let senders: [String] + + var displayName: String? { + guard !senders.isEmpty, senders.count > 1 else { + return senders.first + } + return String.localizable.messageTypingStatusPeople(String(senders.count)) + } + } + + enum Status { + case action(ActionStatus) + /// undefined means that there are more then one active typing in chat with different statuses. + case undefined + + var title: String { + switch self { + case let .action(actionStatus): + return actionStatus.title + case .undefined: + return String.localizable.messageTypingStatusUndefined + } + } + + var action: ActionStatus? { + if case let .action(actionStatus) = self { + return actionStatus + } + return nil + } + + var isDone: Bool { + return action?.isDone ?? false + } + } +} diff --git a/Nynja/Statuses/TypingObservable.swift b/Nynja/Statuses/TypingObservable.swift new file mode 100644 index 000000000..34a6b75e9 --- /dev/null +++ b/Nynja/Statuses/TypingObservable.swift @@ -0,0 +1,12 @@ +// +// TypingObservable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 02.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol TypingObservable: class { + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) + func removeObserver(for feedId: FeedId) +} diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift new file mode 100644 index 000000000..99d383c97 --- /dev/null +++ b/Nynja/Statuses/TypingProvider.swift @@ -0,0 +1,229 @@ +// +// TypingProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +typealias FeedId = String + +protocol TypingProvider: class { + typealias Callback = (FeedId, TypingDisplayModel) -> Void + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: FeedId, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: FeedId) + + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? +} + +final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDelegate { + + let observableContainer = KeyedObservableContainer() + + private var data: [FeedId: TypingData] = [:] + + private var workItems: [FeedId: DispatchWorkItem] = [:] + + private let isolationQueue = DispatchQueue( + label: "\(Bundle(for: TypingProviderImpl.self).bundleIdentifier).typing-isolation-queue)", + attributes: .concurrent + ) + + private let processingQueue: DispatchQueue + + private let notifyQueue: DispatchQueue + + private let typingDismissInterval = 10.0 + + + // MARK: - Singleton + + // FIXME: singleton is a temporary solution until we don't have fullly implemented DI using ServiceFactory in the whole application. + static let shared: TypingProvider = TypingProviderImpl( + dependencies: .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) + + + // MARK: - Dependencies + + struct Dependencies { + let storageService: StorageService + let typingHandler: TypingHandler + let processingQueue = DispatchQueue.global(qos: .default) + let notifyQueue = DispatchQueue.main + } + + private let storageService: StorageService + + private let typingHandler: TypingHandler + + + // MARK: - Init + + private init(dependencies: Dependencies) { + storageService = dependencies.storageService + typingHandler = dependencies.typingHandler + processingQueue = dependencies.processingQueue + notifyQueue = dependencies.notifyQueue + typingHandler.addObserver(self) + } + + deinit { + typingHandler.removeObserver(self) + } + + + // MARK: - TypingProvider + + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { + return typing(for: feedId).flatMap { displayInfo(for: $0) } + } + + + // MARK: - TypingHandlerDelegate + + func didReceiveTyping(_ typing: Typing) { + guard let feedId = typing.feed_id, let typingType = typing.type, shouldHandleTyping(typing) else { + return + } + let status = ActionStatus(typingModelType: typingType) + + let senderInfo = TypingSenderInfo(feedId: feedId, + senderId: typing.sender_id, + senderName: typing.sender_alias, + status: status) + + processingQueue.async { + self.save(senderInfo) + } + } + + private func shouldHandleTyping(_ typing: Typing) -> Bool { + // Ignore typing from myself (in group chat). + if let senderId = typing.sender_id, senderId == storageService.phoneId { + return false + } + return true + } + + private func save(_ typingSenderInfo: TypingSenderInfo) { + let feedId = typingSenderInfo.feedId + + let typing: TypingData + if let oldTypingData = self.typing(for: feedId) { + + var newTypingData = oldTypingData + + newTypingData.senders.removeAll { $0.senderId == typingSenderInfo.senderId } + newTypingData.senders.append(typingSenderInfo) + + typing = newTypingData + + } else { + typing = TypingData(senders: [typingSenderInfo]) + } + + update(typing, for: feedId) + + dismiss(typingSenderInfo, after: typingDismissInterval) + } + + private func update(_ typing: TypingData?, for feedId: FeedId) { + set(typing, for: feedId) + + let displayInfo = self.displayInfo(for: typing) + notifyQueue.async { + self.observableContainer.notify(feedId, with: displayInfo) + } + } + + private func remove(_ typing: TypingSenderInfo) { + let feedId = typing.feedId + guard let currentTypingData = self.typing(for: feedId) else { + return + } + var newData = currentTypingData + newData.senders.removeAll { $0 === typing } + + update(newData, for: feedId) + } + + + // MARK: - Dismiss Timer + + private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval) { + let dismissKey = "\(typing.feedId)\(typing.senderId)" + + workItems[dismissKey]?.cancel() + + let workItem = DispatchWorkItem { + self.remove(typing) + } + workItems[dismissKey] = workItem + + processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) + } + + + // MARK: - Display Model + + private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { + guard let senderInfo = typing?.senders, !senderInfo.isEmpty, let lastStatus = senderInfo.last?.status else { + return .none + } + let senders = senderInfo.compactMap { $0.senderName } + + let shouldDisplayStatus = !senderInfo.contains { $0.status != lastStatus } + + return .typing(.init(senders: senders), shouldDisplayStatus ? .action(lastStatus) : .undefined) + } + + + // MARK: - Thread-Safe Data Access + + private func set(_ data: TypingData?, for feedId: FeedId) { + isolationQueue.async(flags: .barrier) { + self.data[feedId] = data + } + } + + private func typing(for feedId: FeedId) -> TypingData? { + var typing: TypingData? + + isolationQueue.sync { + typing = data[feedId] + } + + return typing + } +} + + +// MARK: - Inner Types + +private extension TypingProviderImpl { + + struct TypingData { + var senders: [TypingSenderInfo] + } + + final class TypingSenderInfo { + let feedId: FeedId + let senderId: String + let senderName: String? + let status: ActionStatus + + init(feedId: FeedId, senderId: String?, senderName: String?, status: ActionStatus) { + self.feedId = feedId + self.senderId = senderId ?? feedId + self.senderName = senderName + self.status = status + } + } +} diff --git a/Shared/Services/Handlers/Base/BaseHandler.swift b/Shared/Services/Handlers/Base/BaseHandler.swift index cdf0cebee..3021b29cd 100644 --- a/Shared/Services/Handlers/Base/BaseHandler.swift +++ b/Shared/Services/Handlers/Base/BaseHandler.swift @@ -6,23 +6,23 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol BaseHandler { - static func executeHandle(data: BertTuple) - static func executeHandle(data: BertList) +protocol BaseHandler: class { + func executeHandle(data: BertTuple) + func executeHandle(data: BertList) - static func executeHandle(data: BertTuple, codes: StatusCodes) - static func executeHandle(data: BertList, codes: StatusCodes) + func executeHandle(data: BertTuple, codes: StatusCodes) + func executeHandle(data: BertList, codes: StatusCodes) } extension BaseHandler { - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { executeHandle(data: data, codes: []) } - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { executeHandle(data: data, codes: []) } - static func executeHandle(data: BertTuple, codes: StatusCodes) {} - static func executeHandle(data: BertList, codes: StatusCodes) {} + func executeHandle(data: BertTuple, codes: StatusCodes) {} + func executeHandle(data: BertList, codes: StatusCodes) {} } diff --git a/Shared/Services/Handlers/ErrorsHandler.swift b/Shared/Services/Handlers/ErrorsHandler.swift index d284a35cb..d85e43db2 100644 --- a/Shared/Services/Handlers/ErrorsHandler.swift +++ b/Shared/Services/Handlers/ErrorsHandler.swift @@ -8,7 +8,11 @@ final class ErrorsHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = ErrorsHandler() + + private init() {} + + func executeHandle(data: BertTuple) { guard let errors = get_errors().parse(bert: data) as? errors, let dataTuple = data.elements.last as? BertTuple, let handlerKind = dataTuple.handlerKind else { @@ -20,5 +24,4 @@ final class ErrorsHandler: BaseHandler { let handler = HandlerFactory.handler(for: handlerKind) handler.executeHandle(data: dataTuple, codes: Set(codes)) } - } diff --git a/Shared/Services/Handlers/IoHandler.swift b/Shared/Services/Handlers/IoHandler.swift index f6fd64282..478cce95e 100644 --- a/Shared/Services/Handlers/IoHandler.swift +++ b/Shared/Services/Handlers/IoHandler.swift @@ -66,23 +66,27 @@ extension IoHandlerDelegate { } -class IoHandler:BaseHandler { +final class IoHandler: BaseHandler { - static weak var delegate: IoHandlerDelegate? + static let shared = IoHandler() - static var storageService: StorageService { + private init() {} + + weak var delegate: IoHandlerDelegate? + + var storageService: StorageService { return .sharedInstance } - static var mqttService: MQTTService { + var mqttService: MQTTService { return .sharedInstance } - static var keychainService: KeychainService { + var keychainService: KeychainService { return .standard } - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { if let IO = get_io().parse(bert: data) as? io { var code: String? = nil if let value = ((IO.code as? ok)?.code as? StringAtom)?.string { diff --git a/Shared/Services/Messaging/TypingSenderService.swift b/Shared/Services/Messaging/TypingSenderService.swift index 5b6ac91d9..7b32f4628 100644 --- a/Shared/Services/Messaging/TypingSenderService.swift +++ b/Shared/Services/Messaging/TypingSenderService.swift @@ -45,10 +45,10 @@ final class TypingSenderService: TypingSenderServiceProtocol, InitializeInjectab return } if let contact = chat as? Contact, let phone = contact.phoneNumber { - sendTyping(for: .p2p(phone: phone), type: type, ownPhoneId: ownPhoneId) + sendTyping(for: .p2p(phoneNumber: phone), type: type, feedId: ownPhoneId, senderId: nil, senderAlias: nil) - } else if let room = chat as? Room, let roomId = room.id { - sendTyping(for: .room(id: roomId), type: type, ownPhoneId: ownPhoneId) + } else if let room = chat as? Room, let roomId = room.id, let alias = memberAlias(roomId: roomId, phoneId: ownPhoneId) { + sendTyping(for: .room(id: roomId), type: type, feedId: roomId, senderId: ownPhoneId, senderAlias: alias) } } @@ -61,31 +61,36 @@ final class TypingSenderService: TypingSenderServiceProtocol, InitializeInjectab return } if message.feed_id is p2p, let phoneId = message.to, let phone = Contact.phoneNumber(from: phoneId) { - sendTyping(for: .p2p(phone: phone), type: type, ownPhoneId: ownPhoneId) + sendTyping(for: .p2p(phoneNumber: phone), type: type, feedId: ownPhoneId, senderId: nil, senderAlias: nil) - } else if message.feed_id is muc, let roomId = message.to { - sendTyping(for: .room(id: roomId), type: type, ownPhoneId: ownPhoneId) + } else if message.feed_id is muc, let roomId = message.to, let alias = memberAlias(roomId: roomId, phoneId: ownPhoneId) { + sendTyping(for: .room(id: roomId), type: type, feedId: roomId, senderId: ownPhoneId, senderAlias: alias) } } - private func sendTyping(for destination: TypingModel.Topic, type: TypingModelType, ownPhoneId: String) { - let typingModel = self.typingModel(for: destination, type: type, ownPhoneId: ownPhoneId) - mqttService.sendTyping(typingModel) - } - - private func typingModel(for destination: TypingModel.Topic, type: TypingModelType, ownPhoneId: String) -> TypingModel { + private func sendTyping(for topic: TypingModel.Topic, type: TypingModelType, + feedId: String, senderId: String?, senderAlias: String?) { let typing = Typing() - switch destination { - case .p2p: - // Send own phone id for p2p - typing.phone_id = ownPhoneId - case let .room(id): - // Send room id for muc - typing.phone_id = id - } + typing.feed_id = feedId + typing.sender_id = senderId + typing.sender_alias = senderAlias typing.comments = type.rawValue as AnyObject - return TypingModel(typing: typing, to: destination) + let typingModel = TypingModel(typing: typing, to: topic) + + mqttService.sendTyping(typingModel) + } + + + // MARK: - Database + + private func memberAlias(roomId: String, phoneId: String) -> String? { + #if !SHARE_EXTENSION + // FIXME: inject MemberDAO as a dependency + return MemberDAO.fetchMemberAlias(roomId: roomId, phoneId: phoneId) + #else + return nil + #endif } } -- GitLab From df1521a1e45e29aeab90e905f4ff231eccfe21e5 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 7 Nov 2018 19:46:20 +0200 Subject: [PATCH 091/138] [NY-4850] Fixed colors and icons on login screen. --- Nynja/AppDelegate.swift | 5 ++-- Nynja/Generated/ColorsConstants.swift | 4 ++++ Nynja/Modules/Auth/AuthCoordinator.swift | 14 +++++------ .../AuthModule/View/AuthViewController.swift | 19 +++++++++------ .../View/Subviews/AuthHeaderView.swift | 2 +- .../View/Subviews/EmailLoginView.swift | 3 +-- .../View/Subviews/LoginOptionsView.swift | 12 +++++----- .../View/Subviews/PhoneNumberLoginView.swift | 10 ++++---- .../View/ViewsFactory/AuthViewsFactory.swift | 23 +++++++++++++------ Nynja/Resources/Colors.json | 2 ++ 10 files changed, 57 insertions(+), 37 deletions(-) diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 5db510490..26b3ae696 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -102,15 +102,14 @@ private extension AppDelegate { // TODO: only for demo. While custom theme UI won't be implemented - app should start with dark theme. UserSettingsService.shared.theme = .default - window = UIWindow(frame: UIScreen.main.bounds) let navigation = UINavigationController() navigation.isNavigationBarHidden = true - let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) - + window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = navigation window?.makeKeyAndVisible() + let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) coordinator.start() } diff --git a/Nynja/Generated/ColorsConstants.swift b/Nynja/Generated/ColorsConstants.swift index 713effb5a..c4a6c0edd 100644 --- a/Nynja/Generated/ColorsConstants.swift +++ b/Nynja/Generated/ColorsConstants.swift @@ -53,6 +53,8 @@ internal extension SGColor { static let dustyGray = #colorLiteral(red: 0.5882353, green: 0.5882353, blue: 0.5882353, alpha: 1.0) /// 0x666666ff (r: 102, g: 102, b: 102, a: 255) static let gray = #colorLiteral(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) + /// 0x3b5998ff (r: 59, g: 89, b: 152, a: 255) + static let lapisLazuli = #colorLiteral(red: 0.23137255, green: 0.34901962, blue: 0.59607846, alpha: 1.0) /// 0x63666aff (r: 99, g: 102, b: 106, a: 255) static let lightGray = #colorLiteral(red: 0.3882353, green: 0.4, blue: 0.41568628, alpha: 1.0) /// 0xc90010ff (r: 201, g: 0, b: 16, a: 255) @@ -65,6 +67,8 @@ internal extension SGColor { static let mercury = #colorLiteral(red: 0.90588236, green: 0.90588236, blue: 0.90588236, alpha: 1.0) /// 0xccccccff (r: 204, g: 204, b: 204, a: 255) static let middleGray = #colorLiteral(red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0) + /// 0x757575ff (r: 117, g: 117, b: 117, a: 255) + static let oldLavender = #colorLiteral(red: 0.45882353, green: 0.45882353, blue: 0.45882353, alpha: 1.0) /// 0xf5b758ff (r: 245, g: 183, b: 88, a: 255) static let orange = #colorLiteral(red: 0.9607843, green: 0.7176471, blue: 0.34509805, alpha: 1.0) /// 0x2f353bff (r: 47, g: 53, b: 59, a: 255) diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index f5b5691e9..06f61209a 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -22,14 +22,14 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt } func start() { - SplashWireFrame().presentSplash(navigation: navigation!) +// SplashWireFrame().presentSplash(navigation: navigation!) -// let wireframe = AuthWireframe(coordinator: self) -// let view = wireframe.prepareModule( -// parameters: NSNull(), -// dependencies: AuthWireframe.Dependencies(countriesProvider: serviceFactory.makeCountriesProvider()) -// ) -// navigation?.pushViewController(view, animated: true) + let wireframe = AuthWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: NSNull(), + dependencies: AuthWireframe.Dependencies(countriesProvider: serviceFactory.makeCountriesProvider()) + ) + navigation?.pushViewController(view, animated: true) } func end() { diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 5460303be..55ea875d1 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -6,8 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - +import UIKit final class AuthViewController: UIViewController, AuthViewProtocol, InitializeInjectable, KeyboardInteractive { private let presenter: AuthPresenterProtocol @@ -21,12 +20,18 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn private weak var emailLoginView: EmailLoginView? private weak var phoneNumberLoginView: PhoneNumberLoginView? - private lazy var bottomView: LoginOptionsView = viewsFactory.makeBottomView(on: view, - presenter: presenter, - showEmailLoginAction: showEmailLogin, - showPhoneNumberLoginAction: showPhoneNumberLogin) + private lazy var bottomView: LoginOptionsView = viewsFactory.makeBottomView( + on: view, + presenter: presenter, + showEmailLoginAction: { [weak self] animated in + self?.showEmailLogin(animated: true) + }, + showPhoneNumberLoginAction: { [weak self] animated in + self?.showPhoneNumberLogin(animated: animated) + } + ) - init(dependencies: AuthViewController.Dependencies) { + init(dependencies: Dependencies) { presenter = dependencies.presenter viewsFactory = dependencies.viewsFactory diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift index d47e3bb5e..59997f035 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +import UIKit final class AuthHeaderView: UIView, Configurable { private lazy var welcomeLabel: UILabel = viewsFactory.makeWelcomeLabel(on: self) diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index 13fc35fb5..851f3a475 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -6,8 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - +import UIKit final class EmailLoginView: UIView, Configurable { private lazy var inputFieldContainer = viewsFactory.makeInputFieldContainer(on: self) diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift index 55c53abd6..6a124e2fd 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift @@ -6,8 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - +import UIKit final class LoginOptionsView: UIView, Configurable { private let viewsFactory: AuthViewsFactoryProtocol @@ -69,7 +68,6 @@ private extension LoginOptionsView { guard let loginOption = switchLoginAction?() else { return } - updateSwitchButton(loginOption: loginOption) } @@ -85,15 +83,17 @@ private extension LoginOptionsView { // MARK: - Private private extension LoginOptionsView { + func updateSwitchButton(loginOption: LoginOption) { switch loginOption { case .email: switchLoginButton.setTitle("Log in with phone number".localized.uppercased(), for: .normal) - switchLoginButton.setImage(UIImage(named: "icons_general_ic_accept_call"), for: .normal) + switchLoginButton.setImage(UIImage.nynja.iconsGeneralIcAcceptCall.image, for: .normal) case .phoneNumber: switchLoginButton.setTitle("Log in with email".localized.uppercased(), for: .normal) - switchLoginButton.setImage(UIImage(named: "icons_general_ic_email"), for: .normal) - default: break + switchLoginButton.setImage(UIImage.nynja.iconsGeneralIcEmail.image, for: .normal) + default: + break } } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index c87d5b8e6..fcd17e5c4 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -6,8 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - +import UIKit final class PhoneNumberLoginView: UIView, Configurable { private lazy var countrySelector: UIButton = viewsFactory.makeCountrySelector(on: self, target: self, selector: #selector(changeCountry(sender:))) @@ -18,7 +17,7 @@ final class PhoneNumberLoginView: UIView, Configurable { private var phoneNumberTextFieldController: TextFieldController? private lazy var phoneNumberContainer: UIView = viewsFactory.makePhoneNumberContainer(on: self, left: countryCodeLabel) - private lazy var phoneNumberTextField: UITextField = viewsFactory.makePhoneNumberTextField(on: phoneNumberContainer) + private lazy var phoneNumberTextField = viewsFactory.makePhoneNumberTextField(on: phoneNumberContainer) private lazy var detailsLabel: UILabel = viewsFactory.makeDetailsNumberLabel(on: self, top: countryCodeContainer) private lazy var nextButton: UIButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) @@ -101,9 +100,12 @@ private extension PhoneNumberLoginView { // MARK: - Text field controller private extension PhoneNumberLoginView { + final class TextFieldController: NSObject, UITextFieldDelegate { + var template: String? - var isFullFilelledAction: ((Bool) -> Void)? + + private let isFullFilelledAction: ((Bool) -> Void)? init(template: String?, isFullFilelledAction: ((Bool) -> Void)?) { self.template = template diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index c6c897c40..b2ba2d6e6 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -134,7 +134,9 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { return loginView } - func makeBottomView(on view: UIView, presenter: AuthPresenterProtocol, showEmailLoginAction: @escaping (Bool) -> Void, + func makeBottomView(on view: UIView, + presenter: AuthPresenterProtocol, + showEmailLoginAction: @escaping (Bool) -> Void, showPhoneNumberLoginAction: @escaping (Bool) -> Void) -> LoginOptionsView { let bottom = LoginOptionsView(viewsFactory: self) @@ -179,10 +181,12 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.backgroundColor = UIColor.nynja.white button.setTitle("Log in with Google".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.subtitleGray, for: .normal) - button.setImage(UIImage(named: "icons_general_ic_google"), for: .normal) + button.setTitleColor(UIColor.nynja.oldLavender, for: .normal) + button.setImage(UIImage.nynja.iconsGeneralIcGoogle.image, for: .normal) button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + button.titleEdgeInsets.left = 8 + button.imageEdgeInsets.right = 8 button.layer.cornerRadius = 22 button.addTarget(target, action: selector, for: .touchUpInside) @@ -201,12 +205,14 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { let button = UIButton() view.addSubview(button) - button.backgroundColor = UIColor.nynja.dodgerBlue + button.backgroundColor = UIColor.nynja.lapisLazuli button.setTitle("Log in with Facebook".localized.uppercased(), for: .normal) button.setTitleColor(UIColor.nynja.white, for: .normal) - button.setImage(UIImage(named: "ic_facebook"), for: .normal) + button.setImage(UIImage.nynja.icFacebook.image, for: .normal) button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + button.titleEdgeInsets.left = 8 + button.imageEdgeInsets.right = 8 button.layer.cornerRadius = 22 button.addTarget(target, action: selector, for: .touchUpInside) @@ -229,6 +235,8 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.setTitleColor(UIColor.nynja.white, for: .normal) button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) + button.titleEdgeInsets.left = 8 + button.imageEdgeInsets.right = 8 button.layer.cornerRadius = 22 button.addTarget(target, action: selector, for: .touchUpInside) @@ -347,7 +355,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.layer.cornerRadius = 22 button.setTitle("next".localized.uppercased(), for: .normal) button.setTitleColor(UIColor.nynja.white, for: .normal) - button.backgroundColor = UIColor.nynja.darkRed + button.backgroundColor = UIColor.nynja.mainRed button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) button.isEnabled = false @@ -445,6 +453,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + textField.tintColor = UIColor.nynja.mainRed textField.font = FontFamily.NotoSans.medium.font(size: 16) textField.textColor = UIColor.nynja.white textField.keyboardType = .numberPad @@ -482,7 +491,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.layer.cornerRadius = 22 button.setTitle("next".localized.uppercased(), for: .normal) button.setTitleColor(UIColor.nynja.white, for: .normal) - button.backgroundColor = UIColor.nynja.darkRed + button.backgroundColor = UIColor.nynja.mainRed button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) button.isEnabled = false diff --git a/Nynja/Resources/Colors.json b/Nynja/Resources/Colors.json index 050637876..72df59316 100644 --- a/Nynja/Resources/Colors.json +++ b/Nynja/Resources/Colors.json @@ -12,6 +12,7 @@ "darkRed": "#a4000d", "blue": "#45a5ff", "dodgerBlue": "#3891ff", + "lapisLazuli": "#3b5998", "almostBlack": "#262626", "darkLight": "#2c2e33", "selfBubleColor": "#d8d8d8", @@ -46,4 +47,5 @@ "separatorGrayColor": "#3f3f3f", "callGradientStart": "#2c2e33ff", "callGradientEnd": "#2c2e3300", + "oldLavender": "#757575" } -- GitLab From 6d396beca90a349d92d034a4265785e0988763c0 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 8 Nov 2018 13:48:09 +0200 Subject: [PATCH 092/138] [NY-5168] Implemented Facebook login flow. --- Nynja.xcodeproj/project.pbxproj | 60 +++++++ .../Library/UI/Extensions/URLExtensions.swift | 7 + Nynja/Modules/Auth/AuthCoordinator.swift | 21 ++- .../Auth/AuthModule/AuthProtocols.swift | 15 +- .../Interactor/AuthInteractor.swift | 8 +- .../AuthModule/Presenter/AuthPresenter.swift | 51 +++--- .../View/ViewsFactory/AuthViewsFactory.swift | 30 +--- .../AuthModule/Wireframe/AuthWireframe.swift | 7 +- .../Auth/Facebook/FacebookAuthProtocols.swift | 34 ++++ .../Intreractor/FacebookAuthInteractor.swift | 88 +++++++++++ .../Presenter/FacebookAuthPresenter.swift | 57 +++++++ .../View/FacebookAuthViewController.swift | 149 ++++++++++++++++++ .../Wireframe/FacebookAuthWireframe.swift | 49 ++++++ 13 files changed, 513 insertions(+), 63 deletions(-) create mode 100644 Nynja/Modules/Auth/Facebook/FacebookAuthProtocols.swift create mode 100644 Nynja/Modules/Auth/Facebook/Intreractor/FacebookAuthInteractor.swift create mode 100644 Nynja/Modules/Auth/Facebook/Presenter/FacebookAuthPresenter.swift create mode 100644 Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift create mode 100644 Nynja/Modules/Auth/Facebook/Wireframe/FacebookAuthWireframe.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index d66da3eb1..5d8aaaffa 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -951,6 +951,11 @@ 8526187D20D05BF700824357 /* StickerGridPlaceholderCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8526187B20D05BF700824357 /* StickerGridPlaceholderCellModel.swift */; }; 8528E50C2072724600A8644A /* StarDateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8528E50B2072724600A8644A /* StarDateConverter.swift */; }; 8528E50E2072835E00A8644A /* AudioDurationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8528E50D2072835E00A8644A /* AudioDurationFormatter.swift */; }; + 852BB8CE2194256600F2E8E4 /* FacebookAuthPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8C92194256600F2E8E4 /* FacebookAuthPresenter.swift */; }; + 852BB8CF2194256600F2E8E4 /* FacebookAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8CA2194256600F2E8E4 /* FacebookAuthViewController.swift */; }; + 852BB8D02194256600F2E8E4 /* FacebookAuthProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8CB2194256600F2E8E4 /* FacebookAuthProtocols.swift */; }; + 852BB8D12194256600F2E8E4 /* FacebookAuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8CC2194256600F2E8E4 /* FacebookAuthInteractor.swift */; }; + 852BB8D22194256600F2E8E4 /* FacebookAuthWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8CD2194256600F2E8E4 /* FacebookAuthWireframe.swift */; }; 852C3DCD216E34FC00447878 /* TypingSenderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852C3DCC216E34FC00447878 /* TypingSenderService.swift */; }; 852C3DCE216E357300447878 /* TypingSenderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852C3DCC216E34FC00447878 /* TypingSenderService.swift */; }; 852DF26120371FB400A4F8B6 /* FileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852DF26020371FB400A4F8B6 /* FileExtension.swift */; }; @@ -3254,6 +3259,11 @@ 8526187B20D05BF700824357 /* StickerGridPlaceholderCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerGridPlaceholderCellModel.swift; sourceTree = ""; }; 8528E50B2072724600A8644A /* StarDateConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarDateConverter.swift; sourceTree = ""; }; 8528E50D2072835E00A8644A /* AudioDurationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDurationFormatter.swift; sourceTree = ""; }; + 852BB8C92194256600F2E8E4 /* FacebookAuthPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthPresenter.swift; sourceTree = ""; }; + 852BB8CA2194256600F2E8E4 /* FacebookAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthViewController.swift; sourceTree = ""; }; + 852BB8CB2194256600F2E8E4 /* FacebookAuthProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthProtocols.swift; sourceTree = ""; }; + 852BB8CC2194256600F2E8E4 /* FacebookAuthInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthInteractor.swift; sourceTree = ""; }; + 852BB8CD2194256600F2E8E4 /* FacebookAuthWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthWireframe.swift; sourceTree = ""; }; 852C3DCC216E34FC00447878 /* TypingSenderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingSenderService.swift; sourceTree = ""; }; 852DF26020371FB400A4F8B6 /* FileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtension.swift; sourceTree = ""; }; 852DF262203720E600A4F8B6 /* FileIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileIcons.swift; sourceTree = ""; }; @@ -6928,6 +6938,7 @@ 4B749F0E214FEFC8002F3A33 /* Auth */ = { isa = PBXGroup; children = ( + 852BB8C7219424EA00F2E8E4 /* Facebook */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, @@ -8637,6 +8648,50 @@ path = ControlsContainer; sourceTree = ""; }; + 852BB8C7219424EA00F2E8E4 /* Facebook */ = { + isa = PBXGroup; + children = ( + 852BB8CB2194256600F2E8E4 /* FacebookAuthProtocols.swift */, + 852BB8D42194259700F2E8E4 /* View */, + 852BB8D32194259100F2E8E4 /* Presenter */, + 852BB8D5219425A200F2E8E4 /* Intreractor */, + 852BB8D6219425AF00F2E8E4 /* Wireframe */, + ); + path = Facebook; + sourceTree = ""; + }; + 852BB8D32194259100F2E8E4 /* Presenter */ = { + isa = PBXGroup; + children = ( + 852BB8C92194256600F2E8E4 /* FacebookAuthPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 852BB8D42194259700F2E8E4 /* View */ = { + isa = PBXGroup; + children = ( + 852BB8CA2194256600F2E8E4 /* FacebookAuthViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 852BB8D5219425A200F2E8E4 /* Intreractor */ = { + isa = PBXGroup; + children = ( + 852BB8CC2194256600F2E8E4 /* FacebookAuthInteractor.swift */, + ); + path = Intreractor; + sourceTree = ""; + }; + 852BB8D6219425AF00F2E8E4 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 852BB8CD2194256600F2E8E4 /* FacebookAuthWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; 852C3DD0216E3A4300447878 /* Messaging */ = { isa = PBXGroup; children = ( @@ -15397,6 +15452,7 @@ 4B1D7E072029D00000703228 /* OtherUserProfileItemsFactory.swift in Sources */, 855AC532208E441500DC2335 /* StickersInputPresenter.swift in Sources */, 3A1DC73C1EF15330006A8E9F /* HandlerService.swift in Sources */, + 852BB8D12194256600F2E8E4 /* FacebookAuthInteractor.swift in Sources */, E7302A951FC86424002892F8 /* P2pTable.swift in Sources */, 5EDD45552188601400C50BC8 /* AccountSettingsViewController.swift in Sources */, F117871120ACF018007A9A1B /* CameraQualitySettingsInteractor.swift in Sources */, @@ -16639,6 +16695,7 @@ 9B96709E215151D20058E98F /* LeaveVoiceMessageWireFrame.swift in Sources */, 4B8996F2204EF5E900DCB183 /* ChatCheckpointDAO.swift in Sources */, 265F5D2E209B8C1C008ACCC8 /* MessageEditActionDAO.swift in Sources */, + 852BB8CF2194256600F2E8E4 /* FacebookAuthViewController.swift in Sources */, 5EEB73B2216046FE00D8ECE6 /* CodeConfirmationProtocols.swift in Sources */, 853FB0772049B7CA000996C5 /* TextCellViewModel.swift in Sources */, 5BC1D38120D3B54B002A44B3 /* CallInfoView.swift in Sources */, @@ -17053,6 +17110,7 @@ B723C630204D9E1500884FFD /* PickableEnum.swift in Sources */, A432CF1F20B44C0000993AFB /* MaterialTextContainer.swift in Sources */, 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */, + 852BB8D22194256600F2E8E4 /* FacebookAuthWireframe.swift in Sources */, A43B259520AB1DFA00FF8107 /* InputContentProtocol.swift in Sources */, 8509FC89215908B300734D93 /* AppGroupFlagObserver.swift in Sources */, 2625DBF620EFC52E00E01C05 /* AudioFileConvertOperation.swift in Sources */, @@ -17077,6 +17135,7 @@ A4330A712109EBB30060BD93 /* CountriesProviding.swift in Sources */, A43B25A520AB1DFA00FF8107 /* DrawableAudioWaveform.swift in Sources */, 4B4266BC204D89A100194BC1 /* ForwardSelectorMode.swift in Sources */, + 852BB8D02194256600F2E8E4 /* FacebookAuthProtocols.swift in Sources */, 553819525871F7D28AB90364 /* GroupRulesPresenter.swift in Sources */, 0D520AAAA7FD1F0464C8174F /* GroupRulesInteractor.swift in Sources */, A45F115E20B422AF00F45004 /* Contact+DB.swift in Sources */, @@ -17157,6 +17216,7 @@ B745F2E62109BB0100488A91 /* InterpretationLayout.swift in Sources */, 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */, FEA655D52167777E00B44029 /* CreateWalletViewController.swift in Sources */, + 852BB8CE2194256600F2E8E4 /* FacebookAuthPresenter.swift in Sources */, BDC42BA204F86F13E9FE24FA /* ScheduleMessageProtocols.swift in Sources */, 0062D9462062EC4100B915AC /* InviteFriendsViewControllerLayout.swift in Sources */, A4679BA720B2DD0F0021FE9C /* ChannelSubscriber.swift in Sources */, diff --git a/Nynja/Library/UI/Extensions/URLExtensions.swift b/Nynja/Library/UI/Extensions/URLExtensions.swift index 569ace4b7..2d07c490d 100644 --- a/Nynja/Library/UI/Extensions/URLExtensions.swift +++ b/Nynja/Library/UI/Extensions/URLExtensions.swift @@ -39,3 +39,10 @@ extension URL { return URL(fileURLWithPath: path) } } + +extension URLComponents { + + func queryValue(forKey key: String) -> String? { + return queryItems?.first { $0.name == key }?.value + } +} diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 06f61209a..dfd9de6a4 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -26,7 +26,6 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt let wireframe = AuthWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: NSNull(), dependencies: AuthWireframe.Dependencies(countriesProvider: serviceFactory.makeCountriesProvider()) ) navigation?.pushViewController(view, animated: true) @@ -92,6 +91,12 @@ extension AuthCoordinator { ) ) + navigation?.pushViewController(view, animated: true) + + case .showFacebookAuth: + let wireframe = FacebookAuthWireframe(coordinator: self) + let view = wireframe.prepareModule() + navigation?.pushViewController(view, animated: true) } } @@ -170,3 +175,17 @@ extension AuthCoordinator { } } +// MARK: - FacebookAuthCoordinatorProtocol + +extension AuthCoordinator: FacebookAuthCoordinatorProtocol { + + func wireframe(_ wireframe: FacebookAuthWireframe, didEndWithState state: FacebookAuthWireframe.State) { + switch state { + case .dismiss: + break + case let .authenticated(code): + break + } + navigation?.popViewController(animated: true) + } +} diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index c02148984..f6be19382 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -11,6 +11,7 @@ import Foundation protocol AuthWireframeProtocol: class { func selectCountry(completion: @escaping (Result) -> Void) func continueLogin(loginOption: LoginOption) + func showFacebookAuth() } protocol AuthViewProtocol: class where Self: UIViewController { @@ -24,10 +25,10 @@ protocol AuthPresenterProtocol: class { func switchLoginOption() - func loginViaFacebook(completion: @escaping (Result) -> Void) - func loginViaGoogle(completion: @escaping (Result) -> Void) - func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) - func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) + func loginViaFacebook() + func loginViaGoogle() + func loginViaEmail(_ email: String) + func loginViaPhoneNumber(_ phoneNumber: String) func selectCountry() } @@ -35,8 +36,8 @@ protocol AuthPresenterProtocol: class { protocol AuthInputInteractorProtocol: class { typealias Code = String - func loginViaFacebook(completion: @escaping (Result) -> Void) - func loginViaGoogle(completion: @escaping (Result) -> Void) + func loginViaFacebook() + func loginViaGoogle() func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) @@ -44,4 +45,6 @@ protocol AuthInputInteractorProtocol: class { } protocol AuthOutputInteractorProtocol: class { + func startFacebookLoginFlow() + func startGoogleLoginFlow() } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 0393de6ef..d4d88ec2a 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -37,12 +37,12 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { return countriesProvider.fetchDefaultCountry() } - func loginViaFacebook(completion: @escaping (Result) -> Void) { - completion(.success("Some code")) + func loginViaFacebook() { + presenter?.startFacebookLoginFlow() } - func loginViaGoogle(completion: @escaping (Result) -> Void) { - completion(.success("Some code")) + func loginViaGoogle() { + presenter?.startGoogleLoginFlow() } func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) { diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 69a9b65ff..2306b5f28 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -30,54 +30,36 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } } - func loginViaFacebook(completion: @escaping (Result) -> Void) { - interactor.loginViaFacebook { [weak self] result in - switch result { - case let .success(code): - completion(.success(())) - self?.wireframe?.continueLogin(loginOption: .facebook(code: code)) - - case let .failure(error): - completion(.failure(error)) - } - } + func loginViaFacebook() { + // self?.wireframe?.continueLogin(loginOption: .facebook(code: code)) + interactor.loginViaFacebook() } - func loginViaGoogle(completion: @escaping (Result) -> Void) { - interactor.loginViaGoogle { [weak self] result in - switch result { - case let .success(code): - completion(.success(())) - self?.wireframe?.continueLogin(loginOption: .google(code: code)) - - case let .failure(error): - completion(.failure(error)) - } - } + func loginViaGoogle() { + // self?.wireframe?.continueLogin(loginOption: .google(code: code)) + interactor.loginViaGoogle() } - func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) { + func loginViaEmail(_ email: String) { interactor.loginViaEmail(email) { [weak self] result in switch result { case .success: - completion(.success(())) self?.wireframe?.continueLogin(loginOption: .email(email: email)) case let .failure(error): - completion(.failure(error)) + break } } } - func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) { + func loginViaPhoneNumber(_ phoneNumber: String) { interactor.loginViaPhoneNumber(phoneNumber) { [weak self] result in switch result { case .success: - completion(.success(())) self?.wireframe?.continueLogin(loginOption: .phoneNumber(number: phoneNumber)) case let .failure(error): - completion(.failure(error)) + break } } } @@ -95,6 +77,19 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } } +// MARK: - Interactor Output + +extension AuthPresenter { + + func startFacebookLoginFlow() { + wireframe.showFacebookAuth() + } + + func startGoogleLoginFlow() { + + } +} + // MARK: - SetInjectable extension AuthPresenter { diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index b2ba2d6e6..1f89065ac 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -97,10 +97,8 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { let loginView = EmailLoginView(viewsFactory: self) view.addSubview(loginView) - loginView.configure(config: EmailLoginView.Config(nextAction: { - presenter.loginViaEmail($0) { (result) in - print(#function) - } + loginView.configure(config: EmailLoginView.Config(nextAction: { [weak presenter] in + presenter?.loginViaEmail($0) })) loginView.snp.makeConstraints { (make) in @@ -117,14 +115,9 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { loginView.configure(config: PhoneNumberLoginView.Config( country: country, - countrySelectorAction: { [weak presenter] in - presenter?.selectCountry() - }, - nextAction: { [weak presenter] in - presenter?.loginViaPhoneNumber($0) { (result) in - print(#function) - } - })) + countrySelectorAction: { [weak presenter] in presenter?.selectCountry() }, + nextAction: { [weak presenter] in presenter?.loginViaPhoneNumber($0) }) + ) loginView.snp.makeConstraints { (make) in make.top.left.right.equalToSuperview() @@ -154,16 +147,9 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { return loginOption }, - facebookLoginAction: { - presenter.loginViaFacebook { (result) in - print(#function) - } - }, - googleLoginAction: { - presenter.loginViaGoogle { (result) in - print(#function) - } - })) + facebookLoginAction: { [weak presenter] in presenter?.loginViaFacebook() }, + googleLoginAction: { [weak presenter] in presenter?.loginViaGoogle() }) + ) view.addSubview(bottom) bottom.snp.makeConstraints { (make) in diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 1e41c67dc..b3cab1d47 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -20,8 +20,6 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { self.coordinator = coordinator } - typealias Parameters = NSNull - struct Dependencies { let countriesProvider: CountriesProviding } @@ -29,6 +27,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { enum State { case continueLogin(loginOption: LoginOption) case getCountry(callback: (Result) -> Void) + case showFacebookAuth } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -53,4 +52,8 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { func continueLogin(loginOption: LoginOption) { coordinator.wireframe(self, didEndWithState: .continueLogin(loginOption: loginOption)) } + + func showFacebookAuth() { + coordinator.wireframe(self, didEndWithState: .showFacebookAuth) + } } diff --git a/Nynja/Modules/Auth/Facebook/FacebookAuthProtocols.swift b/Nynja/Modules/Auth/Facebook/FacebookAuthProtocols.swift new file mode 100644 index 000000000..a33c753fa --- /dev/null +++ b/Nynja/Modules/Auth/Facebook/FacebookAuthProtocols.swift @@ -0,0 +1,34 @@ +// +// FacebookAuthProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol FacebookAuthWireframeProtocol: class { + func finishAuthentication(code: String) + func dismiss() +} + +protocol FacebookAuthViewProtocol: class { + func load(_ request: URLRequest) +} + +protocol FacebookAuthPresenterProtocol: class { + func viewDidLoad() + func dismiss() + func handleRedirect(to url: URL) -> Bool +} + +protocol FacebookAuthInteractorInputProtocol: class { + func authenticate() + func handleRedirect(to url: URL) -> Bool +} + +protocol FacebookAuthInteractorOutputProtocol: class { + func load(_ request: URLRequest) + func didAuthenticated(code: String) +} diff --git a/Nynja/Modules/Auth/Facebook/Intreractor/FacebookAuthInteractor.swift b/Nynja/Modules/Auth/Facebook/Intreractor/FacebookAuthInteractor.swift new file mode 100644 index 000000000..a6ea8c3a2 --- /dev/null +++ b/Nynja/Modules/Auth/Facebook/Intreractor/FacebookAuthInteractor.swift @@ -0,0 +1,88 @@ +// +// FacebookAuthInteractor.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class FacebookAuthInteractor: FacebookAuthInteractorInputProtocol, InitializeInjectable { + + private(set) weak var presenter: FacebookAuthInteractorOutputProtocol! + + // MARK: - API + + private let loginURL = "https://www.facebook.com/v3.1/dialog/oauth" + + private enum Request { + static let fbRedirectURL = "https://beta.nynja.net/oauth" + static let fbClientId = "915846355282708" + } + + private enum Parameters { + static let cliendId = "client_id" + static let redirectURI = "redirect_uri" + static let state = "state" + static let code = "code" + } + + /// Temporary value that will be used in redirect checking logic. + private var state: String? + + + // MARK: - Init + + struct Dependencies { + let presenter: FacebookAuthInteractorOutputProtocol + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + } + + + // MARK: - FacebookAuthInteractorInputProtocol + + func authenticate() { + let request = makeAuthenticationRequest() + presenter.load(request) + } + + func handleRedirect(to url: URL) -> Bool { + guard url.absoluteString.starts(with: Request.fbRedirectURL), + let components = URLComponents(string: url.absoluteString) else { + return true + } + + if let state = components.queryValue(forKey: Parameters.state), self.state == state, + let code = components.queryValue(forKey: Parameters.code), !code.isEmpty { + + handle(code: code) + return false + } + + return true + } + + private func handle(code: String) { + presenter.didAuthenticated(code: code) + } + + private func makeAuthenticationRequest() -> URLRequest { + let state = UUID().uuidString + + var urlComponents = URLComponents(string: loginURL)! + + urlComponents.queryItems = [ + URLQueryItem(name: Parameters.cliendId, value: Request.fbClientId), + URLQueryItem(name: Parameters.redirectURI, value: Request.fbRedirectURL), + URLQueryItem(name: Parameters.state, value: state) + ] + + self.state = state + + return URLRequest(url: urlComponents.url!) + } +} diff --git a/Nynja/Modules/Auth/Facebook/Presenter/FacebookAuthPresenter.swift b/Nynja/Modules/Auth/Facebook/Presenter/FacebookAuthPresenter.swift new file mode 100644 index 000000000..a654aee84 --- /dev/null +++ b/Nynja/Modules/Auth/Facebook/Presenter/FacebookAuthPresenter.swift @@ -0,0 +1,57 @@ +// +// FacebookAuthPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class FacebookAuthPresenter: FacebookAuthPresenterProtocol, FacebookAuthInteractorOutputProtocol { + + private(set) weak var view: FacebookAuthViewProtocol? + private(set) var interactor: FacebookAuthInteractorInputProtocol! + private(set) var wireframe: FacebookAuthWireframeProtocol! + + + // MARK: - FacebookAuthPresenterProtocol + + func viewDidLoad() { + interactor.authenticate() + } + + func dismiss() { + wireframe.dismiss() + } + + func handleRedirect(to url: URL) -> Bool { + return interactor.handleRedirect(to: url) + } + + + // MARK: - FacebookAuthInteractorOutputProtocol + + func load(_ request: URLRequest) { + view?.load(request) + } + + func didAuthenticated(code: String) { + wireframe.finishAuthentication(code: code) + } +} + +extension FacebookAuthPresenter: SetInjectable { + + struct Dependencies { + let view: FacebookAuthViewProtocol + let interactor: FacebookAuthInteractorInputProtocol + let wireframe: FacebookAuthWireframeProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift b/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift new file mode 100644 index 000000000..9f0a2a715 --- /dev/null +++ b/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift @@ -0,0 +1,149 @@ +// +// FacebookAuthViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import WebKit + +final class FacebookAuthViewController: UIViewController, InitializeInjectable, FacebookAuthViewProtocol { + + private let presenter: FacebookAuthPresenterProtocol + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + + // MARK: - Views + + private lazy var swipeBackHelper = SwipeBackHelper(with: self) + + private lazy var safeAreaPlaceholder: UIView = { + let safeAreaPlaceholder = UIView() + + safeAreaPlaceholder.backgroundColor = UIColor.nynja.lapisLazuli + + view.addSubview(safeAreaPlaceholder) + safeAreaPlaceholder.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + maker.bottom.equalTo(view.safeArea.top) + } + + return safeAreaPlaceholder + }() + + private lazy var backButton: UIButton = { + let button = UIButton() + button.setImage(UIImage.nynja.icBackNavigation.image, for: .normal) + button.addTarget(self, action: #selector(actionBackButtonTapped(sender:)), for: .touchUpInside) + + view.addSubview(button) + button.snp.makeConstraints { maker in + // Don't adjust it in order to have the same size as in the webView header. + maker.width.height.equalTo(Constraints.backButton.size) + maker.left.equalToSuperview().offset(Constraints.backButton.left.adjustedByWidth) + maker.top.equalTo(safeAreaPlaceholder.snp.bottom) + } + + return button + }() + + private lazy var webView: WKWebView = { + let webView = WKWebView() + + view.addSubview(webView) + webView.snp.makeConstraints { maker in + maker.top.equalTo(view.safeArea.top) + maker.left.right.bottom.equalToSuperview() + } + + return webView + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: FacebookAuthPresenterProtocol + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + presenter.viewDidLoad() + } + + + // MARK: - UI Setup + + private func setupUI() { + webView.isHidden = false + backButton.isHidden = false + webView.navigationDelegate = self + swipeBackHelper.addGesture() + } + + + // MARK: - FacebookAuthViewProtocol + + func load(_ request: URLRequest) { + webView.load(request) + } + + + // MARK: - Actions + + @objc private func actionBackButtonTapped(sender: UIButton) { + presenter.dismiss() + } +} + +// MARK: - WKNavigationDelegate + +extension FacebookAuthViewController: WKNavigationDelegate { + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + guard let redirectURL = navigationAction.request.url else { + decisionHandler(.allow) + return + } + if presenter.handleRedirect(to: redirectURL) { + decisionHandler(.allow) + } else { + decisionHandler(.cancel) + } + } +} + +// MARK: - Layout + +private extension FacebookAuthViewController { + + enum Constraints { + + enum backButton { + static let left: CGFloat = 8 + static let size: CGFloat = 44 + } + } +} diff --git a/Nynja/Modules/Auth/Facebook/Wireframe/FacebookAuthWireframe.swift b/Nynja/Modules/Auth/Facebook/Wireframe/FacebookAuthWireframe.swift new file mode 100644 index 000000000..29405b53b --- /dev/null +++ b/Nynja/Modules/Auth/Facebook/Wireframe/FacebookAuthWireframe.swift @@ -0,0 +1,49 @@ +// +// FacebookAuthWireframe.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol FacebookAuthCoordinatorProtocol: class { + func wireframe(_ wireframe: FacebookAuthWireframe, didEndWithState state: FacebookAuthWireframe.State) +} + +final class FacebookAuthWireframe: Wireframe, FacebookAuthWireframeProtocol { + + private let coordinator: FacebookAuthCoordinatorProtocol + + init(coordinator: FacebookAuthCoordinatorProtocol) { + self.coordinator = coordinator + } + + enum State { + case authenticated(code: String) + case dismiss + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = FacebookAuthPresenter() + + let viewDependencies = FacebookAuthViewController.Dependencies(presenter: presenter) + let view = FacebookAuthViewController(dependencies: viewDependencies) + + let interactor = FacebookAuthInteractor(dependencies: .init(presenter: presenter)) + + let presenterDependencies = FacebookAuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) + presenter.inject(dependencies: presenterDependencies) + + return view + } + + func finishAuthentication(code: String) { + coordinator.wireframe(self, didEndWithState: .authenticated(code: code)) + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } +} -- GitLab From b98aa8c5aa1c1ca66038fa1fe8a4246e58d38e67 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 8 Nov 2018 14:13:02 +0200 Subject: [PATCH 093/138] [NY-5168] Pass facebook code to AuthInteractor. --- Nynja/Modules/Auth/AuthCoordinator.swift | 10 +++-- .../Auth/AuthModule/AuthProtocols.swift | 12 +++--- .../AuthModule/Entities/LoginOption.swift | 4 +- .../Interactor/AuthInteractor.swift | 14 +++---- .../AuthModule/Presenter/AuthPresenter.swift | 39 +++++++------------ .../AuthModule/Wireframe/AuthWireframe.swift | 6 +-- 6 files changed, 39 insertions(+), 46 deletions(-) diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index dfd9de6a4..9f877ae57 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -16,6 +16,8 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt private var selectCountryCallback: ((Result) -> Void)? + private var facebookAuthCodeCallback: ((Result) -> Void)? + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { self.navigation = navigation self.serviceFactory = serviceFactory @@ -93,10 +95,11 @@ extension AuthCoordinator { navigation?.pushViewController(view, animated: true) - case .showFacebookAuth: + case .showFacebookAuth(let callback): let wireframe = FacebookAuthWireframe(coordinator: self) let view = wireframe.prepareModule() + facebookAuthCodeCallback = callback navigation?.pushViewController(view, animated: true) } } @@ -181,11 +184,12 @@ extension AuthCoordinator: FacebookAuthCoordinatorProtocol { func wireframe(_ wireframe: FacebookAuthWireframe, didEndWithState state: FacebookAuthWireframe.State) { switch state { - case .dismiss: - break case let .authenticated(code): + facebookAuthCodeCallback?(.success(code)) + case .dismiss: break } + facebookAuthCodeCallback = nil navigation?.popViewController(animated: true) } } diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index f6be19382..fcade67db 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -11,7 +11,7 @@ import Foundation protocol AuthWireframeProtocol: class { func selectCountry(completion: @escaping (Result) -> Void) func continueLogin(loginOption: LoginOption) - func showFacebookAuth() + func showFacebookAuth(completion: @escaping (Result) -> Void) } protocol AuthViewProtocol: class where Self: UIViewController { @@ -36,15 +36,15 @@ protocol AuthPresenterProtocol: class { protocol AuthInputInteractorProtocol: class { typealias Code = String - func loginViaFacebook() + func loginViaFacebook(code: String) func loginViaGoogle() - func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) - func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) + func loginViaEmail(_ email: String) + func loginViaPhoneNumber(_ phoneNumber: String) func fetchDefaultCountry() -> Country } protocol AuthOutputInteractorProtocol: class { - func startFacebookLoginFlow() - func startGoogleLoginFlow() + func didAuthenticated(with loginOption: LoginOption) + func didReceiveAuthenticationFailure() } diff --git a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift index 8e95c2273..3004c10c0 100644 --- a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift +++ b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift @@ -11,6 +11,6 @@ import Foundation enum LoginOption { case phoneNumber(number: String) case email(email: String) - case facebook(code: String) - case google(code: String) + case facebook + case google } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index d4d88ec2a..5813a708a 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -37,19 +37,19 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { return countriesProvider.fetchDefaultCountry() } - func loginViaFacebook() { - presenter?.startFacebookLoginFlow() + func loginViaFacebook(code: String) { + presenter?.didAuthenticated(with: .facebook) } func loginViaGoogle() { - presenter?.startGoogleLoginFlow() + presenter?.didAuthenticated(with: .google) } - func loginViaEmail(_ email: String, completion: @escaping (Result) -> Void) { - completion(.success(())) + func loginViaEmail(_ email: String) { + presenter?.didAuthenticated(with: .email(email: email)) } - func loginViaPhoneNumber(_ phoneNumber: String, completion: @escaping (Result) -> Void) { - completion(.success(())) + func loginViaPhoneNumber(_ phoneNumber: String) { + presenter?.didAuthenticated(with: .phoneNumber(number: phoneNumber)) } } diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 2306b5f28..75c21169a 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -31,37 +31,26 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } func loginViaFacebook() { - // self?.wireframe?.continueLogin(loginOption: .facebook(code: code)) - interactor.loginViaFacebook() + wireframe.showFacebookAuth { [weak self] result in + switch result { + case let .success(code): + self?.interactor?.loginViaFacebook(code: code) + case .failure: + break + } + } } func loginViaGoogle() { - // self?.wireframe?.continueLogin(loginOption: .google(code: code)) interactor.loginViaGoogle() } func loginViaEmail(_ email: String) { - interactor.loginViaEmail(email) { [weak self] result in - switch result { - case .success: - self?.wireframe?.continueLogin(loginOption: .email(email: email)) - - case let .failure(error): - break - } - } + interactor.loginViaEmail(email) } func loginViaPhoneNumber(_ phoneNumber: String) { - interactor.loginViaPhoneNumber(phoneNumber) { [weak self] result in - switch result { - case .success: - self?.wireframe?.continueLogin(loginOption: .phoneNumber(number: phoneNumber)) - - case let .failure(error): - break - } - } + interactor.loginViaPhoneNumber(phoneNumber) } func selectCountry() { @@ -81,12 +70,12 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, extension AuthPresenter { - func startFacebookLoginFlow() { - wireframe.showFacebookAuth() + func didAuthenticated(with loginOption: LoginOption) { + wireframe?.continueLogin(loginOption: loginOption) } - func startGoogleLoginFlow() { - + func didReceiveAuthenticationFailure() { + // TODO: handle failure } } diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index b3cab1d47..36edf1d68 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -27,7 +27,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { enum State { case continueLogin(loginOption: LoginOption) case getCountry(callback: (Result) -> Void) - case showFacebookAuth + case showFacebookAuth(callback: (Result) -> Void) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -53,7 +53,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { coordinator.wireframe(self, didEndWithState: .continueLogin(loginOption: loginOption)) } - func showFacebookAuth() { - coordinator.wireframe(self, didEndWithState: .showFacebookAuth) + func showFacebookAuth(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .showFacebookAuth(callback: completion)) } } -- GitLab From 5fc78ff88b2a5eff79e9dcf89c214d479ac4ba00 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 8 Nov 2018 19:13:09 +0200 Subject: [PATCH 094/138] [NY-5169] Implemented Google login flow. --- Nynja.xcodeproj/project.pbxproj | 36 +++++++- Nynja/AppDelegate.swift | 14 ++++ Nynja/Auth/GoogleAuthError.swift | 20 +++++ Nynja/Auth/GoogleAuthService.swift | 83 +++++++++++++++++++ Nynja/Auth/GoogleAuthServiceUIDelegate.swift | 21 +++++ Nynja/Extensions/Bundle+Keys.swift | 4 + Nynja/Modules/Auth/AuthCoordinator.swift | 9 +- .../Auth/AuthModule/AuthProtocols.swift | 5 +- .../Interactor/AuthInteractor.swift | 17 +++- .../AuthModule/Presenter/AuthPresenter.swift | 21 ++++- .../AuthModule/Wireframe/AuthWireframe.swift | 16 +++- Nynja/Resources/DevAutoTests.xcconfig | 15 +++- Nynja/Resources/DevConfig.xcconfig | 2 + Nynja/Resources/Info.plist | 15 ++++ Nynja/Resources/PrereleaseConfig.xcconfig | 2 + Nynja/Resources/ReleaseConfig.xcconfig | 2 + Nynja/Resources/ThirdPartyServices.swift | 25 ++++-- .../ServiceFactory/ServiceFactory.swift | 4 + .../ServiceFactoryProtocol.swift | 2 + Podfile | 2 + Podfile.lock | 68 ++++++++++----- 21 files changed, 342 insertions(+), 41 deletions(-) create mode 100644 Nynja/Auth/GoogleAuthError.swift create mode 100644 Nynja/Auth/GoogleAuthService.swift create mode 100644 Nynja/Auth/GoogleAuthServiceUIDelegate.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 5d8aaaffa..3ed62665a 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -956,6 +956,9 @@ 852BB8D02194256600F2E8E4 /* FacebookAuthProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8CB2194256600F2E8E4 /* FacebookAuthProtocols.swift */; }; 852BB8D12194256600F2E8E4 /* FacebookAuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8CC2194256600F2E8E4 /* FacebookAuthInteractor.swift */; }; 852BB8D22194256600F2E8E4 /* FacebookAuthWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8CD2194256600F2E8E4 /* FacebookAuthWireframe.swift */; }; + 852BB8F921947A3A00F2E8E4 /* GoogleAuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8F821947A3A00F2E8E4 /* GoogleAuthService.swift */; }; + 852BB8FB2194807500F2E8E4 /* GoogleAuthServiceUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8FA2194807500F2E8E4 /* GoogleAuthServiceUIDelegate.swift */; }; + 852BB8FD21949F2C00F2E8E4 /* GoogleAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852BB8FC21949F2C00F2E8E4 /* GoogleAuthError.swift */; }; 852C3DCD216E34FC00447878 /* TypingSenderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852C3DCC216E34FC00447878 /* TypingSenderService.swift */; }; 852C3DCE216E357300447878 /* TypingSenderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852C3DCC216E34FC00447878 /* TypingSenderService.swift */; }; 852DF26120371FB400A4F8B6 /* FileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852DF26020371FB400A4F8B6 /* FileExtension.swift */; }; @@ -3264,6 +3267,9 @@ 852BB8CB2194256600F2E8E4 /* FacebookAuthProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthProtocols.swift; sourceTree = ""; }; 852BB8CC2194256600F2E8E4 /* FacebookAuthInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthInteractor.swift; sourceTree = ""; }; 852BB8CD2194256600F2E8E4 /* FacebookAuthWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthWireframe.swift; sourceTree = ""; }; + 852BB8F821947A3A00F2E8E4 /* GoogleAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthService.swift; sourceTree = ""; }; + 852BB8FA2194807500F2E8E4 /* GoogleAuthServiceUIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthServiceUIDelegate.swift; sourceTree = ""; }; + 852BB8FC21949F2C00F2E8E4 /* GoogleAuthError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthError.swift; sourceTree = ""; }; 852C3DCC216E34FC00447878 /* TypingSenderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingSenderService.swift; sourceTree = ""; }; 852DF26020371FB400A4F8B6 /* FileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtension.swift; sourceTree = ""; }; 852DF262203720E600A4F8B6 /* FileIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileIcons.swift; sourceTree = ""; }; @@ -6097,6 +6103,7 @@ 3A768E1C1ECD152300108F7C /* Services */ = { isa = PBXGroup; children = ( + 852BB8F721947A0800F2E8E4 /* Auth */, FEA656082167797E00B44029 /* WalletFundingNetworkService */, 4B71AC4021622A5600E4583B /* Notifications */, 4B7C73E7215A5508007924DB /* Debug */, @@ -6938,10 +6945,10 @@ 4B749F0E214FEFC8002F3A33 /* Auth */ = { isa = PBXGroup; children = ( - 852BB8C7219424EA00F2E8E4 /* Facebook */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, + 852BB8C7219424EA00F2E8E4 /* Facebook */, 115A968821FB24FA3C58A6D5 /* SelectCountry */, 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, @@ -8692,6 +8699,16 @@ path = Wireframe; sourceTree = ""; }; + 852BB8F721947A0800F2E8E4 /* Auth */ = { + isa = PBXGroup; + children = ( + 852BB8F821947A3A00F2E8E4 /* GoogleAuthService.swift */, + 852BB8FA2194807500F2E8E4 /* GoogleAuthServiceUIDelegate.swift */, + 852BB8FC21949F2C00F2E8E4 /* GoogleAuthError.swift */, + ); + path = Auth; + sourceTree = ""; + }; 852C3DD0216E3A4300447878 /* Messaging */ = { isa = PBXGroup; children = ( @@ -14865,11 +14882,13 @@ "${SRCROOT}/Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests-resources.sh", "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", "${PODS_ROOT}/GooglePlaces/Frameworks/GooglePlaces.framework/Resources/GooglePlaces.bundle", + "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GooglePlaces.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -14891,7 +14910,9 @@ "${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework", "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", + "${BUILT_PRODUCTS_DIR}/GTMOAuth2/GTMOAuth2.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", @@ -14916,7 +14937,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaMQTT.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMOAuth2.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", @@ -14950,6 +14973,9 @@ "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework", "${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", + "${BUILT_PRODUCTS_DIR}/GTMOAuth2/GTMOAuth2.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", @@ -14969,6 +14995,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaMQTT.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMOAuth2.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", @@ -15011,6 +15040,7 @@ "${SRCROOT}/Pods/Target Support Files/Pods-Nynja/Pods-Nynja-resources.sh", "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", "${PODS_ROOT}/GooglePlaces/Frameworks/GooglePlaces.framework/Resources/GooglePlaces.bundle", + "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/Intercom.bundle", "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/IntercomTranslations.bundle", "${PODS_ROOT}/TestFairy/upload-dsym.sh", @@ -15019,6 +15049,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GooglePlaces.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Intercom.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IntercomTranslations.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/upload-dsym.sh", @@ -15464,6 +15495,7 @@ 854574CC21933190001D43CF /* NavigableWireframeProtocol.swift in Sources */, E76D132C1FA35CCF00B07F0E /* ProfilePlaceholderCell.swift in Sources */, A4BE4AB52068E98C00C041D1 /* ALTextInputBar+Trim.swift in Sources */, + 852BB8FD21949F2C00F2E8E4 /* GoogleAuthError.swift in Sources */, E7EED2341F740BF3005DAE20 /* ChatsItem.swift in Sources */, 260313A620A0A4BA009AC66D /* SwitchableActionCellViewModel.swift in Sources */, A42D51AE206A361400EEB952 /* Message.swift in Sources */, @@ -16083,6 +16115,7 @@ A409B1CF2108D48E0051C20B /* QueryFactory.swift in Sources */, A42D52B7206A53AA00EEB952 /* reader_Spec.swift in Sources */, F119E66A20D24B960043A532 /* MultiplePreviewProtocols.swift in Sources */, + 852BB8F921947A3A00F2E8E4 /* GoogleAuthService.swift in Sources */, 5E07BC53216F6661000E4558 /* CreateProfilePresenter.swift in Sources */, 850FC611203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift in Sources */, 0062D93D2062EC4100B915AC /* InviteFriendsWireframe.swift in Sources */, @@ -16444,6 +16477,7 @@ 26DCB24E2064B9DC001EF0AB /* ContactsTableDS.swift in Sources */, A45F114820B421AB00F45004 /* Contact+BaseChatModel.swift in Sources */, 6CED2C4CE125011A3A731D62 /* AddContactViaPhoneInteractor.swift in Sources */, + 852BB8FB2194807500F2E8E4 /* GoogleAuthServiceUIDelegate.swift in Sources */, 5EEB73C5216199ED00D8ECE6 /* AuthProtocols.swift in Sources */, 260313AA20A0A4BA009AC66D /* ChatLanguageSettingsViewController.swift in Sources */, 859B862F204820DC003272B2 /* ThemePickerInteractor.swift in Sources */, diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 26b3ae696..e536a1877 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -11,6 +11,7 @@ import Crashlytics import Fabric import GoogleMaps import GooglePlaces +import GoogleSignIn import AWSCore import AWSS3 import UserNotifications @@ -50,6 +51,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD LogService.log(topic: .system) { return "Avaliable logs:\n\(LogServiceTopic.allValuesStrings)" } MotionManager.shared.startAccelerometers() + return true } @@ -63,6 +65,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return NynjaJoinByLinkService.shared().handleJoin(by: url) } + func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + return GIDSignIn.sharedInstance().handle(url, + sourceApplication: options[.sourceApplication] as? String, + annotation: options[.annotation]) + } + func applicationWillResignActive(_ application: UIApplication) { window?.addSubview(backgroundImageView) } @@ -117,6 +125,7 @@ private extension AppDelegate { setupTestFairy() setupCrashlytics() setupGoogleMaps() + setupGoogleSignIn() setupAmazon() setupIntercom() try? AudioSessionManager.shared.configureDefaultSession() @@ -174,6 +183,11 @@ private extension AppDelegate { GMSServices.provideAPIKey(key) GMSPlacesClient.provideAPIKey(key) } + + func setupGoogleSignIn() { + let key = ThirdPartyServicesFactory.googleSignIn.serviceConfig.apiKey + GIDSignIn.sharedInstance().clientID = key + } func setupAmazon() { ServiceFactory().makeAmazonInitializer().initialize() diff --git a/Nynja/Auth/GoogleAuthError.swift b/Nynja/Auth/GoogleAuthError.swift new file mode 100644 index 000000000..7f24924e9 --- /dev/null +++ b/Nynja/Auth/GoogleAuthError.swift @@ -0,0 +1,20 @@ +// +// GoogleAuthError.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum GoogleAuthError: Error { + case emptyServerAuthCode + + var localizedDescription: String { + switch self { + case .emptyServerAuthCode: + return "\(String(describing: type(of: self))): serverAuthCode is empty" + } + } +} diff --git a/Nynja/Auth/GoogleAuthService.swift b/Nynja/Auth/GoogleAuthService.swift new file mode 100644 index 000000000..14993dd60 --- /dev/null +++ b/Nynja/Auth/GoogleAuthService.swift @@ -0,0 +1,83 @@ +// +// GoogleAuthService.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import GoogleSignIn + +protocol GoogleAuthService: class { + + typealias Completion = (Result) -> Void + + var uiDelegate: GoogleAuthServiceUIDelegate? { get set } + + func signIn(completion: @escaping Completion) +} + +final class GoogleAuthServiceImpl: NSObject, GoogleAuthService, GIDSignInDelegate, GIDSignInUIDelegate { + + private let googleSignIn = GIDSignIn.sharedInstance() + + private var signInCompletion: GoogleAuthService.Completion? + + weak var uiDelegate: GoogleAuthServiceUIDelegate? + + + // MARK: - Init + + override init() { + super.init() + googleSignIn?.delegate = self + googleSignIn?.uiDelegate = self + } + + + // MARK: - GoogleAuthService + + func signIn(completion: @escaping GoogleAuthService.Completion) { + signInCompletion = completion + googleSignIn?.signIn() + } + + + // MARK: - GIDSignInDelegate + + func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) { + if let error = error { + signInCompletion?(.failure(error)) + + } else if let code = user?.serverAuthCode { + signInCompletion?(.success(code)) + + } else { + signInCompletion?(.failure(GoogleAuthError.emptyServerAuthCode)) + } + + signInCompletion = nil + } + + + // MARK: - GIDSignInUIDelegate + + func sign(inWillDispatch signIn: GIDSignIn!, error: Error!) { + uiDelegate?.googleAuthWillStart(self) + } + + func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) { + guard let viewController = viewController else { + return + } + uiDelegate?.googleAuth(self, dismiss: viewController) + } + + func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) { + guard let viewController = viewController else { + return + } + uiDelegate?.googleAuth(self, present: viewController) + } +} diff --git a/Nynja/Auth/GoogleAuthServiceUIDelegate.swift b/Nynja/Auth/GoogleAuthServiceUIDelegate.swift new file mode 100644 index 000000000..e07dfae9e --- /dev/null +++ b/Nynja/Auth/GoogleAuthServiceUIDelegate.swift @@ -0,0 +1,21 @@ +// +// GoogleAuthServiceUIDelegate.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol GoogleAuthServiceUIDelegate: class { + + /// Animations like spinner should be stopped here + func googleAuthWillStart(_ googleAuthService: GoogleAuthService) + + /// UIViewController must be presented modally + func googleAuth(_ googleAuthService: GoogleAuthService, present viewController: UIViewController) + + /// Modally presented UIViewController must be dismissed + func googleAuth(_ googleAuthService: GoogleAuthService, dismiss viewController: UIViewController) +} diff --git a/Nynja/Extensions/Bundle+Keys.swift b/Nynja/Extensions/Bundle+Keys.swift index 05d3a2dd9..5f423c496 100644 --- a/Nynja/Extensions/Bundle+Keys.swift +++ b/Nynja/Extensions/Bundle+Keys.swift @@ -75,4 +75,8 @@ extension Bundle { var associatedDomain: String { return object(forInfoDictionaryKey: "AssociatedDomain") as! String } + + var googleClientId: String { + return object(forInfoDictionaryKey: "GoogleClientId") as! String + } } diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 9f877ae57..c9b6bcbd8 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -28,7 +28,8 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt let wireframe = AuthWireframe(coordinator: self) let view = wireframe.prepareModule( - dependencies: AuthWireframe.Dependencies(countriesProvider: serviceFactory.makeCountriesProvider()) + dependencies: .init(countriesProvider: serviceFactory.makeCountriesProvider(), + googleAuthService: serviceFactory.makeGoogleAuthService()) ) navigation?.pushViewController(view, animated: true) } @@ -101,6 +102,12 @@ extension AuthCoordinator { facebookAuthCodeCallback = callback navigation?.pushViewController(view, animated: true) + + case let .present(viewController): + navigation?.present(viewController, animated: true) + + case let .dismiss(viewController): + viewController.dismiss(animated: true) } } diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index fcade67db..7f5ab8b3e 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -12,6 +12,9 @@ protocol AuthWireframeProtocol: class { func selectCountry(completion: @escaping (Result) -> Void) func continueLogin(loginOption: LoginOption) func showFacebookAuth(completion: @escaping (Result) -> Void) + + func present(_ viewController: UIViewController) + func dismiss(_ viewController: UIViewController) } protocol AuthViewProtocol: class where Self: UIViewController { @@ -34,8 +37,6 @@ protocol AuthPresenterProtocol: class { } protocol AuthInputInteractorProtocol: class { - typealias Code = String - func loginViaFacebook(code: String) func loginViaGoogle() func loginViaEmail(_ email: String) diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 5813a708a..b4a247e80 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -8,7 +8,6 @@ import Foundation - final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { private weak var presenter: AuthOutputInteractorProtocol? @@ -16,6 +15,7 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { // MARK: - Services private let countriesProvider: CountriesProviding + private let googleAuthService: GoogleAuthService // MARK: - Init @@ -23,11 +23,13 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { struct Dependencies { let presenter: AuthOutputInteractorProtocol let countriesProvider: CountriesProviding + let googleAuthService: GoogleAuthService } - init(dependencies: AuthInteractor.Dependencies) { + init(dependencies: Dependencies) { presenter = dependencies.presenter countriesProvider = dependencies.countriesProvider + googleAuthService = dependencies.googleAuthService } @@ -42,7 +44,16 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { } func loginViaGoogle() { - presenter?.didAuthenticated(with: .google) + googleAuthService.signIn { [weak self] result in + switch result { + case let .success(code): + print("Google server auth code: \(code)") + self?.presenter?.didAuthenticated(with: .google) + case let .failure(error): + print("Google auth error: \(error.localizedDescription)") + self?.presenter?.didReceiveAuthenticationFailure() + } + } } func loginViaEmail(_ email: String) { diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 75c21169a..c392213c5 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -8,7 +8,7 @@ import Foundation -final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, SetInjectable { +final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, GoogleAuthServiceUIDelegate { private weak var view: AuthViewProtocol? private var interactor: AuthInputInteractorProtocol! private var wireframe: AuthWireframeProtocol! @@ -66,6 +66,23 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } } +// MARK: - GoogleAuthServiceUIDelegate + +extension AuthPresenter { + + func googleAuthWillStart(_ googleAuthService: GoogleAuthService) { + // TODO: hide loading indicators if needed - google auth viewController will be presented. + } + + func googleAuth(_ googleAuthService: GoogleAuthService, dismiss viewController: UIViewController) { + wireframe.dismiss(viewController) + } + + func googleAuth(_ googleAuthService: GoogleAuthService, present viewController: UIViewController) { + wireframe.present(viewController) + } +} + // MARK: - Interactor Output extension AuthPresenter { @@ -81,7 +98,7 @@ extension AuthPresenter { // MARK: - SetInjectable -extension AuthPresenter { +extension AuthPresenter: SetInjectable { struct Dependencies { let view: AuthViewProtocol let interactor: AuthInteractor diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 36edf1d68..bdc828e50 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -7,6 +7,7 @@ // import Foundation +import UIKit.UIViewController protocol AuthCoordinatorProtocol: class { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) @@ -22,12 +23,15 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { struct Dependencies { let countriesProvider: CountriesProviding + let googleAuthService: GoogleAuthService } enum State { case continueLogin(loginOption: LoginOption) case getCountry(callback: (Result) -> Void) case showFacebookAuth(callback: (Result) -> Void) + case present(UIViewController) + case dismiss(UIViewController) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -36,7 +40,9 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { let viewDependencies = AuthViewController.Dependencies(presenter: presenter, viewsFactory: AuthViewsFactory()) let view = AuthViewController(dependencies: viewDependencies) - let interactorDependencies = AuthInteractor.Dependencies(presenter: presenter, countriesProvider: dependencies.countriesProvider) + let interactorDependencies = AuthInteractor.Dependencies(presenter: presenter, + countriesProvider: dependencies.countriesProvider, + googleAuthService: dependencies.googleAuthService) let interactor = AuthInteractor(dependencies: interactorDependencies) let presenterDependencies = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) @@ -56,4 +62,12 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { func showFacebookAuth(completion: @escaping (Result) -> Void) { coordinator.wireframe(self, didEndWithState: .showFacebookAuth(callback: completion)) } + + func present(_ viewController: UIViewController) { + coordinator.wireframe(self, didEndWithState: .present(viewController)) + } + + func dismiss(_ viewController: UIViewController) { + coordinator.wireframe(self, didEndWithState: .dismiss(viewController)) + } } diff --git a/Nynja/Resources/DevAutoTests.xcconfig b/Nynja/Resources/DevAutoTests.xcconfig index 35a7a2f0e..baf74d144 100644 --- a/Nynja/Resources/DevAutoTests.xcconfig +++ b/Nynja/Resources/DevAutoTests.xcconfig @@ -9,9 +9,16 @@ BundleIdentifier = com.nynja.dev.mobile.communicator ExtensionBundleIdentifier = com.nynja.dev.mobile.communicator.NynjaShare -ServerURL = "We can't use this IP: 34.217.41.22/bigDick.com" -AppName = NYNJAAutoTests +ServerURL = dev.ci.nynja.net +AppName = NYNJADev ServerPort = 1883 -Config = devAutoTests +Config = dev AppGroup = group.com.nynja.mobile.communicator.dev -ModelsVersion = 7 +ModelsVersion = 10 +isServerConnectionSecure = false +ConfServerAddress = 35.198.118.190 +ConfServerPort = 80 +ConfServerSecure = false +AssociatedDomain = applinks:join.dev-eu.nynja.net +GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com +ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a diff --git a/Nynja/Resources/DevConfig.xcconfig b/Nynja/Resources/DevConfig.xcconfig index c48955c46..baf74d144 100644 --- a/Nynja/Resources/DevConfig.xcconfig +++ b/Nynja/Resources/DevConfig.xcconfig @@ -20,3 +20,5 @@ ConfServerAddress = 35.198.118.190 ConfServerPort = 80 ConfServerSecure = false AssociatedDomain = applinks:join.dev-eu.nynja.net +GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com +ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index dda23f384..4c78ef078 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -22,6 +22,17 @@ APPL CFBundleShortVersionString 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + $(ReversedGoogleClientId) + + + CFBundleVersion 0.5.4.Dev ConfServerAddress @@ -46,6 +57,8 @@ + GoogleClientId + $(GoogleClientId) LSApplicationQueriesSchemes cydia @@ -76,6 +89,8 @@ NYNJA needs it to save photos and video to your device. NSPhotoLibraryUsageDescription NYNJA needs it so that you can use your local images. + ReversedGoogleClientId + $(ReversedGoogleClientId) ServerPort $(ServerPort) ServerURL diff --git a/Nynja/Resources/PrereleaseConfig.xcconfig b/Nynja/Resources/PrereleaseConfig.xcconfig index 1ffefb6cf..9c4c18538 100644 --- a/Nynja/Resources/PrereleaseConfig.xcconfig +++ b/Nynja/Resources/PrereleaseConfig.xcconfig @@ -20,3 +20,5 @@ ConfServerAddress = call.staging.nynja.net ConfServerPort = 443 ConfServerSecure = true AssociatedDomain = applinks:join.staging.nynja.net +GoogleClientId = 13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng.apps.googleusercontent.com +ReversedGoogleClientId = com.googleusercontent.apps.13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng diff --git a/Nynja/Resources/ReleaseConfig.xcconfig b/Nynja/Resources/ReleaseConfig.xcconfig index f8b763d10..0681be443 100644 --- a/Nynja/Resources/ReleaseConfig.xcconfig +++ b/Nynja/Resources/ReleaseConfig.xcconfig @@ -20,3 +20,5 @@ ConfServerAddress = call.nynja.net ConfServerPort = 443 ConfServerSecure = true AssociatedDomain = applinks:join.nynja.net +GoogleClientId = 13807320472-002cda3sovo7hef61dm4niekkm8jdaf1.apps.googleusercontent.com +ReversedGoogleClientId = com.googleusercontent.apps.13807320472-002cda3sovo7hef61dm4niekkm8jdaf1 diff --git a/Nynja/Resources/ThirdPartyServices.swift b/Nynja/Resources/ThirdPartyServices.swift index 1a85a5e7b..0b58e580a 100644 --- a/Nynja/Resources/ThirdPartyServices.swift +++ b/Nynja/Resources/ThirdPartyServices.swift @@ -9,12 +9,13 @@ import Foundation //TODO: temporary solution for convenience -struct ThirdPartyServicesFactory { - static let google: GoogleService = GoogleService(config: Bundle.main.config) - static let amazon: AmazonService = AmazonService(config: Bundle.main.config) - static let support: SupportService = SupportService(config: Bundle.main.config) - static let testFairy: TestFairyService = TestFairyService(config: Bundle.main.config) - static let intercom: IntercomService = IntercomService(config: Bundle.main.config) +enum ThirdPartyServicesFactory { + static let google = GoogleService(config: Bundle.main.config) + static let googleSignIn = GoogleSignInService(config: Bundle.main.config) + static let amazon = AmazonService(config: Bundle.main.config) + static let support = SupportService(config: Bundle.main.config) + static let testFairy = TestFairyService(config: Bundle.main.config) + static let intercom = IntercomService(config: Bundle.main.config) } enum AppConfig: String { @@ -81,6 +82,18 @@ struct GoogleService: ThirdPartyService { } } +struct GoogleSignInService: ThirdPartyService { + struct Config { + let apiKey: String + } + + let serviceConfig: Config + + init(config: AppConfig) { + serviceConfig = Config(apiKey: Bundle.main.googleClientId) + } +} + struct IntercomService: ThirdPartyService { struct Config { let apiKey: String diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 69dc443ff..d94925967 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -54,6 +54,10 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return HistoryRequestModelFactory() } + func makeGoogleAuthService() -> GoogleAuthService { + return GoogleAuthServiceImpl() + } + func makeTypingProvider() -> TypingProvider { return TypingProviderImpl.shared } diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift index 9247df943..953f7344c 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -20,6 +20,8 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTHandlerFactor func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol + func makeGoogleAuthService() -> GoogleAuthService + func makeTypingProvider() -> TypingProvider func makeContactsProvider() -> ContactsProviding func makeConversationsProvider() -> ConversationsProviding diff --git a/Podfile b/Podfile index a50c4ef09..42d25f890 100644 --- a/Podfile +++ b/Podfile @@ -30,6 +30,7 @@ def commonPodsForNynja pod 'GoogleMaps', '= 2.7.0' pod 'GooglePlaces', '= 2.7.0' + pod 'GoogleSignIn', '= 4.3.0' pod 'Firebase/Storage' pod 'Firebase/Auth' pod 'GRDBCipher', '= 2.10.0' @@ -61,6 +62,7 @@ def commonPodsForNynjaTests pod 'GoogleMaps', '= 2.7.0' pod 'GooglePlaces', '= 2.7.0' + pod 'GoogleSignIn', '= 4.3.0' pod 'GRDBCipher', '= 2.10.0' pod 'AutoScrollLabel', '= 0.4.3' diff --git a/Podfile.lock b/Podfile.lock index 839be3a78..9f36491a7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -17,25 +17,25 @@ PODS: - Fabric (~> 1.6.3) - CryptoSwift (0.10.0) - Fabric (1.6.13) - - Firebase/Auth (5.8.1): + - Firebase/Auth (5.12.0): - Firebase/CoreOnly - - FirebaseAuth (= 5.0.4) - - Firebase/CoreOnly (5.8.1): - - FirebaseCore (= 5.1.3) - - Firebase/Storage (5.8.1): + - FirebaseAuth (= 5.0.5) + - Firebase/CoreOnly (5.12.0): + - FirebaseCore (= 5.1.7) + - Firebase/Storage (5.12.0): - Firebase/CoreOnly - - FirebaseStorage (= 3.0.2) - - FirebaseAuth (5.0.4): + - FirebaseStorage (= 3.0.3) + - FirebaseAuth (5.0.5): - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 5.0) + - FirebaseCore (~> 5.1) - GoogleUtilities/Environment (~> 5.2) - GTMSessionFetcher/Core (~> 1.1) - FirebaseAuthInterop (1.0.0) - - FirebaseCore (5.1.3): + - FirebaseCore (5.1.7): - GoogleUtilities/Logger (~> 5.2) - - FirebaseStorage (3.0.2): + - FirebaseStorage (3.0.3): - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 5.0) + - FirebaseCore (~> 5.1) - GTMSessionFetcher/Core (~> 1.1) - GoogleMaps (2.7.0): - GoogleMaps/Maps (= 2.7.0) @@ -44,12 +44,31 @@ PODS: - GoogleMaps/Base - GooglePlaces (2.7.0): - GoogleMaps/Base (= 2.7.0) - - GoogleUtilities/Environment (5.3.0) - - GoogleUtilities/Logger (5.3.0): + - GoogleSignIn (4.3.0): + - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" + - "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)" + - GTMOAuth2 (~> 1.0) + - GTMSessionFetcher/Core (~> 1.1) + - GoogleToolboxForMac/DebugUtils (2.1.4): + - GoogleToolboxForMac/Defines (= 2.1.4) + - GoogleToolboxForMac/Defines (2.1.4) + - "GoogleToolboxForMac/NSDictionary+URLArguments (2.1.4)": + - GoogleToolboxForMac/DebugUtils (= 2.1.4) + - GoogleToolboxForMac/Defines (= 2.1.4) + - "GoogleToolboxForMac/NSString+URLArguments (= 2.1.4)" + - "GoogleToolboxForMac/NSString+URLArguments (2.1.4)" + - GoogleUtilities/Environment (5.3.4) + - GoogleUtilities/Logger (5.3.4): - GoogleUtilities/Environment - GRDBCipher (2.10.0): - SQLCipher (~> 3.4.1) - - GTMSessionFetcher/Core (1.1.15) + - GTMOAuth2 (1.1.6): + - GTMSessionFetcher (~> 1.1) + - GTMSessionFetcher (1.2.0): + - GTMSessionFetcher/Full (= 1.2.0) + - GTMSessionFetcher/Core (1.2.0) + - GTMSessionFetcher/Full (1.2.0): + - GTMSessionFetcher/Core (= 1.2.0) - Intercom (5.1.6) - JTAppleCalendar (7.1.5) - libPhoneNumber-iOS (0.9.13) @@ -89,6 +108,7 @@ DEPENDENCIES: - Firebase/Storage - GoogleMaps (= 2.7.0) - GooglePlaces (= 2.7.0) + - GoogleSignIn (= 4.3.0) - GRDBCipher (= 2.10.0) - Intercom (= 5.1.6) - JTAppleCalendar (= 7.1.5) @@ -119,8 +139,11 @@ SPEC REPOS: - FirebaseStorage - GoogleMaps - GooglePlaces + - GoogleSignIn + - GoogleToolboxForMac - GoogleUtilities - GRDBCipher + - GTMOAuth2 - GTMSessionFetcher - Intercom - JTAppleCalendar @@ -158,16 +181,19 @@ SPEC CHECKSUMS: Crashlytics: 95d05f4e4c19a771250c4bd9ce344d996de32bbf CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 Fabric: 2fb5676bc811af011a04513451f463dac6803206 - Firebase: a870ed114d769b424021a0c8ddb0c86c3250a0c5 - FirebaseAuth: 504b198ceb3472dca5c65bb95544ea44cfc9439e + Firebase: 9190018e296139d938b99521cde0c15a6e8d2946 + FirebaseAuth: 9299ab178271bec7426967b05b2718bb6fc31f17 FirebaseAuthInterop: 0ffa57668be100582bb7643d4fcb7615496c41fc - FirebaseCore: 27bd80e5bfaaf9552a1f5cacb4c7e8bb925bab22 - FirebaseStorage: fd82e5e5c474897e19972b34b22ac0f589dce04e + FirebaseCore: 027d350adc039aa5483357c6f56556f117c5170e + FirebaseStorage: 3d22c041370593e639fba013d1eb698a8dae2881 GoogleMaps: f79af95cb24d869457b1f961c93d3ce8b2f3b848 GooglePlaces: 3d06e6c99654545b4738ce49648745779c25f2ef - GoogleUtilities: 760ccb53b7c7f40f9c02d8c241f76f841a7a6162 + GoogleSignIn: 11183592dc63e105475c7305a325045ff95e02b7 + GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f + GoogleUtilities: abb092d2c12e817fa3e0e7b274987dd72fb86ec3 GRDBCipher: eef21d242c727a21e0f87ad44f8ea2df03edd252 - GTMSessionFetcher: 5fa5b80fd20e439ef5f545fb2cb3ca6c6714caa2 + GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2 + GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e Intercom: 083a05bf222811b0b5e0a0b24c863544123397f0 JTAppleCalendar: 2d4f974f9f3c8b4964d51ca1f6e004883c031fbe libPhoneNumber-iOS: e444379ac18bbfbdefad571da735b2cd7e096caa @@ -183,6 +209,6 @@ SPEC CHECKSUMS: SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: f206ab413c7bb2af1ba8a5ce0e66489b21851a6f +PODFILE CHECKSUM: b8798c400dc98c68f9a2fbc734665cfa4fdc24d4 COCOAPODS: 1.5.3 -- GitLab From de30b6669b69cd00a177451b2dfa0a7a7fff21ad Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 8 Nov 2018 19:36:31 +0200 Subject: [PATCH 095/138] [NY-5169] Setup google serverClientId and get `serverAuthCode` in response. --- Nynja.xcodeproj/project.pbxproj | 2 -- Nynja/AppDelegate.swift | 7 +++++-- Nynja/Extensions/Bundle+Keys.swift | 4 ++++ .../Auth/AuthModule/Interactor/AuthInteractor.swift | 8 +++++++- Nynja/Resources/DevAutoTests.xcconfig | 1 + Nynja/Resources/DevConfig.xcconfig | 1 + Nynja/Resources/Info.plist | 2 ++ Nynja/Resources/PrereleaseConfig.xcconfig | 1 + Nynja/Resources/ReleaseConfig.xcconfig | 1 + Nynja/Resources/ThirdPartyServices.swift | 5 +++-- 10 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 3ed62665a..b2674bc54 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -2112,7 +2112,6 @@ F105C6BC20A1347E0091786A /* PhotoPreviewWireframeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F105C6B520A1347E0091786A /* PhotoPreviewWireframeProtocol.swift */; }; F105C6BD20A1347E0091786A /* PhotoPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F105C6B720A1347E0091786A /* PhotoPreviewViewController.swift */; }; F105C6BE20A1347E0091786A /* PhotoPreviewInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F105C6B920A1347E0091786A /* PhotoPreviewInteractor.swift */; }; - F10AFE9B20EF8B9B00C7CE83 /* DevAutoTests.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = F10AFE9A20EF8B9A00C7CE83 /* DevAutoTests.xcconfig */; }; F10AFEB420F7B1B000C7CE83 /* WheelPreviewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AFEAE20F7B1AF00C7CE83 /* WheelPreviewProtocol.swift */; }; F10AFEB520F7B1B000C7CE83 /* WheelDefaultItemPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AFEAF20F7B1AF00C7CE83 /* WheelDefaultItemPreview.swift */; }; F10AFEB620F7B1B000C7CE83 /* WheelImageFullItemPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AFEB020F7B1AF00C7CE83 /* WheelImageFullItemPreview.swift */; }; @@ -14782,7 +14781,6 @@ A4B544F820EFC0AD00EB7B0F /* StatusCodes.strings in Resources */, 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */, E77B9B7C1FDEC6E20035CA12 /* NotoSans-Bold.ttf in Resources */, - F10AFE9B20EF8B9B00C7CE83 /* DevAutoTests.xcconfig in Resources */, 00F7B348202B317000E443E1 /* timezones.json in Resources */, 5BBEF53C212DE09F00F10768 /* ringback.m4a in Resources */, 3A2843291EF9317100EFE21A /* Avenir.ttc in Resources */, diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index e536a1877..c703a6491 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -185,8 +185,11 @@ private extension AppDelegate { } func setupGoogleSignIn() { - let key = ThirdPartyServicesFactory.googleSignIn.serviceConfig.apiKey - GIDSignIn.sharedInstance().clientID = key + let clientId = ThirdPartyServicesFactory.googleSignIn.serviceConfig.clientId + let serverClientId = ThirdPartyServicesFactory.googleSignIn.serviceConfig.serverClientId + + GIDSignIn.sharedInstance().clientID = clientId + GIDSignIn.sharedInstance().serverClientID = serverClientId } func setupAmazon() { diff --git a/Nynja/Extensions/Bundle+Keys.swift b/Nynja/Extensions/Bundle+Keys.swift index 5f423c496..5c307e99d 100644 --- a/Nynja/Extensions/Bundle+Keys.swift +++ b/Nynja/Extensions/Bundle+Keys.swift @@ -79,4 +79,8 @@ extension Bundle { var googleClientId: String { return object(forInfoDictionaryKey: "GoogleClientId") as! String } + + var googleServerClientId: String { + return object(forInfoDictionaryKey: "GoogleServerClientId") as! String + } } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index b4a247e80..972620329 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -49,8 +49,14 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { case let .success(code): print("Google server auth code: \(code)") self?.presenter?.didAuthenticated(with: .google) + case let .failure(error): - print("Google auth error: \(error.localizedDescription)") + switch error { + case let error as GoogleAuthError: + print("Google auth error: \(error.localizedDescription)") + default: + print("Google unknown error: \(error.localizedDescription)") + } self?.presenter?.didReceiveAuthenticationFailure() } } diff --git a/Nynja/Resources/DevAutoTests.xcconfig b/Nynja/Resources/DevAutoTests.xcconfig index baf74d144..33b38aa95 100644 --- a/Nynja/Resources/DevAutoTests.xcconfig +++ b/Nynja/Resources/DevAutoTests.xcconfig @@ -22,3 +22,4 @@ ConfServerSecure = false AssociatedDomain = applinks:join.dev-eu.nynja.net GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a +GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com diff --git a/Nynja/Resources/DevConfig.xcconfig b/Nynja/Resources/DevConfig.xcconfig index baf74d144..33b38aa95 100644 --- a/Nynja/Resources/DevConfig.xcconfig +++ b/Nynja/Resources/DevConfig.xcconfig @@ -22,3 +22,4 @@ ConfServerSecure = false AssociatedDomain = applinks:join.dev-eu.nynja.net GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a +GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 4c78ef078..0ce4809ab 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -59,6 +59,8 @@ GoogleClientId $(GoogleClientId) + GoogleServerClientId + $(GoogleServerClientId) LSApplicationQueriesSchemes cydia diff --git a/Nynja/Resources/PrereleaseConfig.xcconfig b/Nynja/Resources/PrereleaseConfig.xcconfig index 9c4c18538..45a953207 100644 --- a/Nynja/Resources/PrereleaseConfig.xcconfig +++ b/Nynja/Resources/PrereleaseConfig.xcconfig @@ -22,3 +22,4 @@ ConfServerSecure = true AssociatedDomain = applinks:join.staging.nynja.net GoogleClientId = 13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng +GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com diff --git a/Nynja/Resources/ReleaseConfig.xcconfig b/Nynja/Resources/ReleaseConfig.xcconfig index 0681be443..6dd468399 100644 --- a/Nynja/Resources/ReleaseConfig.xcconfig +++ b/Nynja/Resources/ReleaseConfig.xcconfig @@ -22,3 +22,4 @@ ConfServerSecure = true AssociatedDomain = applinks:join.nynja.net GoogleClientId = 13807320472-002cda3sovo7hef61dm4niekkm8jdaf1.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-002cda3sovo7hef61dm4niekkm8jdaf1 +GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com diff --git a/Nynja/Resources/ThirdPartyServices.swift b/Nynja/Resources/ThirdPartyServices.swift index 0b58e580a..b061a5649 100644 --- a/Nynja/Resources/ThirdPartyServices.swift +++ b/Nynja/Resources/ThirdPartyServices.swift @@ -84,13 +84,14 @@ struct GoogleService: ThirdPartyService { struct GoogleSignInService: ThirdPartyService { struct Config { - let apiKey: String + let clientId: String + let serverClientId: String } let serviceConfig: Config init(config: AppConfig) { - serviceConfig = Config(apiKey: Bundle.main.googleClientId) + serviceConfig = Config(clientId: Bundle.main.googleClientId, serverClientId: Bundle.main.googleServerClientId) } } -- GitLab From 325500e685861d6a0e889183c1101775ad4a86b5 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 9 Nov 2018 15:13:41 +0200 Subject: [PATCH 096/138] [NY-5275] Implemented email AutoFill. --- Nynja/Library/UI/TextInput/Material/MaterialTextField.swift | 4 ++++ .../AuthModule/View/ViewsFactory/AuthViewsFactory.swift | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift index 76b86dc89..c00a53aff 100644 --- a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift +++ b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift @@ -61,6 +61,10 @@ class MaterialTextField: MaterialTextContainer { didSet { textField.autocapitalizationType = autocapitalizationType } } + var textContentType: UITextContentType! { + didSet { textField.textContentType = textContentType } + } + var returnHandler: ((MaterialTextField) -> Bool)? diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index 1f89065ac..9417c4d02 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -294,7 +294,8 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { func makeInputField(on view: UIView) -> MaterialTextField { let textField = MaterialTextField() - view.addSubview(textField) + + textField.textContentType = .emailAddress textField.placeholderColor = UIColor.nynja.dustyGray textField.placeholder = "Email".localized @@ -307,6 +308,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { textField.keyboardType = .emailAddress textField.returnKeyType = .done + view.addSubview(textField) textField.snp.makeConstraints { make in make.centerY.equalToSuperview() make.left.equalToSuperview() @@ -435,7 +437,6 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { func makePhoneNumberTextField(on view: UIView) -> UITextField { let textField = UITextField() - view.addSubview(textField) textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) @@ -444,6 +445,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { textField.textColor = UIColor.nynja.white textField.keyboardType = .numberPad + view.addSubview(textField) textField.snp.makeConstraints { (make) in make.centerY.right.equalToSuperview() make.left.equalToSuperview().offset(16) -- GitLab From 39dffebdb4f076ba1560c0c1795063ba5acc5fe1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 12 Nov 2018 13:56:12 +0200 Subject: [PATCH 097/138] [NY-5165] Updated link to terms of service. --- Nynja/Generated/LocalizableConstants.swift | 2 + Nynja/Modules/Auth/AuthCoordinator.swift | 60 +++++++++++++------ .../CreateProfileProtocols.swift | 2 + .../Presenter/CreateProfilePresenter.swift | 4 ++ .../View/CreateProfileViewController.swift | 12 ++++ .../Subviews/CreateProfileContentView.swift | 23 ++++--- .../CreateProfileViewsFactory.swift | 4 +- .../Wireframe/CreateProfileWireframe.swift | 5 ++ .../View/WebFullScreenViewController.swift | 40 ++++++------- Nynja/Resources/en.lproj/Localizable.strings | 7 +++ 10 files changed, 110 insertions(+), 49 deletions(-) diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 2d32f364f..89fe5c09d 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1086,6 +1086,8 @@ internal extension String { static var targetLocation: String { return localizable.tr("Localizable", "target_location") } /// Terms of Service static var termsOfService: String { return localizable.tr("Localizable", "terms_of_service") } + /// terms of use + static var termsOfUse: String { return localizable.tr("Localizable", "terms_of_use") } /// Th static var th: String { return localizable.tr("Localizable", "th") } /// MOBILE COMMUNICATOR diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index c9b6bcbd8..cb1939ff6 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -70,8 +70,10 @@ extension AuthCoordinator { navigation?.pushViewController(view, animated: true) switch type { - case .login: break - case .register: break + case .login: + break + case .register: + break } } } @@ -113,8 +115,10 @@ extension AuthCoordinator { private func continueLoginProcess(with loginOption: LoginOption) { switch loginOption { - case .email, .phoneNumber: showConfirmationPopup(loginOption: loginOption) - default: break + case .email, .phoneNumber: + showConfirmationPopup(loginOption: loginOption) + default: + break } } @@ -131,18 +135,19 @@ extension AuthCoordinator { case .email(let email): let wireframe = CodeConfirmationWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: CodeConfirmationWireframe.Parameters(address: email, authType: .email), - dependencies: CodeConfirmationWireframe.Dependencies()) + parameters: CodeConfirmationWireframe.Parameters(address: email, authType: .email) + ) self.navigation?.pushViewController(view, animated: true) case .phoneNumber(let number): let wireframe = CodeConfirmationWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: CodeConfirmationWireframe.Parameters(address: number, authType: .phoneNumber), - dependencies: CodeConfirmationWireframe.Dependencies()) + parameters: CodeConfirmationWireframe.Parameters(address: number, authType: .phoneNumber) + ) self.navigation?.pushViewController(view, animated: true) - default: break + default: + break } } @@ -153,9 +158,12 @@ extension AuthCoordinator { private func titleForPopup(loginOption: LoginOption) -> String { switch loginOption { - case .email: return "Please confirm the email you entered is correct".localized - case .phoneNumber: return "Please confirm the number you entered is correct".localized - default: return "" + case .email: + return "Please confirm the email you entered is correct".localized + case .phoneNumber: + return "Please confirm the number you entered is correct".localized + default: + return "" } } @@ -173,14 +181,30 @@ extension AuthCoordinator { extension AuthCoordinator { func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) { switch state { - case .back: navigation?.popViewController(animated: true) - case .next: end() - case .chooseAvatar(let completion): - let dependencies = SelectAvatarFlowCoordinator.Dependencies(source: .gallery, rootViewController: navigation!.viewControllers.last!, serviceFactory: serviceFactory) { (url) in - completion(UIImage.sd_image(with: try? Data(contentsOf: url))) + case .back: + navigation?.popViewController(animated: true) + + case .next: + end() + + case let .chooseAvatar(completion): + guard let navigation = navigation else { return } + + let dependencies = SelectAvatarFlowCoordinator.Dependencies( + source: .gallery, + rootViewController: navigation.viewControllers.last!, + serviceFactory: serviceFactory) { url in + completion(UIImage.sd_image(with: try? Data(contentsOf: url))) } - let chooseAvatarCoordinator = SelectAvatarFlowCoordinator.init(dependencies: dependencies) + let chooseAvatarCoordinator = SelectAvatarFlowCoordinator(dependencies: dependencies) chooseAvatarCoordinator.start() + + case let .openTerms(url): + guard let navigation = navigation else { return } + + WebFullScreenWireFrame().presentWebFullScreen(navigation: navigation, + title: String.localizable.termsOfUse.uppercased(), + inputURL: url) } } } diff --git a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift index 1bde28915..7a6d2c0f4 100644 --- a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift +++ b/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift @@ -12,6 +12,7 @@ protocol CreateProfileWireframeProtocol: class { func back() func end() func chooseAvatar(completion: @escaping (UIImage?) -> Void) + func open(url: URL) } protocol CreateProfileViewProtocol: class where Self: UIViewController { @@ -25,6 +26,7 @@ protocol CreateProfilePresenterProtocol: NavigationProtocol { func setProfileField(value: String, field: ProfileField) func chooseAvatar(completion: @escaping (UIImage?) -> Void) func checkTermsOfUse() -> Bool + func open(url: URL) } protocol CreateProfileInputInteractorProtocol: class { diff --git a/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift index 05b6812a8..3886a7387 100644 --- a/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift +++ b/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -44,6 +44,10 @@ final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, Creat return interactor.checkTermsOfUse() } + func open(url: URL) { + wireframe.open(url: url) + } + // MARK: - CreateProfileOutputInteractorProtocol diff --git a/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift index 1b02b7f05..b196db657 100644 --- a/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift +++ b/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift @@ -37,6 +37,8 @@ final class CreateProfileViewController: UIViewController, CreateProfileViewProt _ = [topHeaderLayoutGuide, headerView, createButton, container, scrollView, contentContainer] + contentContainer.termsOfUseTextView.delegate = self + enableKeyboardHidingWhenTappedAround() } @@ -94,6 +96,16 @@ extension CreateProfileViewController { } } +// MARK: - UITextViewDelegate + +extension CreateProfileViewController: UITextViewDelegate { + + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + presenter.open(url: URL) + return false + } +} + //MARK: - Actions extension CreateProfileViewController { diff --git a/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift b/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift index 102dd7e62..e4fa858e3 100644 --- a/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift +++ b/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift @@ -27,37 +27,42 @@ final class CreateProfileContentView: UIView, Configurable { fatalError("init(coder:) has not been implemented") } - private lazy var avatarbutton: UIButton = viewsFactory.makeAvatarButton(on: self, target: self, selector: #selector(chooseAvatar(sender:))) + private(set) lazy var avatarbutton = viewsFactory.makeAvatarButton(on: self, target: self, selector: #selector(chooseAvatar(sender:))) - private lazy var firstNameTextField: MaterialTextField = viewsFactory.makeFirstNameTextField( + private(set) lazy var firstNameTextField = viewsFactory.makeFirstNameTextField( on: self, top: avatarbutton, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - private lazy var lastNameTextField: MaterialTextField = viewsFactory.makeLastNameTextField( + private(set) lazy var lastNameTextField = viewsFactory.makeLastNameTextField( on: self, top: firstNameTextField, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - private lazy var accountNameTextField: MaterialTextField = viewsFactory.makeAccountNameTextField( + private(set) lazy var accountNameTextField = viewsFactory.makeAccountNameTextField( on: self, top: lastNameTextField, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - private lazy var usernameTextField: MaterialTextField = viewsFactory.makeUsernameTextField( + private(set) lazy var usernameTextField = viewsFactory.makeUsernameTextField( on: self, top: accountNameTextField, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - private lazy var descriptionLabel: UILabel = viewsFactory.makeDescriptionLabel(on: self, top: usernameTextField) + private(set) lazy var descriptionLabel = viewsFactory.makeDescriptionLabel(on: self, top: usernameTextField) - private lazy var checkBoxContainerView: UIView = viewsFactory.makeCheckBoxContainerView(on: self, top: descriptionLabel) - private lazy var checkButton: UIButton = viewsFactory.makeCheckButton(on: checkBoxContainerView, target: self, selector: #selector(markCheck(sender:))) - private lazy var termsOfUseTextView: UITextView = viewsFactory.makeTermsOfUseTextView(on: checkBoxContainerView, left: checkButton) + private(set) lazy var checkBoxContainerView = viewsFactory.makeCheckBoxContainerView(on: self, top: descriptionLabel) + + private(set) lazy var checkButton = viewsFactory.makeCheckButton(on: checkBoxContainerView, + target: self, + selector: #selector(markCheck(sender:))) + + private(set) lazy var termsOfUseTextView = viewsFactory.makeTermsOfUseTextView(on: checkBoxContainerView, + left: checkButton) } // MARK: - Configurable diff --git a/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift index 112757754..5c93b34c2 100644 --- a/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift +++ b/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift @@ -252,13 +252,13 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { beginOfStr.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.nynja.dustyGray, NSAttributedStringKey.font: FontFamily.NotoSans.regular.font(size: 14)], range: NSMakeRange(0, beginOfStr.length)) - let attributes = [NSAttributedStringKey.link : "http://www.google.com", + let attributes = [NSAttributedStringKey.link : "https://landing.nynja.io/terms-of-use", NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue, NSAttributedStringKey.underlineColor: UIColor.nynja.blue, NSAttributedStringKey.foregroundColor: UIColor.nynja.blue, NSAttributedStringKey.font: FontFamily.NotoSans.regular.font(size: 14)] as [NSAttributedStringKey : Any] - let termsOfUseStr = NSMutableAttributedString(string: "terms of use".localized) + let termsOfUseStr = NSMutableAttributedString(string: String.localizable.termsOfUse) termsOfUseStr.addAttributes(attributes, range: NSMakeRange(0, termsOfUseStr.length)) beginOfStr.append(NSAttributedString(string: " ")) diff --git a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift index d53043eb5..cf46ca174 100644 --- a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -18,6 +18,7 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { case back case next case chooseAvatar(completion: (UIImage?) -> Void) + case openTerms(url: URL) } private let coordinator: CreateProfileCoordinatorProtocol @@ -52,4 +53,8 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { func chooseAvatar(completion: @escaping (UIImage?) -> Void) { coordinator.wireframe(self, didEndWithState: .chooseAvatar(completion: completion)) } + + func open(url: URL) { + coordinator.wireframe(self, didEndWithState: .openTerms(url: url)) + } } diff --git a/Nynja/Modules/WebFullScreen/View/WebFullScreenViewController.swift b/Nynja/Modules/WebFullScreen/View/WebFullScreenViewController.swift index 4760fc1f4..786c84823 100644 --- a/Nynja/Modules/WebFullScreen/View/WebFullScreenViewController.swift +++ b/Nynja/Modules/WebFullScreen/View/WebFullScreenViewController.swift @@ -9,7 +9,7 @@ import UIKit import SnapKit -final class WebFullScreenViewController: BaseVC, WebFullScreenViewProtocol { +final class WebFullScreenViewController: BaseVC, WebFullScreenViewProtocol, NavigationProtocol { var presenter: WebFullScreenPresenterProtocol! { didSet { @@ -23,8 +23,8 @@ final class WebFullScreenViewController: BaseVC, WebFullScreenViewProtocol { private lazy var webView: UIWebView = { let webView = UIWebView() webView.backgroundColor = UIColor.nynja.white - self.view.addSubview(webView) + view.addSubview(webView) webView.snp.makeConstraints { maker in maker.top.equalTo(navigationView.snp.bottom) maker.leading.trailing.bottom.equalToSuperview() @@ -38,26 +38,14 @@ final class WebFullScreenViewController: BaseVC, WebFullScreenViewProtocol { let colors: [UIColor] = [color.withAlphaComponent(0), color] let gradientView = GradientView(colors: colors) - self.view.addSubview(gradientView) + view.addSubview(gradientView) gradientView.snp.makeConstraints { maker in maker.left.right.equalToSuperview() maker.bottom.equalToSuperview() maker.height.equalTo(Constraints.gradientView.height.adjustedByWidth) } - return gradientView - }() - - private lazy var closeButton: NynjaCloseButton = { - let button = NynjaCloseButton() - button.addTarget(self, action: #selector(actionClose(_:)), for: .touchUpInside) - self.gradientView.addSubview(button) - button.snp.makeConstraints { maker in - maker.width.height.equalTo(Constraints.closeButton.size.adjustedByWidth) - maker.left.equalToSuperview().offset(Constraints.closeButton.leftOffset.adjustedByWidth) - maker.centerY.equalToSuperview() - } - return button + return gradientView }() @@ -73,16 +61,28 @@ final class WebFullScreenViewController: BaseVC, WebFullScreenViewProtocol { // MARK: - UI Setup private func setupUI() { - shouldShowSeparator = true + view.backgroundColor = UIColor.nynja.darkLight + backImage.image = nil + + setupNavigationView() + webView.isHidden = false - closeButton.isHidden = false + } + + private func setupNavigationView() { + let config = NavigationView.Config(isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: self, + backButtonImage: UIImage.nynja.icBackNavigation.image) + + navigationView.configure(config: config) } // MARK: - Actions - @objc private func actionClose(_ sender: UIButton) { - sender.isUserInteractionEnabled = false + func back() { presenter.didClickClose() } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 7f0020501..46757f520 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -987,3 +987,10 @@ "BY DATE" = "BY DATE"; "BY FOLDER" = "BY FOLDER"; "ALL" = "ALL"; + + +// MARK: - Auth + +// MARK: Create Profile + +"terms_of_use" = "terms of use"; -- GitLab From a58fa4cea9e5b9bfc85086ee16d65b9f1904d5d9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 12 Nov 2018 18:05:24 +0200 Subject: [PATCH 098/138] [NY-5299] Fixed wrong colors for buttons and button states. Fixed email validation. Add background image. --- Nynja.xcodeproj/project.pbxproj | 10 +++ Nynja/Generated/ColorsConstants.swift | 10 +-- .../Buttons/NynjaButton/BaseNynjaButton.swift | 52 +++++++++------ .../UI/Buttons/NynjaButton/NynjaButton.swift | 12 ++-- .../Buttons/NynjaButton/NynjaCellButton.swift | 22 ++----- .../NynjaButton/NynjaImageButton.swift | 33 ++++++++++ .../NynjaButton/RoundNynjaButton.swift | 19 ++++++ .../AuthModule/View/AuthViewController.swift | 13 +++- .../View/Subviews/EmailLoginView.swift | 30 +++------ .../View/Subviews/PhoneNumberLoginView.swift | 6 -- .../View/ViewsFactory/AuthViewsFactory.swift | 65 +++++++------------ .../View/FacebookAuthViewController.swift | 2 +- Nynja/Resources/Colors.json | 7 +- 13 files changed, 161 insertions(+), 120 deletions(-) create mode 100644 Nynja/Library/UI/Buttons/NynjaButton/NynjaImageButton.swift create mode 100644 Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b2674bc54..c05b2eba6 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1062,6 +1062,9 @@ 8557989C209368E7007050B8 /* StickerPackHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557989A209368E7007050B8 /* StickerPackHeaderView.swift */; }; 8557989D209368E7007050B8 /* StickerPackHeaderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557989B209368E7007050B8 /* StickerPackHeaderModel.swift */; }; 855A393D213E76E20002B8DC /* LoadingInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A393C213E76E20002B8DC /* LoadingInteractive.swift */; }; + 855A4E7F2199B4FE00B6E90B /* NynjaImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */; }; + 855A4E812199C16F00B6E90B /* RoundNynjaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */; }; + 855A4E822199C1A100B6E90B /* RoundNynjaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */; }; 855AC532208E441500DC2335 /* StickersInputPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52D208E441500DC2335 /* StickersInputPresenter.swift */; }; 855AC533208E441500DC2335 /* StickersInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52E208E441500DC2335 /* StickersInputViewController.swift */; }; 855AC534208E441500DC2335 /* StickersInputProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52F208E441500DC2335 /* StickersInputProtocols.swift */; }; @@ -3346,6 +3349,8 @@ 8557989A209368E7007050B8 /* StickerPackHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackHeaderView.swift; sourceTree = ""; }; 8557989B209368E7007050B8 /* StickerPackHeaderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackHeaderModel.swift; sourceTree = ""; }; 855A393C213E76E20002B8DC /* LoadingInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoadingInteractive.swift; path = BaseVC/LoadingInteractive.swift; sourceTree = ""; }; + 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaImageButton.swift; sourceTree = ""; }; + 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundNynjaButton.swift; sourceTree = ""; }; 855AC52D208E441500DC2335 /* StickersInputPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputPresenter.swift; sourceTree = ""; }; 855AC52E208E441500DC2335 /* StickersInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputViewController.swift; sourceTree = ""; }; 855AC52F208E441500DC2335 /* StickersInputProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputProtocols.swift; sourceTree = ""; }; @@ -10172,9 +10177,11 @@ isa = PBXGroup; children = ( 8503B51A205036F2006F0593 /* BaseNynjaButton.swift */, + 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */, E761A0DB1F8B8F3900C088E0 /* NynjaButton.swift */, 8503B51820503683006F0593 /* NynjaCellButton.swift */, 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */, + 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */, ); path = NynjaButton; sourceTree = ""; @@ -15135,6 +15142,7 @@ FEA6546F2167749D00B44029 /* ContactExtension.swift in Sources */, 855792D52122206400D0AB57 /* Collection.swift in Sources */, E785F15A1FF3E38D006C52D9 /* UIImageView+Rounded.swift in Sources */, + 855A4E822199C1A100B6E90B /* RoundNynjaButton.swift in Sources */, A42CE56A20692EDB000889CC /* Typing.swift in Sources */, 26EEA5482091F84E0066D3B0 /* CollectionsExtensions.swift in Sources */, 4B3B35DB217119C9005A214A /* AmazonInitializerImpl.swift in Sources */, @@ -16367,6 +16375,7 @@ 2657BE51201233E300F21935 /* ImageFilledItemModel.swift in Sources */, 85BA176120BEA7BD001EF8AC /* StickerPreviewContainerView.swift in Sources */, 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */, + 855A4E7F2199B4FE00B6E90B /* NynjaImageButton.swift in Sources */, 8575E5342191A9E70080DD4A /* CountryTableViewCell.swift in Sources */, 00E8513B2021E96E007DC792 /* GApiResponse.swift in Sources */, 850571252050B0AD00EDF794 /* NotificationAlertSoundsWireFrame.swift in Sources */, @@ -16639,6 +16648,7 @@ F112B19020E0FBE800B06E3E /* AsyncBlockOperation.swift in Sources */, FB816EBA20B59E0500093DCD /* HistoryRequestModelFactory.swift in Sources */, A42D51C2206A361400EEB952 /* sequenceFlow.swift in Sources */, + 855A4E812199C16F00B6E90B /* RoundNynjaButton.swift in Sources */, F117872820ACF2DB007A9A1B /* CameraSettings.swift in Sources */, A45F110C20B4218D00F45004 /* ActionStatus.swift in Sources */, B7F5051B20611A0900C28FA1 /* DownloadSettingsArrowModel.swift in Sources */, diff --git a/Nynja/Generated/ColorsConstants.swift b/Nynja/Generated/ColorsConstants.swift index c4a6c0edd..9508aa8ea 100644 --- a/Nynja/Generated/ColorsConstants.swift +++ b/Nynja/Generated/ColorsConstants.swift @@ -51,10 +51,12 @@ internal extension SGColor { static let dodgerBlue = #colorLiteral(red: 0.21960784, green: 0.5686275, blue: 1.0, alpha: 1.0) /// 0x969696ff (r: 150, g: 150, b: 150, a: 255) static let dustyGray = #colorLiteral(red: 0.5882353, green: 0.5882353, blue: 0.5882353, alpha: 1.0) + /// 0x3b5998ff (r: 59, g: 89, b: 152, a: 255) + static let facebookBackground = #colorLiteral(red: 0.23137255, green: 0.34901962, blue: 0.59607846, alpha: 1.0) + /// 0x213980ff (r: 33, g: 57, b: 128, a: 255) + static let facebookHighlighted = #colorLiteral(red: 0.12941177, green: 0.22352941, blue: 0.5019608, alpha: 1.0) /// 0x666666ff (r: 102, g: 102, b: 102, a: 255) static let gray = #colorLiteral(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) - /// 0x3b5998ff (r: 59, g: 89, b: 152, a: 255) - static let lapisLazuli = #colorLiteral(red: 0.23137255, green: 0.34901962, blue: 0.59607846, alpha: 1.0) /// 0x63666aff (r: 99, g: 102, b: 106, a: 255) static let lightGray = #colorLiteral(red: 0.3882353, green: 0.4, blue: 0.41568628, alpha: 1.0) /// 0xc90010ff (r: 201, g: 0, b: 16, a: 255) @@ -67,8 +69,6 @@ internal extension SGColor { static let mercury = #colorLiteral(red: 0.90588236, green: 0.90588236, blue: 0.90588236, alpha: 1.0) /// 0xccccccff (r: 204, g: 204, b: 204, a: 255) static let middleGray = #colorLiteral(red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0) - /// 0x757575ff (r: 117, g: 117, b: 117, a: 255) - static let oldLavender = #colorLiteral(red: 0.45882353, green: 0.45882353, blue: 0.45882353, alpha: 1.0) /// 0xf5b758ff (r: 245, g: 183, b: 88, a: 255) static let orange = #colorLiteral(red: 0.9607843, green: 0.7176471, blue: 0.34509805, alpha: 1.0) /// 0x2f353bff (r: 47, g: 53, b: 59, a: 255) @@ -107,6 +107,8 @@ internal extension SGColor { static let wheelTopLevelSeparatorColor = #colorLiteral(red: 0.19607843, green: 0.20784314, blue: 0.23137255, alpha: 1.0) /// 0xffffffff (r: 255, g: 255, b: 255, a: 255) static let white = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) + /// 0xd3d9e0ff (r: 211, g: 217, b: 224, a: 255) + static let whiteHighlighted = #colorLiteral(red: 0.827451, green: 0.8509804, blue: 0.8784314, alpha: 1.0) } } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift index 3c6d9ec9d..406964a17 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift @@ -9,13 +9,6 @@ import UIKit class BaseNynjaButton: UIButton { - - var shadowRadius: CGFloat = 8.0 { - didSet { - layer.shadowRadius = shadowRadius - layoutIfNeeded() - } - } override var isEnabled: Bool { didSet { alpha = isEnabled ? 1 : 0.5 } @@ -27,23 +20,41 @@ class BaseNynjaButton: UIButton { private static var defaultLabelHeight = CGFloat(22.adjustedByWidth) - var defaultColor: UIColor = UIColor.nynja.mainRed - var highlightedColor: UIColor = UIColor.nynja.darkRed + var textColor: UIColor = UIColor.nynja.white { + didSet { + setTitleColor(textColor, for: .normal) + setTitleColor(textColor.withAlphaComponent(0.3), for: .disabled) + } + } + + var defaultColor: UIColor = UIColor.nynja.mainRed { + didSet { + backgroundColor = defaultColor + } + } + + var highlightedColor: UIColor = UIColor.nynja.darkRed { + didSet { + if isHighlighted { + backgroundColor = highlightedColor + } + } + } + // MARK: - Init - override init(frame: CGRect) { - super.init(frame: frame) - baseSetup() + override convenience init(frame: CGRect) { + self.init(frame: frame, fontName: FontFamily.NotoSans.medium.name) } convenience init(height: CGFloat) { self.init(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 0, height: height))) } - convenience init(frame: CGRect = .zero, fontName: String, labelHeight: CGFloat = BaseNynjaButton.defaultLabelHeight) { - self.init(frame: frame) - + init(frame: CGRect = .zero, fontName: String, labelHeight: CGFloat = BaseNynjaButton.defaultLabelHeight) { + super.init(frame: frame) + baseSetup() self.titleLabel?.font = UIFont.makeFont(with: fontName, height: labelHeight) } @@ -55,13 +66,12 @@ class BaseNynjaButton: UIButton { // MARK: - Setup - private func baseSetup() { - self.backgroundColor = defaultColor + func baseSetup() { + backgroundColor = defaultColor - self.setTitleColor(UIColor.nynja.white, for: .normal) - self.setTitleColor(UIColor.nynja.white.withAlphaComponent(0.3), for: .disabled) + setTitleColor(textColor, for: .normal) + setTitleColor(textColor.withAlphaComponent(0.3), for: .disabled) - self.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: BaseNynjaButton.defaultLabelHeight) + titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: BaseNynjaButton.defaultLabelHeight) } - } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/NynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaButton.swift index 78abccc65..9838dba4e 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/NynjaButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/NynjaButton.swift @@ -8,16 +8,20 @@ import UIKit -class NynjaButton: BaseNynjaButton { +class NynjaButton: RoundNynjaButton { + + var shadowRadius: CGFloat = 8.0 { + didSet { + layer.shadowRadius = shadowRadius + layoutIfNeeded() + } + } // MARK: - Life Cycle override func layoutSubviews() { super.layoutSubviews() - adjustShadow(with: shadowRadius) - layer.cornerRadius = bounds.height / 2 } - } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift index 699f0cf38..b3dfa5c76 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift @@ -8,25 +8,13 @@ import UIKit -class NynjaCellButton: BaseNynjaButton { - - - // MARK: - Init - - override init(frame: CGRect) { - super.init(frame: frame) - baseSetup() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - baseSetup() - } - - +final class NynjaCellButton: BaseNynjaButton { + // MARK: - Setup - private func baseSetup() { + override func baseSetup() { + super.baseSetup() + contentEdgeInsets = Constraints.contentEdgeInsets.adjustedByWidth setContentHuggingPriority(.required, for: .horizontal) setContentCompressionResistancePriority(.required, for: .horizontal) diff --git a/Nynja/Library/UI/Buttons/NynjaButton/NynjaImageButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaImageButton.swift new file mode 100644 index 000000000..589671af9 --- /dev/null +++ b/Nynja/Library/UI/Buttons/NynjaButton/NynjaImageButton.swift @@ -0,0 +1,33 @@ +// +// NynjaImageButton.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class NynjaImageButton: RoundNynjaButton { + + // MARK: - Properties + + var imagePadding: CGFloat = 0 { + didSet { + setupImagePadding() + } + } + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + setupImagePadding() + } + + private func setupImagePadding() { + titleEdgeInsets.left = imagePadding + imageEdgeInsets.right = imagePadding + } +} diff --git a/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift new file mode 100644 index 000000000..f7859b08b --- /dev/null +++ b/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift @@ -0,0 +1,19 @@ +// +// RoundNynjaButton.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class RoundNynjaButton: BaseNynjaButton { + + // MARK: - Life Cycle + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = bounds.height / 2 + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 55ea875d1..f52855b66 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -12,6 +12,17 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn private let presenter: AuthPresenterProtocol private let viewsFactory: AuthViewsFactoryProtocol + private lazy var backgroundImageView: UIImageView = { + let backgroundImageView = UIImageView() + + view.addSubview(backgroundImageView) + backgroundImageView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return backgroundImageView + }() + private lazy var headerView: AuthHeaderView = viewsFactory.makeHeaderView(on: view) private lazy var scrollView: UIScrollView = viewsFactory.makeScrollView(on: view, top: headerView, bottom: bottomView) @@ -45,7 +56,7 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.nynja.backgroundColor + backgroundImageView.image = UIImage.nynja.Background.background.image _ = [headerView, scrollView, scrollContentView, bottomView] diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index 851f3a475..3b9ba7585 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -37,24 +37,15 @@ extension EmailLoginView { let nextAction: (String) -> Void } - func configure(config: EmailLoginView.Config) { - textFieldController = TextFieldController(validator: Validator()) { [weak self] (result) in - if result { - self?.nextButton.backgroundColor = UIColor.nynja.mainRed - } else { - self?.nextButton.backgroundColor = UIColor.nynja.darkRed - } - + func configure(config: Config) { + textFieldController = TextFieldController(validator: Validator()) { [weak self] result in self?.nextButton.isEnabled = result } nextAction = config.nextAction - inputField.shouldTextChanged = { [weak self] textInput, range, string in - return self?.textFieldController? - .textInput(textInput, - shouldChangeCharactersIn: range, - replacementString: string) ?? true + inputField.textChanged = { [weak self] textInput in + self?.textFieldController?.textDidChange(textInput) } inputField.returnHandler = { [weak self] textInput in @@ -79,7 +70,7 @@ private extension EmailLoginView { struct Validator { func isValid(email: String) -> Bool { let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx) return emailTest.evaluate(with: email) } @@ -99,14 +90,9 @@ private extension EmailLoginView { self.validationAction = validationAction } - func textInput(_ textInput: MaterialTextInput, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - - if let str = textInput.text as NSString? { - let resultStr = str.replacingCharacters(in: range, with: string) - validationAction(validator.isValid(email: resultStr)) - } - - return true + func textDidChange(_ textInput: MaterialTextInput) { + textInput.text.trim() + validationAction(validator.isValid(email: textInput.text)) } func textInputShouldReturn(_ textInput: MaterialTextField) -> Bool { diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index fcd17e5c4..49b38e93f 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -53,12 +53,6 @@ extension PhoneNumberLoginView { countrySelectorAction = config.countrySelectorAction nextAction = config.nextAction phoneNumberTextFieldController = TextFieldController(template: config.country.placeHolder) { [weak self] result in - if result { - self?.nextButton.backgroundColor = UIColor.nynja.mainRed - } else { - self?.nextButton.backgroundColor = UIColor.nynja.darkRed - } - self?.nextButton.isEnabled = result } diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index 9417c4d02..705acfdff 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -162,22 +162,19 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { // MARK: - Login Options View func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) view.addSubview(button) - button.backgroundColor = UIColor.nynja.white + button.imagePadding = 8 + button.defaultColor = UIColor.nynja.white + button.highlightedColor = UIColor.nynja.whiteHighlighted + button.textColor = UIColor.nynja.darkLight + button.setTitle("Log in with Google".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.oldLavender, for: .normal) button.setImage(UIImage.nynja.iconsGeneralIcGoogle.image, for: .normal) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) - - button.titleEdgeInsets.left = 8 - button.imageEdgeInsets.right = 8 - button.layer.cornerRadius = 22 - button.addTarget(target, action: selector, for: .touchUpInside) - button.snp.makeConstraints { (make) in + button.snp.makeConstraints { make in make.bottom.equalToSuperview().offset(-30) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) @@ -188,22 +185,19 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { } func makeLoginWithFacebookButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) view.addSubview(button) - button.backgroundColor = UIColor.nynja.lapisLazuli + button.imagePadding = 8 + button.defaultColor = UIColor.nynja.facebookBackground + button.highlightedColor = UIColor.nynja.facebookHighlighted + button.textColor = UIColor.nynja.white + button.setTitle("Log in with Facebook".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.white, for: .normal) button.setImage(UIImage.nynja.icFacebook.image, for: .normal) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) - - button.titleEdgeInsets.left = 8 - button.imageEdgeInsets.right = 8 - button.layer.cornerRadius = 22 - button.addTarget(target, action: selector, for: .touchUpInside) - button.snp.makeConstraints { (make) in + button.snp.makeConstraints { make in make.bottom.equalTo(bottom.snp.top).offset(-16) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) @@ -214,20 +208,15 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { } func makeSwitchLoginButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) view.addSubview(button) - button.backgroundColor = UIColor.nynja.mainRed - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 14) - - button.titleEdgeInsets.left = 8 - button.imageEdgeInsets.right = 8 - button.layer.cornerRadius = 22 + button.imagePadding = 8 + button.textColor = UIColor.nynja.white button.addTarget(target, action: selector, for: .touchUpInside) - button.snp.makeConstraints { (make) in + button.snp.makeConstraints { make in make.top.equalToSuperview().offset(30) make.bottom.equalTo(bottom.snp.top).offset(-16) make.left.equalToSuperview().offset(16) @@ -337,14 +326,11 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { } func makeEmailNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) view.addSubview(button) - button.layer.cornerRadius = 22 + button.textColor = UIColor.nynja.white button.setTitle("next".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.backgroundColor = UIColor.nynja.mainRed - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) button.isEnabled = false @@ -473,19 +459,16 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { } func makeNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) view.addSubview(button) - button.layer.cornerRadius = 22 + button.textColor = UIColor.nynja.white button.setTitle("next".localized.uppercased(), for: .normal) - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.backgroundColor = UIColor.nynja.mainRed - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - - button.isEnabled = false button.addTarget(target, action: selector, for: .touchUpInside) + button.isEnabled = false + button.snp.makeConstraints { (make) in make.height.equalTo(44) make.bottom.equalToSuperview().offset(-16) diff --git a/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift b/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift index 9f0a2a715..1f6f8eff0 100644 --- a/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift +++ b/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift @@ -26,7 +26,7 @@ final class FacebookAuthViewController: UIViewController, InitializeInjectable, private lazy var safeAreaPlaceholder: UIView = { let safeAreaPlaceholder = UIView() - safeAreaPlaceholder.backgroundColor = UIColor.nynja.lapisLazuli + safeAreaPlaceholder.backgroundColor = UIColor.nynja.facebookBackground view.addSubview(safeAreaPlaceholder) safeAreaPlaceholder.snp.makeConstraints { maker in diff --git a/Nynja/Resources/Colors.json b/Nynja/Resources/Colors.json index 72df59316..0162b355b 100644 --- a/Nynja/Resources/Colors.json +++ b/Nynja/Resources/Colors.json @@ -12,7 +12,9 @@ "darkRed": "#a4000d", "blue": "#45a5ff", "dodgerBlue": "#3891ff", - "lapisLazuli": "#3b5998", + "facebookBackground": "#3b5998", + "facebookHighlighted": "#213980", + "whiteHighlighted": "#d3d9e0", "almostBlack": "#262626", "darkLight": "#2c2e33", "selfBubleColor": "#d8d8d8", @@ -46,6 +48,5 @@ "callBackground": "#2c2e33", "separatorGrayColor": "#3f3f3f", "callGradientStart": "#2c2e33ff", - "callGradientEnd": "#2c2e3300", - "oldLavender": "#757575" + "callGradientEnd": "#2c2e3300" } -- GitLab From 44fe9f634cbf813d439199f46c3e1c80f66d94d9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 13 Nov 2018 14:28:21 +0200 Subject: [PATCH 099/138] [NY-5299] Fixed constraints and localization in Auth module. --- .../Views/Controls/UnderlinedTextField.swift | 9 + Nynja/Generated/LocalizableConstants.swift | 18 ++ .../Buttons/NynjaButton/BaseNynjaButton.swift | 12 +- .../UI/Extensions/UI/LabelExtensions.swift | 7 + .../UI/UIInterfaces/Configurable.swift | 10 +- .../AuthModule/View/AuthViewController.swift | 230 ++++++++++++++---- .../View/Subviews/AuthHeaderView.swift | 59 ++++- .../View/Subviews/EmailLoginView.swift | 1 + .../View/Subviews/LoginOptionsView.swift | 4 +- .../View/ViewsFactory/AuthViewsFactory.swift | 185 ++------------ Nynja/Resources/en.lproj/Localizable.strings | 16 +- 11 files changed, 311 insertions(+), 240 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift new file mode 100644 index 000000000..9d60ac80b --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift @@ -0,0 +1,9 @@ +// +// UnderlinedTextField.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 89fe5c09d..6c7a8c88c 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1486,6 +1486,24 @@ internal extension String { static var youWasRemoved: String { return localizable.tr("Localizable", "you_was_removed") } /// Your device appears to be rooted. The security of your app can be compromised. static var yourDeviceIsRooted: String { return localizable.tr("Localizable", "your_device_is_rooted") } + /// OR + static var authAlternativeLabel: String { return localizable.tr("Localizable", "auth.alternative_label") } + /// Email + static var authEmailPlaceholder: String { return localizable.tr("Localizable", "auth.email_placeholder") } + /// Enter your email address to receive the login code. + static var authEnterEmailAddressComment: String { return localizable.tr("Localizable", "auth.enter_email_address_comment") } + /// Please choose your country code and enter your phone number. + static var authEnterPhoneNumberComment: String { return localizable.tr("Localizable", "auth.enter_phone_number_comment") } + /// Log in with email + static var authLoginWithEmail: String { return localizable.tr("Localizable", "auth.login_with_email") } + /// Log in with Facebook + static var authLoginWithFacebook: String { return localizable.tr("Localizable", "auth.login_with_facebook") } + /// Log in with Google + static var authLoginWithGoogle: String { return localizable.tr("Localizable", "auth.login_with_google") } + /// Log in with phone number + static var authLoginWithPhoneNumber: String { return localizable.tr("Localizable", "auth.login_with_phone_number") } + /// Welcome to + static var authWelcome: String { return localizable.tr("Localizable", "auth.welcome") } } } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift index 406964a17..af231e306 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift @@ -22,8 +22,7 @@ class BaseNynjaButton: UIButton { var textColor: UIColor = UIColor.nynja.white { didSet { - setTitleColor(textColor, for: .normal) - setTitleColor(textColor.withAlphaComponent(0.3), for: .disabled) + setupTextColor() } } @@ -68,10 +67,13 @@ class BaseNynjaButton: UIButton { func baseSetup() { backgroundColor = defaultColor - - setTitleColor(textColor, for: .normal) - setTitleColor(textColor.withAlphaComponent(0.3), for: .disabled) + setupTextColor() titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: BaseNynjaButton.defaultLabelHeight) } + + private func setupTextColor() { + setTitleColor(textColor, for: .normal) + setTitleColor(textColor.withAlphaComponent(0.5), for: .disabled) + } } diff --git a/Nynja/Library/UI/Extensions/UI/LabelExtensions.swift b/Nynja/Library/UI/Extensions/UI/LabelExtensions.swift index 9380c77a7..7d00ae114 100644 --- a/Nynja/Library/UI/Extensions/UI/LabelExtensions.swift +++ b/Nynja/Library/UI/Extensions/UI/LabelExtensions.swift @@ -25,5 +25,12 @@ extension UILabel { convenience init(height: CGFloat, color: UIColor, fontName: String, textAlignment: NSTextAlignment = .left) { self.init(size: CGSize(width: 100, height: height), color: color, fontName: fontName, textAlignment: textAlignment) } + + convenience init(size: CGSize, color: UIColor, font: FontConvertible, textAlignment: NSTextAlignment = .left) { + self.init(size: size, color: color, fontName: font.name, textAlignment: textAlignment) + } + convenience init(height: CGFloat, color: UIColor, font: FontConvertible, textAlignment: NSTextAlignment = .left) { + self.init(height: height, color: color, fontName: font.name, textAlignment: textAlignment) + } } diff --git a/Nynja/Library/UI/UIInterfaces/Configurable.swift b/Nynja/Library/UI/UIInterfaces/Configurable.swift index 06c7a598d..df73b9c28 100644 --- a/Nynja/Library/UI/UIInterfaces/Configurable.swift +++ b/Nynja/Library/UI/UIInterfaces/Configurable.swift @@ -10,12 +10,16 @@ import Foundation protocol InitializeConfigurable { associatedtype Config - init(config: Config) } protocol Configurable { - associatedtype Config - + associatedtype Config = Void func configure(config: Config) } + +extension Configurable where Config == Void { + func configure() { + configure(config: ()) + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index f52855b66..611234626 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -9,9 +9,14 @@ import UIKit final class AuthViewController: UIViewController, AuthViewProtocol, InitializeInjectable, KeyboardInteractive { + private let presenter: AuthPresenterProtocol + private let viewsFactory: AuthViewsFactoryProtocol + + // MARK: - Views + private lazy var backgroundImageView: UIImageView = { let backgroundImageView = UIImageView() @@ -23,24 +28,125 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn return backgroundImageView }() - private lazy var headerView: AuthHeaderView = viewsFactory.makeHeaderView(on: view) + // MARK: Scroll Container - private lazy var scrollView: UIScrollView = viewsFactory.makeScrollView(on: view, top: headerView, bottom: bottomView) - private lazy var scrollContentView: UIView = viewsFactory.makeScrollContentView(on: scrollView, baseView: view) + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + + view.addSubview(scrollView) + scrollView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return scrollView + }() + + private lazy var contentView: UIView = { + let contentView = UIView() + + scrollView.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + maker.width.equalToSuperview() + maker.height.equalToSuperview().priority(999) + } + + return contentView + }() + + // MARK: Header + + private lazy var headerView: AuthHeaderView = { + let header = AuthHeaderView() + header.configure() + + contentView.addSubview(header) + header.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + } + + return header + }() + + // MARK: Center Content private weak var emailLoginView: EmailLoginView? private weak var phoneNumberLoginView: PhoneNumberLoginView? - private lazy var bottomView: LoginOptionsView = viewsFactory.makeBottomView( - on: view, - presenter: presenter, - showEmailLoginAction: { [weak self] animated in - self?.showEmailLogin(animated: true) - }, - showPhoneNumberLoginAction: { [weak self] animated in - self?.showPhoneNumberLogin(animated: animated) + private lazy var loginContainerView: UIView = { + let containerView = UIView() + + contentView.addSubview(containerView) + containerView.snp.makeConstraints { maker in + maker.top.equalTo(headerView.snp.bottom) + maker.left.right.equalToSuperview() + } + + return containerView + }() + + // MARK: Bottom Content + + private lazy var alternativeLabel: UILabel = { + let label = UILabel(height: 22.0, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.medium.name) + + label.setContentHuggingPriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .vertical) + + label.text = String.localizable.authAlternativeLabel + + contentView.addSubview(label) + label.snp.makeConstraints { maker in + maker.centerX.equalToSuperview() + maker.top.greaterThanOrEqualTo(loginContainerView.snp.bottom).offset(16) + maker.top.equalTo(loginContainerView.snp.bottom).offset(16).priority(.high) } - ) + + return label + }() + + private lazy var bottomView: LoginOptionsView = { + let bottomView = LoginOptionsView(viewsFactory: viewsFactory) + + bottomView.configure(config: LoginOptionsView.Config( + loginOption: presenter.loginOption, + switchLoginAction: { [unowned self] () -> LoginOption in + self.presenter.switchLoginOption() + + let loginOption = self.presenter.loginOption + + switch loginOption { + case .email: + self.showEmailLogin(animated: true) + case .phoneNumber: + self.showPhoneNumberLogin(animated: true) + default: + break + } + + return loginOption + }, + facebookLoginAction: { [weak presenter] in presenter?.loginViaFacebook() }, + googleLoginAction: { [weak presenter] in presenter?.loginViaGoogle() }) + ) + + contentView.addSubview(bottomView) + bottomView.snp.makeConstraints { maker in + maker.top.equalTo(alternativeLabel.snp.bottom).offset(32) + maker.left.right.equalToSuperview() + maker.bottom.equalToSuperview().inset(30 + UIWindow.safeAreaBottomPadding()) + } + + return bottomView + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: AuthPresenterProtocol + let viewsFactory: AuthViewsFactoryProtocol + } init(dependencies: Dependencies) { presenter = dependencies.presenter @@ -52,13 +158,23 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + + // MARK: - Appearance + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() backgroundImageView.image = UIImage.nynja.Background.background.image - _ = [headerView, scrollView, scrollContentView, bottomView] + _ = [headerView, scrollView, contentView, bottomView] showPhoneNumberLogin(animated: false) @@ -74,10 +190,6 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn super.viewDidDisappear(animated) unregisterForKeyboardNotifications() } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } } // MARK: - AuthViewProtocol @@ -92,26 +204,33 @@ extension AuthViewController { // MARK: - KeyboardInteractive extension AuthViewController { + func keyboardNotified(endFrame: CGRect) { - var bottomInset: CGFloat = 0 - - if endFrame.origin.y < UIScreen.main.bounds.size.height { - bottomInset = endFrame.height - bottomView.bounds.size.height - } - - scrollView.snp.updateConstraints { (make) in - make.bottom.equalTo(bottomView.snp.top).inset(-bottomInset) - } + scrollView.contentInset.bottom = view.bounds.height - endFrame.minY } } -// MARK: - Private +// MARK: - Animations private extension AuthViewController { + + func animateChangingViews(first: UIView?, second: UIView?) { + second?.alpha = 0 + view.layoutIfNeeded() + + UIView.animate( + withDuration: 0.25, + animations: { + first?.alpha = 0 + second?.alpha = 1 + }, completion: { _ in + first?.removeFromSuperview() + } + ) + } + func showPhoneNumberLogin(animated: Bool) { - phoneNumberLoginView = viewsFactory.makePhoneNumberLoginView(on: scrollContentView, - presenter: presenter, - country: presenter.selectedCountry) + phoneNumberLoginView = makePhoneNumberLoginView(on: loginContainerView, country: presenter.selectedCountry) if animated { animateChangingViews(first: emailLoginView, second: phoneNumberLoginView) @@ -121,7 +240,7 @@ private extension AuthViewController { } func showEmailLogin(animated: Bool) { - emailLoginView = viewsFactory.makeEmailLoginView(on: scrollContentView, presenter: presenter) + emailLoginView = makeEmailLoginView(on: loginContainerView) if animated { animateChangingViews(first: phoneNumberLoginView, second: emailLoginView) @@ -130,27 +249,38 @@ private extension AuthViewController { } } - func animateChangingViews(first: UIView?, second: UIView?) { - second?.alpha = 0 - view.layoutIfNeeded() + func makeEmailLoginView(on view: UIView) -> EmailLoginView { + let loginView = EmailLoginView(viewsFactory: viewsFactory) + view.addSubview(loginView) - UIView.animate( - withDuration: 0.25, - animations: { [weak self] in - first?.alpha = 0 - second?.alpha = 1 - self?.view.layoutIfNeeded() - }) { _ in - first?.removeFromSuperview() + loginView.configure(config: EmailLoginView.Config(nextAction: { [weak presenter] in + presenter?.loginViaEmail($0) + })) + + loginView.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + make.bottom.equalToSuperview().priority(.high) } + + return loginView } -} - -// MARK: - InitializeInjectable - -extension AuthViewController { - struct Dependencies { - let presenter: AuthPresenterProtocol - let viewsFactory: AuthViewsFactoryProtocol + + func makePhoneNumberLoginView(on view: UIView, country: Country) -> PhoneNumberLoginView { + let loginView = PhoneNumberLoginView(viewsFactory: viewsFactory) + view.addSubview(loginView) + + loginView.configure(config: PhoneNumberLoginView.Config( + country: country, + countrySelectorAction: { [weak presenter] in presenter?.selectCountry() }, + nextAction: { [weak presenter] in presenter?.loginViaPhoneNumber($0) }) + ) + + loginView.snp.makeConstraints { (make) in + make.top.left.right.equalToSuperview() + make.bottom.equalToSuperview() + } + + return loginView } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift index 59997f035..67d5cf3e0 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift @@ -9,15 +9,22 @@ import UIKit final class AuthHeaderView: UIView, Configurable { - private lazy var welcomeLabel: UILabel = viewsFactory.makeWelcomeLabel(on: self) - private lazy var logoImageView: UIImageView = viewsFactory.makeLogoImageView(on: self, top: welcomeLabel) - private let viewsFactory: AuthViewsFactoryProtocol + // MARK: - Views - init(viewsFactory: AuthViewsFactoryProtocol) { - self.viewsFactory = viewsFactory + private(set) lazy var welcomeLabel = makeWelcomeLabel(on: self) + private(set) lazy var logoImageView = makeLogoImageView(on: self, top: welcomeLabel) + + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) - super.init(frame: CGRect.zero) + welcomeLabel.setContentHuggingPriority(.required, for: .vertical) + welcomeLabel.setContentCompressionResistancePriority(.required, for: .vertical) + + _ = [welcomeLabel, logoImageView] } required init?(coder aDecoder: NSCoder) { @@ -25,13 +32,47 @@ final class AuthHeaderView: UIView, Configurable { } } +// MARK: - Layout + +private extension AuthHeaderView { + + func makeWelcomeLabel(on view: UIView) -> UILabel { + let label = UILabel.init(height: 22.0, color: UIColor.nynja.white, font: FontFamily.NotoSans.medium) + label.text = String.localizable.authWelcome + + view.addSubview(label) + label.snp.makeConstraints { maker in + maker.top.equalToSuperview().offset(70.0) + maker.centerX.equalToSuperview() + } + + return label + } + + func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { + let imageView = UIImageView() + + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage.nynja.logo2.image + + view.addSubview(imageView) + imageView.snp.makeConstraints { maker in + maker.top.equalTo(top.snp.bottom).offset(16.0) + maker.centerX.equalToSuperview() + maker.bottom.equalToSuperview().offset(-32.0) + maker.width.equalToSuperview().multipliedBy(0.45) + maker.height.equalTo(imageView.snp.width).multipliedBy(0.25) + } + + return imageView + } +} + // MARK: - Configurable extension AuthHeaderView { - typealias Config = NSNull - func configure(config: NSNull) { + func configure(config: Config) { backgroundColor = UIColor.nynja.clear - _ = [welcomeLabel, logoImageView] } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index 3b9ba7585..f3197285b 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -9,6 +9,7 @@ import UIKit final class EmailLoginView: UIView, Configurable { + private lazy var inputFieldContainer = viewsFactory.makeInputFieldContainer(on: self) private lazy var inputField = viewsFactory.makeInputField(on: inputFieldContainer) private lazy var detailsLabel = viewsFactory.makeDetailsLabel(on: self, top: inputFieldContainer) diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift index 6a124e2fd..fc9050c8c 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift @@ -87,10 +87,10 @@ private extension LoginOptionsView { func updateSwitchButton(loginOption: LoginOption) { switch loginOption { case .email: - switchLoginButton.setTitle("Log in with phone number".localized.uppercased(), for: .normal) + switchLoginButton.setTitle(String.localizable.authLoginWithPhoneNumber.uppercased(), for: .normal) switchLoginButton.setImage(UIImage.nynja.iconsGeneralIcAcceptCall.image, for: .normal) case .phoneNumber: - switchLoginButton.setTitle("Log in with email".localized.uppercased(), for: .normal) + switchLoginButton.setTitle(String.localizable.authLoginWithEmail.uppercased(), for: .normal) switchLoginButton.setImage(UIImage.nynja.iconsGeneralIcEmail.image, for: .normal) default: break diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index 705acfdff..8888eb0ff 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -10,27 +10,12 @@ import Foundation protocol AuthViewsFactoryProtocol { - // MARK: - Auth View Controller - - func makeHeaderView(on view: UIView) -> AuthHeaderView - func makeScrollView(on view: UIView, top: UIView, bottom: UIView) -> UIScrollView - func makeScrollContentView(on view: UIView, baseView: UIView) -> UIView - func makeEmailLoginView(on view: UIView, presenter: AuthPresenterProtocol) -> EmailLoginView - func makePhoneNumberLoginView(on view: UIView, presenter: AuthPresenterProtocol, country: Country) -> PhoneNumberLoginView - func makeBottomView(on view: UIView, presenter: AuthPresenterProtocol, showEmailLoginAction: @escaping (Bool) -> Void, - showPhoneNumberLoginAction: @escaping (Bool) -> Void) -> LoginOptionsView - // MARK: - Login Options View func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton func makeLoginWithFacebookButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton func makeSwitchLoginButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton - // MARK: - AuthHeaderView - - func makeWelcomeLabel(on view: UIView) -> UILabel - func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView - // MARK: - Email Login View func makeInputFieldContainer(on view: UIView) -> UIView @@ -51,114 +36,6 @@ protocol AuthViewsFactoryProtocol { final class AuthViewsFactory: AuthViewsFactoryProtocol { - // MARK: - Auth View Controller - - func makeHeaderView(on view: UIView) -> AuthHeaderView { - let header = AuthHeaderView(viewsFactory: self) - header.configure(config: NSNull()) - - view.addSubview(header) - header.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - } - - return header - } - - func makeScrollView(on view: UIView, top: UIView, bottom: UIView) -> UIScrollView { - let scrollView = UIScrollView() - view.addSubview(scrollView) - - scrollView.snp.makeConstraints { (make) in - make.left.right.equalToSuperview() - make.bottom.equalTo(bottom.snp.top) - make.top.equalTo(top.snp.bottom) - } - - return scrollView - } - - func makeScrollContentView(on view: UIView, baseView: UIView) -> UIView { - let contentView = UIView() - view.addSubview(contentView) - - contentView.backgroundColor = UIColor.nynja.clear - - contentView.snp.makeConstraints { (make) in - make.right.left.top.bottom.equalToSuperview() - make.width.equalTo(baseView.snp.width) - make.height.equalTo(baseView.snp.height) - } - - return contentView - } - - func makeEmailLoginView(on view: UIView, presenter: AuthPresenterProtocol) -> EmailLoginView { - let loginView = EmailLoginView(viewsFactory: self) - view.addSubview(loginView) - - loginView.configure(config: EmailLoginView.Config(nextAction: { [weak presenter] in - presenter?.loginViaEmail($0) - })) - - loginView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - - return loginView - } - - func makePhoneNumberLoginView(on view: UIView, presenter: AuthPresenterProtocol, country: Country) -> PhoneNumberLoginView { - let loginView = PhoneNumberLoginView(viewsFactory: self) - view.addSubview(loginView) - - loginView.configure(config: PhoneNumberLoginView.Config( - country: country, - countrySelectorAction: { [weak presenter] in presenter?.selectCountry() }, - nextAction: { [weak presenter] in presenter?.loginViaPhoneNumber($0) }) - ) - - loginView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - - return loginView - } - - func makeBottomView(on view: UIView, - presenter: AuthPresenterProtocol, - showEmailLoginAction: @escaping (Bool) -> Void, - showPhoneNumberLoginAction: @escaping (Bool) -> Void) -> LoginOptionsView { - let bottom = LoginOptionsView(viewsFactory: self) - - bottom.configure(config: LoginOptionsView.Config( - loginOption: presenter.loginOption, - switchLoginAction: { [weak self] () -> LoginOption in - presenter.switchLoginOption() - let loginOption = presenter.loginOption - - switch loginOption { - case .email: showEmailLoginAction(true) - case .phoneNumber: showPhoneNumberLoginAction(true) - default: break - } - - return loginOption - }, - facebookLoginAction: { [weak presenter] in presenter?.loginViaFacebook() }, - googleLoginAction: { [weak presenter] in presenter?.loginViaGoogle() }) - ) - - view.addSubview(bottom) - bottom.snp.makeConstraints { (make) in - make.bottom.left.right.equalToSuperview() - } - - return bottom - } - // MARK: - Login Options View func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { @@ -170,12 +47,12 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.highlightedColor = UIColor.nynja.whiteHighlighted button.textColor = UIColor.nynja.darkLight - button.setTitle("Log in with Google".localized.uppercased(), for: .normal) + button.setTitle(String.localizable.authLoginWithGoogle.uppercased(), for: .normal) button.setImage(UIImage.nynja.iconsGeneralIcGoogle.image, for: .normal) button.addTarget(target, action: selector, for: .touchUpInside) button.snp.makeConstraints { make in - make.bottom.equalToSuperview().offset(-30) + make.bottom.equalToSuperview() make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) make.height.equalTo(44) @@ -193,7 +70,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.highlightedColor = UIColor.nynja.facebookHighlighted button.textColor = UIColor.nynja.white - button.setTitle("Log in with Facebook".localized.uppercased(), for: .normal) + button.setTitle(String.localizable.authLoginWithFacebook.uppercased(), for: .normal) button.setImage(UIImage.nynja.icFacebook.image, for: .normal) button.addTarget(target, action: selector, for: .touchUpInside) @@ -217,7 +94,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.addTarget(target, action: selector, for: .touchUpInside) button.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) + make.top.equalToSuperview() make.bottom.equalTo(bottom.snp.top).offset(-16) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) @@ -226,42 +103,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { return button } - - // MARK: - AuthHeaderView - - func makeWelcomeLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white - - label.text = "Welcome to".localized - - label.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(70) - make.centerX.equalToSuperview() - } - - return label - } - - func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { - let imageView = UIImageView() - view.addSubview(imageView) - - imageView.contentMode = .scaleAspectFill - imageView.image = UIImage.nynja.logo2.image - - imageView.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom).offset(16) - make.bottom.equalToSuperview().offset(-16) - make.centerX.equalToSuperview() - make.width.equalToSuperview().multipliedBy(9/20) - } - - return imageView - } + // MARK: - Email Login View @@ -287,7 +129,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { textField.textContentType = .emailAddress textField.placeholderColor = UIColor.nynja.dustyGray - textField.placeholder = "Email".localized + textField.placeholder = String.localizable.authEmailPlaceholder textField.textColor = UIColor.nynja.white textField.font = FontFamily.NotoSans.medium.font(size: 16) @@ -311,7 +153,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { let label = UILabel() view.addSubview(label) - label.text = "Enter your email address to receive the login code.".localized + label.text = String.localizable.authEnterEmailAddressComment label.font = FontFamily.NotoSans.regular.font(size: 14) label.textColor = UIColor.nynja.dustyGray @@ -330,13 +172,13 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { view.addSubview(button) button.textColor = UIColor.nynja.white - button.setTitle("next".localized.uppercased(), for: .normal) + button.setTitle(String.localizable.next.uppercased(), for: .normal) button.isEnabled = false button.snp.makeConstraints { (make) in make.height.equalTo(44) - make.bottom.equalToSuperview().offset(-16) + make.bottom.equalToSuperview() make.top.equalTo(top.snp.bottom).offset(88) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) @@ -444,11 +286,14 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { let label = UILabel() view.addSubview(label) - label.text = "Please choose your country code and enter your phone number.".localized + label.text = String.localizable.authEnterPhoneNumberComment label.font = FontFamily.NotoSans.regular.font(size: 14) label.textColor = UIColor.nynja.dustyGray label.numberOfLines = 0 + label.setContentHuggingPriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.snp.makeConstraints { (make) in make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) @@ -463,7 +308,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { view.addSubview(button) button.textColor = UIColor.nynja.white - button.setTitle("next".localized.uppercased(), for: .normal) + button.setTitle(String.localizable.next.uppercased(), for: .normal) button.addTarget(target, action: selector, for: .touchUpInside) @@ -471,7 +316,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.snp.makeConstraints { (make) in make.height.equalTo(44) - make.bottom.equalToSuperview().offset(-16) + make.bottom.equalToSuperview() make.top.equalTo(top.snp.bottom).offset(32) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 46757f520..c2507da65 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -989,7 +989,21 @@ "ALL" = "ALL"; -// MARK: - Auth +// MARK: - Login Flow + +// MARK: Auth + +"auth.welcome" = "Welcome to"; +"auth.login_with_phone_number" = "Log in with phone number"; +"auth.login_with_email" = "Log in with email"; +"auth.login_with_facebook" = "Log in with Facebook"; +"auth.login_with_google" = "Log in with Google"; +"auth.alternative_label" = "OR"; + +"auth.enter_email_address_comment" = "Enter your email address to receive the login code."; +"auth.enter_phone_number_comment" = "Please choose your country code and enter your phone number."; + +"auth.email_placeholder" = "Email"; // MARK: Create Profile -- GitLab From 6519c1ed0bb55edc975665c2e5105df5dab8b8fb Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 13 Nov 2018 15:26:52 +0200 Subject: [PATCH 100/138] [NY-5299] Move BaseView to NynjaUIKit --- .../NynjaUIKit.xcodeproj/project.pbxproj | 32 +++++++++++++++ .../Core/Controls}/InputsCachePolicy.swift | 7 +--- .../Core/Controls/ProhibitedOptions.swift | 21 ++++++++++ .../Views/Avatar/AvatarStatusView.swift | 4 +- .../NynjaUIKit/Views/BaseView.swift | 17 ++++++-- .../ContextMenu/View/NynjaContextMenu.swift | 4 +- .../Views/Controls}/TextField.swift | 15 ------- .../Views/Typing/RecordingIndicatorView.swift | 4 +- .../Views/Typing/TypingIndicatorView.swift | 4 +- .../NynjaUIKit/Views/Typing/TypingView.swift | 4 +- .../NynjaUIKit/Views/Utils/RoundView.swift | 4 +- Nynja.xcodeproj/project.pbxproj | 30 -------------- .../PartialCheckableButton.swift | 1 + .../Lists/EmptyStateView/EmptyStateView.swift | 1 + .../Cell/ChatListMessageDetailsView.swift | 1 + .../Cell/ChatListMessageIndicatorsView.swift | 1 + .../Lists/TableView/FastScroll/PinView.swift | 2 + .../TableView/FastScroll/ScrollBar.swift | 1 + .../MentionCounter/MentionCounterView.swift | 1 + Nynja/Library/UI/SnackBar/SnackBar.swift | 1 + .../UI/TextInput/InputField/CodeField.swift | 1 + .../UI/TextInput/InputField/EditField.swift | 2 +- .../InputField/ImagePlaceholderField.swift | 1 + .../UI/TextInput/InputField/MyTextField.swift | 1 + .../InputField/NynjaSearchField.swift | 1 + .../UI/TextInput/InputField/PhoneField.swift | 1 + .../Material/Base/MaterialTextContainer.swift | 1 + .../Material/MaterialTextField.swift | 1 + .../FloatingPlaceholderContainer.swift | 1 + .../InputInfoContainer.swift | 1 + Nynja/Library/UI/View/BaseView.swift | 40 ------------------- .../UI/View/ImagesView/ImagesView.swift | 1 + .../View/NavigationView/NavigationView.swift | 2 + .../Location/PlaceDescriptionView.swift | 2 + .../View/ViewsFactory/AuthViewsFactory.swift | 2 +- .../View/InviteFriendHeaderView.swift | 1 + .../View/Calendar/NynjaCalendarView.swift | 1 + .../View/TimeView/NynjaTimeControl.swift | 1 + .../DateTimePicker/View/XDoneView.swift | 1 + .../Cell/Header/ShareNynjaHeaderView.swift | 1 + .../Main/View/ScheduleView/ScheduleView.swift | 1 + .../Cells/Views/Base/MessageContentView.swift | 2 + .../Views/Converting/ChatCellFooterView.swift | 1 + .../Views/Converting/ConvertionInfoView.swift | 1 + .../Cells/Views/FileTransferInfoView.swift | 1 + .../Views/Info/CountView/CountView.swift | 1 + .../Cells/Views/Info/InfoChannelView.swift | 1 + .../Cells/Views/Info/InfoDateView.swift | 2 + .../Cells/Views/Info/InfoView.swift | 2 + .../Cells/Views/Reply/ReplyInfoView.swift | 1 + .../Views/ReplyPreview/ReplyPreview.swift | 2 + .../Views/DateTime/TimeZoneItemView.swift | 1 + .../Views/MessageContent/AudioItemView.swift | 1 + .../Views/MessageContent/OtherItemView.swift | 2 + .../Views/MessageContent/TextItemView.swift | 1 + .../View/Views/SaveDeleteView.swift | 1 + .../View/ChangeNumberView.swift | 1 + .../View/ChangeNumberCodeView.swift | 1 + .../ViewController/StickersInputView.swift | 1 + 59 files changed, 134 insertions(+), 108 deletions(-) rename {Nynja/Library/UI/TextInput => Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls}/InputsCachePolicy.swift (87%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/ProhibitedOptions.swift rename {Nynja/Library/UI/TextInput/InputField/TextField => Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls}/TextField.swift (83%) delete mode 100644 Nynja/Library/UI/View/BaseView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 215806154..751fe8f9a 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -38,6 +38,10 @@ 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00B2181EBD2003A010F /* BaseView.swift */; }; 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */; }; 8540A019218213E2003A010F /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A018218213E2003A010F /* RoundView.swift */; }; + 855A4E86219AFA8200B6E90B /* UnderlinedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */; }; + 855A4E88219AFB0F00B6E90B /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E87219AFB0F00B6E90B /* TextField.swift */; }; + 855A4E8A219AFB9D00B6E90B /* InputsCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */; }; + 855A4E8D219AFF0300B6E90B /* ProhibitedOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */; }; 85EB37F621832D41003A2D6F /* RecordingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F521832D41003A2D6F /* RecordingIndicatorView.swift */; }; /* End PBXBuildFile section */ @@ -76,6 +80,10 @@ 8540A00B2181EBD2003A010F /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Animation.swift"; sourceTree = ""; }; 8540A018218213E2003A010F /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; + 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlinedTextField.swift; sourceTree = ""; }; + 855A4E87219AFB0F00B6E90B /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; + 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputsCachePolicy.swift; sourceTree = ""; }; + 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProhibitedOptions.swift; sourceTree = ""; }; 85EB37F521832D41003A2D6F /* RecordingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingIndicatorView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -290,6 +298,7 @@ children = ( 8540A00B2181EBD2003A010F /* BaseView.swift */, 8540A01A218213E8003A010F /* Utils */, + 855A4E84219AFA6C00B6E90B /* Controls */, 85409FFD2181C8AF003A010F /* Avatar */, 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, @@ -340,12 +349,31 @@ path = Utils; sourceTree = ""; }; + 855A4E84219AFA6C00B6E90B /* Controls */ = { + isa = PBXGroup; + children = ( + 855A4E87219AFB0F00B6E90B /* TextField.swift */, + 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */, + ); + path = Controls; + sourceTree = ""; + }; + 855A4E8B219AFEE000B6E90B /* Controls */ = { + isa = PBXGroup; + children = ( + 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */, + 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */, + ); + path = Controls; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( 8514D51920EE41AC0002378A /* Extensions */, 8514D4DD20EE2D970002378A /* Layout */, 8514D4CC20EE2D970002378A /* Collection */, + 855A4E8B219AFEE000B6E90B /* Controls */, ); path = Core; sourceTree = ""; @@ -461,13 +489,16 @@ 8514D4DF20EE2D970002378A /* InteractiveCellViewModel.swift in Sources */, 8514D51720EE40540002378A /* NynjaContextMenu.swift in Sources */, 8514D4E720EE2D970002378A /* XibInitializable.swift in Sources */, + 855A4E8A219AFB9D00B6E90B /* InputsCachePolicy.swift in Sources */, 8514D4E820EE2D970002378A /* UITableView+ViewModels.swift in Sources */, 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */, 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, + 855A4E86219AFA8200B6E90B /* UnderlinedTextField.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */, + 855A4E8D219AFF0300B6E90B /* ProhibitedOptions.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, @@ -476,6 +507,7 @@ 8540A019218213E2003A010F /* RoundView.swift in Sources */, 8514D51620EE40540002378A /* NynjaContextMenuArrowView.swift in Sources */, 8514D4E620EE2D970002378A /* Reusable.swift in Sources */, + 855A4E88219AFB0F00B6E90B /* TextField.swift in Sources */, 8514D4E320EE2D970002378A /* SupplementaryViewModel.swift in Sources */, 8514D51C20EE41E90002378A /* UIWindowExtensions.swift in Sources */, 8514D4E520EE2D970002378A /* AccessibilityConfigurable.swift in Sources */, diff --git a/Nynja/Library/UI/TextInput/InputsCachePolicy.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/InputsCachePolicy.swift similarity index 87% rename from Nynja/Library/UI/TextInput/InputsCachePolicy.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/InputsCachePolicy.swift index a29237124..10293290e 100644 --- a/Nynja/Library/UI/TextInput/InputsCachePolicy.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/InputsCachePolicy.swift @@ -8,16 +8,14 @@ import UIKit -protocol InputsCachePolicy: UITextInputTraits { - +public protocol InputsCachePolicy: UITextInputTraits { var autocorrectionType: UITextAutocorrectionType { get set } var shouldCacheInputs: Bool { get set } - } extension InputsCachePolicy { - var shouldCacheInputs: Bool { + public var shouldCacheInputs: Bool { get { return autocorrectionType != .no } @@ -29,7 +27,6 @@ extension InputsCachePolicy { } } } - } extension UITextField: InputsCachePolicy {} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/ProhibitedOptions.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/ProhibitedOptions.swift new file mode 100644 index 000000000..4495fecaa --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/ProhibitedOptions.swift @@ -0,0 +1,21 @@ +// +// ProhibitedOptions.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +/// Defines which items in clipboard should be prohibited (not visible and allowed) +public struct ProhibitedOptions: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let none = ProhibitedOptions(rawValue: 0) + public static let copy = ProhibitedOptions(rawValue: 1 << 0) + public static let paste = ProhibitedOptions(rawValue: 1 << 1) + public static let all = ProhibitedOptions(rawValue: Int.max) +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index 33ae6778e..8a6f570c7 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -48,8 +48,8 @@ public final class AvatarStatusView: BaseView { // MARK: - Setup - public override func setup() { - super.setup() + public override func baseSetup() { + super.baseSetup() backgroundColor = .clear diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift index dc6523cbb..35ef92e8f 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift @@ -8,25 +8,34 @@ import UIKit -public class BaseView: UIView { +open class BaseView: UIView { + + open var activatedViews: [UIView] { + return [] + } + // MARK: - Init public override init(frame: CGRect) { super.init(frame: frame) - setup() + baseSetup() } public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - setup() + baseSetup() } // MARK: - Setup - public func setup() { + open func baseSetup() { + activatedViews.activate() // should be implemented in childs } } +private extension Array where Element == UIView { + func activate() { } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift index 23e4a01d3..56c394e4c 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift @@ -101,8 +101,8 @@ public final class NynjaContextMenu: BaseView { // MARK: - Setup - public override func setup() { - super.setup() + public override func baseSetup() { + super.baseSetup() clipsToBounds = true contentView.layer.cornerRadius = cornerRadius contentView.clipsToBounds = true diff --git a/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextField.swift similarity index 83% rename from Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextField.swift index 56b144fad..74932f026 100644 --- a/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextField.swift @@ -8,20 +8,6 @@ import UIKit -/// Defines which items in clipboard should be prohibited (not visible and allowed) -public struct ProhibitedOptions: OptionSet { - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - static let none = ProhibitedOptions(rawValue: 0) - static let copy = ProhibitedOptions(rawValue: 1 << 0) - static let paste = ProhibitedOptions(rawValue: 1 << 1) - static let all: ProhibitedOptions = ProhibitedOptions(rawValue: Int.max) -} - open class TextField: UITextField { /// Determines which options are prohibited @@ -114,5 +100,4 @@ open class TextField: UITextField { selectedTextRange = textRange(from: beginningOfDocument, to: beginningOfDocument) didResetHandler?() } - } diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift index 30aadaa93..96f8d786e 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift @@ -41,8 +41,8 @@ public final class RecordingIndicatorView: BaseView { // MARK: - Setup - public override func setup() { - super.setup() + public override func baseSetup() { + super.baseSetup() backgroundColor = .clear indicatorView.backgroundColor = backgroundColor diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift index a3946fc2d..e0a8a5503 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift @@ -58,8 +58,8 @@ public final class TypingIndicatorView: BaseView { // MARK: - Setup - public override func setup() { - super.setup() + public override func baseSetup() { + super.baseSetup() animationLayer.addSublayer(itemLayer) animationLayer.masksToBounds = true setupColor() diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 01ec22cda..2bb7e2080 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -56,8 +56,8 @@ public final class TypingView: BaseView { // MARK: - Setup - public override func setup() { - super.setup() + public override func baseSetup() { + super.baseSetup() indicatorContainer.snp.makeConstraints { maker in maker.top.bottom.left.equalToSuperview() diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift index ecab2ee22..1ea96fd8e 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift @@ -10,8 +10,8 @@ import UIKit public class RoundView: BaseView { - public override func setup() { - super.setup() + public override func baseSetup() { + super.baseSetup() layer.masksToBounds = true } diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index c05b2eba6..3cd75bfb0 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1575,7 +1575,6 @@ A43B25A120AB1DFA00FF8107 /* EditFieldLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258420AB1DFA00FF8107 /* EditFieldLayout.swift */; }; A43B25A220AB1DFA00FF8107 /* EditField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258520AB1DFA00FF8107 /* EditField.swift */; }; A43B25A320AB1DFA00FF8107 /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258620AB1DFA00FF8107 /* Country.swift */; }; - A43B25A420AB1DFA00FF8107 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258720AB1DFA00FF8107 /* TextField.swift */; }; A43B25A520AB1DFA00FF8107 /* DrawableAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */; }; A43B25A620AB1DFA00FF8107 /* RecordingAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258920AB1DFA00FF8107 /* RecordingAudioWaveform.swift */; }; A43B25A720AB1DFA00FF8107 /* ALKeyboardObservingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258B20AB1DFA00FF8107 /* ALKeyboardObservingView.swift */; }; @@ -1589,7 +1588,6 @@ A43B25B120AB1E1B00FF8107 /* NynjaSearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B257F20AB1DFA00FF8107 /* NynjaSearchField.swift */; }; A43B25B220AB1E2600FF8107 /* ImagePlaceholderField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B259320AB1DFA00FF8107 /* ImagePlaceholderField.swift */; }; A43B25B320AB1E2E00FF8107 /* DrawableAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */; }; - A43B25B420AB1E3C00FF8107 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258720AB1DFA00FF8107 /* TextField.swift */; }; A43B25B920AB1E7600FF8107 /* String+Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B25B620AB1E7600FF8107 /* String+Range.swift */; }; A43B25BA20AB1E7600FF8107 /* String+Links.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B25B720AB1E7600FF8107 /* String+Links.swift */; }; A43B25BB20AB1E7600FF8107 /* String+LocationURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B25B820AB1E7600FF8107 /* String+LocationURL.swift */; }; @@ -1699,8 +1697,6 @@ A45F116120B422AF00F45004 /* Message+System.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F115D20B422AF00F45004 /* Message+System.swift */; }; A45F59AB205825FC00EAA780 /* RosterDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F59AA205825FC00EAA780 /* RosterDAOProtocol.swift */; }; A45F59AD2058263F00EAA780 /* RosterDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F59AC2058263F00EAA780 /* RosterDAO.swift */; }; - A460324F2105C9A1009783DA /* InputsCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A460324E2105C9A1009783DA /* InputsCachePolicy.swift */; }; - A46032502105D357009783DA /* InputsCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A460324E2105C9A1009783DA /* InputsCachePolicy.swift */; }; A46032522105D3E1009783DA /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46032512105D3E1009783DA /* TextView.swift */; }; A4626EAF20D96EF9000F37EE /* MainViewController+Gallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4626EAE20D96EF9000F37EE /* MainViewController+Gallery.swift */; }; A4626EB320D96FAE000F37EE /* TopLevelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4626EB220D96FAE000F37EE /* TopLevelInfo.swift */; }; @@ -1732,7 +1728,6 @@ A46C36342121999100172773 /* DDMechanism.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46C36332121999100172773 /* DDMechanism.swift */; }; A46C363721219A9000172773 /* DDSysctlMechanism.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46C363621219A9000172773 /* DDSysctlMechanism.swift */; }; A46CF04321147BAE0072F185 /* HistoryRequestModelTypeRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46CF04221147BAE0072F185 /* HistoryRequestModelTypeRepresentable.swift */; }; - A47785A220D18D4A0053E0D2 /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70402BC1FF6972B00182D81 /* BaseView.swift */; }; A47785A420D286680053E0D2 /* ChannelChatItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47785A320D286680053E0D2 /* ChannelChatItemsFactory.swift */; }; A477CE7D2061236800081D34 /* MessageLinkDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A477CE7C2061236800081D34 /* MessageLinkDAOProtocol.swift */; }; A477CE8420613A5A00081D34 /* StarMessageDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = A477CE8320613A5A00081D34 /* StarMessageDAO.swift */; }; @@ -1773,9 +1768,7 @@ A4A242492060373A00B0A804 /* BaseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A242472060373000B0A804 /* BaseHandler.swift */; }; A4A2424F2060390000B0A804 /* Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A2424E2060390000B0A804 /* Handlers.swift */; }; A4AB8E512105EB60005F9B0C /* TextFieldTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AB8E502105EB60005F9B0C /* TextFieldTest.swift */; }; - A4AB8E522105EC46005F9B0C /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258720AB1DFA00FF8107 /* TextField.swift */; }; A4AB8E532105EC4B005F9B0C /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46032512105D3E1009783DA /* TextView.swift */; }; - A4AB8E542105EC9A005F9B0C /* InputsCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A460324E2105C9A1009783DA /* InputsCachePolicy.swift */; }; A4AB8E562105ECD7005F9B0C /* TextViewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AB8E552105ECD7005F9B0C /* TextViewTest.swift */; }; A4AB8E582105F47C005F9B0C /* InputsCachePolicyTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AB8E572105F47C005F9B0C /* InputsCachePolicyTest.swift */; }; A4B32EB5207E89A4009838C8 /* PlaceDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B32EB4207E89A4009838C8 /* PlaceDescriptionView.swift */; }; @@ -1963,7 +1956,6 @@ E70189BB1F9107AD00CA7005 /* ProximitySensorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70189BA1F9107AD00CA7005 /* ProximitySensorManager.swift */; }; E701A27D1FB33E1700D995C3 /* ParticipantsActionsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E701A27C1FB33E1600D995C3 /* ParticipantsActionsDelegate.swift */; }; E701A27F1FB36B3500D995C3 /* Array+Participant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E701A27E1FB36B3500D995C3 /* Array+Participant.swift */; }; - E70402BD1FF6972B00182D81 /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70402BC1FF6972B00182D81 /* BaseView.swift */; }; E707C4AF1FA0F6E700B86137 /* ProfileActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E707C4AE1FA0F6E700B86137 /* ProfileActionCell.swift */; }; E70938371FBEDA2B006CCDC6 /* ProfileTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70938361FBEDA2B006CCDC6 /* ProfileTable.swift */; }; E709383D1FBEE176006CCDC6 /* ServiceTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E709383C1FBEE176006CCDC6 /* ServiceTable.swift */; }; @@ -3739,7 +3731,6 @@ A43B258420AB1DFA00FF8107 /* EditFieldLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditFieldLayout.swift; sourceTree = ""; }; A43B258520AB1DFA00FF8107 /* EditField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditField.swift; sourceTree = ""; }; A43B258620AB1DFA00FF8107 /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; - A43B258720AB1DFA00FF8107 /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawableAudioWaveform.swift; sourceTree = ""; }; A43B258920AB1DFA00FF8107 /* RecordingAudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingAudioWaveform.swift; sourceTree = ""; }; A43B258B20AB1DFA00FF8107 /* ALKeyboardObservingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ALKeyboardObservingView.swift; sourceTree = ""; }; @@ -3848,7 +3839,6 @@ A45F115D20B422AF00F45004 /* Message+System.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Message+System.swift"; sourceTree = ""; }; A45F59AA205825FC00EAA780 /* RosterDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RosterDAOProtocol.swift; sourceTree = ""; }; A45F59AC2058263F00EAA780 /* RosterDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RosterDAO.swift; sourceTree = ""; }; - A460324E2105C9A1009783DA /* InputsCachePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputsCachePolicy.swift; sourceTree = ""; }; A46032512105D3E1009783DA /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; A4626EAE20D96EF9000F37EE /* MainViewController+Gallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+Gallery.swift"; sourceTree = ""; }; A4626EB220D96FAE000F37EE /* TopLevelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopLevelInfo.swift; sourceTree = ""; }; @@ -4091,7 +4081,6 @@ E70189BA1F9107AD00CA7005 /* ProximitySensorManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximitySensorManager.swift; path = Services/ProximitySensorManager.swift; sourceTree = ""; }; E701A27C1FB33E1600D995C3 /* ParticipantsActionsDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsActionsDelegate.swift; sourceTree = ""; }; E701A27E1FB36B3500D995C3 /* Array+Participant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Participant.swift"; sourceTree = ""; }; - E70402BC1FF6972B00182D81 /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; E707C4AE1FA0F6E700B86137 /* ProfileActionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionCell.swift; sourceTree = ""; }; E70938361FBEDA2B006CCDC6 /* ProfileTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTable.swift; sourceTree = ""; }; E709383C1FBEE176006CCDC6 /* ServiceTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceTable.swift; sourceTree = ""; }; @@ -10624,7 +10613,6 @@ A43B257120AB1DFA00FF8107 /* TextInput */ = { isa = PBXGroup; children = ( - A460324E2105C9A1009783DA /* InputsCachePolicy.swift */, 8580BAE320BD99DC00239D9D /* UITextInput+Cursor.swift */, A432CF0320B4347C00993AFB /* Material */, A43B257220AB1DFA00FF8107 /* InputBar */, @@ -10679,7 +10667,6 @@ A43B257C20AB1DFA00FF8107 /* InputField */ = { isa = PBXGroup; children = ( - A460324B2105C2CE009783DA /* TextField */, A4679BB620B305360021FE9C /* LinkField */, A43B257D20AB1DFA00FF8107 /* CodeField.swift */, A43B257F20AB1DFA00FF8107 /* NynjaSearchField.swift */, @@ -11171,14 +11158,6 @@ name = Roster; sourceTree = ""; }; - A460324B2105C2CE009783DA /* TextField */ = { - isa = PBXGroup; - children = ( - A43B258720AB1DFA00FF8107 /* TextField.swift */, - ); - path = TextField; - sourceTree = ""; - }; A46679EF20F10B2B00DBC6B4 /* RequestModelFactory */ = { isa = PBXGroup; children = ( @@ -12879,7 +12858,6 @@ 4B8BEDDF2049798C00C7D625 /* ImagesView */, E74597761FA2226900D3C88C /* NavigationView */, E79117891F97874D00462D68 /* GradientView.swift */, - E70402BC1FF6972B00182D81 /* BaseView.swift */, F11DF05E20BD93FB00F3E005 /* UIViewExtensions.swift */, ); path = View; @@ -15199,7 +15177,6 @@ 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */, A42CE57C20692EDB000889CC /* io.swift in Sources */, 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */, - A47785A220D18D4A0053E0D2 /* BaseView.swift in Sources */, A42CE58020692EDB000889CC /* error2.swift in Sources */, 8509FC8A2159095900734D93 /* AppGroupFlagContainer.swift in Sources */, 4B5A0B84216F4A61002C4160 /* AttachmentTransformer.swift in Sources */, @@ -15214,7 +15191,6 @@ A42CE5AA20692EDB000889CC /* Decoder.swift in Sources */, 853E595520D6B217007799B9 /* Desc+Room.swift in Sources */, 359EB23B1F9A1BC700147437 /* ReachabilityService.swift in Sources */, - A46032502105D357009783DA /* InputsCachePolicy.swift in Sources */, 852003FB20D45B47007C0036 /* BertBinConvertible.swift in Sources */, A42CE5EA20692EDB000889CC /* operation_Spec.swift in Sources */, A42CE61020692EDB000889CC /* process_Spec.swift in Sources */, @@ -15405,7 +15381,6 @@ 853E595420D6B214007799B9 /* Desc+Messages.swift in Sources */, A42CE5CC20692EDB000889CC /* Member_Spec.swift in Sources */, A4E6D14C208F043400519472 /* GApiResponse.swift in Sources */, - A43B25B420AB1E3C00FF8107 /* TextField.swift in Sources */, A42CE5BA20692EDB000889CC /* boundaryEvent_Spec.swift in Sources */, A42CE54C20692EDB000889CC /* Desc.swift in Sources */, A42CE5F220692EDB000889CC /* chain_Spec.swift in Sources */, @@ -16524,7 +16499,6 @@ B77C11E92109254800CCB42E /* InterpretationTypeInteractor.swift in Sources */, 26DCB23E2064B9A7001EF0AB /* ContactsWireframe.swift in Sources */, 853E594F20D6AED2007799B9 /* Desc+Messages.swift in Sources */, - A460324F2105C9A1009783DA /* InputsCachePolicy.swift in Sources */, 855AC534208E441500DC2335 /* StickersInputProtocols.swift in Sources */, 266F04CB2015050400B97A83 /* DBStarMessage.swift in Sources */, 1D31D13E6E53E71F8279C55C /* HistoryProtocols.swift in Sources */, @@ -16659,7 +16633,6 @@ 4B6D20EF2164D4AB003ADB29 /* ConvertionMessageModel.swift in Sources */, 85CB25DA20D723B900D5E565 /* StickerPackDAO.swift in Sources */, 2603068D20FFF9CA00C10DD9 /* MessageCallView.swift in Sources */, - E70402BD1FF6972B00182D81 /* BaseView.swift in Sources */, A45F59AD2058263F00EAA780 /* RosterDAO.swift in Sources */, FEA655DB2167777E00B44029 /* WalletDetailsWireFrame.swift in Sources */, 4C5EEA13EBC6A8398F08DCD1 /* MainWireframe.swift in Sources */, @@ -17001,7 +16974,6 @@ 2910A0129CA29C35161DD692 /* EditPhotoInteractor.swift in Sources */, 705B483A1FCDEA2273CEFE2C /* EditPhotoWireframe.swift in Sources */, 5EEB73D82162227B00D8ECE6 /* PhoneNumberLoginView.swift in Sources */, - A43B25A420AB1DFA00FF8107 /* TextField.swift in Sources */, E743B58F1FB0A32700F72F92 /* ParticipantsHeaderView.swift in Sources */, A4679BAA20B2DD100021FE9C /* SubscribersTableDataSource.swift in Sources */, 26770A571FFD6CAC009AC870 /* SharedParameters.swift in Sources */, @@ -17350,7 +17322,6 @@ 4BF090B621635B3000DCCA5C /* LogServiceStub.swift in Sources */, 4B8C05952164A9D60034D8F3 /* ChatCellModelMock.swift in Sources */, 85458D01212D7C1A00BA8814 /* StringAtomExtension.swift in Sources */, - A4AB8E522105EC46005F9B0C /* TextField.swift in Sources */, 85458D00212D7C0C00BA8814 /* Int+AnyObject.swift in Sources */, 4B6D20F02164D4B0003ADB29 /* ChatCellModelType.swift in Sources */, FB816EF820B5B89700093DCD /* StringAtom.swift in Sources */, @@ -17383,7 +17354,6 @@ 4BF2C3E42188BABC00E59F6C /* UIDeviceExtension.swift in Sources */, FB816EF320B5B85900093DCD /* Contact.swift in Sources */, 4BF090C621635F0200DCCA5C /* Message+LinkedId.swift in Sources */, - A4AB8E542105EC9A005F9B0C /* InputsCachePolicy.swift in Sources */, 85458CDB212D6FFE00BA8814 /* String+Split.swift in Sources */, A4AB8E532105EC4B005F9B0C /* TextView.swift in Sources */, FB816EFA20B5B8B000093DCD /* Member.swift in Sources */, diff --git a/Nynja/Library/UI/Buttons/PartialCheckableButton/PartialCheckableButton.swift b/Nynja/Library/UI/Buttons/PartialCheckableButton/PartialCheckableButton.swift index 670020688..cc9e65ccc 100644 --- a/Nynja/Library/UI/Buttons/PartialCheckableButton/PartialCheckableButton.swift +++ b/Nynja/Library/UI/Buttons/PartialCheckableButton/PartialCheckableButton.swift @@ -8,6 +8,7 @@ // import UIKit +import NynjaUIKit class PartialCheckableButton: BaseView { diff --git a/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift b/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift index 97a8adaa4..ea7d85c82 100644 --- a/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift +++ b/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit class EmptyStateView: BaseView { typealias ActionHandler = () -> Void diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageDetailsView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageDetailsView.swift index 5dbfa3eda..e9b983d66 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageDetailsView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageDetailsView.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageDetailsView: BaseView { diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift index a4883c26e..d3d20588f 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageIndicatorsView: BaseView { diff --git a/Nynja/Library/UI/Lists/TableView/FastScroll/PinView.swift b/Nynja/Library/UI/Lists/TableView/FastScroll/PinView.swift index e3a19ec9b..23411fed7 100644 --- a/Nynja/Library/UI/Lists/TableView/FastScroll/PinView.swift +++ b/Nynja/Library/UI/Lists/TableView/FastScroll/PinView.swift @@ -6,6 +6,8 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class PinView: BaseView { // MARK: Views diff --git a/Nynja/Library/UI/Lists/TableView/FastScroll/ScrollBar.swift b/Nynja/Library/UI/Lists/TableView/FastScroll/ScrollBar.swift index 399300621..a37b44d93 100644 --- a/Nynja/Library/UI/Lists/TableView/FastScroll/ScrollBar.swift +++ b/Nynja/Library/UI/Lists/TableView/FastScroll/ScrollBar.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit fileprivate let _scrollBarWidth: CGFloat = CGFloat(22.0.adjustedByWidth) fileprivate let _sliderSize = CGSize(width: _scrollBarWidth, height: CGFloat(54.0.adjustedByWidth)) diff --git a/Nynja/Library/UI/MentionCounter/MentionCounterView.swift b/Nynja/Library/UI/MentionCounter/MentionCounterView.swift index d74a4859d..965286c06 100644 --- a/Nynja/Library/UI/MentionCounter/MentionCounterView.swift +++ b/Nynja/Library/UI/MentionCounter/MentionCounterView.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit final class MentionCounterView: BaseView { diff --git a/Nynja/Library/UI/SnackBar/SnackBar.swift b/Nynja/Library/UI/SnackBar/SnackBar.swift index 441d5b68a..63dc096d3 100644 --- a/Nynja/Library/UI/SnackBar/SnackBar.swift +++ b/Nynja/Library/UI/SnackBar/SnackBar.swift @@ -8,6 +8,7 @@ import Foundation import MulticastDelegateSwift +import NynjaUIKit protocol SnackBarLayoutDelegate: class { func snackBar(_ snackBar: SnackBar, diff --git a/Nynja/Library/UI/TextInput/InputField/CodeField.swift b/Nynja/Library/UI/TextInput/InputField/CodeField.swift index e52fd8c16..f23f874e8 100644 --- a/Nynja/Library/UI/TextInput/InputField/CodeField.swift +++ b/Nynja/Library/UI/TextInput/InputField/CodeField.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import NynjaUIKit protocol CodeFieldDelegate: class { func codeEntered(code: String?) diff --git a/Nynja/Library/UI/TextInput/InputField/EditField.swift b/Nynja/Library/UI/TextInput/InputField/EditField.swift index 340e9c0c2..9d74aeaf9 100644 --- a/Nynja/Library/UI/TextInput/InputField/EditField.swift +++ b/Nynja/Library/UI/TextInput/InputField/EditField.swift @@ -7,7 +7,7 @@ // import UIKit - +import NynjaUIKit class EditField: BaseInputView, UITextFieldDelegate { diff --git a/Nynja/Library/UI/TextInput/InputField/ImagePlaceholderField.swift b/Nynja/Library/UI/TextInput/InputField/ImagePlaceholderField.swift index 576ffba5c..33f4c4923 100644 --- a/Nynja/Library/UI/TextInput/InputField/ImagePlaceholderField.swift +++ b/Nynja/Library/UI/TextInput/InputField/ImagePlaceholderField.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit fileprivate enum Side { case left diff --git a/Nynja/Library/UI/TextInput/InputField/MyTextField.swift b/Nynja/Library/UI/TextInput/InputField/MyTextField.swift index d29d87654..2c1ea0c69 100644 --- a/Nynja/Library/UI/TextInput/InputField/MyTextField.swift +++ b/Nynja/Library/UI/TextInput/InputField/MyTextField.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit class MyTextField: TextField { diff --git a/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift b/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift index f8e36ee99..075f26218 100644 --- a/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift +++ b/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit class NynjaSearchField: ImagePlaceholderField { diff --git a/Nynja/Library/UI/TextInput/InputField/PhoneField.swift b/Nynja/Library/UI/TextInput/InputField/PhoneField.swift index 4564c66ab..155f11efc 100644 --- a/Nynja/Library/UI/TextInput/InputField/PhoneField.swift +++ b/Nynja/Library/UI/TextInput/InputField/PhoneField.swift @@ -8,6 +8,7 @@ import UIKit import libPhoneNumber_iOS +import NynjaUIKit protocol PhoneFieldDelegate: class { func phoneChanged(text: String) -> Bool diff --git a/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift index 917623cf6..562696ec9 100644 --- a/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift +++ b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit fileprivate let defaultConfig = NynjaMTIConfig() diff --git a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift index c00a53aff..98d13a570 100644 --- a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift +++ b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit class MaterialTextField: MaterialTextContainer { diff --git a/Nynja/Library/UI/TextInput/Material/Private/FloatingPlaceholderContainer/FloatingPlaceholderContainer.swift b/Nynja/Library/UI/TextInput/Material/Private/FloatingPlaceholderContainer/FloatingPlaceholderContainer.swift index cafe631aa..5e9012000 100644 --- a/Nynja/Library/UI/TextInput/Material/Private/FloatingPlaceholderContainer/FloatingPlaceholderContainer.swift +++ b/Nynja/Library/UI/TextInput/Material/Private/FloatingPlaceholderContainer/FloatingPlaceholderContainer.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit fileprivate let collapsedFontSize: CGFloat = 14.0 diff --git a/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfoContainer.swift b/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfoContainer.swift index 37a2d77ec..c10ca6aed 100644 --- a/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfoContainer.swift +++ b/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfoContainer.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit fileprivate let _separatorHeight: CGFloat = 1.0 diff --git a/Nynja/Library/UI/View/BaseView.swift b/Nynja/Library/UI/View/BaseView.swift deleted file mode 100644 index 55360529e..000000000 --- a/Nynja/Library/UI/View/BaseView.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// BaseView.swift -// Nynja -// -// Created by Volodymyr Hryhoriev on 12/29/17. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class BaseView: UIView { - - var activatedViews: [UIView] { - return [] - } - - // MARK: - Init - override init(frame: CGRect) { - super.init(frame: frame) - baseSetup() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - baseSetup() - } - - // MARK: - Base Setup - func baseSetup() { - activatedViews.activate() - // NOTE: implement in subclasse if needed - } - -} - -extension Array where Element == UIView { - - func activate() {} - -} diff --git a/Nynja/Library/UI/View/ImagesView/ImagesView.swift b/Nynja/Library/UI/View/ImagesView/ImagesView.swift index 78aecf9c3..f9914228d 100644 --- a/Nynja/Library/UI/View/ImagesView/ImagesView.swift +++ b/Nynja/Library/UI/View/ImagesView/ImagesView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit fileprivate let _spacing: Double = 4.0 fileprivate let _borderWidth: CGFloat = 2.0 diff --git a/Nynja/Library/UI/View/NavigationView/NavigationView.swift b/Nynja/Library/UI/View/NavigationView/NavigationView.swift index 77e2bf686..a523b3c45 100644 --- a/Nynja/Library/UI/View/NavigationView/NavigationView.swift +++ b/Nynja/Library/UI/View/NavigationView/NavigationView.swift @@ -5,7 +5,9 @@ // Created by Volodymyr Hryhoriev on 10/26/17. // Copyright © 2017 TecSynt Solutions. All rights reserved. // + import UIKit +import NynjaUIKit class NavigationView: BaseView, Configurable { static let calculatedHeight = CGFloat(LogoImageViewLayout.heightWithInsets) + TitleLabelLayout.heightWithInsets diff --git a/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/Location/PlaceDescriptionView.swift b/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/Location/PlaceDescriptionView.swift index 83754c4b4..7507bb5df 100644 --- a/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/Location/PlaceDescriptionView.swift +++ b/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/Location/PlaceDescriptionView.swift @@ -6,6 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class PlaceDescriptionView: BaseView { private let height = CGFloat(17.adjustedByWidth) diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index 8888eb0ff..84dc5c091 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -317,7 +317,7 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.snp.makeConstraints { (make) in make.height.equalTo(44) make.bottom.equalToSuperview() - make.top.equalTo(top.snp.bottom).offset(32) + make.top.equalTo(top.snp.bottom).offset(24) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) } diff --git a/Nynja/Modules/Contacts/View/InviteFriendHeaderView.swift b/Nynja/Modules/Contacts/View/InviteFriendHeaderView.swift index 465b4c4a0..831e5d21d 100644 --- a/Nynja/Modules/Contacts/View/InviteFriendHeaderView.swift +++ b/Nynja/Modules/Contacts/View/InviteFriendHeaderView.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit class InviteFriendHeaderView: BaseView { diff --git a/Nynja/Modules/DateTimePicker/View/Calendar/NynjaCalendarView.swift b/Nynja/Modules/DateTimePicker/View/Calendar/NynjaCalendarView.swift index c8cc88e56..877ffc758 100644 --- a/Nynja/Modules/DateTimePicker/View/Calendar/NynjaCalendarView.swift +++ b/Nynja/Modules/DateTimePicker/View/Calendar/NynjaCalendarView.swift @@ -8,6 +8,7 @@ import UIKit import JTAppleCalendar +import NynjaUIKit protocol NynjaCalendarViewDelegate : class { func didDateSelect(date : Date) diff --git a/Nynja/Modules/DateTimePicker/View/TimeView/NynjaTimeControl.swift b/Nynja/Modules/DateTimePicker/View/TimeView/NynjaTimeControl.swift index 0c072f526..cb57dddc5 100644 --- a/Nynja/Modules/DateTimePicker/View/TimeView/NynjaTimeControl.swift +++ b/Nynja/Modules/DateTimePicker/View/TimeView/NynjaTimeControl.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit protocol NynjaTimeControlDelegate : class { func didTimeSelect(seconds : Int) diff --git a/Nynja/Modules/DateTimePicker/View/XDoneView.swift b/Nynja/Modules/DateTimePicker/View/XDoneView.swift index 6309d4b21..c4adb2abb 100644 --- a/Nynja/Modules/DateTimePicker/View/XDoneView.swift +++ b/Nynja/Modules/DateTimePicker/View/XDoneView.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit protocol XDoneViewDelegate : class { func xDoneViewXTapped() diff --git a/Nynja/Modules/InviteFriends/View/Cell/Header/ShareNynjaHeaderView.swift b/Nynja/Modules/InviteFriends/View/Cell/Header/ShareNynjaHeaderView.swift index 33400098f..8cbdc88ac 100644 --- a/Nynja/Modules/InviteFriends/View/Cell/Header/ShareNynjaHeaderView.swift +++ b/Nynja/Modules/InviteFriends/View/Cell/Header/ShareNynjaHeaderView.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit class ShareNynjaHeaderView: BaseView { diff --git a/Nynja/Modules/Main/View/ScheduleView/ScheduleView.swift b/Nynja/Modules/Main/View/ScheduleView/ScheduleView.swift index bc49da473..12c577703 100644 --- a/Nynja/Modules/Main/View/ScheduleView/ScheduleView.swift +++ b/Nynja/Modules/Main/View/ScheduleView/ScheduleView.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit class ScheduleView: BaseView { diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Base/MessageContentView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Base/MessageContentView.swift index 60ec6e6c1..ecba9dec4 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Base/MessageContentView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Base/MessageContentView.swift @@ -6,6 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class MessageContentView: BaseView, BubbleInjectible { weak var contentAppearance: MessageContentAppearance? diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ChatCellFooterView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ChatCellFooterView.swift index 0a365e53d..3a1130dc5 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ChatCellFooterView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ChatCellFooterView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit final class ChatCellFooterView: BaseView { diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ConvertionInfoView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ConvertionInfoView.swift index 39d0530fc..433400aba 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ConvertionInfoView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Converting/ConvertionInfoView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit final class ConvertionInfoView: BaseView { diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/FileTransferInfoView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/FileTransferInfoView.swift index 25aee671f..ab221ee63 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/FileTransferInfoView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/FileTransferInfoView.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit class FileTransferInfoView: BaseView { diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/CountView/CountView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/CountView/CountView.swift index 644566615..d4be93976 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/CountView/CountView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/CountView/CountView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit final class CountView: BaseView { typealias ActionHandler = () -> Void diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoChannelView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoChannelView.swift index 059ee84d7..6cf1858ee 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoChannelView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoChannelView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit class InfoChannelView: BaseView, InfoInjectible { diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoDateView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoDateView.swift index b9a02c8d8..c1a7bf2c0 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoDateView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoDateView.swift @@ -6,6 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class InfoDateView: BaseView, InfoInjectible { // MARK: - Subviews diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoView.swift index b15d94a3b..f0068cc95 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Info/InfoView.swift @@ -6,6 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class InfoView: BaseView, InfoInjectible { private let textColor = InfoInjectableConstants.Appearance.textColor diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Reply/ReplyInfoView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Reply/ReplyInfoView.swift index 8d971ad2b..8cc6d37ac 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Reply/ReplyInfoView.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Reply/ReplyInfoView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit protocol ReplyInfoViewDelegate: class { func calculateLabelSize(with text: String, font: UIFont) -> CGSize diff --git a/Nynja/Modules/Message/View/Views/ReplyPreview/ReplyPreview.swift b/Nynja/Modules/Message/View/Views/ReplyPreview/ReplyPreview.swift index 15ea3399e..61c88c584 100644 --- a/Nynja/Modules/Message/View/Views/ReplyPreview/ReplyPreview.swift +++ b/Nynja/Modules/Message/View/Views/ReplyPreview/ReplyPreview.swift @@ -6,6 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + protocol ReplyPreviewDelegate: class { func didCloseTapped(_ replyPreview: ReplyPreview) func didPreviewContentTapped(_ replyPreview: ReplyPreview) diff --git a/Nynja/Modules/ScheduleMessage/View/Views/DateTime/TimeZoneItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/DateTime/TimeZoneItemView.swift index da57fb00a..25e7601b7 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/DateTime/TimeZoneItemView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/DateTime/TimeZoneItemView.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit class TimeZoneItemView: BaseView { diff --git a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift index d9dd77e7b..65fafbb7b 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit struct AudioItemModel { let url: URL diff --git a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/OtherItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/OtherItemView.swift index 2233183eb..2357f1222 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/OtherItemView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/OtherItemView.swift @@ -6,6 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + struct OtherItemModel { let image: UIImage? let description: String diff --git a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift index c4d0115ca..bfe3eedec 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit struct TextItemModel { var text: String diff --git a/Nynja/Modules/ScheduleMessage/View/Views/SaveDeleteView.swift b/Nynja/Modules/ScheduleMessage/View/Views/SaveDeleteView.swift index 03c6d3292..467485105 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/SaveDeleteView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/SaveDeleteView.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit protocol SaveDeleteViewDelegate : class { func saveDeleteViewDeleteTapped() diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift index dee6e46f8..dbc960588 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit class ChangeNumberView: UIView, UserSettingsRespondable, UITextFieldDelegate, TestableViewProtocol { var countryModel: Country? { diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep3/View/ChangeNumberCodeView.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep3/View/ChangeNumberCodeView.swift index 3c8eb562d..e9de10836 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep3/View/ChangeNumberCodeView.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep3/View/ChangeNumberCodeView.swift @@ -7,6 +7,7 @@ // import SnapKit +import NynjaUIKit class ChangeNumberCodeView: UIView, UITextFieldDelegate { diff --git a/Nynja/Modules/Stickers/View/ViewController/StickersInputView.swift b/Nynja/Modules/Stickers/View/ViewController/StickersInputView.swift index 472a2e7db..2b22f359c 100644 --- a/Nynja/Modules/Stickers/View/ViewController/StickersInputView.swift +++ b/Nynja/Modules/Stickers/View/ViewController/StickersInputView.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit final class StickersInputView: BaseView { -- GitLab From 74937327cd7d6c8f009dd8f63d0551bffbb865dd Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 15 Nov 2018 20:40:58 +0200 Subject: [PATCH 101/138] Fixed scrolling behavior --- .../NynjaUIKit.xcodeproj/project.pbxproj | 4 + .../Views/Controls/UnderlinedButton.swift | 104 ++++++++++++++++++ .../Views/Controls/UnderlinedTextField.swift | 93 +++++++++++++++- .../AuthModule/View/AuthViewController.swift | 7 +- .../View/Subviews/PhoneNumberLoginView.swift | 16 +-- .../View/ViewsFactory/AuthViewsFactory.swift | 97 ++++++++-------- 6 files changed, 263 insertions(+), 58 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedButton.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 751fe8f9a..fdeb14d9a 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 855A4E88219AFB0F00B6E90B /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E87219AFB0F00B6E90B /* TextField.swift */; }; 855A4E8A219AFB9D00B6E90B /* InputsCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */; }; 855A4E8D219AFF0300B6E90B /* ProhibitedOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */; }; + 855A4E92219B0C5600B6E90B /* UnderlinedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E91219B0C5600B6E90B /* UnderlinedButton.swift */; }; 85EB37F621832D41003A2D6F /* RecordingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F521832D41003A2D6F /* RecordingIndicatorView.swift */; }; /* End PBXBuildFile section */ @@ -84,6 +85,7 @@ 855A4E87219AFB0F00B6E90B /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputsCachePolicy.swift; sourceTree = ""; }; 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProhibitedOptions.swift; sourceTree = ""; }; + 855A4E91219B0C5600B6E90B /* UnderlinedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlinedButton.swift; sourceTree = ""; }; 85EB37F521832D41003A2D6F /* RecordingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingIndicatorView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -354,6 +356,7 @@ children = ( 855A4E87219AFB0F00B6E90B /* TextField.swift */, 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */, + 855A4E91219B0C5600B6E90B /* UnderlinedButton.swift */, ); path = Controls; sourceTree = ""; @@ -496,6 +499,7 @@ 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, 855A4E86219AFA8200B6E90B /* UnderlinedTextField.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, + 855A4E92219B0C5600B6E90B /* UnderlinedButton.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */, 855A4E8D219AFF0300B6E90B /* ProhibitedOptions.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedButton.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedButton.swift new file mode 100644 index 000000000..93c2c0269 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedButton.swift @@ -0,0 +1,104 @@ +// +// UnderlinedButton.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +open class UnderlineButton: UIButton { + + public var underlineColor: UIColor = .lightGray { + didSet { + separator.backgroundColor = underlineColor + } + } + + public var highlighedUnderlineColor: UIColor? { + didSet { + if isHighlighted { + separator.backgroundColor = highlighedUnderlineColor + } + } + } + + public var lineWidth: CGFloat = 1 { + didSet { + separator.snp.updateConstraints { maker in + maker.height.equalTo(lineWidth) + } + } + } + + public var lineOffset: CGFloat = 3 { + didSet { + separator.snp.updateConstraints { maker in + if let titleLabel = titleLabel { + maker.top.equalTo(titleLabel.snp.bottom).offset(lineOffset) + } + } + } + } + + open override var isHighlighted: Bool { + didSet { + if isHighlighted, let highlighedUnderlineColor = highlighedUnderlineColor { + separator.backgroundColor = highlighedUnderlineColor + } else { + separator.backgroundColor = underlineColor + } + } + } + + + // MARK: - Views + + private(set) lazy var separator: UIView = { + let view = UIView() + view.backgroundColor = underlineColor + + if let titleLabel = titleLabel { + titleLabel.setContentHuggingPriority(.required, for: .vertical) + titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + + titleLabel.addSubview(view) + view.snp.makeConstraints { maker in + maker.top.equalTo(titleLabel.snp.bottom).offset(lineOffset) + maker.left.right.equalToSuperview() + maker.bottom.greaterThanOrEqualToSuperview() + maker.height.equalTo(lineWidth) + } + } + + return view + }() + + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + separator.isHidden = false + + contentMode = .center + + titleLabel?.snp.makeConstraints { maker in + maker.width.equalToSuperview() + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift index 9d60ac80b..efee26262 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift @@ -6,4 +6,95 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +import UIKit +import SnapKit + +open class UnderlinedTextField: BaseView { + + public var underlineColor: UIColor = .lightGray { + didSet { + separator.backgroundColor = underlineColor + } + } + + public var highlighedUnderlineColor: UIColor? + + public var lineWidth: CGFloat = 1 { + didSet { + separator.snp.updateConstraints { maker in + maker.height.equalTo(lineWidth) + } + } + } + + public var lineOffset: CGFloat = 3 { + didSet { + separator.snp.updateConstraints { maker in + maker.top.equalTo(textField.snp.bottom).offset(lineOffset) + } + } + } + + public var text: String? { + get { return textField.text } + set { textField.text = newValue } + } + + public var delegate: UITextFieldDelegate? { + get { return textField.delegate } + set { textField.delegate = newValue } + } + + + // MARK: - Views + + public private(set) lazy var textField: TextField = { + let textField = TextField() + textField.setContentHuggingPriority(.required, for: .vertical) + textField.setContentCompressionResistancePriority(.required, for: .vertical) + + addSubview(textField) + textField.snp.makeConstraints { maker in + maker.centerY.left.right.equalToSuperview() + } + + return textField + }() + + private lazy var separator: UIView = { + let view = UIView() + view.backgroundColor = underlineColor + + addSubview(view) + view.snp.makeConstraints { maker in + maker.top.equalTo(textField.snp.bottom).offset(lineOffset) + maker.bottom.left.right.equalToSuperview() + maker.height.equalTo(lineWidth) + } + + return view + }() + + + // MARK: - Setup + + open override func baseSetup() { + super.baseSetup() + + separator.isHidden = false + + textField.addTarget(self, action: #selector(actionBeginEditing), for: .editingDidBegin) + textField.addTarget(self, action: #selector(actionEndEditing), for: .editingDidEnd) + } + + + // MARK: - Actions + + @objc private func actionBeginEditing() { + separator.backgroundColor = highlighedUnderlineColor ?? underlineColor + } + + @objc private func actionEndEditing() { + separator.backgroundColor = underlineColor + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 611234626..fc1b65222 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -98,8 +98,8 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn contentView.addSubview(label) label.snp.makeConstraints { maker in maker.centerX.equalToSuperview() - maker.top.greaterThanOrEqualTo(loginContainerView.snp.bottom).offset(16) - maker.top.equalTo(loginContainerView.snp.bottom).offset(16).priority(.high) + maker.top.greaterThanOrEqualTo(loginContainerView.snp.bottom).offset(32) + maker.top.equalTo(loginContainerView.snp.bottom).offset(32).priority(.high) } return label @@ -225,8 +225,7 @@ private extension AuthViewController { second?.alpha = 1 }, completion: { _ in first?.removeFromSuperview() - } - ) + }) } func showPhoneNumberLogin(animated: Bool) { diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index 49b38e93f..e19ca3064 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -9,18 +9,18 @@ import UIKit final class PhoneNumberLoginView: UIView, Configurable { - private lazy var countrySelector: UIButton = viewsFactory.makeCountrySelector(on: self, target: self, selector: #selector(changeCountry(sender:))) + private lazy var countrySelector = viewsFactory.makeCountrySelector(on: self, target: self, selector: #selector(changeCountry(sender:))) - private lazy var countryCodeContainer: UIView = viewsFactory.makeCountryCodeContainer(on: self, top: countrySelector) - private lazy var countryCodeLabel: UILabel = viewsFactory.makeCountryCodeLabel(on: countryCodeContainer) + private lazy var countryCodeContainer = viewsFactory.makeCountryCodeContainer(on: self, top: countrySelector) + private lazy var countryCodeField = viewsFactory.makeCountryCodeField(on: countryCodeContainer) private var phoneNumberTextFieldController: TextFieldController? - private lazy var phoneNumberContainer: UIView = viewsFactory.makePhoneNumberContainer(on: self, left: countryCodeLabel) + private lazy var phoneNumberContainer = viewsFactory.makePhoneNumberContainer(on: self, left: countryCodeContainer) private lazy var phoneNumberTextField = viewsFactory.makePhoneNumberTextField(on: phoneNumberContainer) - private lazy var detailsLabel: UILabel = viewsFactory.makeDetailsNumberLabel(on: self, top: countryCodeContainer) - private lazy var nextButton: UIButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) + private lazy var detailsLabel = viewsFactory.makeDetailsNumberLabel(on: self, top: countryCodeContainer) + private lazy var nextButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) private var country: Country? private var countrySelectorAction: (() -> Void)? @@ -59,7 +59,7 @@ extension PhoneNumberLoginView { phoneNumberTextField.delegate = phoneNumberTextFieldController updateCountry(config.country) - _ = [countrySelector, countryCodeContainer, countryCodeLabel, phoneNumberContainer, phoneNumberTextField, detailsLabel, nextButton] + _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField, detailsLabel, nextButton] } } @@ -72,7 +72,7 @@ extension PhoneNumberLoginView { phoneNumberTextFieldController?.template = country.placeHolder countrySelector.setTitle(country.name, for: .normal) - countryCodeLabel.text = "+" + country.code + countryCodeField.setTitle("+\(country.code)", for: .normal) phoneNumberTextField.text = "".updateWithMask(placeHolder: country.placeHolder) } diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index 84dc5c091..d4bc95dc5 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit protocol AuthViewsFactoryProtocol { @@ -20,16 +21,20 @@ protocol AuthViewsFactoryProtocol { func makeInputFieldContainer(on view: UIView) -> UIView func makeInputField(on view: UIView) -> MaterialTextField + func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel func makeEmailNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton // MARK: - Phone Number Login View func makeCountrySelector(on view: UIView, target: AnyObject, selector: Selector) -> UIButton + func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView - func makeCountryCodeLabel(on view: UIView) -> UILabel + func makeCountryCodeField(on view: UIView) -> UIButton + func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView - func makePhoneNumberTextField(on view: UIView) -> UITextField + func makePhoneNumberTextField(on view: UIView) -> UnderlinedTextField + func makeDetailsNumberLabel(on view: UIView, top: UIView) -> UILabel func makeNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton } @@ -190,25 +195,22 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { // MARK: - Phone Number Login View func makeCountrySelector(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() - view.addSubview(button) + let button = UnderlineButton() + + button.underlineColor = UIColor.nynja.dustyGray + button.highlighedUnderlineColor = UIColor.nynja.mainRed button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) button.setTitleColor(UIColor.nynja.white, for: .normal) button.addTarget(target, action: selector, for: .touchUpInside) - button.titleLabel?.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) - - button.titleLabel?.snp.makeConstraints { (make) in - make.width.equalToSuperview() - } - + view.addSubview(button) button.snp.makeConstraints { (make) in make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - make.top.equalToSuperview().offset(10) + make.height.equalTo(64) + make.top.equalToSuperview() } return button @@ -216,13 +218,12 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView { let container = UIView() - view.addSubview(container) - container.backgroundColor = UIColor.nynja.clear + view.addSubview(container) container.snp.makeConstraints { (make) in make.top.equalTo(top.snp.bottom) - make.left.equalToSuperview().offset(16) + make.left.equalToSuperview() make.width.equalTo(100) make.height.equalTo(64) } @@ -230,53 +231,59 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { return container } - func makeCountryCodeLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) + func makeCountryCodeField(on view: UIView) -> UIButton { + let field = UnderlineButton() - label.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white + field.underlineColor = UIColor.nynja.dustyGray + field.highlighedUnderlineColor = UIColor.nynja.mainRed - label.snp.makeConstraints { (make) in - make.left.equalToSuperview() - make.right.equalToSuperview().offset(-16) + field.setContentHuggingPriority(.required, for: .horizontal) + field.setContentCompressionResistancePriority(.required, for: .horizontal) + + field.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + field.setTitleColor(UIColor.nynja.white, for: .normal) + + view.addSubview(field) + field.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().inset(16) make.centerY.equalToSuperview() } - return label + return field } func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView { let container = UIView() - view.addSubview(container) - container.backgroundColor = UIColor.nynja.clear - container.snp.makeConstraints { (make) in - make.height.equalTo(left.snp.height) - make.right.equalToSuperview().offset(-16) + view.addSubview(container) + container.snp.makeConstraints { make in make.left.equalTo(left.snp.right) + make.right.equalToSuperview() + make.height.equalTo(left.snp.height) make.centerY.equalTo(left.snp.centerY) } return container } - func makePhoneNumberTextField(on view: UIView) -> UITextField { - let textField = UITextField() + func makePhoneNumberTextField(on view: UIView) -> UnderlinedTextField { + let textField = UnderlinedTextField() - textField.appendBottomBorder(color: UIColor.nynja.dustyGray, width: 1) + textField.underlineColor = UIColor.nynja.dustyGray + textField.highlighedUnderlineColor = UIColor.nynja.mainRed - textField.tintColor = UIColor.nynja.mainRed - textField.font = FontFamily.NotoSans.medium.font(size: 16) - textField.textColor = UIColor.nynja.white - textField.keyboardType = .numberPad + textField.textField.tintColor = UIColor.nynja.mainRed + textField.textField.font = FontFamily.NotoSans.medium.font(size: 16) + textField.textField.textColor = UIColor.nynja.white + textField.textField.keyboardType = .numberPad view.addSubview(textField) - textField.snp.makeConstraints { (make) in - make.centerY.right.equalToSuperview() + textField.snp.makeConstraints { make in + make.centerY.equalToSuperview() make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().inset(16) } return textField @@ -284,7 +291,6 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { func makeDetailsNumberLabel(on view: UIView, top: UIView) -> UILabel { let label = UILabel() - view.addSubview(label) label.text = String.localizable.authEnterPhoneNumberComment label.font = FontFamily.NotoSans.regular.font(size: 14) @@ -294,9 +300,10 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { label.setContentHuggingPriority(.required, for: .vertical) label.setContentCompressionResistancePriority(.required, for: .vertical) - label.snp.makeConstraints { (make) in + view.addSubview(label) + label.snp.makeConstraints { make in make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) + make.right.equalToSuperview().inset(16) make.top.equalTo(top.snp.bottom) } @@ -305,7 +312,6 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { func makeNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) - view.addSubview(button) button.textColor = UIColor.nynja.white button.setTitle(String.localizable.next.uppercased(), for: .normal) @@ -314,12 +320,13 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { button.isEnabled = false - button.snp.makeConstraints { (make) in + view.addSubview(button) + button.snp.makeConstraints { make in make.height.equalTo(44) make.bottom.equalToSuperview() make.top.equalTo(top.snp.bottom).offset(24) make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) + make.right.equalToSuperview().inset(16) } return button -- GitLab From 03661e6dfe5101155baeb7e76ab5db25d5e4d866 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 15 Nov 2018 23:02:48 +0200 Subject: [PATCH 102/138] Adjusted AuthHeaderView --- .../UIWindow/UIWindowExtensions.swift | 19 +++++++--- Nynja/Library/UI/View/UIViewExtensions.swift | 9 +---- .../View/AccountSettingsViewController.swift | 2 +- .../AuthModule/View/AuthViewController.swift | 5 +-- .../View/Subviews/AuthHeaderView.swift | 36 +++++++++++++------ .../CreateProfileViewsFactory.swift | 4 +-- .../View/Subviews/QRNotificationVIew.swift | 2 +- 7 files changed, 48 insertions(+), 29 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/UIWindow/UIWindowExtensions.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/UIWindow/UIWindowExtensions.swift index bbb5f64e3..73e8ed26d 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/UIWindow/UIWindowExtensions.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/UIWindow/UIWindowExtensions.swift @@ -14,13 +14,22 @@ extension UIWindow { return rootViewController?.presentedViewController ?? rootViewController } - public static func safeAreaTopPadding() -> CGFloat { - let window = UIApplication.shared.keyWindow - + public static func safeAreaTopPadding(consideringStatusBar: Bool = true) -> CGFloat { + let application = UIApplication.shared + guard let window = application.keyWindow else { + return 0 + } + let statusBarHeight = application.statusBarFrame.height + if #available(iOS 11.0, *) { - return window?.safeAreaInsets.top ?? 0 + let inset = window.safeAreaInsets.top + if consideringStatusBar { + return inset > 0 ? inset : statusBarHeight + } else { + return inset + } } else { - return 0 + return consideringStatusBar ? statusBarHeight : 0 } } diff --git a/Nynja/Library/UI/View/UIViewExtensions.swift b/Nynja/Library/UI/View/UIViewExtensions.swift index 54c8edd1d..3d4091ed4 100644 --- a/Nynja/Library/UI/View/UIViewExtensions.swift +++ b/Nynja/Library/UI/View/UIViewExtensions.swift @@ -23,10 +23,6 @@ extension UIView { } static func makeStatusBarBackgroundView(on view: UIView) -> UIView { - struct StatusBarBackgroundLayout { - static let height = 20 - } - let background = UIView() view.addSubview(background) @@ -34,10 +30,7 @@ extension UIView { background.snp.makeConstraints { (maker) in maker.top.left.right.equalToSuperview() - - let topPadding = UIWindow.safeAreaTopPadding() - - maker.height.equalTo(topPadding > 0 ? topPadding : StatusBarBackgroundLayout.height) + maker.height.equalTo(UIWindow.safeAreaTopPadding()) } return background diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift index 4fa883749..5f0095db8 100644 --- a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift @@ -19,7 +19,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView layoutGuide.snp.makeConstraints { (make) in make.top.left.right.equalToSuperview() - make.height.equalTo(20 + UIWindow.safeAreaTopPadding()) + make.height.equalTo(UIWindow.safeAreaTopPadding()) } return layoutGuide diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index fc1b65222..ab7dcfd92 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -61,8 +61,9 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn header.configure() contentView.addSubview(header) - header.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() + header.snp.makeConstraints { maker in + maker.top.equalToSuperview() + maker.left.right.equalToSuperview() } return header diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift index 67d5cf3e0..e613e9973 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift @@ -12,8 +12,8 @@ final class AuthHeaderView: UIView, Configurable { // MARK: - Views - private(set) lazy var welcomeLabel = makeWelcomeLabel(on: self) - private(set) lazy var logoImageView = makeLogoImageView(on: self, top: welcomeLabel) + private(set) lazy var welcomeLabel = makeWelcomeLabel() + private(set) lazy var logoImageView = makeLogoImageView() // MARK: - Init @@ -36,36 +36,52 @@ final class AuthHeaderView: UIView, Configurable { private extension AuthHeaderView { - func makeWelcomeLabel(on view: UIView) -> UILabel { - let label = UILabel.init(height: 22.0, color: UIColor.nynja.white, font: FontFamily.NotoSans.medium) + func makeWelcomeLabel() -> UILabel { + let height = Constraints.welcomeLabel.height.adjustedByWidth + let top = Constraints.welcomeLabel.top.adjustedByWidth + + let label = UILabel(height: height, color: UIColor.nynja.white, font: FontFamily.NotoSans.medium) label.text = String.localizable.authWelcome - view.addSubview(label) + addSubview(label) label.snp.makeConstraints { maker in - maker.top.equalToSuperview().offset(70.0) + maker.top.equalToSuperview().offset(top) maker.centerX.equalToSuperview() } return label } - func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { + func makeLogoImageView() -> UIImageView { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.image = UIImage.nynja.logo2.image - view.addSubview(imageView) + addSubview(imageView) imageView.snp.makeConstraints { maker in - maker.top.equalTo(top.snp.bottom).offset(16.0) maker.centerX.equalToSuperview() - maker.bottom.equalToSuperview().offset(-32.0) + maker.top.equalTo(welcomeLabel.snp.bottom).offset(Constraints.logoImageVIew.top.adjustedByWidth) + maker.bottom.equalToSuperview().inset(Constraints.logoImageVIew.bottom.adjustedByWidth) maker.width.equalToSuperview().multipliedBy(0.45) maker.height.equalTo(imageView.snp.width).multipliedBy(0.25) } return imageView } + + enum Constraints { + + enum welcomeLabel { + static let height: CGFloat = 22 + static let top: CGFloat = 50 + } + + enum logoImageVIew { + static let top: CGFloat = 16 + static let bottom: CGFloat = 32 + } + } } // MARK: - Configurable diff --git a/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift index 5c93b34c2..dae66ada8 100644 --- a/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift +++ b/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift @@ -44,7 +44,7 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { layoutGuide.snp.makeConstraints { (make) in make.top.left.right.equalToSuperview() - make.height.equalTo(20 + UIWindow.safeAreaTopPadding()) + make.height.equalTo(UIWindow.safeAreaTopPadding()) } return layoutGuide @@ -137,7 +137,7 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { contentContainer.configure(config: config) contentContainer.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(UIWindow.safeAreaTopPadding()) + make.top.equalToSuperview() make.bottom.equalToSuperview().offset(-16) make.left.right.equalTo(widthView) } diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/View/Subviews/QRNotificationVIew.swift b/Nynja/Modules/Flows/CameraFlow/Camera/View/Subviews/QRNotificationVIew.swift index 4f94eec60..404d99af1 100644 --- a/Nynja/Modules/Flows/CameraFlow/Camera/View/Subviews/QRNotificationVIew.swift +++ b/Nynja/Modules/Flows/CameraFlow/Camera/View/Subviews/QRNotificationVIew.swift @@ -158,7 +158,7 @@ private extension QRNotificationView { button.contentMode = .center button.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(CGFloat(CloseButtonLayout.top) + UIWindow.safeAreaTopPadding()) + make.top.equalToSuperview().offset(CGFloat(CloseButtonLayout.top) + UIWindow.safeAreaTopPadding(consideringStatusBar: false)) make.left.equalToSuperview().offset(CloseButtonLayout.left) make.width.equalTo(CloseButtonLayout.width) make.height.equalTo(CloseButtonLayout.height) -- GitLab From 63d23c7b63887b4dba85662ff57ae42daea1eebe Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 16 Nov 2018 00:04:25 +0200 Subject: [PATCH 103/138] Fixed layout of EmailLoginView and PhoneNumberLoginView --- .../AuthModule/View/AuthViewController.swift | 27 +- .../View/Subviews/EmailLoginView.swift | 106 +++++++- .../View/Subviews/PhoneNumberLoginView.swift | 174 ++++++++++++- .../View/ViewsFactory/AuthViewsFactory.swift | 244 ------------------ 4 files changed, 273 insertions(+), 278 deletions(-) diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index ab7dcfd92..53020794a 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -71,8 +71,8 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn // MARK: Center Content - private weak var emailLoginView: EmailLoginView? - private weak var phoneNumberLoginView: PhoneNumberLoginView? + private lazy var emailLoginView = makeEmailLoginView(on: loginContainerView) + private lazy var phoneNumberLoginView = makePhoneNumberLoginView(on: loginContainerView, country: presenter.selectedCountry) private lazy var loginContainerView: UIView = { let containerView = UIView() @@ -179,6 +179,10 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn showPhoneNumberLogin(animated: false) + emailLoginView.snp.makeConstraints { maker in + maker.height.equalTo(phoneNumberLoginView) + } + enableKeyboardHidingWhenTappedAround() } @@ -198,7 +202,7 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn extension AuthViewController { func update(country: Country) { - phoneNumberLoginView?.updateCountry(country) + phoneNumberLoginView.updateCountry(country) } } @@ -216,6 +220,7 @@ extension AuthViewController { private extension AuthViewController { func animateChangingViews(first: UIView?, second: UIView?) { + second?.isHidden = false second?.alpha = 0 view.layoutIfNeeded() @@ -225,32 +230,30 @@ private extension AuthViewController { first?.alpha = 0 second?.alpha = 1 }, completion: { _ in - first?.removeFromSuperview() + first?.isHidden = true }) } func showPhoneNumberLogin(animated: Bool) { - phoneNumberLoginView = makePhoneNumberLoginView(on: loginContainerView, country: presenter.selectedCountry) - if animated { animateChangingViews(first: emailLoginView, second: phoneNumberLoginView) } else { - emailLoginView?.removeFromSuperview() + emailLoginView.isHidden = true + phoneNumberLoginView.isHidden = false } } func showEmailLogin(animated: Bool) { - emailLoginView = makeEmailLoginView(on: loginContainerView) - if animated { animateChangingViews(first: phoneNumberLoginView, second: emailLoginView) } else { - phoneNumberLoginView?.removeFromSuperview() + emailLoginView.isHidden = false + phoneNumberLoginView.isHidden = true } } func makeEmailLoginView(on view: UIView) -> EmailLoginView { - let loginView = EmailLoginView(viewsFactory: viewsFactory) + let loginView = EmailLoginView() view.addSubview(loginView) loginView.configure(config: EmailLoginView.Config(nextAction: { [weak presenter] in @@ -267,7 +270,7 @@ private extension AuthViewController { } func makePhoneNumberLoginView(on view: UIView, country: Country) -> PhoneNumberLoginView { - let loginView = PhoneNumberLoginView(viewsFactory: viewsFactory) + let loginView = PhoneNumberLoginView() view.addSubview(loginView) loginView.configure(config: PhoneNumberLoginView.Config( diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index f3197285b..3ea535b0b 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -10,20 +10,24 @@ import UIKit final class EmailLoginView: UIView, Configurable { - private lazy var inputFieldContainer = viewsFactory.makeInputFieldContainer(on: self) - private lazy var inputField = viewsFactory.makeInputField(on: inputFieldContainer) - private lazy var detailsLabel = viewsFactory.makeDetailsLabel(on: self, top: inputFieldContainer) - private lazy var nextButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) + // MARK: - Views + + private lazy var inputFieldContainer = makeInputFieldContainer() + private lazy var inputField = makeInputField() + private lazy var detailsLabel = makeDetailsLabel() + private lazy var nextButton = makeNextButton() private var textFieldController: TextFieldController? private var nextAction: ((String) -> Void)? - private let viewsFactory: AuthViewsFactoryProtocol - init(viewsFactory: AuthViewsFactoryProtocol) { - self.viewsFactory = viewsFactory + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) - super.init(frame: CGRect.zero) + nextButton.isEnabled = false + inputField.isHidden = false } required init?(coder aDecoder: NSCoder) { @@ -65,6 +69,90 @@ private extension EmailLoginView { } } +private extension EmailLoginView { + + func makeInputFieldContainer() -> UIView { + let container = UIView() + container.backgroundColor = UIColor.nynja.clear + + addSubview(container) + container.snp.makeConstraints { maker in + maker.top.equalToSuperview().offset(16) + maker.height.equalTo(44) + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + } + + return container + } + + func makeInputField() -> MaterialTextField { + let textField = MaterialTextField() + + textField.textContentType = .emailAddress + + textField.placeholderColor = UIColor.nynja.dustyGray + textField.placeholder = String.localizable.authEmailPlaceholder + + textField.textColor = UIColor.nynja.white + textField.font = FontFamily.NotoSans.medium.font(size: 16) + + textField.separatorColor = UIColor.nynja.dustyGray + + textField.keyboardType = .emailAddress + textField.returnKeyType = .done + + inputFieldContainer.addSubview(textField) + textField.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview() + maker.right.equalToSuperview() + } + + return textField + } + + func makeDetailsLabel() -> UILabel { + let label = UILabel() + + label.text = String.localizable.authEnterEmailAddressComment + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.setContentHuggingPriority(.required, for: .vertical) + + label.numberOfLines = 0 + + addSubview(label) + label.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.top.equalTo(inputFieldContainer.snp.bottom) + } + + return label + } + + func makeNextButton() -> UIButton { + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) + + button.textColor = UIColor.nynja.white + button.setTitle(String.localizable.next.uppercased(), for: .normal) + + addSubview(button) + button.snp.makeConstraints { maker in + maker.height.equalTo(44) + maker.top.greaterThanOrEqualTo(detailsLabel.snp.bottom).offset(16) + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.bottom.equalToSuperview() + } + + return button + } +} + // MARK: - Validator private extension EmailLoginView { @@ -97,7 +185,7 @@ private extension EmailLoginView { } func textInputShouldReturn(_ textInput: MaterialTextField) -> Bool { - textInput.resignFirstResponder() + _ = textInput.resignFirstResponder() return false } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index e19ca3064..f9639623b 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -7,31 +7,36 @@ // import UIKit +import NynjaUIKit final class PhoneNumberLoginView: UIView, Configurable { - private lazy var countrySelector = viewsFactory.makeCountrySelector(on: self, target: self, selector: #selector(changeCountry(sender:))) - private lazy var countryCodeContainer = viewsFactory.makeCountryCodeContainer(on: self, top: countrySelector) - private lazy var countryCodeField = viewsFactory.makeCountryCodeField(on: countryCodeContainer) + // MARK: - Views - private var phoneNumberTextFieldController: TextFieldController? + private lazy var countrySelector = makeCountrySelector() + + private lazy var countryCodeContainer = makeCountryCodeContainer() + private lazy var countryCodeField = makeCountryCodeField() - private lazy var phoneNumberContainer = viewsFactory.makePhoneNumberContainer(on: self, left: countryCodeContainer) - private lazy var phoneNumberTextField = viewsFactory.makePhoneNumberTextField(on: phoneNumberContainer) + private lazy var phoneNumberContainer = makePhoneNumberContainer() + private lazy var phoneNumberTextField = makePhoneNumberTextField() - private lazy var detailsLabel = viewsFactory.makeDetailsNumberLabel(on: self, top: countryCodeContainer) - private lazy var nextButton = viewsFactory.makeNextButton(on: self, top: detailsLabel, target: self, selector: #selector(next(sender:))) + private lazy var detailsLabel = makeDetailsNumberLabel() + private lazy var nextButton = makeNextButton() + + private var phoneNumberTextFieldController: TextFieldController? private var country: Country? private var countrySelectorAction: (() -> Void)? private var nextAction: ((String) -> Void)? - private let viewsFactory: AuthViewsFactoryProtocol - init(viewsFactory: AuthViewsFactoryProtocol) { - self.viewsFactory = viewsFactory + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) - super.init(frame: CGRect.zero) + nextButton.isEnabled = false } required init?(coder aDecoder: NSCoder) { @@ -48,7 +53,7 @@ extension PhoneNumberLoginView { let nextAction: (String) -> Void } - func configure(config: PhoneNumberLoginView.Config) { + func configure(config: Config) { country = config.country countrySelectorAction = config.countrySelectorAction nextAction = config.nextAction @@ -91,6 +96,149 @@ private extension PhoneNumberLoginView { } } +// MARK: - Layout + +private extension PhoneNumberLoginView { + + func makeCountrySelector() -> UIButton { + let button = UnderlineButton() + + button.underlineColor = UIColor.nynja.dustyGray + button.highlighedUnderlineColor = UIColor.nynja.mainRed + + button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.setTitleColor(UIColor.nynja.white, for: .normal) + + button.addTarget(self, action: #selector(changeCountry(sender:)), for: .touchUpInside) + + addSubview(button) + button.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.height.equalTo(64) + maker.top.equalToSuperview() + } + + return button + } + + func makeCountryCodeContainer() -> UIView { + let container = UIView() + container.backgroundColor = UIColor.nynja.clear + + addSubview(container) + container.snp.makeConstraints { maker in + maker.top.equalTo(countrySelector.snp.bottom) + maker.left.equalToSuperview() + maker.width.equalTo(100) + maker.height.equalTo(64) + } + + return container + } + + func makeCountryCodeField() -> UIButton { + let field = UnderlineButton() + + field.underlineColor = UIColor.nynja.dustyGray + field.highlighedUnderlineColor = UIColor.nynja.mainRed + + field.setContentHuggingPriority(.required, for: .horizontal) + field.setContentCompressionResistancePriority(.required, for: .horizontal) + + field.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + field.setTitleColor(UIColor.nynja.white, for: .normal) + + countryCodeContainer.addSubview(field) + field.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.centerY.equalToSuperview() + } + + return field + } + + func makePhoneNumberContainer() -> UIView { + let container = UIView() + container.backgroundColor = UIColor.nynja.clear + + let left = countryCodeContainer + + addSubview(container) + container.snp.makeConstraints { maker in + maker.left.equalTo(left.snp.right) + maker.right.equalToSuperview() + maker.height.equalTo(left.snp.height) + maker.centerY.equalTo(left.snp.centerY) + } + + return container + } + + func makePhoneNumberTextField() -> UnderlinedTextField { + let textField = UnderlinedTextField() + + textField.underlineColor = UIColor.nynja.dustyGray + textField.highlighedUnderlineColor = UIColor.nynja.mainRed + + textField.textField.tintColor = UIColor.nynja.mainRed + textField.textField.font = FontFamily.NotoSans.medium.font(size: 16) + textField.textField.textColor = UIColor.nynja.white + textField.textField.keyboardType = .numberPad + + phoneNumberContainer.addSubview(textField) + textField.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + } + + return textField + } + + func makeDetailsNumberLabel() -> UILabel { + let label = UILabel() + + label.text = String.localizable.authEnterPhoneNumberComment + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + label.numberOfLines = 0 + + label.setContentHuggingPriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .vertical) + + addSubview(label) + label.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.top.equalTo(countryCodeContainer.snp.bottom) + } + + return label + } + + func makeNextButton() -> UIButton { + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) + + button.textColor = UIColor.nynja.white + button.setTitle(String.localizable.next.uppercased(), for: .normal) + + button.addTarget(target, action: #selector(next(sender:)), for: .touchUpInside) + + addSubview(button) + button.snp.makeConstraints { maker in + maker.height.equalTo(44) + maker.bottom.equalToSuperview() + maker.top.equalTo(detailsLabel.snp.bottom).offset(24) + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + } + + return button + } +} + // MARK: - Text field controller private extension PhoneNumberLoginView { diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift index d4bc95dc5..7573da25c 100644 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift @@ -16,27 +16,6 @@ protocol AuthViewsFactoryProtocol { func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton func makeLoginWithFacebookButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton func makeSwitchLoginButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton - - // MARK: - Email Login View - - func makeInputFieldContainer(on view: UIView) -> UIView - func makeInputField(on view: UIView) -> MaterialTextField - - func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel - func makeEmailNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton - - // MARK: - Phone Number Login View - - func makeCountrySelector(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - - func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView - func makeCountryCodeField(on view: UIView) -> UIButton - - func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView - func makePhoneNumberTextField(on view: UIView) -> UnderlinedTextField - - func makeDetailsNumberLabel(on view: UIView, top: UIView) -> UILabel - func makeNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton } final class AuthViewsFactory: AuthViewsFactoryProtocol { @@ -108,227 +87,4 @@ final class AuthViewsFactory: AuthViewsFactoryProtocol { return button } - - - // MARK: - Email Login View - - func makeInputFieldContainer(on view: UIView) -> UIView { - let container = UIView() - view.addSubview(container) - - container.backgroundColor = UIColor.nynja.clear - - container.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(16) - make.height.equalTo(44) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - - return container - } - - func makeInputField(on view: UIView) -> MaterialTextField { - let textField = MaterialTextField() - - textField.textContentType = .emailAddress - - textField.placeholderColor = UIColor.nynja.dustyGray - textField.placeholder = String.localizable.authEmailPlaceholder - - textField.textColor = UIColor.nynja.white - textField.font = FontFamily.NotoSans.medium.font(size: 16) - - textField.separatorColor = UIColor.nynja.dustyGray - - textField.keyboardType = .emailAddress - textField.returnKeyType = .done - - view.addSubview(textField) - textField.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - return textField - } - - func makeDetailsLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.text = String.localizable.authEnterEmailAddressComment - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - - label.snp.makeConstraints { (make) in - make.height.equalTo(40) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.top.equalTo(top.snp.bottom) - } - - return label - } - - func makeEmailNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) - view.addSubview(button) - - button.textColor = UIColor.nynja.white - button.setTitle(String.localizable.next.uppercased(), for: .normal) - - button.isEnabled = false - - button.snp.makeConstraints { (make) in - make.height.equalTo(44) - make.bottom.equalToSuperview() - make.top.equalTo(top.snp.bottom).offset(88) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - - return button - } - - // MARK: - Phone Number Login View - - func makeCountrySelector(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UnderlineButton() - - button.underlineColor = UIColor.nynja.dustyGray - button.highlighedUnderlineColor = UIColor.nynja.mainRed - - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - button.setTitleColor(UIColor.nynja.white, for: .normal) - - button.addTarget(target, action: selector, for: .touchUpInside) - - view.addSubview(button) - button.snp.makeConstraints { (make) in - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(64) - make.top.equalToSuperview() - } - - return button - } - - func makeCountryCodeContainer(on view: UIView, top: UIView) -> UIView { - let container = UIView() - container.backgroundColor = UIColor.nynja.clear - - view.addSubview(container) - container.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom) - make.left.equalToSuperview() - make.width.equalTo(100) - make.height.equalTo(64) - } - - return container - } - - func makeCountryCodeField(on view: UIView) -> UIButton { - let field = UnderlineButton() - - field.underlineColor = UIColor.nynja.dustyGray - field.highlighedUnderlineColor = UIColor.nynja.mainRed - - field.setContentHuggingPriority(.required, for: .horizontal) - field.setContentCompressionResistancePriority(.required, for: .horizontal) - - field.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - field.setTitleColor(UIColor.nynja.white, for: .normal) - - view.addSubview(field) - field.snp.makeConstraints { make in - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().inset(16) - make.centerY.equalToSuperview() - } - - return field - } - - func makePhoneNumberContainer(on view: UIView, left: UIView) -> UIView { - let container = UIView() - container.backgroundColor = UIColor.nynja.clear - - view.addSubview(container) - container.snp.makeConstraints { make in - make.left.equalTo(left.snp.right) - make.right.equalToSuperview() - make.height.equalTo(left.snp.height) - make.centerY.equalTo(left.snp.centerY) - } - - return container - } - - func makePhoneNumberTextField(on view: UIView) -> UnderlinedTextField { - let textField = UnderlinedTextField() - - textField.underlineColor = UIColor.nynja.dustyGray - textField.highlighedUnderlineColor = UIColor.nynja.mainRed - - textField.textField.tintColor = UIColor.nynja.mainRed - textField.textField.font = FontFamily.NotoSans.medium.font(size: 16) - textField.textField.textColor = UIColor.nynja.white - textField.textField.keyboardType = .numberPad - - view.addSubview(textField) - textField.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().inset(16) - } - - return textField - } - - func makeDetailsNumberLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - - label.text = String.localizable.authEnterPhoneNumberComment - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - label.numberOfLines = 0 - - label.setContentHuggingPriority(.required, for: .vertical) - label.setContentCompressionResistancePriority(.required, for: .vertical) - - view.addSubview(label) - label.snp.makeConstraints { make in - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().inset(16) - make.top.equalTo(top.snp.bottom) - } - - return label - } - - func makeNextButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) - - button.textColor = UIColor.nynja.white - button.setTitle(String.localizable.next.uppercased(), for: .normal) - - button.addTarget(target, action: selector, for: .touchUpInside) - - button.isEnabled = false - - view.addSubview(button) - button.snp.makeConstraints { make in - make.height.equalTo(44) - make.bottom.equalToSuperview() - make.top.equalTo(top.snp.bottom).offset(24) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().inset(16) - } - - return button - } } -- GitLab From 85e69d5cedb095b45103d15180393a44afc6125e Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 16 Nov 2018 00:09:07 +0200 Subject: [PATCH 104/138] [NY-5163] Multiple accounts - Auth SDK integration (#1479) * [NY-5181] Add pod 'NynjaSDK-MultiAcc', '= 0.5.3' * [NY-5181] Implemented AuthService. --- Nynja.xcodeproj/project.pbxproj | 110 +++++++-- Nynja/Extensions/Bundle+Keys.swift | 8 + Nynja/Generated/LocalizableConstants.swift | 34 +++ Nynja/Modules/Auth/AuthCoordinator.swift | 112 ++++----- .../Auth/AuthModule/AuthProtocols.swift | 11 +- .../Auth/AuthModule/Entities/LoginFlow.swift | 14 ++ .../AuthModule/Entities/LoginOption.swift | 10 +- .../Interactor/AuthInteractor.swift | 33 ++- .../AuthModule/Presenter/AuthPresenter.swift | 32 ++- .../AuthModule/View/AuthViewController.swift | 8 +- .../View/Subviews/LoginOptionsView.swift | 2 - .../View/Subviews/PhoneNumberLoginView.swift | 10 +- .../AuthModule/Wireframe/AuthWireframe.swift | 24 +- .../CodeConfirmationProtocols.swift | 16 +- .../Entities/AuthProviderType.swift | 4 +- .../CodeConfirmationInteractor.swift | 81 +++++-- .../Presenter/CodeConfirmationPresenter.swift | 70 ++++-- .../View/CodeConfirmationViewController.swift | 33 ++- .../CodeConfirmationViewsFactory.swift | 8 +- .../Wireframe/CodeConfirmationWireframe.swift | 17 +- Nynja/Resources/DevAutoTests.xcconfig | 9 + Nynja/Resources/DevConfig.xcconfig | 9 + Nynja/Resources/Info.plist | 24 ++ Nynja/Resources/PrereleaseConfig.xcconfig | 9 + Nynja/Resources/ReleaseConfig.xcconfig | 9 + Nynja/Resources/en.lproj/Localizable.strings | 20 ++ Nynja/SDK/App/AppBundleCredentials.swift | 13 ++ Nynja/SDK/App/AppConfigurationProvider.swift | 49 ++++ Nynja/SDK/App/ServerConfig.swift | 13 ++ Nynja/SDK/Auth/AuthConfirmationType.swift | 12 + Nynja/SDK/Auth/AuthResponse.swift | 13 ++ Nynja/SDK/Auth/AuthService.swift | 215 ++++++++++++++++++ Nynja/SDK/Auth/AuthTokenData.swift | 13 ++ .../Auth/AuthenticationType.swift} | 6 +- Nynja/SDK/Auth/PhoneNumberInfo.swift | 33 +++ Nynja/SDK/Session/SessionStorage.swift | 11 + .../NynjaCalls/NynjaCommunicatorService.swift | 8 + ...otocol.swift => MQTTFactoryProtocol.swift} | 4 +- .../MobileSDKFactoryProtocol.swift | 13 ++ .../ServiceFactory/ServiceFactory.swift | 26 +++ .../ServiceFactoryProtocol.swift | 4 +- Nynja/StorageService+UserInfo.swift | 6 +- Podfile | 3 +- Podfile.lock | 10 +- 44 files changed, 961 insertions(+), 208 deletions(-) create mode 100644 Nynja/Modules/Auth/AuthModule/Entities/LoginFlow.swift create mode 100644 Nynja/SDK/App/AppBundleCredentials.swift create mode 100644 Nynja/SDK/App/AppConfigurationProvider.swift create mode 100644 Nynja/SDK/App/ServerConfig.swift create mode 100644 Nynja/SDK/Auth/AuthConfirmationType.swift create mode 100644 Nynja/SDK/Auth/AuthResponse.swift create mode 100644 Nynja/SDK/Auth/AuthService.swift create mode 100644 Nynja/SDK/Auth/AuthTokenData.swift rename Nynja/{Modules/Auth/CodeConfirmation/Entities/AuthType.swift => SDK/Auth/AuthenticationType.swift} (64%) create mode 100644 Nynja/SDK/Auth/PhoneNumberInfo.swift create mode 100644 Nynja/SDK/Session/SessionStorage.swift rename Nynja/Services/ServiceFactory/{MQTTHandlerFactoryProtocol.swift => MQTTFactoryProtocol.swift} (70%) create mode 100644 Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 3cd75bfb0..0da748a17 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -738,7 +738,6 @@ 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */; }; 5E07BC3D216DFD08000E4558 /* AuthViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */; }; 5E07BC40216E09F0000E4558 /* CodeConfirmationViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */; }; - 5E07BC44216F56AF000E4558 /* AuthType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC43216F56AF000E4558 /* AuthType.swift */; }; 5E07BC4D216F64EC000E4558 /* CreateProfileProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */; }; 5E07BC4F216F659E000E4558 /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */; }; 5E07BC51216F6617000E4558 /* CreateProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */; }; @@ -790,7 +789,7 @@ 5EEB73CB2161CBF300D8ECE6 /* AuthPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73CA2161CBF300D8ECE6 /* AuthPresenter.swift */; }; 5EEB73CD2161CC8A00D8ECE6 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73CC2161CC8A00D8ECE6 /* AuthViewController.swift */; }; 5EEB73D02161CE2700D8ECE6 /* LoginOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */; }; - 5EEB73D22161CEA100D8ECE6 /* LoginOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D12161CEA100D8ECE6 /* LoginOption.swift */; }; + 5EEB73D22161CEA100D8ECE6 /* LoginFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D12161CEA100D8ECE6 /* LoginFlow.swift */; }; 5EEB73D42161D5C500D8ECE6 /* AuthHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */; }; 5EEB73D62161DBF100D8ECE6 /* EmailLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D52161DBF100D8ECE6 /* EmailLoginView.swift */; }; 5EEB73D82162227B00D8ECE6 /* PhoneNumberLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73D72162227B00D8ECE6 /* PhoneNumberLoginView.swift */; }; @@ -883,6 +882,15 @@ 850A0C672046B65D004F79AD /* WCItemsFactoryDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A0C662046B65D004F79AD /* WCItemsFactoryDecorator.swift */; }; 850A2BB0203584B000D68FDF /* SearchActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2BAF203584B000D68FDF /* SearchActionsView.swift */; }; 850A2BB22035AE5E00D68FDF /* ForwardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2BB12035AE5E00D68FDF /* ForwardCellViewModel.swift */; }; + 850B9D9C219C117E00EA0CF4 /* SessionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */; }; + 850B9D9F219C131E00EA0CF4 /* AuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */; }; + 850B9DA0219C1E8900EA0CF4 /* SessionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */; }; + 850B9DA1219C1E8A00EA0CF4 /* SessionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */; }; + 850B9DA3219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DA2219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift */; }; + 850B9DA6219C2B9500EA0CF4 /* AppConfigurationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DA5219C2B9500EA0CF4 /* AppConfigurationProvider.swift */; }; + 850B9DA8219C324B00EA0CF4 /* ServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DA7219C324B00EA0CF4 /* ServerConfig.swift */; }; + 850B9DAB219C6EE900EA0CF4 /* PhoneNumberInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */; }; + 850B9DAD219C7ADA00EA0CF4 /* LoginOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DAC219C7ADA00EA0CF4 /* LoginOption.swift */; }; 850C0B2620E00C3E003341D0 /* UIScreen+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C0B2520E00C3E003341D0 /* UIScreen+Keyboard.swift */; }; 850C0B2820E01F7F003341D0 /* NotificationCenter+WheelNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C0B2720E01F7F003341D0 /* NotificationCenter+WheelNotifications.swift */; }; 850C0B5420E0369E003341D0 /* ChatListMessageCellModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */; }; @@ -924,7 +932,7 @@ 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872BE20CD457F007CD6CA /* StickersProviding.swift */; }; 851872C120CD45B3007CD6CA /* StickersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872C020CD45B3007CD6CA /* StickersProvider.swift */; }; 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */; }; - 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */; }; + 851C6A54218B560B0062B148 /* MQTTFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */; }; 851EBD7F20B418890065C644 /* StickersInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EBD7E20B418890065C644 /* StickersInputView.swift */; }; 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F520D4194A007C0036 /* DBRecentSticker.swift */; }; 852003F820D419E9007C0036 /* RecentStickerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F720D419E9007C0036 /* RecentStickerTable.swift */; }; @@ -1065,6 +1073,10 @@ 855A4E7F2199B4FE00B6E90B /* NynjaImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */; }; 855A4E812199C16F00B6E90B /* RoundNynjaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */; }; 855A4E822199C1A100B6E90B /* RoundNynjaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */; }; + 855A4E9B219B321000B6E90B /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E9A219B321000B6E90B /* AuthService.swift */; }; + 855A4E9E219B336000B6E90B /* AppBundleCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E9D219B336000B6E90B /* AppBundleCredentials.swift */; }; + 855A4EA0219B35B700B6E90B /* AuthConfirmationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E9F219B35B700B6E90B /* AuthConfirmationType.swift */; }; + 855A4EA2219B3A9400B6E90B /* AuthTokenData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4EA1219B3A9400B6E90B /* AuthTokenData.swift */; }; 855AC532208E441500DC2335 /* StickersInputPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52D208E441500DC2335 /* StickersInputPresenter.swift */; }; 855AC533208E441500DC2335 /* StickersInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52E208E441500DC2335 /* StickersInputViewController.swift */; }; 855AC534208E441500DC2335 /* StickersInputProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52F208E441500DC2335 /* StickersInputProtocols.swift */; }; @@ -1089,6 +1101,7 @@ 8566BB11215BC39500320E15 /* FetchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566BB10215BC39500320E15 /* FetchType.swift */; }; 8566BB12215BC39D00320E15 /* FetchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566BB10215BC39500320E15 /* FetchType.swift */; }; 8566BB13215BC39E00320E15 /* FetchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566BB10215BC39500320E15 /* FetchType.swift */; }; + 856A8EFC219C8D7A0004E11E /* AuthenticationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856A8EFB219C8D7A0004E11E /* AuthenticationType.swift */; }; 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3B52092315B00E4840C /* CollectionViewDataProxy.swift */; }; 8572C3B92092364C00E4840C /* StickerPackageDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */; }; 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */; }; @@ -3049,7 +3062,6 @@ 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryInteractor.swift; sourceTree = ""; }; 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewsFactory.swift; sourceTree = ""; }; 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationViewsFactory.swift; sourceTree = ""; }; - 5E07BC43216F56AF000E4558 /* AuthType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthType.swift; sourceTree = ""; }; 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileProtocols.swift; sourceTree = ""; }; 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = ""; }; 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileInteractor.swift; sourceTree = ""; }; @@ -3099,7 +3111,7 @@ 5EEB73CA2161CBF300D8ECE6 /* AuthPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPresenter.swift; sourceTree = ""; }; 5EEB73CC2161CC8A00D8ECE6 /* AuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsView.swift; sourceTree = ""; }; - 5EEB73D12161CEA100D8ECE6 /* LoginOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOption.swift; sourceTree = ""; }; + 5EEB73D12161CEA100D8ECE6 /* LoginFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFlow.swift; sourceTree = ""; }; 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthHeaderView.swift; sourceTree = ""; }; 5EEB73D52161DBF100D8ECE6 /* EmailLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailLoginView.swift; sourceTree = ""; }; 5EEB73D72162227B00D8ECE6 /* PhoneNumberLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberLoginView.swift; sourceTree = ""; }; @@ -3195,6 +3207,13 @@ 850A0C662046B65D004F79AD /* WCItemsFactoryDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCItemsFactoryDecorator.swift; sourceTree = ""; }; 850A2BAF203584B000D68FDF /* SearchActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchActionsView.swift; sourceTree = ""; }; 850A2BB12035AE5E00D68FDF /* ForwardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardCellViewModel.swift; sourceTree = ""; }; + 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionStorage.swift; sourceTree = ""; }; + 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthResponse.swift; sourceTree = ""; }; + 850B9DA2219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileSDKFactoryProtocol.swift; sourceTree = ""; }; + 850B9DA5219C2B9500EA0CF4 /* AppConfigurationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationProvider.swift; sourceTree = ""; }; + 850B9DA7219C324B00EA0CF4 /* ServerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfig.swift; sourceTree = ""; }; + 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberInfo.swift; sourceTree = ""; }; + 850B9DAC219C7ADA00EA0CF4 /* LoginOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOption.swift; sourceTree = ""; }; 850C0B2520E00C3E003341D0 /* UIScreen+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScreen+Keyboard.swift"; sourceTree = ""; }; 850C0B2720E01F7F003341D0 /* NotificationCenter+WheelNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+WheelNotifications.swift"; sourceTree = ""; }; 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageCellModelDelegate.swift; sourceTree = ""; }; @@ -3236,7 +3255,7 @@ 851872BE20CD457F007CD6CA /* StickersProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProviding.swift; path = Services/StickersProvider/StickersProviding.swift; sourceTree = ""; }; 851872C020CD45B3007CD6CA /* StickersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProvider.swift; path = Services/StickersProvider/StickersProvider.swift; sourceTree = ""; }; 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceFactoryProtocol.swift; sourceTree = ""; }; - 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTHandlerFactoryProtocol.swift; sourceTree = ""; }; + 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTFactoryProtocol.swift; sourceTree = ""; }; 851EBD7E20B418890065C644 /* StickersInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputView.swift; sourceTree = ""; }; 852003F520D4194A007C0036 /* DBRecentSticker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRecentSticker.swift; sourceTree = ""; }; 852003F720D419E9007C0036 /* RecentStickerTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentStickerTable.swift; sourceTree = ""; }; @@ -3343,6 +3362,10 @@ 855A393C213E76E20002B8DC /* LoadingInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoadingInteractive.swift; path = BaseVC/LoadingInteractive.swift; sourceTree = ""; }; 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaImageButton.swift; sourceTree = ""; }; 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundNynjaButton.swift; sourceTree = ""; }; + 855A4E9A219B321000B6E90B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; + 855A4E9D219B336000B6E90B /* AppBundleCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBundleCredentials.swift; sourceTree = ""; }; + 855A4E9F219B35B700B6E90B /* AuthConfirmationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationType.swift; sourceTree = ""; }; + 855A4EA1219B3A9400B6E90B /* AuthTokenData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTokenData.swift; sourceTree = ""; }; 855AC52D208E441500DC2335 /* StickersInputPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputPresenter.swift; sourceTree = ""; }; 855AC52E208E441500DC2335 /* StickersInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputViewController.swift; sourceTree = ""; }; 855AC52F208E441500DC2335 /* StickersInputProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputProtocols.swift; sourceTree = ""; }; @@ -3365,6 +3388,7 @@ 8566771D20C1579C00DD4204 /* StorageSubscriberReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSubscriberReference.swift; sourceTree = ""; }; 8566771F20C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+MessageHandlerSubscriber.swift"; sourceTree = ""; }; 8566BB10215BC39500320E15 /* FetchType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchType.swift; sourceTree = ""; }; + 856A8EFB219C8D7A0004E11E /* AuthenticationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationType.swift; sourceTree = ""; }; 8572C3B52092315B00E4840C /* CollectionViewDataProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDataProxy.swift; sourceTree = ""; }; 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackageDataSource.swift; sourceTree = ""; }; 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionDataSource.swift; sourceTree = ""; }; @@ -6096,6 +6120,7 @@ 3A768E1C1ECD152300108F7C /* Services */ = { isa = PBXGroup; children = ( + 855A4E99219B31F200B6E90B /* SDK */, 852BB8F721947A0800F2E8E4 /* Auth */, FEA656082167797E00B44029 /* WalletFundingNetworkService */, 4B71AC4021622A5600E4583B /* Notifications */, @@ -6938,14 +6963,14 @@ 4B749F0E214FEFC8002F3A33 /* Auth */ = { isa = PBXGroup; children = ( + 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, - 5E07BC45216F64DB000E4558 /* CreateProfile */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, + 5E07BC45216F64DB000E4558 /* CreateProfile */, 852BB8C7219424EA00F2E8E4 /* Facebook */, 115A968821FB24FA3C58A6D5 /* SelectCountry */, 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, - 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, ); path = Auth; sourceTree = ""; @@ -7684,7 +7709,6 @@ 5EEB73B0216046EA00D8ECE6 /* Entities */ = { isa = PBXGroup; children = ( - 5E07BC43216F56AF000E4558 /* AuthType.swift */, 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */, ); path = Entities; @@ -7749,7 +7773,8 @@ 5EEB73C3216199DE00D8ECE6 /* Entities */ = { isa = PBXGroup; children = ( - 5EEB73D12161CEA100D8ECE6 /* LoginOption.swift */, + 5EEB73D12161CEA100D8ECE6 /* LoginFlow.swift */, + 850B9DAC219C7ADA00EA0CF4 /* LoginOption.swift */, ); path = Entities; sourceTree = ""; @@ -8341,6 +8366,24 @@ path = Files; sourceTree = ""; }; + 850B9D9D219C11BF00EA0CF4 /* Session */ = { + isa = PBXGroup; + children = ( + 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */, + ); + path = Session; + sourceTree = ""; + }; + 850B9DA4219C2B7C00EA0CF4 /* App */ = { + isa = PBXGroup; + children = ( + 850B9DA5219C2B9500EA0CF4 /* AppConfigurationProvider.swift */, + 855A4E9D219B336000B6E90B /* AppBundleCredentials.swift */, + 850B9DA7219C324B00EA0CF4 /* ServerConfig.swift */, + ); + path = App; + sourceTree = ""; + }; 850C3015204DA84400DB26C2 /* Privacy */ = { isa = PBXGroup; children = ( @@ -9102,6 +9145,29 @@ path = StickerPackHeader; sourceTree = ""; }; + 855A4E99219B31F200B6E90B /* SDK */ = { + isa = PBXGroup; + children = ( + 850B9DA4219C2B7C00EA0CF4 /* App */, + 850B9D9D219C11BF00EA0CF4 /* Session */, + 855A4E9C219B323200B6E90B /* Auth */, + ); + path = SDK; + sourceTree = ""; + }; + 855A4E9C219B323200B6E90B /* Auth */ = { + isa = PBXGroup; + children = ( + 855A4E9A219B321000B6E90B /* AuthService.swift */, + 855A4E9F219B35B700B6E90B /* AuthConfirmationType.swift */, + 855A4EA1219B3A9400B6E90B /* AuthTokenData.swift */, + 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */, + 856A8EFB219C8D7A0004E11E /* AuthenticationType.swift */, + 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */, + ); + path = Auth; + sourceTree = ""; + }; 855AC52C208E435700DC2335 /* Stickers */ = { isa = PBXGroup; children = ( @@ -13430,7 +13496,8 @@ children = ( F11786F020AC5482007A9A1B /* ServiceFactory.swift */, 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */, - 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */, + 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */, + 850B9DA2219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift */, ); name = ServiceFactory; path = Services/ServiceFactory; @@ -14901,7 +14968,7 @@ "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", "${BUILT_PRODUCTS_DIR}/MulticastDelegateSwift/MulticastDelegateSwift.framework", - "${PODS_ROOT}/NynjaSDK/NynjaSDK.framework", + "${PODS_ROOT}/NynjaSDK-MultiAcc/NynjaSDK.framework", "${BUILT_PRODUCTS_DIR}/QRCode/QRCode.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SQLCipher/SQLCipher.framework", @@ -15388,6 +15455,7 @@ A42CE56420692EDB000889CC /* Message.swift in Sources */, 4B3B35DA217119C9005A214A /* AmazonInitializer.swift in Sources */, A42CE58220692EDB000889CC /* CDR.swift in Sources */, + 850B9DA0219C1E8900EA0CF4 /* SessionStorage.swift in Sources */, 4B3B35D32171120F005A214A /* SharedServiceFactory.swift in Sources */, A4B544EB20EFB36100EB7B0F /* errors.swift in Sources */, 8551CF12217094CD00829CF1 /* Array+Desc.swift in Sources */, @@ -15532,7 +15600,6 @@ 4B8FC3182163CC0E00602D6B /* ChatCellModel.swift in Sources */, C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */, 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */, - 5E07BC44216F56AF000E4558 /* AuthType.swift in Sources */, A49381AA21355EE1006D28DD /* MessageInteractor+Forward.swift in Sources */, 26FA4210201821B400E6F6EC /* StarHandler.swift in Sources */, 26C0C1E42073DA3A00C530DA /* Muc+DB.swift in Sources */, @@ -15853,6 +15920,7 @@ FEA655F42167777E00B44029 /* PaymentViewController.swift in Sources */, 4B4266C3204D923400194BC1 /* Array+UIView.swift in Sources */, A4CB15232103735200C3B68B /* JDFileBasedMechanism.swift in Sources */, + 855A4EA0219B35B700B6E90B /* AuthConfirmationType.swift in Sources */, 3A771CAA1F191B38008D968A /* ProfileHandler.swift in Sources */, B74BAFFC21076AFA0049CD27 /* SectionView.swift in Sources */, E78EFB871FC867A900C44975 /* DBP2p.swift in Sources */, @@ -16131,6 +16199,7 @@ A42D52D3206A53AB00EEB952 /* Test_Spec.swift in Sources */, 4B7C73F0215A5509007924DB /* SMSCodeProvider.swift in Sources */, E7F2CFE21F5EEF1E00806E43 /* PermissionManager.swift in Sources */, + 850B9DA8219C324B00EA0CF4 /* ServerConfig.swift in Sources */, 26ACC5CE212C3DDB008455E8 /* AudioTranscribeSendOperation.swift in Sources */, F6A317F954DA5B46BFD50E3C /* QRCodeGeneratorWireframe.swift in Sources */, A9C6233FE6A819AAA64C1A35 /* QRCodeReaderProtocols.swift in Sources */, @@ -16152,7 +16221,7 @@ F10B0E1720B4401500528E7A /* GalleryPresenter.swift in Sources */, B7EF8ED9210C71E800E0E981 /* InterpretationType.swift in Sources */, 6D5157D21F30B822002A27DB /* MicrophoneView.swift in Sources */, - 5EEB73D22161CEA100D8ECE6 /* LoginOption.swift in Sources */, + 5EEB73D22161CEA100D8ECE6 /* LoginFlow.swift in Sources */, 260313AF20A0A50D009AC66D /* TranslationService.swift in Sources */, A42D51AD206A361400EEB952 /* cur.swift in Sources */, 8504DEAB206937A2006722AC /* MediaFullWheelItemView.swift in Sources */, @@ -16228,6 +16297,7 @@ A43E67EA206E855600048916 /* BadgeNumberService.swift in Sources */, 4D53FE7454959323B1CCFD96 /* ProfileViewController.swift in Sources */, E79061BC1FBF2D43009FD83A /* ContactTable.swift in Sources */, + 855A4E9E219B336000B6E90B /* AppBundleCredentials.swift in Sources */, E764628C1FCD67AC0091FC2E /* DBModelConvertible.swift in Sources */, 85D66A0620BD963C00FBD803 /* MessagePayloadRendererInput.swift in Sources */, 85D66A2420BD970400FBD803 /* BBCodeParser.swift in Sources */, @@ -16362,6 +16432,7 @@ 26ED2C1A2004276B002DBBE8 /* RepliesCollectionViewDelegate.swift in Sources */, 8514D52220EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift in Sources */, A45F112820B4218D00F45004 /* MessageViewFactory.swift in Sources */, + 850B9DAB219C6EE900EA0CF4 /* PhoneNumberInfo.swift in Sources */, F117871D20ACF1D0007A9A1B /* CameraSettingsService.swift in Sources */, 26245F3F204EF58E00C8D3DD /* BasePresenter.swift in Sources */, 85052E5220D1A62500BCC386 /* StickerPreviewingContent.swift in Sources */, @@ -16428,6 +16499,7 @@ 8580BAF020BD9AAE00239D9D /* ConstraintMaker+Extensions.swift in Sources */, 850A2BB0203584B000D68FDF /* SearchActionsView.swift in Sources */, 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */, + 850B9DAD219C7ADA00EA0CF4 /* LoginOption.swift in Sources */, A4679B8920B2DA550021FE9C /* Array+ChannelSubscriber.swift in Sources */, A4ED79AC20C7056C00A41F67 /* AllChannelsItemsFactory.swift in Sources */, 5E7D5D4C218C6239009B5D8D /* SettingsSetAvatarTVCell.swift in Sources */, @@ -16533,6 +16605,7 @@ F119E67020D24BCF0043A532 /* MultiplePreviewPresenter.swift in Sources */, 269848CE200FB59800590D6F /* StarMessageTable.swift in Sources */, A42D51C5206A361400EEB952 /* History.swift in Sources */, + 850B9DA6219C2B9500EA0CF4 /* AppConfigurationProvider.swift in Sources */, 267BE2AD1FE13AB600C47E18 /* ParticipantsProtocols.swift in Sources */, 268C341121067F1D00F1472A /* AudioLongTranscribeOperation.swift in Sources */, A4ED79B020C8041500A41F67 /* TableViewDataSourceProxy.swift in Sources */, @@ -16728,6 +16801,7 @@ A4A242482060373000B0A804 /* BaseHandler.swift in Sources */, 2600CCBF216D447200EDC9C3 /* OptionallyActionCell.swift in Sources */, 853E595120D6AF59007799B9 /* Desc+Room.swift in Sources */, + 850B9DA3219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift in Sources */, A4679B8820B2DA550021FE9C /* ArrayExtension.swift in Sources */, 4B058F03204EA928004C7D9F /* DAOProtocol.swift in Sources */, A4B544E620EFAECE00EB7B0F /* LinkModel.swift in Sources */, @@ -16782,6 +16856,7 @@ A45F110F20B4218D00F45004 /* ChatInitialMessage.swift in Sources */, C9B8BEFE204DEBD00018748C /* DataDownloadAndUsageMode.swift in Sources */, 2625F29F212463E8007C42B5 /* ProgressIdentifier.swift in Sources */, + 850B9D9F219C131E00EA0CF4 /* AuthResponse.swift in Sources */, A44B4D5220CE9BDF00CA700A /* SettingCellProtocol.swift in Sources */, 0008E9152032D6B7003E316E /* JobExtension+BERT.swift in Sources */, 85D66A0220BD963C00FBD803 /* MentionInfo.swift in Sources */, @@ -17068,6 +17143,7 @@ 8524C4D92177741A003BF374 /* Service+Construct.swift in Sources */, E77764BD1FBDA9B60042541D /* ImageFullWheelItemView.swift in Sources */, B723C627204D86AF00884FFD /* SettingsDataAndStorageProtocols.swift in Sources */, + 855A4E9B219B321000B6E90B /* AuthService.swift in Sources */, 260313A120A0A4BA009AC66D /* DirectableActionCell.swift in Sources */, 6547BE911E492D790E0D4390 /* EditGroupNameInteractor.swift in Sources */, 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */, @@ -17099,6 +17175,7 @@ 267BE2901FDEA0A700C47E18 /* SettingsGroupPresenter.swift in Sources */, FEA655D12167777E00B44029 /* SeedVerificationWalletInput.swift in Sources */, EA9F305BF0215CAC3602D0D9 /* EditGroupPhotoPresenter.swift in Sources */, + 850B9D9C219C117E00EA0CF4 /* SessionStorage.swift in Sources */, 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */, 85D669E620BD956000FBD803 /* UIView+Shadow.swift in Sources */, 896D51F07E2F79C8B5502DBF /* EditGroupPhotoInteractor.swift in Sources */, @@ -17156,7 +17233,7 @@ 8505445720627C7C00E0F2B3 /* HistoryCellModel.swift in Sources */, 2F2A5C12A7202E7834F923DC /* GroupRulesWireframe.swift in Sources */, 2625DBF820EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift in Sources */, - 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */, + 851C6A54218B560B0062B148 /* MQTTFactoryProtocol.swift in Sources */, D3A30AF05BD7C46A9A8C1FC1 /* GroupStorageProtocols.swift in Sources */, 8520040720D4F436007C0036 /* StickerPreviewConfig.swift in Sources */, F1607B1D20B20F7800BDF60A /* GridView.swift in Sources */, @@ -17200,6 +17277,7 @@ 852C3DCD216E34FC00447878 /* TypingSenderService.swift in Sources */, 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */, 26DCB24C2064B9CC001EF0AB /* ContactCellModel.swift in Sources */, + 856A8EFC219C8D7A0004E11E /* AuthenticationType.swift in Sources */, 4B749F06214FEE4F002F3A33 /* VerifyNumberViewController.swift in Sources */, A42D52AF206A53AA00EEB952 /* Room_Spec.swift in Sources */, 26053123212741C2002E1CF1 /* LogOutputWireFrame.swift in Sources */, @@ -17208,6 +17286,7 @@ A7285B8B56BFCA857AD9BA8A /* AddContactByUsernameWireframe.swift in Sources */, 2603139A20A0A4B9009AC66D /* LanguageSelectorTableDelegate.swift in Sources */, 853FB06A2049B193000996C5 /* SupportWireFrame.swift in Sources */, + 855A4EA2219B3A9400B6E90B /* AuthTokenData.swift in Sources */, 853D0F9A20C0514E008C3684 /* UICollectionViewFlowLayout+ItemSize.swift in Sources */, A45F113920B4218D00F45004 /* AvatarViewLayout.swift in Sources */, B16EC832C763628A2EBBD383 /* MapSearchProtocols.swift in Sources */, @@ -17355,6 +17434,7 @@ FB816EF320B5B85900093DCD /* Contact.swift in Sources */, 4BF090C621635F0200DCCA5C /* Message+LinkedId.swift in Sources */, 85458CDB212D6FFE00BA8814 /* String+Split.swift in Sources */, + 850B9DA1219C1E8A00EA0CF4 /* SessionStorage.swift in Sources */, A4AB8E532105EC4B005F9B0C /* TextView.swift in Sources */, FB816EFA20B5B8B000093DCD /* Member.swift in Sources */, 4B8C05902164A9AA0034D8F3 /* ReversableDataSourceTests.swift in Sources */, diff --git a/Nynja/Extensions/Bundle+Keys.swift b/Nynja/Extensions/Bundle+Keys.swift index 5c307e99d..32b391270 100644 --- a/Nynja/Extensions/Bundle+Keys.swift +++ b/Nynja/Extensions/Bundle+Keys.swift @@ -83,4 +83,12 @@ extension Bundle { var googleServerClientId: String { return object(forInfoDictionaryKey: "GoogleServerClientId") as! String } + + private var nynjaAPI: [String: Any] { + return object(forInfoDictionaryKey: "NYNJA_API") as! [String: Any] + } + + var endpoints: [String: Any] { + return nynjaAPI["Endpoints"] as! [String: Any] + } } diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 6c7a8c88c..9810941db 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1494,6 +1494,8 @@ internal extension String { static var authEnterEmailAddressComment: String { return localizable.tr("Localizable", "auth.enter_email_address_comment") } /// Please choose your country code and enter your phone number. static var authEnterPhoneNumberComment: String { return localizable.tr("Localizable", "auth.enter_phone_number_comment") } + /// You have reached the limit of login attempts in 24 hours + static var authLoginAttemptsLimit: String { return localizable.tr("Localizable", "auth.login_attempts_limit") } /// Log in with email static var authLoginWithEmail: String { return localizable.tr("Localizable", "auth.login_with_email") } /// Log in with Facebook @@ -1504,6 +1506,38 @@ internal extension String { static var authLoginWithPhoneNumber: String { return localizable.tr("Localizable", "auth.login_with_phone_number") } /// Welcome to static var authWelcome: String { return localizable.tr("Localizable", "auth.welcome") } + /// Confirm + static var authPopupConfirmAction: String { return localizable.tr("Localizable", "auth.popup.confirm_action") } + /// Please confirm the email you entered is correct + static var authPopupConfirmEmailTitle: String { return localizable.tr("Localizable", "auth.popup.confirm_email_title") } + /// Please confirm the number you entered is correct + static var authPopupConfirmPhoneTitle: String { return localizable.tr("Localizable", "auth.popup.confirm_phone_title") } + /// Modify + static var authPopupModifyAction: String { return localizable.tr("Localizable", "auth.popup.modify_action") } + /// Call me + static var codeConfirmationCall: String { return localizable.tr("Localizable", "code_confirmation.call") } + /// We've sent code to your email + static var codeConfirmationCodeSentToEmail: String { return localizable.tr("Localizable", "code_confirmation.code_sent_to_email") } + /// We've sent code to your phone + static var codeConfirmationCodeSentToPhone: String { return localizable.tr("Localizable", "code_confirmation.code_sent_to_phone") } + /// Incorrect verification code + static var codeConfirmationIncorrectVerificationCode: String { return localizable.tr("Localizable", "code_confirmation.incorrect_verification_code") } + /// Resend code + static var codeConfirmationResendCode: String { return localizable.tr("Localizable", "code_confirmation.resend_code") } + /// You should receive it within %d minutes. + static func codeConfirmationShouldReceiveInMinutes(_ p1: Int) -> String { + return localizable.tr("Localizable", "code_confirmation.should_receive_in_minutes", p1) + } + /// You should receive it within %d second. + static func codeConfirmationShouldReceiveInSecond(_ p1: Int) -> String { + return localizable.tr("Localizable", "code_confirmation.should_receive_in_second", p1) + } + /// You should receive it within %d seconds. + static func codeConfirmationShouldReceiveInSeconds(_ p1: Int) -> String { + return localizable.tr("Localizable", "code_confirmation.should_receive_in_seconds", p1) + } + /// Welcome to + static var codeConfirmationWelcome: String { return localizable.tr("Localizable", "code_confirmation.welcome") } } } diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index cb1939ff6..eaaf20778 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -14,6 +14,8 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt private weak var navigation: UINavigationController? private let serviceFactory: ServiceFactoryProtocol + private var inputConfirmationCallback: ((Bool) -> Void)? + private var selectCountryCallback: ((Result) -> Void)? private var facebookAuthCodeCallback: ((Result) -> Void)? @@ -28,8 +30,9 @@ final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProt let wireframe = AuthWireframe(coordinator: self) let view = wireframe.prepareModule( - dependencies: .init(countriesProvider: serviceFactory.makeCountriesProvider(), - googleAuthService: serviceFactory.makeGoogleAuthService()) + dependencies: .init(authService: serviceFactory.makeAuthService(), + googleAuthService: serviceFactory.makeGoogleAuthService(), + countriesProvider: serviceFactory.makeCountriesProvider()) ) navigation?.pushViewController(view, animated: true) } @@ -58,21 +61,21 @@ extension AuthCoordinator { extension AuthCoordinator { func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { switch state { - case .back: navigation?.popViewController(animated: true) - case .invalidCode: break - case .validCode(let type): handleType(type) + case .back: + navigation?.popViewController(animated: true) + case .invalidCode: + break + case .validCode(let type): + handleType(type) } } private func handleType(_ type: AuthenticationType) { - let view = CreateProfileWireframe(coordinator: self).prepareModule(dependencies: CreateProfileWireframe.Dependencies()) - - navigation?.pushViewController(view, animated: true) - switch type { - case .login: - break case .register: + let view = CreateProfileWireframe(coordinator: self).prepareModule(dependencies: CreateProfileWireframe.Dependencies()) + navigation?.pushViewController(view, animated: true) + case .login: break } } @@ -83,10 +86,14 @@ extension AuthCoordinator { extension AuthCoordinator { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) { switch state { - case .continueLogin(let loginOption): - continueLoginProcess(with: loginOption) + case let .confirmInputData(loginOption, confirmationHandler): + inputConfirmationCallback = confirmationHandler + showConfirmationPopup(loginOption: loginOption) - case .getCountry(let callback): + case let .continueLogin(loginFlow): + continueLoginProcess(with: loginFlow) + + case let .selectCountry(callback): selectCountryCallback = callback let wireframe = SelectCountryWireFrame(coordinator: self) @@ -98,7 +105,7 @@ extension AuthCoordinator { navigation?.pushViewController(view, animated: true) - case .showFacebookAuth(let callback): + case let .showFacebookAuth(callback): let wireframe = FacebookAuthWireframe(coordinator: self) let view = wireframe.prepareModule() @@ -113,42 +120,44 @@ extension AuthCoordinator { } } - private func continueLoginProcess(with loginOption: LoginOption) { - switch loginOption { - case .email, .phoneNumber: - showConfirmationPopup(loginOption: loginOption) - default: + private func continueLoginProcess(with loginFlow: LoginFlow) { + switch loginFlow { + case let .email(email): + let wireframe = CodeConfirmationWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(authType: .email(email)), + dependencies: .init(authService: serviceFactory.makeAuthService()) + ) + navigation?.pushViewController(view, animated: true) + + case let .phoneNumber(numberInfo): + let wireframe = CodeConfirmationWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(authType: .phoneNumber(numberInfo)), + dependencies: .init(authService: serviceFactory.makeAuthService()) + ) + navigation?.pushViewController(view, animated: true) + + case .google: + break + + case .facebook: break } } private func showConfirmationPopup(loginOption: LoginOption) { - let popup = UIAlertController(title: titleForPopup(loginOption: loginOption), message: messageForPopup(loginOption: loginOption), preferredStyle: .alert) + let popup = UIAlertController(title: titleForPopup(loginOption: loginOption), + message: messageForPopup(loginOption: loginOption), + preferredStyle: .alert) - let modify = UIAlertAction(title: "Modify".localized, style: .cancel, handler: nil) - let confirm = UIAlertAction.init(title: "Confirm".localized, style: .default) { [weak self] (action) in - guard let `self` = self else { - return - } - - switch loginOption { - case .email(let email): - let wireframe = CodeConfirmationWireframe(coordinator: self) - let view = wireframe.prepareModule( - parameters: CodeConfirmationWireframe.Parameters(address: email, authType: .email) - ) - - self.navigation?.pushViewController(view, animated: true) - case .phoneNumber(let number): - let wireframe = CodeConfirmationWireframe(coordinator: self) - let view = wireframe.prepareModule( - parameters: CodeConfirmationWireframe.Parameters(address: number, authType: .phoneNumber) - ) - - self.navigation?.pushViewController(view, animated: true) - default: - break - } + let modify = UIAlertAction(title: String.localizable.authPopupModifyAction, style: .cancel) { [weak self] _ in + self?.inputConfirmationCallback?(false) + self?.inputConfirmationCallback = nil + } + let confirm = UIAlertAction.init(title: String.localizable.authPopupConfirmAction, style: .default) { [weak self] _ in + self?.inputConfirmationCallback?(true) + self?.inputConfirmationCallback = nil } [modify, confirm].forEach { popup.addAction($0) } @@ -159,19 +168,18 @@ extension AuthCoordinator { private func titleForPopup(loginOption: LoginOption) -> String { switch loginOption { case .email: - return "Please confirm the email you entered is correct".localized + return String.localizable.authPopupConfirmEmailTitle case .phoneNumber: - return "Please confirm the number you entered is correct".localized - default: - return "" + return String.localizable.authPopupConfirmPhoneTitle } } private func messageForPopup(loginOption: LoginOption) -> String { switch loginOption { - case .email(let email): return email - case .phoneNumber(let number): return "+" + number - default: return "" + case let .email(email): + return email + case let .phoneNumber(number): + return number } } } diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index 7f5ab8b3e..0045aeffd 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -10,7 +10,8 @@ import Foundation protocol AuthWireframeProtocol: class { func selectCountry(completion: @escaping (Result) -> Void) - func continueLogin(loginOption: LoginOption) + func confirmInputData(loginOption: LoginOption, confirmationHandler: @escaping (Bool) -> Void) + func continueLogin(loginFlow: LoginFlow) func showFacebookAuth(completion: @escaping (Result) -> Void) func present(_ viewController: UIViewController) @@ -31,7 +32,7 @@ protocol AuthPresenterProtocol: class { func loginViaFacebook() func loginViaGoogle() func loginViaEmail(_ email: String) - func loginViaPhoneNumber(_ phoneNumber: String) + func loginViaPhoneNumber(_ phoneNumber: String, country: Country) func selectCountry() } @@ -40,12 +41,12 @@ protocol AuthInputInteractorProtocol: class { func loginViaFacebook(code: String) func loginViaGoogle() func loginViaEmail(_ email: String) - func loginViaPhoneNumber(_ phoneNumber: String) + func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) func fetchDefaultCountry() -> Country } protocol AuthOutputInteractorProtocol: class { - func didAuthenticated(with loginOption: LoginOption) - func didReceiveAuthenticationFailure() + func didAuthenticated(with loginFlow: LoginFlow) + func didReceiveAuthenticationFailure(_ error: Error?) } diff --git a/Nynja/Modules/Auth/AuthModule/Entities/LoginFlow.swift b/Nynja/Modules/Auth/AuthModule/Entities/LoginFlow.swift new file mode 100644 index 000000000..e027bebac --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Entities/LoginFlow.swift @@ -0,0 +1,14 @@ +// +// LoginFlow.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum LoginFlow { + case phoneNumber(PhoneNumberInfo) + case email(String) + case facebook + case google +} diff --git a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift index 3004c10c0..d1d020191 100644 --- a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift +++ b/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift @@ -2,15 +2,11 @@ // LoginOption.swift // Nynja // -// Created by Ash on 10/1/18. +// Created by Anton Poltoratskyi on 14.11.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - enum LoginOption { - case phoneNumber(number: String) - case email(email: String) - case facebook - case google + case phoneNumber(String) + case email(String) } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 972620329..71aca41e2 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -14,22 +14,27 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { // MARK: - Services - private let countriesProvider: CountriesProviding + private let authService: AuthService private let googleAuthService: GoogleAuthService + private let countriesProvider: CountriesProviding // MARK: - Init struct Dependencies { let presenter: AuthOutputInteractorProtocol - let countriesProvider: CountriesProviding + let authService: AuthService let googleAuthService: GoogleAuthService + let countriesProvider: CountriesProviding } init(dependencies: Dependencies) { presenter = dependencies.presenter - countriesProvider = dependencies.countriesProvider + authService = dependencies.authService googleAuthService = dependencies.googleAuthService + countriesProvider = dependencies.countriesProvider + + authService.initialize() } @@ -57,16 +62,30 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { default: print("Google unknown error: \(error.localizedDescription)") } - self?.presenter?.didReceiveAuthenticationFailure() + self?.presenter?.didReceiveAuthenticationFailure(error) } } } func loginViaEmail(_ email: String) { - presenter?.didAuthenticated(with: .email(email: email)) + authService.login(by: email) { [weak self] result in + switch result { + case .success: + self?.presenter?.didAuthenticated(with: .email(email)) + case let .failure(error): + self?.presenter?.didReceiveAuthenticationFailure(error) + } + } } - func loginViaPhoneNumber(_ phoneNumber: String) { - presenter?.didAuthenticated(with: .phoneNumber(number: phoneNumber)) + func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) { + authService.login(by: phoneNumberInfo, confirmVia: .sms) { [weak self] result in + switch result { + case .success: + self?.presenter?.didAuthenticated(with: .phoneNumber(phoneNumberInfo)) + case let .failure(error): + self?.presenter?.didReceiveAuthenticationFailure(error) + } + } } } diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index c392213c5..0d407707b 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -13,7 +13,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, private var interactor: AuthInputInteractorProtocol! private var wireframe: AuthWireframeProtocol! - private(set) var loginOption: LoginOption = .phoneNumber(number: "") + private(set) var loginOption: LoginOption = .phoneNumber("") private(set) lazy var selectedCountry: Country = { return interactor.fetchDefaultCountry() @@ -22,11 +22,9 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, func switchLoginOption() { switch loginOption { case .email: - loginOption = .phoneNumber(number: "") + loginOption = .phoneNumber("") case .phoneNumber: - loginOption = .email(email: "") - case .facebook, .google: - break + loginOption = .email("") } } @@ -46,11 +44,22 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } func loginViaEmail(_ email: String) { - interactor.loginViaEmail(email) + let email = email.trimmed() + wireframe.confirmInputData(loginOption: .email(email)) { isConfirmed in + if isConfirmed { + self.interactor.loginViaEmail(email) + } + } } - func loginViaPhoneNumber(_ phoneNumber: String) { - interactor.loginViaPhoneNumber(phoneNumber) + func loginViaPhoneNumber(_ phoneNumber: String, country: Country) { + let numberInfo = PhoneNumberInfo(country: country, number: phoneNumber.replacingOccurrences(of: " ", with: "")) + + wireframe.confirmInputData(loginOption: .phoneNumber(numberInfo.displayString)) { isConfirmed in + if isConfirmed { + self.interactor.loginViaPhoneNumber(numberInfo) + } + } } func selectCountry() { @@ -87,12 +96,13 @@ extension AuthPresenter { extension AuthPresenter { - func didAuthenticated(with loginOption: LoginOption) { - wireframe?.continueLogin(loginOption: loginOption) + func didAuthenticated(with loginFlow: LoginFlow) { + wireframe?.continueLogin(loginFlow: loginFlow) } - func didReceiveAuthenticationFailure() { + func didReceiveAuthenticationFailure(_ error: Error?) { // TODO: handle failure + print("\(#function): error = \(error?.localizedDescription ?? "")") } } diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 53020794a..40d602063 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -275,8 +275,12 @@ private extension AuthViewController { loginView.configure(config: PhoneNumberLoginView.Config( country: country, - countrySelectorAction: { [weak presenter] in presenter?.selectCountry() }, - nextAction: { [weak presenter] in presenter?.loginViaPhoneNumber($0) }) + countrySelectorAction: { [weak presenter] in + presenter?.selectCountry() + }, + nextAction: { [weak presenter] country, number in + presenter?.loginViaPhoneNumber(number, country: country) + }) ) loginView.snp.makeConstraints { (make) in diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift index fc9050c8c..4d704713a 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift @@ -92,8 +92,6 @@ private extension LoginOptionsView { case .phoneNumber: switchLoginButton.setTitle(String.localizable.authLoginWithEmail.uppercased(), for: .normal) switchLoginButton.setImage(UIImage.nynja.iconsGeneralIcEmail.image, for: .normal) - default: - break } } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index f9639623b..e43ab7f1c 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -28,7 +28,7 @@ final class PhoneNumberLoginView: UIView, Configurable { private var country: Country? private var countrySelectorAction: (() -> Void)? - private var nextAction: ((String) -> Void)? + private var nextAction: ((Country, String) -> Void)? // MARK: - Init @@ -50,7 +50,7 @@ extension PhoneNumberLoginView { struct Config { let country: Country let countrySelectorAction: () -> Void - let nextAction: (String) -> Void + let nextAction: (Country, String) -> Void } func configure(config: Config) { @@ -91,8 +91,10 @@ private extension PhoneNumberLoginView { } @objc func next(sender: UIButton) { - let number = (country?.code ?? "") + (phoneNumberTextField.text ?? "") - nextAction?(number) + guard let country = country else { + return + } + nextAction?(country, phoneNumberTextField.text ?? "") } } diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index bdc828e50..5f18247c6 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -22,13 +22,15 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { } struct Dependencies { - let countriesProvider: CountriesProviding + let authService: AuthService let googleAuthService: GoogleAuthService + let countriesProvider: CountriesProviding } enum State { - case continueLogin(loginOption: LoginOption) - case getCountry(callback: (Result) -> Void) + case selectCountry(callback: (Result) -> Void) + case confirmInputData(loginOption: LoginOption, confirmationHandler: (Bool) -> Void) + case continueLogin(loginFlow: LoginFlow) case showFacebookAuth(callback: (Result) -> Void) case present(UIViewController) case dismiss(UIViewController) @@ -41,8 +43,9 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { let view = AuthViewController(dependencies: viewDependencies) let interactorDependencies = AuthInteractor.Dependencies(presenter: presenter, - countriesProvider: dependencies.countriesProvider, - googleAuthService: dependencies.googleAuthService) + authService: dependencies.authService, + googleAuthService: dependencies.googleAuthService, + countriesProvider: dependencies.countriesProvider) let interactor = AuthInteractor(dependencies: interactorDependencies) let presenterDependencies = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) @@ -52,11 +55,16 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { } func selectCountry(completion: @escaping (Result) -> Void) { - coordinator.wireframe(self, didEndWithState: .getCountry(callback: completion)) + coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) + } + + func confirmInputData(loginOption: LoginOption, confirmationHandler: @escaping (Bool) -> Void) { + coordinator.wireframe(self, didEndWithState: .confirmInputData(loginOption: loginOption, + confirmationHandler: confirmationHandler)) } - func continueLogin(loginOption: LoginOption) { - coordinator.wireframe(self, didEndWithState: .continueLogin(loginOption: loginOption)) + func continueLogin(loginFlow: LoginFlow) { + coordinator.wireframe(self, didEndWithState: .continueLogin(loginFlow: loginFlow)) } func showFacebookAuth(completion: @escaping (Result) -> Void) { diff --git a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift index 02eacc517..a94a92bf8 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift @@ -17,17 +17,17 @@ protocol CodeConfirmationWireframeProtocol: class { protocol CodeConfirmationViewProtocol: class where Self: UIViewController { func updateTimerLabel(text: String) func showButtons() + func showHUD() + func hideHUD() } protocol CodeConfirmationPresenterProtocol: NavigationProtocol { - var isCanAskForCall: Bool { get } + var canAskForCall: Bool { get } var address: String { get } var descriptionText: String { get } func viewDidLoad() - - func sendConfirmationCode(code: String, completion: (Result) -> Void) - + func sendConfirmationCode(_ code: String) func resendCode() func askForCall() } @@ -36,11 +36,15 @@ protocol CodeConfirmationInputInteractorProtocol: class { var address: String { get } var authProviderType: AuthProviderType { get } - func sendConfirmationCode(code: String, completion: (Result) -> Void) - + func sendConfirmationCode(_ code: String) func resendCode() func askForCall() } protocol CodeConfirmationOutputInteractorProtocol: class { + func didResendCode() + func didReceiveResendCodeFailure(_ error: Error) + + func didConfirmCode(authenticationType: AuthenticationType) + func didReceiveCodeConfirmationFailure(_ error: Error) } diff --git a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift index 8128b1769..989ef8d5a 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift @@ -9,6 +9,6 @@ import Foundation enum AuthProviderType { - case email - case phoneNumber + case email(String) + case phoneNumber(PhoneNumberInfo) } diff --git a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index 27702ce57..e3e0f99ea 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -8,42 +8,83 @@ import Foundation -final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, SetInjectable { +final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, InitializeInjectable { private weak var presenter: CodeConfirmationOutputInteractorProtocol? - let address: String let authProviderType: AuthProviderType - init(address: String, authProviderType: AuthProviderType) { - self.address = address - self.authProviderType = authProviderType + var address: String { + switch authProviderType { + case let .email(email): + return email + case let .phoneNumber(phoneNumberInfo): + return phoneNumberInfo.displayString + } } - // MARK: - CodeConfirmationInputInteractorProtocol + // MARK: - Services - func sendConfirmationCode(code: String, completion: (Result) -> Void) { - completion(.success(.register)) + private let authService: AuthService + + + // MARK: - Init + + struct Dependencies { + let presenter: CodeConfirmationOutputInteractorProtocol + let authProviderType: AuthProviderType + let authService: AuthService + } + + init(dependencies: Dependencies) { + self.presenter = dependencies.presenter + self.authProviderType = dependencies.authProviderType + self.authService = dependencies.authService + } + + + // MARK: - Interactor Input + + func sendConfirmationCode(_ code: String) { + authService.confirm(code: code, with: "") { [weak self] result in + switch result { + case let .success(response): + self?.presenter?.didConfirmCode(authenticationType: response.authenticationType) + case let .failure(error): + self?.presenter?.didReceiveCodeConfirmationFailure(error) + } + } } func resendCode() { - + switch authProviderType { + case let .email(email): + authService.login(by: email) { [weak self] result in + self?.handleResendCodeResponse(result) + } + case let .phoneNumber(phoneNumberInfo): + authService.login(by: phoneNumberInfo, confirmVia: .sms) { [weak self] result in + self?.handleResendCodeResponse(result) + } + } } func askForCall() { - - } -} - -// MARK: - SetInjectable - -extension CodeConfirmationInteractor { - struct Dependencies { - let presenter: CodeConfirmationOutputInteractorProtocol + guard case let .phoneNumber(phoneNumberInfo) = authProviderType else { + return + } + authService.login(by: phoneNumberInfo, confirmVia: .call) { [weak self] result in + self?.handleResendCodeResponse(result) + } } - func inject(dependencies: CodeConfirmationInteractor.Dependencies) { - presenter = dependencies.presenter + private func handleResendCodeResponse(_ result: Result) { + switch result { + case .success: + presenter?.didResendCode() + case let .failure(error): + presenter?.didReceiveResendCodeFailure(error) + } } } diff --git a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index 9457e5453..527168487 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -17,9 +17,14 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo private var timerValue = 0 { didSet { if timerValue > 60 { - view?.updateTimerLabel(text: "You should receive it within \((timerValue / 60) + 1) minutes.") + let minutesLeft = timerValue / 60 + 1 + view?.updateTimerLabel(text: String.localizable.codeConfirmationShouldReceiveInMinutes(minutesLeft)) + + } else if timerValue == 1 { + view?.updateTimerLabel(text: String.localizable.codeConfirmationShouldReceiveInSecond(timerValue)) + } else { - view?.updateTimerLabel(text: "You should receive it within \(timerValue) seconds.") + view?.updateTimerLabel(text: String.localizable.codeConfirmationShouldReceiveInSeconds(timerValue)) } if timerValue == 0 { @@ -32,11 +37,11 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo private var timer: Timer? - var isCanAskForCall: Bool { - guard let interactor = interactor else { - return false + var canAskForCall: Bool { + if case .phoneNumber = interactor.authProviderType { + return true } - return interactor.authProviderType == .phoneNumber + return false } var address: String { @@ -44,12 +49,12 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } var descriptionText: String { - if interactor.authProviderType == .phoneNumber { - return "We've sent code to your phone".localized - } else { - return "We've sent code to your email".localized + switch interactor.authProviderType { + case .phoneNumber: + return String.localizable.codeConfirmationCodeSentToPhone + case .email: + return String.localizable.codeConfirmationCodeSentToEmail } - } func viewDidLoad() { @@ -65,28 +70,21 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } } - func sendConfirmationCode(code: String, completion: (Result) -> Void) { - interactor.sendConfirmationCode(code: code) { [weak self] result in - switch result { - case let .success(authType): - completion(.success(())) - wireframe?.codeValid(with: authType) - - case let .failure(error): - completion(.failure(error)) - wireframe?.codeInvalid() - } - } + func sendConfirmationCode(_ code: String) { + view?.showHUD() + interactor.sendConfirmationCode(code) } func resendCode() { + view?.showHUD() interactor?.resendCode() } func askForCall() { - guard isCanAskForCall else { + guard canAskForCall else { return } + view?.showHUD() interactor?.askForCall() } @@ -95,6 +93,30 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } } +// MARK: - Interactor Output + +extension CodeConfirmationPresenter { + + func didResendCode() { + view?.hideHUD() + } + + func didReceiveResendCodeFailure(_ error: Error) { + view?.hideHUD() + } + + func didConfirmCode(authenticationType: AuthenticationType) { + view?.hideHUD() + wireframe?.codeValid(with: authenticationType) + } + + func didReceiveCodeConfirmationFailure(_ error: Error) { + view?.hideHUD() + // FIXME: check if it is internal error or real wrong code + wireframe?.codeInvalid() + } +} + // MARK: - SetInjectable extension CodeConfirmationPresenter { diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift index 2196824ba..4097f5342 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -6,8 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - +import UIKit final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, InitializeInjectable { private let viewsFactory: CodeConfirmationViewsFactoryProtocol @@ -65,8 +64,7 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi textFieldsController.add(textFields: [textField1, textField2, textField3, textField4, textField5, textField6]) textFieldsController.allFieldsFilledAction = { [weak self] code in - self?.showHUD() - self?.presenter.sendConfirmationCode(code: code) { _ in self?.hideHUD()} + self?.presenter.sendConfirmationCode(code) } presenter.viewDidLoad() @@ -88,10 +86,18 @@ extension CodeConfirmationViewController { timerLabel.isHidden = true resendCodeButton = viewsFactory.makeResendCodeButton(on: view, target: self, selector: #selector(resendCode(sender:))) - if presenter.isCanAskForCall { + if presenter.canAskForCall { callMeButton = viewsFactory.makeCallMeButton(on: view, top: resendCodeButton!, target: self, selector: #selector(callMe(sender:))) } } + + func showHUD() { + // TODO: implement hud + } + + func hideHUD() { + // TODO: implement hud + } } // MARK: - Actions @@ -119,23 +125,14 @@ extension CodeConfirmationViewController { } } -// MARK: - Private - -private extension CodeConfirmationViewController { - func showHUD() { - - } - - func hideHUD() { - - } -} - // MARK: - Text field controller private extension CodeConfirmationViewController { + final class TextFieldsController: NSObject, UITextFieldDelegate { + private var textFields: [UITextField] = [] + var allFieldsFilledAction: ((_ code: String) -> Void)? func add(textFields: [UITextField]) { @@ -145,7 +142,7 @@ private extension CodeConfirmationViewController { } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if string == "" { + if string.isEmpty { textFields.previous(before: textField)?.becomeFirstResponder() textField.text = string return false diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift b/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift index 1c598c07a..5153d414b 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +import UIKit protocol CodeConfirmationViewsFactoryProtocol { func makeBackButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton @@ -47,7 +47,7 @@ final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { label.font = FontFamily.NotoSans.medium.font(size: 16) label.textColor = UIColor.nynja.white - label.text = "Welcome to".localized + label.text = String.localizable.codeConfirmationWelcome label.snp.makeConstraints { (make) in make.top.equalToSuperview().offset(70) @@ -190,7 +190,7 @@ final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { let button = UIButton() view.addSubview(button) - button.setTitle("Resend code".localized, for: .normal) + button.setTitle(String.localizable.codeConfirmationResendCode, for: .normal) button.setTitleColor(UIColor.nynja.mainRed, for: .normal) button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) @@ -208,7 +208,7 @@ final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { let button = UIButton() view.addSubview(button) - button.setTitle("Call me".localized, for: .normal) + button.setTitle(String.localizable.codeConfirmationCall, for: .normal) button.setTitleColor(UIColor.nynja.mainRed, for: .normal) button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) diff --git a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index 1dc3a59cf..535168fb5 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -21,10 +21,13 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto } struct Parameters { - let address: String let authType: AuthProviderType } + struct Dependencies { + let authService: AuthService + } + enum State { case validCode(type: AuthenticationType) case invalidCode @@ -39,13 +42,13 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto viewsFactory: CodeConfirmationViewsFactory()) ) - let interactor = CodeConfirmationInteractor(address: parameters.address, authProviderType: parameters.authType) - - let interactorDependencies = CodeConfirmationInteractor.Dependencies(presenter: presenter) - interactor.inject(dependencies: interactorDependencies) + let interactor = CodeConfirmationInteractor(dependencies: .init( + presenter: presenter, + authProviderType: parameters.authType, + authService: dependencies.authService) + ) - let presenterDependencies = CodeConfirmationPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) - presenter.inject(dependencies: presenterDependencies) + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) return view } diff --git a/Nynja/Resources/DevAutoTests.xcconfig b/Nynja/Resources/DevAutoTests.xcconfig index 33b38aa95..2393b22d9 100644 --- a/Nynja/Resources/DevAutoTests.xcconfig +++ b/Nynja/Resources/DevAutoTests.xcconfig @@ -20,6 +20,15 @@ ConfServerAddress = 35.198.118.190 ConfServerPort = 80 ConfServerSecure = false AssociatedDomain = applinks:join.dev-eu.nynja.net + GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com + +AUTH_SERVER_HOST = auth.dev-eu.nynja.net +AUTH_SERVER_PORT = 443 +AUTH_SERVER_SECURE = true + +ACCOUNT_SERVER_HOST = account.dev-eu.nynja.net +ACCOUNT_SERVER_PORT = 443 +ACCOUNT_SERVER_SECURE = true diff --git a/Nynja/Resources/DevConfig.xcconfig b/Nynja/Resources/DevConfig.xcconfig index 33b38aa95..2393b22d9 100644 --- a/Nynja/Resources/DevConfig.xcconfig +++ b/Nynja/Resources/DevConfig.xcconfig @@ -20,6 +20,15 @@ ConfServerAddress = 35.198.118.190 ConfServerPort = 80 ConfServerSecure = false AssociatedDomain = applinks:join.dev-eu.nynja.net + GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com + +AUTH_SERVER_HOST = auth.dev-eu.nynja.net +AUTH_SERVER_PORT = 443 +AUTH_SERVER_SECURE = true + +ACCOUNT_SERVER_HOST = account.dev-eu.nynja.net +ACCOUNT_SERVER_PORT = 443 +ACCOUNT_SERVER_SECURE = true diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 0ce4809ab..fc24b6c65 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -2,6 +2,30 @@ + NYNJA_API + + Endpoints + + Auth + + host + $(AUTH_SERVER_HOST) + port + $(AUTH_SERVER_PORT) + secure + $(AUTH_SERVER_SECURE) + + Account + + host + $(ACCOUNT_SERVER_HOST) + port + $(ACCOUNT_SERVER_PORT) + secure + $(ACCOUNT_SERVER_SECURE) + + + AppGroup $(AppGroup) AssociatedDomain diff --git a/Nynja/Resources/PrereleaseConfig.xcconfig b/Nynja/Resources/PrereleaseConfig.xcconfig index 45a953207..e515671dd 100644 --- a/Nynja/Resources/PrereleaseConfig.xcconfig +++ b/Nynja/Resources/PrereleaseConfig.xcconfig @@ -20,6 +20,15 @@ ConfServerAddress = call.staging.nynja.net ConfServerPort = 443 ConfServerSecure = true AssociatedDomain = applinks:join.staging.nynja.net + GoogleClientId = 13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com + +AUTH_SERVER_HOST = auth.dev-eu.nynja.net +AUTH_SERVER_PORT = 443 +AUTH_SERVER_SECURE = true + +ACCOUNT_SERVER_HOST = account.dev-eu.nynja.net +ACCOUNT_SERVER_PORT = 443 +ACCOUNT_SERVER_SECURE = true diff --git a/Nynja/Resources/ReleaseConfig.xcconfig b/Nynja/Resources/ReleaseConfig.xcconfig index 6dd468399..e8fe297d4 100644 --- a/Nynja/Resources/ReleaseConfig.xcconfig +++ b/Nynja/Resources/ReleaseConfig.xcconfig @@ -20,6 +20,15 @@ ConfServerAddress = call.nynja.net ConfServerPort = 443 ConfServerSecure = true AssociatedDomain = applinks:join.nynja.net + GoogleClientId = 13807320472-002cda3sovo7hef61dm4niekkm8jdaf1.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-002cda3sovo7hef61dm4niekkm8jdaf1 GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com + +AUTH_SERVER_HOST = auth.dev-eu.nynja.net +AUTH_SERVER_PORT = 443 +AUTH_SERVER_SECURE = true + +ACCOUNT_SERVER_HOST = account.dev-eu.nynja.net +ACCOUNT_SERVER_PORT = 443 +ACCOUNT_SERVER_SECURE = true diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index c2507da65..b75e8f693 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -1005,6 +1005,26 @@ "auth.email_placeholder" = "Email"; +"auth.popup.confirm_action" = "Confirm"; +"auth.popup.modify_action" = "Modify"; + +"auth.popup.confirm_phone_title" = "Please confirm the number you entered is correct"; +"auth.popup.confirm_email_title" = "Please confirm the email you entered is correct"; + +"auth.login_attempts_limit" = "You have reached the limit of login attempts in 24 hours"; + +// MARK: Code Confirmation + +"code_confirmation.welcome" = "Welcome to"; +"code_confirmation.resend_code" = "Resend code"; +"code_confirmation.call" = "Call me"; +"code_confirmation.code_sent_to_phone" = "We've sent code to your phone"; +"code_confirmation.code_sent_to_email" = "We've sent code to your email"; +"code_confirmation.should_receive_in_second" = "You should receive it within %d second."; +"code_confirmation.should_receive_in_seconds" = "You should receive it within %d seconds."; +"code_confirmation.should_receive_in_minutes" = "You should receive it within %d minutes."; +"code_confirmation.incorrect_verification_code" = "Incorrect verification code"; + // MARK: Create Profile "terms_of_use" = "terms of use"; diff --git a/Nynja/SDK/App/AppBundleCredentials.swift b/Nynja/SDK/App/AppBundleCredentials.swift new file mode 100644 index 000000000..1d32fc1d9 --- /dev/null +++ b/Nynja/SDK/App/AppBundleCredentials.swift @@ -0,0 +1,13 @@ +// +// AppBundleCredentials.swift +// Nynja +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct AppBundleCredentials { + let deviceId: String + let instanceId: String + let bundleId: String +} diff --git a/Nynja/SDK/App/AppConfigurationProvider.swift b/Nynja/SDK/App/AppConfigurationProvider.swift new file mode 100644 index 000000000..40c12cdda --- /dev/null +++ b/Nynja/SDK/App/AppConfigurationProvider.swift @@ -0,0 +1,49 @@ +// +// AppConfigurationProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AppConfigurationProvider: class { + var sdkCredentials: AppBundleCredentials { get } + var authServerConfig: ServerConfig { get } + var accountServerConfig: ServerConfig { get } +} + +final class AppConfigurationProviderImpl: AppConfigurationProvider { + + private let bundle = Bundle.main + + var sdkCredentials: AppBundleCredentials { + return AppBundleCredentials(deviceId: UIDevice.current.persistentIdentifier, + instanceId: UIDevice.current.persistentIdentifier, + bundleId: bundle.bundleIdentifier) + } + + var authServerConfig: ServerConfig { + return serverConfig(from: endpointConfig(forKey: "Auth")) + } + + var accountServerConfig: ServerConfig { + return serverConfig(from: endpointConfig(forKey: "Account")) + } + + private func serverConfig(from dictionary: [String: Any]) -> ServerConfig { + return ServerConfig( + host: dictionary["host"] as! String, + port: Int32(dictionary["port"] as! String)!, + isSecure: Bool(dictionary["secure"] as! String)! + ) + } + + private func endpointConfig(forKey key: String) -> [String: Any] { + guard let config = bundle.endpoints[key] as? [String: Any] else { + fatalError("Couldn't found config for key: \(key)") + } + return config + } +} diff --git a/Nynja/SDK/App/ServerConfig.swift b/Nynja/SDK/App/ServerConfig.swift new file mode 100644 index 000000000..b90d4f0e0 --- /dev/null +++ b/Nynja/SDK/App/ServerConfig.swift @@ -0,0 +1,13 @@ +// +// ServerConfig.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct ServerConfig { + let host: String + let port: Int32 + let isSecure: Bool +} diff --git a/Nynja/SDK/Auth/AuthConfirmationType.swift b/Nynja/SDK/Auth/AuthConfirmationType.swift new file mode 100644 index 000000000..024d4dee8 --- /dev/null +++ b/Nynja/SDK/Auth/AuthConfirmationType.swift @@ -0,0 +1,12 @@ +// +// AuthConfirmationType.swift +// Nynja +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum AuthConfirmationType { + case sms + case call +} diff --git a/Nynja/SDK/Auth/AuthResponse.swift b/Nynja/SDK/Auth/AuthResponse.swift new file mode 100644 index 000000000..c3c318ebb --- /dev/null +++ b/Nynja/SDK/Auth/AuthResponse.swift @@ -0,0 +1,13 @@ +// +// AuthResponse.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct AuthResponse { + let accountId: String + let tokenData: AuthTokenData + let authenticationType: AuthenticationType +} diff --git a/Nynja/SDK/Auth/AuthService.swift b/Nynja/SDK/Auth/AuthService.swift new file mode 100644 index 000000000..c7ce4edd1 --- /dev/null +++ b/Nynja/SDK/Auth/AuthService.swift @@ -0,0 +1,215 @@ +// +// AuthService.swift +// Nynja +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaSDK + +protocol AuthService: class { + typealias LoginCompletion = (Result) -> Void + typealias CodeConfirmationCompletion = (Result) -> Void + typealias RefreshTokenCompletion = (Result) -> Void + + func initialize() + + func login(by email: String, completion: @escaping LoginCompletion) + + func login(by phoneNumber: PhoneNumberInfo, + confirmVia authConfirmationType: AuthConfirmationType, + completion: @escaping LoginCompletion) + + func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) + + func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) +} + +final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLoginManagerDelegate { + + // MARK: - Dependencies + + private let communicator: NynjaCommunicator + + private let loginManager: LoginManager + + private let sessionStorage: SessionStorage + + private let appConfigurationProvider: AppConfigurationProvider + + private let processingQueue: DispatchQueue + + + // MARK: - Handlers + + private var loginByPhoneCompletion: LoginCompletion? + private var loginByEmailCompletion: LoginCompletion? + private var confirmCodeCompletion: CodeConfirmationCompletion? + private var refreshTokenCompletion: RefreshTokenCompletion? + + + // MARK: - Init + + struct Dependencies { + let communicator: NynjaCommunicator + let loginManager: LoginManager + let sessionStorage: SessionStorage + let appConfigurationProvider: AppConfigurationProvider + let processingQueue: DispatchQueue = .main + } + + init(dependencies: Dependencies) { + communicator = dependencies.communicator + loginManager = dependencies.loginManager + sessionStorage = dependencies.sessionStorage + appConfigurationProvider = dependencies.appConfigurationProvider + processingQueue = dependencies.processingQueue + + super.init() + + loginManager.delegate = self + } + + + // MARK: - API + + func initialize() { + setupAuthServer() + initializeSDK() + } + + private func setupAuthServer() { + let config = appConfigurationProvider.authServerConfig + communicator.setAuthServerAddress(config.host, andPort: config.port, secure: config.isSecure) + } + + private func initializeSDK() { + let credentials = appConfigurationProvider.sdkCredentials + + loginManager.initialize(withDeviceId: credentials.deviceId, + withInstanceId: credentials.instanceId, + withAppClass: String(describing: type(of: self)), + withOrgId: credentials.bundleId) + } + + func login(by email: String, completion: @escaping LoginCompletion) { + loginByEmailCompletion = completion + loginManager.sendLogin(byEmail: email, withAppToken: sessionStorage.appToken) + } + + func login(by numberInfo: PhoneNumberInfo, + confirmVia authConfirmationType: AuthConfirmationType, + completion: @escaping LoginCompletion) { + + loginByPhoneCompletion = completion + + let country = numberInfo.country + let numberFormat = "\(country.ISO):\(country.code)\(numberInfo.number)" + + loginManager.sendLogin(byPhone: numberFormat, + withAppToken: sessionStorage.appToken, + withSendTokenVia: authConfirmationType.sdkValue) + } + + func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) { + confirmCodeCompletion = completion + loginManager.confirmCode(code, withCredential: socialToken) + } + + func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) { + refreshTokenCompletion = completion + loginManager.refreshAccessToken(accessToken) + } + + + // MARK: - Delegate + + func sendLogin(byEmailDidFinish error: Error?) { + processingQueue.async { + let completion = self.loginByEmailCompletion + self.loginByEmailCompletion = nil + + if let error = error { + completion?(.failure(error)) + } else { + completion?(.success(())) + } + } + } + + func sendLogin(byPhoneDidFinish error: Error?) { + processingQueue.async { + let completion = self.loginByPhoneCompletion + self.loginByPhoneCompletion = nil + + if let error = error { + completion?(.failure(error)) + } else { + completion?(.success(())) + } + } + } + + func confirmCodeDidFinish(withAccoutnId accountId: String, + withAccessToken accessToken: String, + withRefreshToken refreshToken: String, + withExpiration expiration: NSNumber?, + isPendingAccount pending: Bool, + withError error: Error?) { + processingQueue.async { + let completion = self.confirmCodeCompletion + self.confirmCodeCompletion = nil + + if let error = error { + completion?(.failure(error)) + } else { + let tokenData = self.makeTokenData(accessToken: accessToken, refreshToken: refreshToken, expiration: expiration) + + let response = AuthResponse(accountId: accountId, + tokenData: tokenData, + authenticationType: pending ? .register : .login) + + completion?(.success(response)) + } + } + } + + func refreshTokenDidFinish(withAccessToken accessToken: String, + withRefreshToken refreshToken: String, + withExpiration expiration: NSNumber?, + withError error: Error?) { + processingQueue.async { + let completion = self.refreshTokenCompletion + self.refreshTokenCompletion = nil + + if let error = error { + completion?(.failure(error)) + } else { + let tokenData = self.makeTokenData(accessToken: accessToken, refreshToken: refreshToken, expiration: expiration) + completion?(.success(tokenData)) + } + } + } + + private func makeTokenData(accessToken: String, refreshToken: String, expiration: NSNumber?) -> AuthTokenData { + return AuthTokenData(accessToken: accessToken, + refreshToken: refreshToken, + expiration: expiration?.int64Value) + } +} + +// MARK: - Extensions + +private extension AuthConfirmationType { + + var sdkValue: NYNVerifyTokenSendMethod { + switch self { + case .sms: + return .VTSM_SMS + case .call: + return .VTSM_CALL + } + } +} diff --git a/Nynja/SDK/Auth/AuthTokenData.swift b/Nynja/SDK/Auth/AuthTokenData.swift new file mode 100644 index 000000000..ca22a319b --- /dev/null +++ b/Nynja/SDK/Auth/AuthTokenData.swift @@ -0,0 +1,13 @@ +// +// AuthTokenData.swift +// Nynja +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct AuthTokenData { + let accessToken: String + let refreshToken: String + let expiration: Int64? +} diff --git a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift b/Nynja/SDK/Auth/AuthenticationType.swift similarity index 64% rename from Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift rename to Nynja/SDK/Auth/AuthenticationType.swift index 0382d5f1d..095c711d8 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthType.swift +++ b/Nynja/SDK/Auth/AuthenticationType.swift @@ -1,13 +1,11 @@ // -// AuthType.swift +// AuthenticationType.swift // Nynja // -// Created by Ash on 10/11/18. +// Created by Anton Poltoratskyi on 14.11.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - enum AuthenticationType { case register case login diff --git a/Nynja/SDK/Auth/PhoneNumberInfo.swift b/Nynja/SDK/Auth/PhoneNumberInfo.swift new file mode 100644 index 000000000..ca5f622a3 --- /dev/null +++ b/Nynja/SDK/Auth/PhoneNumberInfo.swift @@ -0,0 +1,33 @@ +// +// PhoneNumberInfo.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct PhoneNumberInfo { + let country: Country + + /// phone number without countrycode whitespaces and '+' sign. + /// - Example: "+380 97 123 12 12" -> "971231212" + let number: String + + var displayString: String { + return "+\(country.code) \(formattedNumber(number, with: country.placeHolder))" + } + + private func formattedNumber(_ number: String, with mask: String?) -> String { + let maskLength = mask?.replacingOccurrences(of: " ", with: "").count + guard let mask = mask, number.count == maskLength else { + return number + } + var temp = number + + return mask.reduce(into: "") { result, char in + result.append(char == " " ? " " : temp.removeFirst()) + } + } +} diff --git a/Nynja/SDK/Session/SessionStorage.swift b/Nynja/SDK/Session/SessionStorage.swift new file mode 100644 index 000000000..ddceccc67 --- /dev/null +++ b/Nynja/SDK/Session/SessionStorage.swift @@ -0,0 +1,11 @@ +// +// SessionStorage.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol SessionStorage: class { + var appToken: String { get } +} diff --git a/Nynja/Services/NynjaCalls/NynjaCommunicatorService.swift b/Nynja/Services/NynjaCalls/NynjaCommunicatorService.swift index 39a8fb5cf..45fc6cf4f 100644 --- a/Nynja/Services/NynjaCalls/NynjaCommunicatorService.swift +++ b/Nynja/Services/NynjaCalls/NynjaCommunicatorService.swift @@ -635,6 +635,14 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } } } + + func callDidPauseAudio(_ call: NYNCall) { + // FIXME: merge + } + + func callDidResumeAudio(_ call: NYNCall) { + // FIXME: merge + } //MARK: NYNCallManagerDelegate func createConference(withRequest requestId: String, didSucceedWithId conferenceId: String) { diff --git a/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift similarity index 70% rename from Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift rename to Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift index 48fa93797..a60e24ba3 100644 --- a/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift @@ -1,5 +1,5 @@ // -// MQTTHandlerFactoryProtocol.swift +// MQTTFactoryProtocol.swift // Nynja // // Created by Anton Poltoratskyi on 01.11.2018. @@ -8,6 +8,6 @@ import Foundation -protocol MQTTHandlerFactoryProtocol: class { +protocol MQTTFactoryProtocol: class { func makeTypingHandler() -> TypingHandler } diff --git a/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift new file mode 100644 index 000000000..55db5e650 --- /dev/null +++ b/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift @@ -0,0 +1,13 @@ +// +// MobileSDKFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import NynjaSDK + +protocol MobileSDKFactoryProtocol: class { + func makeLoginManager() -> LoginManager +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index d94925967..968011c8f 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaSDK final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { @@ -54,10 +55,22 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return HistoryRequestModelFactory() } + func makeAuthService() -> AuthService { + let dependencies = AuthServiceImpl.Dependencies(communicator: makeCommunicator(), + loginManager: makeLoginManager(), + sessionStorage: makeStorageService(), + appConfigurationProvider: makeAppConfigurationProvider()) + return AuthServiceImpl(dependencies: dependencies) + } + func makeGoogleAuthService() -> GoogleAuthService { return GoogleAuthServiceImpl() } + func makeAppConfigurationProvider() -> AppConfigurationProvider { + return AppConfigurationProviderImpl() + } + func makeTypingProvider() -> TypingProvider { return TypingProviderImpl.shared } @@ -150,3 +163,16 @@ extension ServiceFactory { return TypingHandler.shared } } + +// MARK: - SDK Services + +extension ServiceFactory { + + func makeCommunicator() -> NynjaCommunicator { + return NynjaCommunicator.sharedInstance() + } + + func makeLoginManager() -> LoginManager { + return makeCommunicator().getLoginManager() + } +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift index 953f7344c..25e2ce876 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTHandlerFactoryProtocol { +protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtocol, MobileSDKFactoryProtocol { func makeMessageSendingService() -> MessageSendingServiceProtocol func makeResourceManager() -> ResourceManagerProtocol func makeMessageFactory() -> MessageFactoryProtocol @@ -20,7 +20,9 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTHandlerFactor func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol + func makeAuthService() -> AuthService func makeGoogleAuthService() -> GoogleAuthService + func makeAppConfigurationProvider() -> AppConfigurationProvider func makeTypingProvider() -> TypingProvider func makeContactsProvider() -> ContactsProviding diff --git a/Nynja/StorageService+UserInfo.swift b/Nynja/StorageService+UserInfo.swift index b520bbda0..72de8b2c6 100644 --- a/Nynja/StorageService+UserInfo.swift +++ b/Nynja/StorageService+UserInfo.swift @@ -8,7 +8,7 @@ import Foundation -extension StorageService: UserInfo { +extension StorageService: UserInfo, SessionStorage { private var encoding: String.Encoding { return .utf8 @@ -74,6 +74,10 @@ extension StorageService: UserInfo { set { set(newValue, forId: .wasRun) } } + var appToken: String { + return "" + } + func setupAuth(clientId: String, token: String) { self.clientId = clientId self.token = token diff --git a/Podfile b/Podfile index 42d25f890..1dd2a3184 100644 --- a/Podfile +++ b/Podfile @@ -40,7 +40,8 @@ def commonPodsForNynja pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' pod 'JTAppleCalendar', '= 7.1.5' - pod 'NynjaSDK', '= 1.7.1' +# pod 'NynjaSDK', '= 1.7.1' + pod 'NynjaSDK-MultiAcc', '= 0.5.3' pod 'CryptoSwift', '= 0.10.0' diff --git a/Podfile.lock b/Podfile.lock index 9f36491a7..cc146c23c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -81,7 +81,7 @@ PODS: - MaterialComponents/private/Application - MDFTextAccessibility (1.2.0) - MulticastDelegateSwift (2.1.1) - - NynjaSDK (1.7.1) + - NynjaSDK-MultiAcc (0.5.3) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -115,7 +115,7 @@ DEPENDENCIES: - libPhoneNumber-iOS (= 0.9.13) - MaterialComponents/FlexibleHeader (= 55.3.0) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK (= 1.7.1) + - NynjaSDK-MultiAcc (= 0.5.3) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.0.0) @@ -159,7 +159,7 @@ SPEC REPOS: - SwiftyTimer - TestFairy https://nynjagroup.jfrog.io/nynjagroup/api/pods/cocoapods-local: - - NynjaSDK + - NynjaSDK-MultiAcc EXTERNAL SOURCES: CocoaLumberjack: @@ -200,7 +200,7 @@ SPEC CHECKSUMS: MaterialComponents: 915f4e844400a35db3ea4c710a9af40aa8bcb093 MDFTextAccessibility: 94098925e0853551c5a311ce7c1ecefbe297cdb6 MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK: 77eed21eba9e95484230fc0edf1ae9cd00a30e6b + NynjaSDK-MultiAcc: 41bf0b025519ecd0433cf84c854eec61aa493e63 QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: a42d492c16e80209130a3379f73596c3454b7694 @@ -209,6 +209,6 @@ SPEC CHECKSUMS: SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: b8798c400dc98c68f9a2fbc734665cfa4fdc24d4 +PODFILE CHECKSUM: 1827ea38496d552cee076e6c699298204d21257c COCOAPODS: 1.5.3 -- GitLab From bf604fa47a460ab1c9ba46423695b175fbad77c4 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 16 Nov 2018 00:35:09 +0200 Subject: [PATCH 105/138] Remove AuthViewsFactory --- Nynja.xcodeproj/project.pbxproj | 12 -- .../AuthModule/View/AuthViewController.swift | 6 +- .../View/Subviews/EmailLoginView.swift | 4 +- .../View/Subviews/LoginOptionsView.swift | 103 ++++++++++++++---- .../View/ViewsFactory/AuthViewsFactory.swift | 90 --------------- .../AuthModule/Wireframe/AuthWireframe.swift | 17 ++- 6 files changed, 94 insertions(+), 138 deletions(-) delete mode 100644 Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 0da748a17..5c8703a69 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -736,7 +736,6 @@ 5BC1D38420D3B670002A44B3 /* CallCreatorMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */; }; 5C468A609C445962C0D19DD3 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D6900257B1AB2CA0BC834EB /* HistoryViewController.swift */; }; 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */; }; - 5E07BC3D216DFD08000E4558 /* AuthViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */; }; 5E07BC40216E09F0000E4558 /* CodeConfirmationViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */; }; 5E07BC4D216F64EC000E4558 /* CreateProfileProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */; }; 5E07BC4F216F659E000E4558 /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */; }; @@ -3060,7 +3059,6 @@ 5BC1D38020D3B54B002A44B3 /* CallInfoViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInfoViewLayout.swift; sourceTree = ""; }; 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallCreatorMediator.swift; sourceTree = ""; }; 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryInteractor.swift; sourceTree = ""; }; - 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewsFactory.swift; sourceTree = ""; }; 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationViewsFactory.swift; sourceTree = ""; }; 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileProtocols.swift; sourceTree = ""; }; 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = ""; }; @@ -7467,14 +7465,6 @@ path = View; sourceTree = ""; }; - 5E07BC3B216DFCFA000E4558 /* ViewsFactory */ = { - isa = PBXGroup; - children = ( - 5E07BC3C216DFD08000E4558 /* AuthViewsFactory.swift */, - ); - path = ViewsFactory; - sourceTree = ""; - }; 5E07BC3E216E09DF000E4558 /* ViewsFactory */ = { isa = PBXGroup; children = ( @@ -7755,7 +7745,6 @@ 5EEB73C1216199DE00D8ECE6 /* View */ = { isa = PBXGroup; children = ( - 5E07BC3B216DFCFA000E4558 /* ViewsFactory */, 5EEB73CE2161CDF700D8ECE6 /* Subviews */, 5EEB73CC2161CC8A00D8ECE6 /* AuthViewController.swift */, ); @@ -16131,7 +16120,6 @@ 8560C4C6218997DD006635AE /* ChatStatus.swift in Sources */, F1A9FA3590CC1F834B727955 /* AddContactPresenter.swift in Sources */, 6DD72F601F1547AC008CFF83 /* GCD.swift in Sources */, - 5E07BC3D216DFD08000E4558 /* AuthViewsFactory.swift in Sources */, A49CC1D820E4AB2C00879D41 /* DisplayModeConfigFactory.swift in Sources */, FEA655FC2167777F00B44029 /* TransferDetailsViewController.swift in Sources */, 859C429F2056829300AE3797 /* NotificationSettings.swift in Sources */, diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 40d602063..882a42d9a 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -12,8 +12,6 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn private let presenter: AuthPresenterProtocol - private let viewsFactory: AuthViewsFactoryProtocol - // MARK: - Views @@ -107,7 +105,7 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn }() private lazy var bottomView: LoginOptionsView = { - let bottomView = LoginOptionsView(viewsFactory: viewsFactory) + let bottomView = LoginOptionsView() bottomView.configure(config: LoginOptionsView.Config( loginOption: presenter.loginOption, @@ -146,12 +144,10 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn struct Dependencies { let presenter: AuthPresenterProtocol - let viewsFactory: AuthViewsFactoryProtocol } init(dependencies: Dependencies) { presenter = dependencies.presenter - viewsFactory = dependencies.viewsFactory super.init(nibName: nil, bundle: nil) } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index 3ea535b0b..366b1ac32 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -65,7 +65,7 @@ extension EmailLoginView { private extension EmailLoginView { @objc func next(sender: UIButton) { - nextAction?(inputField.text ?? "") + nextAction?(inputField.text) } } @@ -140,6 +140,8 @@ private extension EmailLoginView { button.textColor = UIColor.nynja.white button.setTitle(String.localizable.next.uppercased(), for: .normal) + button.addTarget(target, action: #selector(next(sender:)), for: .touchUpInside) + addSubview(button) button.snp.makeConstraints { maker in maker.height.equalTo(44) diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift index 4d704713a..b506867c0 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift @@ -9,28 +9,23 @@ import UIKit final class LoginOptionsView: UIView, Configurable { - private let viewsFactory: AuthViewsFactoryProtocol - private lazy var switchLoginButton: UIButton = viewsFactory.makeSwitchLoginButton(on: self, - bottom: loginWithFacebook, - target: self, - selector: #selector(switchLogin(sender:))) - private lazy var loginWithFacebook: UIButton = viewsFactory.makeLoginWithFacebookButton(on: self, - bottom: loginWithGoogle, - target: self, - selector: #selector(loginWithFacebook(sender:))) - private lazy var loginWithGoogle: UIButton = viewsFactory.makeLoginWithGoogleButton(on: self, - target: self, - selector: #selector(loginWithGoogle(sender:))) + // MARK: - Views + + private lazy var switchLoginButton = makeSwitchLoginButton() + private lazy var loginWithFacebook = makeLoginWithFacebookButton() + private lazy var loginWithGoogle = makeLoginWithGoogleButton() private var switchLoginAction: (() -> LoginOption)? private var facebookLoginAction: (() -> Void)? private var googleLoginAction: (() -> Void)? - init(viewsFactory: AuthViewsFactoryProtocol) { - self.viewsFactory = viewsFactory - - super.init(frame: CGRect.zero) + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + switchLoginButton.isHidden = false } required init?(coder aDecoder: NSCoder) { @@ -78,11 +73,6 @@ private extension LoginOptionsView { @objc func loginWithGoogle(sender: UIButton) { googleLoginAction?() } -} - -// MARK: - Private - -private extension LoginOptionsView { func updateSwitchButton(loginOption: LoginOption) { switch loginOption { @@ -95,3 +85,74 @@ private extension LoginOptionsView { } } } + +// MARK: - Layout + +private extension LoginOptionsView { + + func makeSwitchLoginButton() -> UIButton { + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) + + button.imagePadding = 8 + button.textColor = UIColor.nynja.white + + button.addTarget(self, action: #selector(switchLogin(sender:)), for: .touchUpInside) + + addSubview(button) + button.snp.makeConstraints { maker in + maker.top.equalToSuperview() + maker.bottom.equalTo(loginWithFacebook.snp.top).offset(-16) + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.height.equalTo(44) + } + + return button + } + + func makeLoginWithFacebookButton() -> UIButton { + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) + + button.imagePadding = 8 + button.defaultColor = UIColor.nynja.facebookBackground + button.highlightedColor = UIColor.nynja.facebookHighlighted + button.textColor = UIColor.nynja.white + + button.setTitle(String.localizable.authLoginWithFacebook.uppercased(), for: .normal) + button.setImage(UIImage.nynja.icFacebook.image, for: .normal) + button.addTarget(self, action: #selector(loginWithFacebook(sender:)), for: .touchUpInside) + + addSubview(button) + button.snp.makeConstraints { maker in + maker.bottom.equalTo(loginWithGoogle.snp.top).offset(-16) + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.height.equalTo(44) + } + + return button + } + + func makeLoginWithGoogleButton() -> UIButton { + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) + + button.imagePadding = 8 + button.defaultColor = UIColor.nynja.white + button.highlightedColor = UIColor.nynja.whiteHighlighted + button.textColor = UIColor.nynja.darkLight + + button.setTitle(String.localizable.authLoginWithGoogle.uppercased(), for: .normal) + button.setImage(UIImage.nynja.iconsGeneralIcGoogle.image, for: .normal) + button.addTarget(self, action: #selector(loginWithGoogle(sender:)), for: .touchUpInside) + + addSubview(button) + button.snp.makeConstraints { maker in + maker.bottom.equalToSuperview() + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.height.equalTo(44) + } + + return button + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift b/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift deleted file mode 100644 index 7573da25c..000000000 --- a/Nynja/Modules/Auth/AuthModule/View/ViewsFactory/AuthViewsFactory.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// AuthViewsFactory.swift -// Nynja -// -// Created by Ash on 10/10/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation -import NynjaUIKit - -protocol AuthViewsFactoryProtocol { - - // MARK: - Login Options View - - func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeLoginWithFacebookButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeSwitchLoginButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton -} - -final class AuthViewsFactory: AuthViewsFactoryProtocol { - - // MARK: - Login Options View - - func makeLoginWithGoogleButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) - view.addSubview(button) - - button.imagePadding = 8 - button.defaultColor = UIColor.nynja.white - button.highlightedColor = UIColor.nynja.whiteHighlighted - button.textColor = UIColor.nynja.darkLight - - button.setTitle(String.localizable.authLoginWithGoogle.uppercased(), for: .normal) - button.setImage(UIImage.nynja.iconsGeneralIcGoogle.image, for: .normal) - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { make in - make.bottom.equalToSuperview() - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - } - - return button - } - - func makeLoginWithFacebookButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) - view.addSubview(button) - - button.imagePadding = 8 - button.defaultColor = UIColor.nynja.facebookBackground - button.highlightedColor = UIColor.nynja.facebookHighlighted - button.textColor = UIColor.nynja.white - - button.setTitle(String.localizable.authLoginWithFacebook.uppercased(), for: .normal) - button.setImage(UIImage.nynja.icFacebook.image, for: .normal) - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { make in - make.bottom.equalTo(bottom.snp.top).offset(-16) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - } - - return button - } - - func makeSwitchLoginButton(on view: UIView, bottom: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 20) - view.addSubview(button) - - button.imagePadding = 8 - button.textColor = UIColor.nynja.white - - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { make in - make.top.equalToSuperview() - make.bottom.equalTo(bottom.snp.top).offset(-16) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.height.equalTo(44) - } - - return button - } -} diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 5f18247c6..8f3354b35 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -39,17 +39,16 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = AuthPresenter() - let viewDependencies = AuthViewController.Dependencies(presenter: presenter, viewsFactory: AuthViewsFactory()) - let view = AuthViewController(dependencies: viewDependencies) + let view = AuthViewController(dependencies: .init(presenter: presenter)) - let interactorDependencies = AuthInteractor.Dependencies(presenter: presenter, - authService: dependencies.authService, - googleAuthService: dependencies.googleAuthService, - countriesProvider: dependencies.countriesProvider) - let interactor = AuthInteractor(dependencies: interactorDependencies) + let interactor = AuthInteractor(dependencies: .init( + presenter: presenter, + authService: dependencies.authService, + googleAuthService: dependencies.googleAuthService, + countriesProvider: dependencies.countriesProvider) + ) - let presenterDependencies = AuthPresenter.Dependencies(view: view, interactor: interactor, wireframe: self) - presenter.inject(dependencies: presenterDependencies) + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) return view } -- GitLab From 2cd4d5d10e5b4b008337d2286b913d16475c4df6 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 16 Nov 2018 11:02:39 +0200 Subject: [PATCH 106/138] Fixed all other UI issues on Auth screen. --- Nynja.xcodeproj/project.pbxproj | 12 ++ .../Entities/EmailTextController.swift | 32 ++++ .../Entities/PhoneNumberTextController.swift | 110 ++++++++++++ .../Auth/AuthModule/Entities/Validator.swift | 22 +++ .../AuthModule/View/AuthViewController.swift | 96 +++++++--- .../View/Subviews/EmailLoginView.swift | 103 ++--------- .../View/Subviews/PhoneNumberLoginView.swift | 165 ++---------------- 7 files changed, 276 insertions(+), 264 deletions(-) create mode 100644 Nynja/Modules/Auth/AuthModule/Entities/EmailTextController.swift create mode 100644 Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift create mode 100644 Nynja/Modules/Auth/AuthModule/Entities/Validator.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 5c8703a69..309b52ece 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -933,6 +933,9 @@ 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */; }; 851C6A54218B560B0062B148 /* MQTTFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */; }; 851EBD7F20B418890065C644 /* StickersInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EBD7E20B418890065C644 /* StickersInputView.swift */; }; + 851FFA66219EAF980015F073 /* EmailTextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851FFA65219EAF980015F073 /* EmailTextController.swift */; }; + 851FFA68219EAFBF0015F073 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851FFA67219EAFBF0015F073 /* Validator.swift */; }; + 851FFA6A219EB29A0015F073 /* PhoneNumberTextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */; }; 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F520D4194A007C0036 /* DBRecentSticker.swift */; }; 852003F820D419E9007C0036 /* RecentStickerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F720D419E9007C0036 /* RecentStickerTable.swift */; }; 852003FA20D459E9007C0036 /* BertBinConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F920D459E9007C0036 /* BertBinConvertible.swift */; }; @@ -3255,6 +3258,9 @@ 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceFactoryProtocol.swift; sourceTree = ""; }; 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTFactoryProtocol.swift; sourceTree = ""; }; 851EBD7E20B418890065C644 /* StickersInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputView.swift; sourceTree = ""; }; + 851FFA65219EAF980015F073 /* EmailTextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailTextController.swift; sourceTree = ""; }; + 851FFA67219EAFBF0015F073 /* Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; + 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberTextController.swift; sourceTree = ""; }; 852003F520D4194A007C0036 /* DBRecentSticker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRecentSticker.swift; sourceTree = ""; }; 852003F720D419E9007C0036 /* RecentStickerTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentStickerTable.swift; sourceTree = ""; }; 852003F920D459E9007C0036 /* BertBinConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BertBinConvertible.swift; sourceTree = ""; }; @@ -7764,6 +7770,9 @@ children = ( 5EEB73D12161CEA100D8ECE6 /* LoginFlow.swift */, 850B9DAC219C7ADA00EA0CF4 /* LoginOption.swift */, + 851FFA67219EAFBF0015F073 /* Validator.swift */, + 851FFA65219EAF980015F073 /* EmailTextController.swift */, + 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */, ); path = Entities; sourceTree = ""; @@ -15507,6 +15516,7 @@ F11DF05F20BD93FB00F3E005 /* UIViewExtensions.swift in Sources */, 4B1D7E112029FF5000703228 /* Array+WheelItemModel.swift in Sources */, A46C36342121999100172773 /* DDMechanism.swift in Sources */, + 851FFA66219EAF980015F073 /* EmailTextController.swift in Sources */, 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */, 3A8045D81F60C98200AED866 /* MQTTServiceHelper.swift in Sources */, 8E9601971FF2EC8100E0C21D /* GroupFilesListVC.swift in Sources */, @@ -15940,8 +15950,10 @@ 268C34152107479600F1472A /* TranscribeLongResponseData.swift in Sources */, 6D36F8E71F0BBFC300FA1AC8 /* ContactManager.swift in Sources */, 32868DD51F31CADF0028B260 /* ChatsListProtocols.swift in Sources */, + 851FFA68219EAFBF0015F073 /* Validator.swift in Sources */, 5E7D5D5E2190415F009B5D8D /* AddContactCell.swift in Sources */, A49CC1D220E4A9C000879D41 /* InputBar+DisplayMode.swift in Sources */, + 851FFA6A219EB29A0015F073 /* PhoneNumberTextController.swift in Sources */, A42D51B3206A361400EEB952 /* Room.swift in Sources */, 85BDD2BA21467A9500695DE5 /* MessageFactoryProtocol.swift in Sources */, 4B06D31E2028A6D6003B275B /* MySelfItemsFactory.swift in Sources */, diff --git a/Nynja/Modules/Auth/AuthModule/Entities/EmailTextController.swift b/Nynja/Modules/Auth/AuthModule/Entities/EmailTextController.swift new file mode 100644 index 000000000..7ece50fa9 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Entities/EmailTextController.swift @@ -0,0 +1,32 @@ +// +// EmailTextController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class EmailTextController { + + private let validator: Validator + private let validationAction: (Bool) -> Void + + private(set) var isValid: Bool = false + + init(validator: Validator, validationAction: @escaping (Bool) -> Void) { + self.validator = validator + self.validationAction = validationAction + } + + func textDidChange(_ textInput: MaterialTextInput) { + isValid = validator.isValid(text: textInput.text.trimmed()) + validationAction(isValid) + } + + func textInputShouldReturn(_ textInput: MaterialTextField) -> Bool { + _ = textInput.resignFirstResponder() + return false + } +} diff --git a/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift b/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift new file mode 100644 index 000000000..f149462e8 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift @@ -0,0 +1,110 @@ +// +// PhoneNumberTextController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class PhoneNumberTextController: NSObject, UITextFieldDelegate { + + var template: String? + + private let validationAction: ((Bool) -> Void)? + + private(set) var isValid: Bool = false + + init(template: String?, validationAction: ((Bool) -> Void)?) { + self.template = template + self.validationAction = validationAction + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + textField.text = textAfterUpdate(textField: textField, range: range, replacementString: string) + .updateWithMask(placeHolder: template) + + let offset = string != "" ? + cursorOffsetForNonEmptyString(textField: textField, range: range) : + cursorOffsetForEmptyString(textField: textField, range: range) + + textField.cursorPosition = range.location + offset + + isValid = isFullfiled(textField: textField, template: template) + + validationAction?(isValid) + + return false + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + textField.cursorPosition = calculatedCursorPosition(on: textField) + } + + + // MARK: - Private + + private func textAfterUpdate(textField: UITextField, range: NSRange, replacementString string: String) -> String { + let text = textField.text ?? "" + + let updatedRange = newRange(text: text, oldRange: range, replacementString: string) + + return (text as NSString) + .replacingCharacters(in: updatedRange, with: string) + .replacingOccurrences(of: " ", with: "") + } + + private func newRange(text: String, oldRange: NSRange,replacementString string: String) -> NSRange { + if string == "", Array(text)[safe: oldRange.location] == " " { + var range = oldRange + range.location = range.location - 1 + return range + } + + return oldRange + } + + private func cursorOffsetForNonEmptyString(textField: UITextField, range: NSRange) -> Int { + guard let text = textField.text else { + return 1 + } + + let index = range.location + 1 + let arr = Array(text) + + if arr.count > index, arr[safe: index] == " " { + return 2 + } + + return 1 + } + + private func cursorOffsetForEmptyString(textField: UITextField, range: NSRange) -> Int { + guard let text = textField.text else { + return 0 + } + + return Array(text)[safe: range.location] == " " ? -1 : 0 + } + + private func calculatedCursorPosition(on textField: UITextField) -> Int { + guard let text = textField.text else { + return 0 + } + let cursorIndex = text.lastIndex { $0 != " " && $0 != "\u{2013}" } ?? text.startIndex + return cursorIndex.encodedOffset + } + + private func isFullfiled(textField: UITextField, template: String?) -> Bool { + guard let text = textField.text else { + return false + } + guard let template = template else { + // If don't have number template, just check for non empty input. + return !text.isEmpty + } + + return text.count { $0 != "\u{2013}" } == template.count + } +} diff --git a/Nynja/Modules/Auth/AuthModule/Entities/Validator.swift b/Nynja/Modules/Auth/AuthModule/Entities/Validator.swift new file mode 100644 index 000000000..c3ce48625 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Entities/Validator.swift @@ -0,0 +1,22 @@ +// +// Validator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol Validator { + func isValid(text: String) -> Bool +} + +struct EmailValidator: Validator { + func isValid(text: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx) + + return emailTest.evaluate(with: text) + } +} diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 882a42d9a..51a12c3fc 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -60,8 +60,7 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn contentView.addSubview(header) header.snp.makeConstraints { maker in - maker.top.equalToSuperview() - maker.left.right.equalToSuperview() + maker.top.left.right.equalToSuperview() } return header @@ -83,6 +82,25 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn return containerView }() + + private(set) lazy var nextButton: UIButton = { + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) + + button.textColor = UIColor.nynja.white + button.setTitle(String.localizable.next.uppercased(), for: .normal) + + button.addTarget(self, action: #selector(next(sender:)), for: .touchUpInside) + + contentView.addSubview(button) + button.snp.makeConstraints { maker in + maker.top.equalTo(loginContainerView.snp.bottom).offset(24) + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.height.equalTo(44) + } + + return button + }() // MARK: Bottom Content @@ -97,8 +115,8 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn contentView.addSubview(label) label.snp.makeConstraints { maker in maker.centerX.equalToSuperview() - maker.top.greaterThanOrEqualTo(loginContainerView.snp.bottom).offset(32) - maker.top.equalTo(loginContainerView.snp.bottom).offset(32).priority(.high) + maker.top.greaterThanOrEqualTo(nextButton.snp.bottom).offset(32) + maker.top.equalTo(nextButton.snp.bottom).offset(32).priority(.high) } return label @@ -140,6 +158,21 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn }() + // MARK: - Validators + + private lazy var emailTextController: EmailTextController = { + return EmailTextController(validator: EmailValidator()) { [weak self] result in + self?.nextButton.isEnabled = result + } + }() + + private lazy var phoneNumberTextController: PhoneNumberTextController = { + return PhoneNumberTextController(template: presenter.selectedCountry.placeHolder) { [weak self] result in + self?.nextButton.isEnabled = result + } + }() + + // MARK: - Init struct Dependencies { @@ -173,6 +206,8 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn _ = [headerView, scrollView, contentView, bottomView] + nextButton.isEnabled = false + showPhoneNumberLogin(animated: false) emailLoginView.snp.makeConstraints { maker in @@ -215,24 +250,41 @@ extension AuthViewController { private extension AuthViewController { - func animateChangingViews(first: UIView?, second: UIView?) { + @objc private func next(sender: UIButton) { + switch presenter.loginOption { + case .email: + let inputText = emailLoginView.inputField.text + presenter.loginViaEmail(inputText) + + case .phoneNumber: + let inputText = phoneNumberLoginView.phoneNumberTextField.text ?? "" + presenter.loginViaPhoneNumber(inputText, country: presenter.selectedCountry) + } + } + + func animateChangingViews(first: UIView?, second: UIView?, isNextActionEnabled: Bool) { second?.isHidden = false second?.alpha = 0 view.layoutIfNeeded() + nextButton.isUserInteractionEnabled = false + UIView.animate( withDuration: 0.25, animations: { first?.alpha = 0 second?.alpha = 1 + self.nextButton.isEnabled = isNextActionEnabled }, completion: { _ in first?.isHidden = true + self.nextButton.isUserInteractionEnabled = true }) } func showPhoneNumberLogin(animated: Bool) { if animated { - animateChangingViews(first: emailLoginView, second: phoneNumberLoginView) + let nextButtonEnabled = phoneNumberTextController.isValid + animateChangingViews(first: emailLoginView, second: phoneNumberLoginView, isNextActionEnabled: nextButtonEnabled) } else { emailLoginView.isHidden = true phoneNumberLoginView.isHidden = false @@ -241,7 +293,8 @@ private extension AuthViewController { func showEmailLogin(animated: Bool) { if animated { - animateChangingViews(first: phoneNumberLoginView, second: emailLoginView) + let nextButtonEnabled = emailTextController.isValid + animateChangingViews(first: phoneNumberLoginView, second: emailLoginView, isNextActionEnabled: nextButtonEnabled) } else { emailLoginView.isHidden = false phoneNumberLoginView.isHidden = true @@ -249,39 +302,34 @@ private extension AuthViewController { } func makeEmailLoginView(on view: UIView) -> EmailLoginView { - let loginView = EmailLoginView() + let loginView = EmailLoginView(textController: emailTextController) view.addSubview(loginView) - loginView.configure(config: EmailLoginView.Config(nextAction: { [weak presenter] in - presenter?.loginViaEmail($0) - })) + loginView.configure() - loginView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() - make.bottom.equalToSuperview().priority(.high) + loginView.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + maker.bottom.lessThanOrEqualToSuperview() + maker.bottom.equalToSuperview().priority(.high) } return loginView } func makePhoneNumberLoginView(on view: UIView, country: Country) -> PhoneNumberLoginView { - let loginView = PhoneNumberLoginView() + let loginView = PhoneNumberLoginView(textController: phoneNumberTextController) view.addSubview(loginView) loginView.configure(config: PhoneNumberLoginView.Config( country: country, countrySelectorAction: { [weak presenter] in presenter?.selectCountry() - }, - nextAction: { [weak presenter] country, number in - presenter?.loginViaPhoneNumber(number, country: country) - }) - ) + } + )) - loginView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.equalToSuperview() + loginView.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + maker.bottom.equalToSuperview() } return loginView diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index 366b1ac32..c661f125a 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -13,20 +13,19 @@ final class EmailLoginView: UIView, Configurable { // MARK: - Views private lazy var inputFieldContainer = makeInputFieldContainer() - private lazy var inputField = makeInputField() - private lazy var detailsLabel = makeDetailsLabel() - private lazy var nextButton = makeNextButton() + private(set) lazy var inputField = makeInputField() + private(set) lazy var detailsLabel = makeDetailsLabel() - private var textFieldController: TextFieldController? - private var nextAction: ((String) -> Void)? + private let textController: EmailTextController // MARK: - Init - override init(frame: CGRect) { - super.init(frame: frame) + init(textController: EmailTextController) { + self.textController = textController + + super.init(frame: .zero) - nextButton.isEnabled = false inputField.isHidden = false } @@ -38,36 +37,21 @@ final class EmailLoginView: UIView, Configurable { // MARK: - Configurable extension EmailLoginView { - struct Config { - let nextAction: (String) -> Void - } func configure(config: Config) { - textFieldController = TextFieldController(validator: Validator()) { [weak self] result in - self?.nextButton.isEnabled = result - } - - nextAction = config.nextAction - - inputField.textChanged = { [weak self] textInput in - self?.textFieldController?.textDidChange(textInput) + inputField.textChanged = { [weak textController] textInput in + textController?.textDidChange(textInput) } - inputField.returnHandler = { [weak self] textInput in - return self?.textFieldController?.textInputShouldReturn(textInput) ?? false + inputField.returnHandler = { [weak textController] textInput in + return textController?.textInputShouldReturn(textInput) ?? false } - _ = [inputFieldContainer, inputField, detailsLabel, nextButton] + _ = [inputFieldContainer, inputField, detailsLabel] } } -// MARK: - Aсtions - -private extension EmailLoginView { - @objc func next(sender: UIButton) { - nextAction?(inputField.text) - } -} +// MARK: - Layout private extension EmailLoginView { @@ -78,9 +62,9 @@ private extension EmailLoginView { addSubview(container) container.snp.makeConstraints { maker in maker.top.equalToSuperview().offset(16) - maker.height.equalTo(44) maker.left.equalToSuperview().offset(16) maker.right.equalToSuperview().inset(16) + maker.height.equalTo(44) } return container @@ -126,69 +110,12 @@ private extension EmailLoginView { addSubview(label) label.snp.makeConstraints { maker in - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) maker.top.equalTo(inputFieldContainer.snp.bottom) - } - - return label - } - - func makeNextButton() -> UIButton { - let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) - - button.textColor = UIColor.nynja.white - button.setTitle(String.localizable.next.uppercased(), for: .normal) - - button.addTarget(target, action: #selector(next(sender:)), for: .touchUpInside) - - addSubview(button) - button.snp.makeConstraints { maker in - maker.height.equalTo(44) - maker.top.greaterThanOrEqualTo(detailsLabel.snp.bottom).offset(16) maker.left.equalToSuperview().offset(16) maker.right.equalToSuperview().inset(16) maker.bottom.equalToSuperview() } - return button - } -} - -// MARK: - Validator - -private extension EmailLoginView { - struct Validator { - func isValid(email: String) -> Bool { - let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx) - - return emailTest.evaluate(with: email) - } - } -} - -// MARK: - TextFieldController - -private extension EmailLoginView { - - final class TextFieldController { - private let validator: Validator - private let validationAction: (Bool) -> Void - - init(validator: Validator, validationAction: @escaping (Bool) -> Void) { - self.validator = validator - self.validationAction = validationAction - } - - func textDidChange(_ textInput: MaterialTextInput) { - textInput.text.trim() - validationAction(validator.isValid(email: textInput.text)) - } - - func textInputShouldReturn(_ textInput: MaterialTextField) -> Bool { - _ = textInput.resignFirstResponder() - return false - } + return label } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index e43ab7f1c..ca96bf2c6 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -19,24 +19,21 @@ final class PhoneNumberLoginView: UIView, Configurable { private lazy var countryCodeField = makeCountryCodeField() private lazy var phoneNumberContainer = makePhoneNumberContainer() - private lazy var phoneNumberTextField = makePhoneNumberTextField() + private(set) lazy var phoneNumberTextField = makePhoneNumberTextField() - private lazy var detailsLabel = makeDetailsNumberLabel() - private lazy var nextButton = makeNextButton() + private(set) lazy var detailsLabel = makeDetailsNumberLabel() - private var phoneNumberTextFieldController: TextFieldController? + private let textController: PhoneNumberTextController private var country: Country? private var countrySelectorAction: (() -> Void)? - private var nextAction: ((Country, String) -> Void)? // MARK: - Init - override init(frame: CGRect) { - super.init(frame: frame) - - nextButton.isEnabled = false + init(textController: PhoneNumberTextController) { + self.textController = textController + super.init(frame: .zero) } required init?(coder aDecoder: NSCoder) { @@ -50,21 +47,15 @@ extension PhoneNumberLoginView { struct Config { let country: Country let countrySelectorAction: () -> Void - let nextAction: (Country, String) -> Void } func configure(config: Config) { country = config.country countrySelectorAction = config.countrySelectorAction - nextAction = config.nextAction - phoneNumberTextFieldController = TextFieldController(template: config.country.placeHolder) { [weak self] result in - self?.nextButton.isEnabled = result - } - - phoneNumberTextField.delegate = phoneNumberTextFieldController + phoneNumberTextField.delegate = textController updateCountry(config.country) - _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField, detailsLabel, nextButton] + _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField, detailsLabel] } } @@ -74,8 +65,6 @@ extension PhoneNumberLoginView { func updateCountry(_ country: Country) { self.country = country - phoneNumberTextFieldController?.template = country.placeHolder - countrySelector.setTitle(country.name, for: .normal) countryCodeField.setTitle("+\(country.code)", for: .normal) @@ -89,13 +78,6 @@ private extension PhoneNumberLoginView { @objc func changeCountry(sender: UIButton) { countrySelectorAction?() } - - @objc func next(sender: UIButton) { - guard let country = country else { - return - } - nextAction?(country, phoneNumberTextField.text ?? "") - } } // MARK: - Layout @@ -115,10 +97,10 @@ private extension PhoneNumberLoginView { addSubview(button) button.snp.makeConstraints { maker in + maker.top.equalToSuperview() maker.left.equalToSuperview().offset(16) maker.right.equalToSuperview().inset(16) maker.height.equalTo(64) - maker.top.equalToSuperview() } return button @@ -153,9 +135,9 @@ private extension PhoneNumberLoginView { countryCodeContainer.addSubview(field) field.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() maker.left.equalToSuperview().offset(16) maker.right.equalToSuperview().inset(16) - maker.centerY.equalToSuperview() } return field @@ -169,10 +151,10 @@ private extension PhoneNumberLoginView { addSubview(container) container.snp.makeConstraints { maker in + maker.centerY.equalTo(left.snp.centerY) maker.left.equalTo(left.snp.right) maker.right.equalToSuperview() maker.height.equalTo(left.snp.height) - maker.centerY.equalTo(left.snp.centerY) } return container @@ -212,133 +194,12 @@ private extension PhoneNumberLoginView { addSubview(label) label.snp.makeConstraints { maker in - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) maker.top.equalTo(countryCodeContainer.snp.bottom) - } - - return label - } - - func makeNextButton() -> UIButton { - let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) - - button.textColor = UIColor.nynja.white - button.setTitle(String.localizable.next.uppercased(), for: .normal) - - button.addTarget(target, action: #selector(next(sender:)), for: .touchUpInside) - - addSubview(button) - button.snp.makeConstraints { maker in - maker.height.equalTo(44) - maker.bottom.equalToSuperview() - maker.top.equalTo(detailsLabel.snp.bottom).offset(24) maker.left.equalToSuperview().offset(16) maker.right.equalToSuperview().inset(16) + maker.bottom.equalToSuperview() } - return button - } -} - -// MARK: - Text field controller - -private extension PhoneNumberLoginView { - - final class TextFieldController: NSObject, UITextFieldDelegate { - - var template: String? - - private let isFullFilelledAction: ((Bool) -> Void)? - - init(template: String?, isFullFilelledAction: ((Bool) -> Void)?) { - self.template = template - self.isFullFilelledAction = isFullFilelledAction - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - textField.text = textAfterUpdate(textField: textField, range: range, replacementString: string) - .updateWithMask(placeHolder: template) - - let offset = string != "" ? - cursorOffsetForNonEmptyString(textField: textField, range: range) : - cursorOffsetForEmptyString(textField: textField, range: range) - - textField.cursorPosition = range.location + offset - - isFullFilelledAction?(isFullfiled(textField: textField, template: template)) - - return false - } - - func textFieldDidBeginEditing(_ textField: UITextField) { - textField.cursorPosition = calculatedCursorPosition(on: textField) - } - - - // MARK: - Private - - private func textAfterUpdate(textField: UITextField, range: NSRange, replacementString string: String) -> String { - let text = textField.text ?? "" - - let updatedRange = newRange(text: text, oldRange: range, replacementString: string) - - return (text as NSString) - .replacingCharacters(in: updatedRange, with: string) - .replacingOccurrences(of: " ", with: "") - } - - private func newRange(text: String, oldRange: NSRange,replacementString string: String) -> NSRange { - if string == "", Array(text)[safe: oldRange.location] == " " { - var range = oldRange - range.location = range.location - 1 - return range - } - - return oldRange - } - - private func cursorOffsetForNonEmptyString(textField: UITextField, range: NSRange) -> Int { - guard let text = textField.text else { - return 1 - } - - let index = range.location + 1 - let arr = Array(text) - - if arr.count > index, arr[safe: index] == " " { - return 2 - } - - return 1 - } - - private func cursorOffsetForEmptyString(textField: UITextField, range: NSRange) -> Int { - guard let text = textField.text else { - return 0 - } - - return Array(text)[safe: range.location] == " " ? -1 : 0 - } - - private func calculatedCursorPosition(on textField: UITextField) -> Int { - guard let text = textField.text else { - return 0 - } - let cursorIndex = text.lastIndex { $0 != " " && $0 != "\u{2013}" } ?? text.startIndex - return cursorIndex.encodedOffset - } - - private func isFullfiled(textField: UITextField, template: String?) -> Bool { - guard let text = textField.text else { - return false - } - guard let template = template else { - // If don't have number template, just check for non empty input. - return !text.isEmpty - } - - return text.count { $0 != "\u{2013}" } == template.count - } + return label } } -- GitLab From 244d5cee37cede26991b5f8d3b4c13bc2fed1325 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 16 Nov 2018 11:32:49 +0200 Subject: [PATCH 107/138] Fixed email details label. --- .../Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift index c661f125a..ea0c63a4d 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift @@ -113,7 +113,7 @@ private extension EmailLoginView { maker.top.equalTo(inputFieldContainer.snp.bottom) maker.left.equalToSuperview().offset(16) maker.right.equalToSuperview().inset(16) - maker.bottom.equalToSuperview() + maker.bottom.lessThanOrEqualToSuperview() } return label -- GitLab From b10158b57081282d88dab5e8f87e504cda6a14f2 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 16 Nov 2018 16:00:34 +0200 Subject: [PATCH 108/138] Update SDK version. Add popup for errors. --- Nynja.xcodeproj/project.pbxproj | 14 +++++- Nynja/Library/UI/Alert/AlertDisplayable.swift | 46 +++++++++++++++++++ .../Library/UI/{ => Alert}/AlertManager.swift | 0 Nynja/Modules/Auth/AuthCoordinator.swift | 8 ++-- .../Auth/AuthModule/AuthProtocols.swift | 1 + .../AuthModule/Presenter/AuthPresenter.swift | 8 +++- .../AuthModule/Wireframe/AuthWireframe.swift | 6 ++- Nynja/SDK/Auth/AuthService.swift | 8 ++++ .../BaseModule/Wireframe/Wireframe.swift | 2 +- Podfile | 2 +- Podfile.lock | 8 ++-- 11 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 Nynja/Library/UI/Alert/AlertDisplayable.swift rename Nynja/Library/UI/{ => Alert}/AlertManager.swift (100%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 309b52ece..15770a833 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -881,6 +881,7 @@ 850A0C672046B65D004F79AD /* WCItemsFactoryDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A0C662046B65D004F79AD /* WCItemsFactoryDecorator.swift */; }; 850A2BB0203584B000D68FDF /* SearchActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2BAF203584B000D68FDF /* SearchActionsView.swift */; }; 850A2BB22035AE5E00D68FDF /* ForwardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2BB12035AE5E00D68FDF /* ForwardCellViewModel.swift */; }; + 850A2E94219EF9B800C784D9 /* AlertDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2E93219EF9B800C784D9 /* AlertDisplayable.swift */; }; 850B9D9C219C117E00EA0CF4 /* SessionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */; }; 850B9D9F219C131E00EA0CF4 /* AuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */; }; 850B9DA0219C1E8900EA0CF4 /* SessionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */; }; @@ -3208,6 +3209,7 @@ 850A0C662046B65D004F79AD /* WCItemsFactoryDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCItemsFactoryDecorator.swift; sourceTree = ""; }; 850A2BAF203584B000D68FDF /* SearchActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchActionsView.swift; sourceTree = ""; }; 850A2BB12035AE5E00D68FDF /* ForwardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardCellViewModel.swift; sourceTree = ""; }; + 850A2E93219EF9B800C784D9 /* AlertDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertDisplayable.swift; sourceTree = ""; }; 850B9D9B219C117E00EA0CF4 /* SessionStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionStorage.swift; sourceTree = ""; }; 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthResponse.swift; sourceTree = ""; }; 850B9DA2219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileSDKFactoryProtocol.swift; sourceTree = ""; }; @@ -6218,7 +6220,7 @@ 5EEB73DC21623FED00D8ECE6 /* UIViewControllerExtensions */, 26A421CB217E026100120542 /* SnackBar */, 4B749EF2214FEABB002F3A33 /* LoginView */, - 4BB0EFBA2151347900704136 /* AlertManager.swift */, + 850A2E92219EF9A800C784D9 /* Alert */, 4BB0EFB62151347900704136 /* CustomPopup */, 8514D52020EE48750002378A /* ContextMenu */, 8514F16520EA219E00883513 /* ContextMenuOLD */, @@ -8364,6 +8366,15 @@ path = Files; sourceTree = ""; }; + 850A2E92219EF9A800C784D9 /* Alert */ = { + isa = PBXGroup; + children = ( + 4BB0EFBA2151347900704136 /* AlertManager.swift */, + 850A2E93219EF9B800C784D9 /* AlertDisplayable.swift */, + ); + path = Alert; + sourceTree = ""; + }; 850B9D9D219C11BF00EA0CF4 /* Session */ = { isa = PBXGroup; children = ( @@ -16655,6 +16666,7 @@ A408A0BA20C174040029F54B /* ChannelsListPresenter.swift in Sources */, 850FC60F203310D200832D87 /* SelectionAvatarView.swift in Sources */, 8E6C4BE41FF6A7AD009C8374 /* GroupStorageListItems.swift in Sources */, + 850A2E94219EF9B800C784D9 /* AlertDisplayable.swift in Sources */, B7B546B3210DD1EC002DCA55 /* AlertTextFieldViewController.swift in Sources */, FEA655EB2167777E00B44029 /* TransferHistoryTableDataSource.swift in Sources */, F117871220ACF018007A9A1B /* CameraSettingsProtocols.swift in Sources */, diff --git a/Nynja/Library/UI/Alert/AlertDisplayable.swift b/Nynja/Library/UI/Alert/AlertDisplayable.swift new file mode 100644 index 000000000..434595160 --- /dev/null +++ b/Nynja/Library/UI/Alert/AlertDisplayable.swift @@ -0,0 +1,46 @@ +// +// AlertDisplayable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AlertDisplayable: class { + func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?, completion: (() -> Void)?) + func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?, completion: (() -> Void)?) + + func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?) + func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?) +} + +extension AlertDisplayable { + + func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?, completion: (() -> Void)?) { + presentAlert(title: title, message: message, style: .alert, actions: actions, completion: completion) + } + + func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?) { + presentAlert(title: title, message: message, style: style, actions: actions, completion: nil) + } + + func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?) { + presentAlert(title: title, message: message, style: .alert, actions: actions, completion: nil) + } +} + +protocol NavigationContainer: AlertDisplayable { + var navigation: UINavigationController? { get } +} + +extension NavigationContainer { + func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?, completion: (() -> Void)?) { + let alert = UIAlertController(title: title, message: message, preferredStyle: style) + + actions?.forEach { alert.addAction($0) } + + navigation?.present(alert, animated: true, completion: completion) + } +} diff --git a/Nynja/Library/UI/AlertManager.swift b/Nynja/Library/UI/Alert/AlertManager.swift similarity index 100% rename from Nynja/Library/UI/AlertManager.swift rename to Nynja/Library/UI/Alert/AlertManager.swift diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index eaaf20778..5827a5d14 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -9,9 +9,10 @@ import Foundation import SDWebImage -final class AuthCoordinator: CoordinatorProtocol, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol, CreateProfileCoordinatorProtocol { +final class AuthCoordinator: CoordinatorProtocol, NavigationContainer, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol, CreateProfileCoordinatorProtocol { + + private(set) weak var navigation: UINavigationController? - private weak var navigation: UINavigationController? private let serviceFactory: ServiceFactoryProtocol private var inputConfirmationCallback: ((Bool) -> Void)? @@ -64,7 +65,8 @@ extension AuthCoordinator { case .back: navigation?.popViewController(animated: true) case .invalidCode: - break + let actions = [UIAlertAction(title: "OK", style: .default, handler: nil)] + presentAlert(title: "Failure", message: "Code is invalid or internal error", actions: actions) case .validCode(let type): handleType(type) } diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index 0045aeffd..2be2f176d 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -16,6 +16,7 @@ protocol AuthWireframeProtocol: class { func present(_ viewController: UIViewController) func dismiss(_ viewController: UIViewController) + func presentAlert(title: String, message: String, actions: [UIAlertAction]) } protocol AuthViewProtocol: class where Self: UIViewController { diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index 0d407707b..f7115581e 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -101,8 +101,12 @@ extension AuthPresenter { } func didReceiveAuthenticationFailure(_ error: Error?) { - // TODO: handle failure - print("\(#function): error = \(error?.localizedDescription ?? "")") + let actions = [ + UIAlertAction(title: "OK", style: .default, handler: nil) + ] + wireframe.presentAlert(title: "Failure", + message: error?.localizedDescription ?? "Something went wrong", + actions: actions) } } diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift index 8f3354b35..8918e0911 100644 --- a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift @@ -9,7 +9,7 @@ import Foundation import UIKit.UIViewController -protocol AuthCoordinatorProtocol: class { +protocol AuthCoordinatorProtocol: AlertDisplayable { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) } @@ -77,4 +77,8 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { func dismiss(_ viewController: UIViewController) { coordinator.wireframe(self, didEndWithState: .dismiss(viewController)) } + + func presentAlert(title: String, message: String, actions: [UIAlertAction]) { + coordinator.presentAlert(title: title, message: message, actions: actions) + } } diff --git a/Nynja/SDK/Auth/AuthService.swift b/Nynja/SDK/Auth/AuthService.swift index c7ce4edd1..250659983 100644 --- a/Nynja/SDK/Auth/AuthService.swift +++ b/Nynja/SDK/Auth/AuthService.swift @@ -152,6 +152,14 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } } + func sendLogin(byFacebookDidFinish error: Error?) { + + } + + func sendLogin(byGooglePlusDidFinish error: Error?) { + + } + func confirmCodeDidFinish(withAccoutnId accountId: String, withAccessToken accessToken: String, withRefreshToken refreshToken: String, diff --git a/Nynja/Viper/BaseModule/Wireframe/Wireframe.swift b/Nynja/Viper/BaseModule/Wireframe/Wireframe.swift index e5f03fbf2..a620bb88f 100644 --- a/Nynja/Viper/BaseModule/Wireframe/Wireframe.swift +++ b/Nynja/Viper/BaseModule/Wireframe/Wireframe.swift @@ -12,7 +12,7 @@ protocol Wireframe: class { associatedtype Parameters = Void associatedtype Dependencies = Void associatedtype State - + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController } diff --git a/Podfile b/Podfile index 1dd2a3184..61d4fae39 100644 --- a/Podfile +++ b/Podfile @@ -41,7 +41,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.5' # pod 'NynjaSDK', '= 1.7.1' - pod 'NynjaSDK-MultiAcc', '= 0.5.3' + pod 'NynjaSDK-MultiAcc', '= 0.5.4' pod 'CryptoSwift', '= 0.10.0' diff --git a/Podfile.lock b/Podfile.lock index cc146c23c..f7417d1dd 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -81,7 +81,7 @@ PODS: - MaterialComponents/private/Application - MDFTextAccessibility (1.2.0) - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.3) + - NynjaSDK-MultiAcc (0.5.4) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -115,7 +115,7 @@ DEPENDENCIES: - libPhoneNumber-iOS (= 0.9.13) - MaterialComponents/FlexibleHeader (= 55.3.0) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.3) + - NynjaSDK-MultiAcc (= 0.5.4) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.0.0) @@ -200,7 +200,7 @@ SPEC CHECKSUMS: MaterialComponents: 915f4e844400a35db3ea4c710a9af40aa8bcb093 MDFTextAccessibility: 94098925e0853551c5a311ce7c1ecefbe297cdb6 MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: 41bf0b025519ecd0433cf84c854eec61aa493e63 + NynjaSDK-MultiAcc: 2747f9a2b306841e013a3cb3ef91eb444015758c QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: a42d492c16e80209130a3379f73596c3454b7694 @@ -209,6 +209,6 @@ SPEC CHECKSUMS: SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: 1827ea38496d552cee076e6c699298204d21257c +PODFILE CHECKSUM: 55e88d70a9ddb48844e0930259f60d171dbd5cd1 COCOAPODS: 1.5.3 -- GitLab From f9af16d0846760732135d9e10f7c5d42ed46e497 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 16 Nov 2018 16:18:26 +0200 Subject: [PATCH 109/138] Remove unused imports --- Nynja/Library/UI/TextInput/InputField/PhoneField.swift | 1 - Nynja/Modules/History/View/HistoryCell.swift | 1 - .../InviteFriends/Interactor/InviteFriendsInteractor.swift | 1 - Nynja/Services/ContactManager.swift | 3 +-- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Nynja/Library/UI/TextInput/InputField/PhoneField.swift b/Nynja/Library/UI/TextInput/InputField/PhoneField.swift index 155f11efc..25d592ea0 100644 --- a/Nynja/Library/UI/TextInput/InputField/PhoneField.swift +++ b/Nynja/Library/UI/TextInput/InputField/PhoneField.swift @@ -7,7 +7,6 @@ // import UIKit -import libPhoneNumber_iOS import NynjaUIKit protocol PhoneFieldDelegate: class { diff --git a/Nynja/Modules/History/View/HistoryCell.swift b/Nynja/Modules/History/View/HistoryCell.swift index 50e19b2cc..b50ae2906 100644 --- a/Nynja/Modules/History/View/HistoryCell.swift +++ b/Nynja/Modules/History/View/HistoryCell.swift @@ -7,7 +7,6 @@ // import UIKit -import libPhoneNumber_iOS protocol HistoryCellDelegate: class { func acceptTupped(index: Int) diff --git a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift index 6608c5d16..72a3df200 100644 --- a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift +++ b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift @@ -8,7 +8,6 @@ import AddressBook import Contacts -import libPhoneNumber_iOS class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProtocol { diff --git a/Nynja/Services/ContactManager.swift b/Nynja/Services/ContactManager.swift index e3857083c..9404d4ce5 100644 --- a/Nynja/Services/ContactManager.swift +++ b/Nynja/Services/ContactManager.swift @@ -7,9 +7,8 @@ // import Contacts -import libPhoneNumber_iOS -class ContactManager { +final class ContactManager { static var shared = ContactManager() -- GitLab From 1e96e24f16c402c0ca8677e07ae550f21bb6fbed Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 19 Nov 2018 11:47:44 +0200 Subject: [PATCH 110/138] [NY-5274] Phone number AutoFill (#1482) * [NY-5274] Started implementation of phonenumber autofill * [NY-5274] Update UI when autofilled country is selected * [NY-5274] Fixed issue with cursor position. * [NY-5274] Implemented phone number formatting * [NY-5274] Implemented phone number autofill with formatting. * [NY-5274] Commented libPhoneNumber formatter. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 10 +- .../InputsCachePolicy.swift | 0 .../ProhibitedOptions.swift | 0 .../Core/TextInput/TextInputUtils.swift | 25 +++ .../Views/Controls/UnderlinedTextField.swift | 34 ++++ Nynja.xcodeproj/project.pbxproj | 4 + .../Auth/AuthModule/AuthProtocols.swift | 5 +- .../Entities/PhoneNumberFormatter.swift | 98 ++++++++++++ .../Entities/PhoneNumberTextController.swift | 149 +++++++++--------- .../Interactor/AuthInteractor.swift | 4 + .../AuthModule/Presenter/AuthPresenter.swift | 10 +- .../AuthModule/View/AuthViewController.swift | 20 ++- .../View/Subviews/PhoneNumberLoginView.swift | 33 +++- .../Auth/SelectCountry/Entities/Country.swift | 4 + .../InputController/MentionController.swift | 10 +- 15 files changed, 312 insertions(+), 94 deletions(-) rename Frameworks/NynjaUIKit/NynjaUIKit/Core/{Controls => TextInput}/InputsCachePolicy.swift (100%) rename Frameworks/NynjaUIKit/NynjaUIKit/Core/{Controls => TextInput}/ProhibitedOptions.swift (100%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/TextInputUtils.swift create mode 100644 Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberFormatter.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index d037fad94..cf1432f1a 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 297D4FB3B2A977EBC50F9621 /* Pods_NynjaUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */; }; + 850A2E9E219F37AD00C784D9 /* TextInputUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */; }; 8514D4C620EE27080002378A /* NynjaUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 8514D4C420EE27080002378A /* NynjaUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8514D4DF20EE2D970002378A /* InteractiveCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D4CF20EE2D970002378A /* InteractiveCellViewModel.swift */; }; 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D4D020EE2D970002378A /* LayoutRepresentableCellViewModel.swift */; }; @@ -49,6 +50,7 @@ /* Begin PBXFileReference section */ 1820AD65D6897E3E306C16A2 /* Pods-NynjaUIKit.translate.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.translate.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.translate.xcconfig"; sourceTree = ""; }; 7336042AC840197E622730FD /* Pods-NynjaUIKit.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.prerelease.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.prerelease.xcconfig"; sourceTree = ""; }; + 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputUtils.swift; sourceTree = ""; }; 8514D4C120EE27080002378A /* NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8514D4C420EE27080002378A /* NynjaUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NynjaUIKit.h; sourceTree = ""; }; 8514D4C520EE27080002378A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -363,13 +365,14 @@ path = Controls; sourceTree = ""; }; - 855A4E8B219AFEE000B6E90B /* Controls */ = { + 855A4E8B219AFEE000B6E90B /* TextInput */ = { isa = PBXGroup; children = ( 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */, 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */, + 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */, ); - path = Controls; + path = TextInput; sourceTree = ""; }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { @@ -378,7 +381,7 @@ 8514D51920EE41AC0002378A /* Extensions */, 8514D4DD20EE2D970002378A /* Layout */, 8514D4CC20EE2D970002378A /* Collection */, - 855A4E8B219AFEE000B6E90B /* Controls */, + 855A4E8B219AFEE000B6E90B /* TextInput */, ); path = Core; sourceTree = ""; @@ -520,6 +523,7 @@ 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */, 8514D4E920EE2D970002378A /* UICollectionView+ViewModel.swift in Sources */, 8514D51120EE40540002378A /* ContextMenuItem.swift in Sources */, + 850A2E9E219F37AD00C784D9 /* TextInputUtils.swift in Sources */, 8514D51820EE40540002378A /* NynjaContextMenuItemsFactory.swift in Sources */, 8540A00A2181EB87003A010F /* TypingView.swift in Sources */, 85EB37F621832D41003A2D6F /* RecordingIndicatorView.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/InputsCachePolicy.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/InputsCachePolicy.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/InputsCachePolicy.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/InputsCachePolicy.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/ProhibitedOptions.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/ProhibitedOptions.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Core/Controls/ProhibitedOptions.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/ProhibitedOptions.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/TextInputUtils.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/TextInputUtils.swift new file mode 100644 index 000000000..93b1ff63b --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/TextInputUtils.swift @@ -0,0 +1,25 @@ +// +// TextInputUtils.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 16.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +public enum TextInputUtils { + + public static func updatedCursor(for replacementText: String, in replacementRange: NSRange, currentText text: String) -> Int { + let newCursorPosition: Int + if replacementRange.length > replacementText.utf16.count { + // delete or replace + newCursorPosition = replacementRange.upperBound - (replacementRange.length - replacementText.utf16.count) + } else { + // insert + newCursorPosition = replacementRange.upperBound + (replacementText.utf16.count - replacementRange.length) + } + + return newCursorPosition + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift index efee26262..7a99724af 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift @@ -40,11 +40,45 @@ open class UnderlinedTextField: BaseView { set { textField.text = newValue } } + public var textColor: UIColor? { + get { return textField.textColor } + set { textField.textColor = newValue } + } + + public var font: UIFont? { + get { return textField.font } + set { textField.font = newValue } + } + + public var placeholder: String? { + get { return textField.placeholder } + set { textField.placeholder = newValue } + } + + public var attributedPlaceholder: NSAttributedString? { + get { return textField.attributedPlaceholder } + set { textField.attributedPlaceholder = newValue } + } + + public var textContentType: UITextContentType! { + get { return textField.textContentType } + set { textField.textContentType = newValue } + } + + public var keyboardType: UIKeyboardType! { + get { return textField.keyboardType } + set { textField.keyboardType = newValue } + } + public var delegate: UITextFieldDelegate? { get { return textField.delegate } set { textField.delegate = newValue } } + public var prohibitedOptions: ProhibitedOptions { + get { return textField.prohibitedOptions } + set { textField.prohibitedOptions = newValue } + } // MARK: - Views diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index db767dd04..50b121a4a 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1007,6 +1007,7 @@ 853FB0772049B7CA000996C5 /* TextCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853FB0762049B7CA000996C5 /* TextCellViewModel.swift */; }; 8540A331211B34B4007F65AF /* MessageCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */; }; 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */; }; + 8541995221A2B003004009F7 /* PhoneNumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541995121A2B003004009F7 /* PhoneNumberFormatter.swift */; }; 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B811218879B100A286E5 /* TypingDisplayModel.swift */; }; @@ -3326,6 +3327,7 @@ 853FB0762049B7CA000996C5 /* TextCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCellViewModel.swift; sourceTree = ""; }; 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDataSource.swift; sourceTree = ""; }; 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDelegate.swift; sourceTree = ""; }; + 8541995121A2B003004009F7 /* PhoneNumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberFormatter.swift; sourceTree = ""; }; 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; 8542B811218879B100A286E5 /* TypingDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingDisplayModel.swift; sourceTree = ""; }; @@ -7783,6 +7785,7 @@ 851FFA67219EAFBF0015F073 /* Validator.swift */, 851FFA65219EAF980015F073 /* EmailTextController.swift */, 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */, + 8541995121A2B003004009F7 /* PhoneNumberFormatter.swift */, ); path = Entities; sourceTree = ""; @@ -16852,6 +16855,7 @@ C9B8BEF7204DD7890018748C /* SettingsDataAndStorageLayout.swift in Sources */, A49E6C4220D9A27D007D85F5 /* MainViewController+Container.swift in Sources */, 85788C4620442392003600C9 /* BuildNumberInteractor.swift in Sources */, + 8541995221A2B003004009F7 /* PhoneNumberFormatter.swift in Sources */, 2605312921298BEF002E1CF1 /* Logoutputcell.swift in Sources */, 6B3D349607A18D5650BF47E6 /* SplashInteractor.swift in Sources */, 859B863720485F01003272B2 /* CarouselPickerViewController.swift in Sources */, diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index 2be2f176d..b0dba3178 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -20,7 +20,8 @@ protocol AuthWireframeProtocol: class { } protocol AuthViewProtocol: class where Self: UIViewController { - func update(country: Country) + func select(country: Country) + func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) } protocol AuthPresenterProtocol: class { @@ -36,6 +37,7 @@ protocol AuthPresenterProtocol: class { func loginViaPhoneNumber(_ phoneNumber: String, country: Country) func selectCountry() + func processPhoneAutoFillInfo(_ autofillInfo: PhoneAutoFillInfo) } protocol AuthInputInteractorProtocol: class { @@ -45,6 +47,7 @@ protocol AuthInputInteractorProtocol: class { func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) func fetchDefaultCountry() -> Country + func fetchCountry(by code: String) -> Country? } protocol AuthOutputInteractorProtocol: class { diff --git a/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberFormatter.swift b/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberFormatter.swift new file mode 100644 index 000000000..2aa4b1ac7 --- /dev/null +++ b/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberFormatter.swift @@ -0,0 +1,98 @@ +// +// PhoneNumberFormatter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 19.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import libPhoneNumber_iOS + +protocol PhoneNumberFormatter: class { + func format(nationalNumber: String, country: Country) -> String + func isValid(nationalNumber: String) -> Bool +} + +final class CountryMaskFormatter: PhoneNumberFormatter { + + private let country: Country + + init(country: Country) { + self.country = country + } + + func format(nationalNumber: String, country: Country) -> String { + return formattedNumber(nationalNumber, with: country.placeHolder ?? country.defaultMask) + } + + private func formattedNumber(_ number: String, with mask: String?) -> String { + guard let mask = mask, case let maskLength = mask.replacingOccurrences(of: " ", with: "").count, number.count <= maskLength else { + return number + } + var temp = number + + return mask.reduce(into: "") { result, char in + if !temp.isEmpty { + result.append(char == " " ? " " : temp.removeFirst()) + } + } + } + + func isValid(nationalNumber: String) -> Bool { + return isValid(nationalNumber, for: country.placeHolder) + } + + private func isValid(_ number: String, for mask: String?) -> Bool { + guard let mask = mask else { + return !number.isEmpty + } + let maskLength = mask.replacingOccurrences(of: " ", with: "").count + return number.count == maskLength + } +} + +// Don't remove, may be helpful in the future updates +//final class PhoneNumberGoogleLibFormatter: PhoneNumberFormatter { +// +// private let country: Country +// +// private let phoneNumberUtil: NBPhoneNumberUtil +// +// private let formatter: NBAsYouTypeFormatter +// +// init(country: Country) { +// self.country = country +// self.phoneNumberUtil = NBPhoneNumberUtil.sharedInstance() +// self.formatter = NBAsYouTypeFormatter(regionCode: country.ISO) +// } +// +// func format(nationalNumber: String, country: Country) -> String { +// var number = nationalNumber +// +// let countryPrefix = "+\(country.code)" +// let numberString = "\(countryPrefix)\(number)" +// +// guard let phoneNumber = try? phoneNumberUtil.parse(numberString, defaultRegion: country.ISO) else { +// return nationalNumber +// } +// +// let leadingZeros = phoneNumber.numberOfLeadingZeros?.intValue ?? 0 +// (0.. Bool { +// do { +// let result = "+\(country.code)\(nationalNumber)" +// let phoneNumber = try phoneNumberUtil.parse(result, defaultRegion: nil) +// return phoneNumberUtil.isValidNumber(phoneNumber) +// } catch { +// return false +// } +// } +//} diff --git a/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift b/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift index f149462e8..c2182a678 100644 --- a/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift +++ b/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift @@ -7,104 +7,109 @@ // import UIKit +import NynjaUIKit +import libPhoneNumber_iOS + +struct PhoneAutoFillInfo { + let countryCode: String + let phoneNumber: String +} final class PhoneNumberTextController: NSObject, UITextFieldDelegate { + + var country: Country { + didSet { + self.formatter = CountryMaskFormatter(country: country) + } + } - var template: String? + var validationAction: ((Bool) -> Void)? - private let validationAction: ((Bool) -> Void)? + var autofillHandler: ((PhoneAutoFillInfo) -> Void)? private(set) var isValid: Bool = false - init(template: String?, validationAction: ((Bool) -> Void)?) { - self.template = template - self.validationAction = validationAction - } - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - textField.text = textAfterUpdate(textField: textField, range: range, replacementString: string) - .updateWithMask(placeHolder: template) - - let offset = string != "" ? - cursorOffsetForNonEmptyString(textField: textField, range: range) : - cursorOffsetForEmptyString(textField: textField, range: range) - - textField.cursorPosition = range.location + offset - - isValid = isFullfiled(textField: textField, template: template) - - validationAction?(isValid) - - return false - } + // MARK: - Dependencies + + private let phoneNumberUtil = NBPhoneNumberUtil.sharedInstance()! - func textFieldDidBeginEditing(_ textField: UITextField) { - textField.cursorPosition = calculatedCursorPosition(on: textField) + private var formatter: PhoneNumberFormatter + + + // MARK: - Init + + init(country: Country) { + self.country = country + self.formatter = CountryMaskFormatter(country: country) } - // MARK: - Private + // MARK: - UITextFieldDelegate - private func textAfterUpdate(textField: UITextField, range: NSRange, replacementString string: String) -> String { - let text = textField.text ?? "" + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + defer { validationAction?(isValid) } - let updatedRange = newRange(text: text, oldRange: range, replacementString: string) + let currentText = textField.text ?? "" - return (text as NSString) - .replacingCharacters(in: updatedRange, with: string) - .replacingOccurrences(of: " ", with: "") - } - - private func newRange(text: String, oldRange: NSRange,replacementString string: String) -> NSRange { - if string == "", Array(text)[safe: oldRange.location] == " " { - var range = oldRange - range.location = range.location - 1 - return range + if currentText.isEmpty, string.starts(with: "+"), case let possibleNumber = string { + // AutoFill recognized + do { + // +380 + let countryCode = extractCountryCode(from: possibleNumber) + // 971231212 - without country code + let nationalNumber = try extractNationalNumber(from: possibleNumber, countryCode: countryCode) + + let autofillInfo = PhoneAutoFillInfo(countryCode: countryCode, phoneNumber: nationalNumber) + autofillHandler?(autofillInfo) + + isValid = check(nationalNumber) + + textField.text = formattedResult(for: nationalNumber, country: country) + + return false + + } catch { } } - return oldRange - } - - private func cursorOffsetForNonEmptyString(textField: UITextField, range: NSRange) -> Int { - guard let text = textField.text else { - return 1 - } + // 971231212 - without country code + let result = (currentText as NSString).replacingCharacters(in: range, with: string) + let nationalNumber = formattedResult(for: result, country: country) - let index = range.location + 1 - let arr = Array(text) + isValid = check(nationalNumber) - if arr.count > index, arr[safe: index] == " " { - return 2 + let shouldChange = string == " " + + if !shouldChange { + textField.text = nationalNumber +// textField.cursorPosition = TextInputUtils.updatedCursor(for: string, in: range, currentText: currentText) } - return 1 + return shouldChange } - private func cursorOffsetForEmptyString(textField: UITextField, range: NSRange) -> Int { - guard let text = textField.text else { - return 0 - } - - return Array(text)[safe: range.location] == " " ? -1 : 0 + + // MARK: - Utils + + private func extractCountryCode(from possibleNumber: String) -> String { + return phoneNumberUtil.extractCountryCode(possibleNumber, nationalNumber: nil).stringValue } - private func calculatedCursorPosition(on textField: UITextField) -> Int { - guard let text = textField.text else { - return 0 - } - let cursorIndex = text.lastIndex { $0 != " " && $0 != "\u{2013}" } ?? text.startIndex - return cursorIndex.encodedOffset + private func extractNationalNumber(from possibleNumber: String, countryCode: String) throws -> String { + let phoneNumber = try phoneNumberUtil.parse(possibleNumber, defaultRegion: nil) + let formattedNumber = try phoneNumberUtil.format(phoneNumber, numberFormat: .E164) + return formattedNumber.replacingOccurrences(of: "+\(countryCode)", with: "") } - private func isFullfiled(textField: UITextField, template: String?) -> Bool { - guard let text = textField.text else { - return false - } - guard let template = template else { - // If don't have number template, just check for non empty input. - return !text.isEmpty - } - - return text.count { $0 != "\u{2013}" } == template.count + private func formattedResult(for input: String, country: Country) -> String { + return formatter.format(nationalNumber: rawNumber(for: input), country: country) + } + + private func check(_ result: String) -> Bool { + return formatter.isValid(nationalNumber: rawNumber(for: result)) + } + + private func rawNumber(for input: String) -> String { + return input.replacingOccurrences(of: " ", with: "") } } diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 71aca41e2..6edc1e0aa 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -88,4 +88,8 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { } } } + + func fetchCountry(by code: String) -> Country? { + return countriesProvider.fetchCountries().first { $0.code == code } + } } diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index f7115581e..e93d54bfe 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -67,12 +67,20 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, switch result { case let .success(country): self.selectedCountry = country - self.view?.update(country: country) + self.view?.select(country: country) case .failure: break } } } + + func processPhoneAutoFillInfo(_ autofillInfo: PhoneAutoFillInfo) { + guard let country = interactor.fetchCountry(by: autofillInfo.countryCode) else { + return + } + let phoneNumberInfo = PhoneNumberInfo(country: country, number: autofillInfo.phoneNumber) + view?.update(phone: phoneNumberInfo) + } } // MARK: - GoogleAuthServiceUIDelegate diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 51a12c3fc..0c99f1c83 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -167,9 +167,17 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn }() private lazy var phoneNumberTextController: PhoneNumberTextController = { - return PhoneNumberTextController(template: presenter.selectedCountry.placeHolder) { [weak self] result in + let controller = PhoneNumberTextController(country: presenter.selectedCountry) + + controller.validationAction = { [weak self] result in self?.nextButton.isEnabled = result } + + controller.autofillHandler = { [weak presenter] autofillInfo in + presenter?.processPhoneAutoFillInfo(autofillInfo) + } + + return controller }() @@ -232,8 +240,14 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn extension AuthViewController { - func update(country: Country) { - phoneNumberLoginView.updateCountry(country) + func select(country: Country) { + phoneNumberTextController.country = country + phoneNumberLoginView.selectCountry(country) + } + + func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) { + phoneNumberTextController.country = autofillPhoneNumberInfo.country + phoneNumberLoginView.updatePhone(autofillPhoneNumberInfo) } } diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift index ca96bf2c6..9f752e923 100644 --- a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -53,7 +53,7 @@ extension PhoneNumberLoginView { country = config.country countrySelectorAction = config.countrySelectorAction phoneNumberTextField.delegate = textController - updateCountry(config.country) + selectCountry(config.country) _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField, detailsLabel] } @@ -62,13 +62,32 @@ extension PhoneNumberLoginView { // MARK: - Public extension PhoneNumberLoginView { - func updateCountry(_ country: Country) { + + private func updateCountryInfo(for country: Country) { self.country = country countrySelector.setTitle(country.name, for: .normal) countryCodeField.setTitle("+\(country.code)", for: .normal) - phoneNumberTextField.text = "".updateWithMask(placeHolder: country.placeHolder) + // FIXME: remove default mask + let placeholder = "".updateWithMask(placeHolder: country.placeHolder ?? country.defaultMask) + phoneNumberTextField.attributedPlaceholder = NSAttributedString( + string: placeholder, + attributes: [ + .font: phoneNumberTextField.font, + .foregroundColor: phoneNumberTextField.textColor + ] + ) + } + + func selectCountry(_ country: Country) { + updateCountryInfo(for: country) + phoneNumberTextField.text = "" + } + + func updatePhone(_ autofillPhoneNumberInfo: PhoneNumberInfo) { + updateCountryInfo(for: autofillPhoneNumberInfo.country) + phoneNumberTextField.text = autofillPhoneNumberInfo.number } } @@ -163,13 +182,15 @@ private extension PhoneNumberLoginView { func makePhoneNumberTextField() -> UnderlinedTextField { let textField = UnderlinedTextField() + textField.prohibitedOptions = .all textField.underlineColor = UIColor.nynja.dustyGray textField.highlighedUnderlineColor = UIColor.nynja.mainRed textField.textField.tintColor = UIColor.nynja.mainRed - textField.textField.font = FontFamily.NotoSans.medium.font(size: 16) - textField.textField.textColor = UIColor.nynja.white - textField.textField.keyboardType = .numberPad + textField.font = FontFamily.NotoSans.medium.font(size: 16) + textField.textColor = UIColor.nynja.white + textField.keyboardType = .numberPad + textField.textContentType = .telephoneNumber phoneNumberContainer.addSubview(textField) textField.snp.makeConstraints { maker in diff --git a/Nynja/Modules/Auth/SelectCountry/Entities/Country.swift b/Nynja/Modules/Auth/SelectCountry/Entities/Country.swift index d2cc4363c..ef01331f0 100644 --- a/Nynja/Modules/Auth/SelectCountry/Entities/Country.swift +++ b/Nynja/Modules/Auth/SelectCountry/Entities/Country.swift @@ -13,6 +13,10 @@ final class Country: WheelItemModel { let code: String let placeHolder: String? + var defaultMask: String { + return "XXX XX XX" + } + var hasPhonePattern: Bool { return placeHolder != nil } diff --git a/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift b/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift index a7dc95f7b..b60a2fa7a 100644 --- a/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift +++ b/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit // MARK: - Protocol @@ -198,15 +199,8 @@ extension MentionController { /// Manually replace text in output handler private func replace(_ replacementText: String, in replacementRange: NSRange, currentText text: String) { let updatedText = (text as NSString).replacingCharacters(in: replacementRange, with: replacementText) as String + let newCursorPosition = TextInputUtils.updatedCursor(for: replacementText, in: replacementRange, currentText: text) - let newCursorPosition: Int - if replacementRange.length > replacementText.utf16.count { - // delete or replace - newCursorPosition = replacementRange.upperBound - (replacementRange.length - replacementText.utf16.count) - } else { - // insert - newCursorPosition = replacementRange.upperBound + (replacementText.utf16.count - replacementRange.length) - } save(text: updatedText, cursorPosition: newCursorPosition) handleUpdate(text: updatedText, cursor: newCursorPosition) } -- GitLab From 316881f119af100945ef6d6219994daabf79b5f6 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 19 Nov 2018 14:42:53 +0200 Subject: [PATCH 111/138] [NY-5421] Implemented ProgressHUD. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 12 ++ .../Views/LoadingIndicator/ProgressHUD.swift | 130 ++++++++++++++++++ Nynja.xcodeproj/project.pbxproj | 18 +-- Nynja/AppDelegate.swift | 11 ++ .../Auth/AuthModule/AuthProtocols.swift | 2 + .../AuthModule/Presenter/AuthPresenter.swift | 11 +- .../AuthModule/View/AuthViewController.swift | 21 +++ Podfile | 6 +- Podfile.lock | 19 ++- 9 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index cf1432f1a..54ece8a19 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00B2181EBD2003A010F /* BaseView.swift */; }; 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */; }; 8540A019218213E2003A010F /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A018218213E2003A010F /* RoundView.swift */; }; + 8541995C21A2C2B7004009F7 /* ProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541995B21A2C2B7004009F7 /* ProgressHUD.swift */; }; 855A4E86219AFA8200B6E90B /* UnderlinedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */; }; 855A4E88219AFB0F00B6E90B /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E87219AFB0F00B6E90B /* TextField.swift */; }; 855A4E8A219AFB9D00B6E90B /* InputsCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */; }; @@ -83,6 +84,7 @@ 8540A00B2181EBD2003A010F /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Animation.swift"; sourceTree = ""; }; 8540A018218213E2003A010F /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; + 8541995B21A2C2B7004009F7 /* ProgressHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressHUD.swift; sourceTree = ""; }; 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlinedTextField.swift; sourceTree = ""; }; 855A4E87219AFB0F00B6E90B /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputsCachePolicy.swift; sourceTree = ""; }; @@ -305,6 +307,7 @@ 8540A00B2181EBD2003A010F /* BaseView.swift */, 8540A01A218213E8003A010F /* Utils */, 855A4E84219AFA6C00B6E90B /* Controls */, + 8541995821A2C272004009F7 /* LoadingIndicator */, 85409FFD2181C8AF003A010F /* Avatar */, 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, @@ -355,6 +358,14 @@ path = Utils; sourceTree = ""; }; + 8541995821A2C272004009F7 /* LoadingIndicator */ = { + isa = PBXGroup; + children = ( + 8541995B21A2C2B7004009F7 /* ProgressHUD.swift */, + ); + path = LoadingIndicator; + sourceTree = ""; + }; 855A4E84219AFA6C00B6E90B /* Controls */ = { isa = PBXGroup; children = ( @@ -495,6 +506,7 @@ buildActionMask = 2147483647; files = ( 8514D4DF20EE2D970002378A /* InteractiveCellViewModel.swift in Sources */, + 8541995C21A2C2B7004009F7 /* ProgressHUD.swift in Sources */, 8514D51720EE40540002378A /* NynjaContextMenu.swift in Sources */, 8514D4E720EE2D970002378A /* XibInitializable.swift in Sources */, 855A4E8A219AFB9D00B6E90B /* InputsCachePolicy.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift new file mode 100644 index 000000000..45aa19b8e --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift @@ -0,0 +1,130 @@ +// +// ProgressHUD.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 19.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import MaterialComponents.MaterialActivityIndicator + +public final class ProgressHUD: BaseView { + + public var radius: CGFloat = ProgressHUD.config.radius { + didSet { + activityIndicator.radius = radius + } + } + + public var strokeWidth: CGFloat = ProgressHUD.config.strokeWidth { + didSet { + activityIndicator.strokeWidth = strokeWidth + } + } + + public var color: UIColor = ProgressHUD.config.color { + didSet { + activityIndicator.cycleColors = [color] + } + } + + public var bgColor: UIColor = ProgressHUD.config.backgroundColor { + didSet { + backgroundView.backgroundColor = bgColor + } + } + + + // MARK: - Views + + private lazy var backgroundView: UIView = { + let backgroundView = UIView() + + addSubview(backgroundView) + backgroundView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return backgroundView + }() + + private lazy var activityIndicator: MDCActivityIndicator = { + let activityIndicator = MDCActivityIndicator() + + addSubview(activityIndicator) + activityIndicator.snp.makeConstraints { maker in + maker.center.equalToSuperview() + } + + return activityIndicator + }() + + public override func baseSetup() { + super.baseSetup() + backgroundView.backgroundColor = bgColor + backgroundView.alpha = 0 + activityIndicator.indicatorMode = .indeterminate + activityIndicator.radius = radius + activityIndicator.strokeWidth = strokeWidth + activityIndicator.cycleColors = [color] + } + + public override func layoutSubviews() { + super.layoutSubviews() + activityIndicator.sizeToFit() + } + + public func startAnimating() { + superview?.bringSubview(toFront: self) + isUserInteractionEnabled = true + + UIView.animate(withDuration: 0.3, delay: 0, options: .beginFromCurrentState, animations: { + self.backgroundView.alpha = 1 + }, completion: nil) + + activityIndicator.startAnimating() + } + + public func stopAnimating() { + activityIndicator.stopAnimating() + + UIView.animate(withDuration: 0.3, delay: 0, options: .beginFromCurrentState, animations: { + self.backgroundView.alpha = 0 + }, completion: { isCompleted in + if isCompleted { + self.superview?.sendSubview(toBack: self) + } + self.isUserInteractionEnabled = false + }) + } +} + +extension ProgressHUD { + + public struct UIConfig { + public let radius: CGFloat + public let strokeWidth: CGFloat + public let color: UIColor + public let backgroundColor: UIColor + + public init(radius: CGFloat, strokeWidth: CGFloat, color: UIColor, backgroundColor: UIColor) { + self.radius = radius + self.strokeWidth = strokeWidth + self.color = color + self.backgroundColor = backgroundColor + } + + public static let `default` = UIConfig(radius: 42, + strokeWidth: 8, + color: .red, + backgroundColor: UIColor.black.withAlphaComponent(0.4)) + } + + private static var config: UIConfig = .default + + public static func updateConfig(_ config: UIConfig) { + self.config = config + } +} diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 50b121a4a..c77ff430b 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -14988,8 +14988,6 @@ "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", - "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", - "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", "${BUILT_PRODUCTS_DIR}/MulticastDelegateSwift/MulticastDelegateSwift.framework", "${PODS_ROOT}/NynjaSDK-MultiAcc/NynjaSDK.framework", "${BUILT_PRODUCTS_DIR}/QRCode/QRCode.framework", @@ -14999,6 +14997,11 @@ "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", "${BUILT_PRODUCTS_DIR}/SwiftyTimer/SwiftyTimer.framework", "${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework", + "${BUILT_PRODUCTS_DIR}/MDFInternationalization/MDFInternationalization.framework", + "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", + "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", + "${BUILT_PRODUCTS_DIR}/MotionAnimator/MotionAnimator.framework", + "${BUILT_PRODUCTS_DIR}/MotionInterchange/MotionInterchange.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( @@ -15015,8 +15018,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MulticastDelegateSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NynjaSDK.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QRCode.framework", @@ -15026,6 +15027,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyTimer.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFInternationalization.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionAnimator.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionInterchange.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -15050,8 +15056,6 @@ "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", - "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", - "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", "${BUILT_PRODUCTS_DIR}/QRCode/QRCode.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SQLCipher/SQLCipher.framework", @@ -15072,8 +15076,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QRCode.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SQLCipher.framework", diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index c703a6491..0591c1ff8 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -17,6 +17,7 @@ import AWSS3 import UserNotifications import Firebase import Intercom +import NynjaUIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { @@ -47,6 +48,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD wipeStorage() + configureAppearance() + configureWindow() LogService.log(topic: .system) { return "Avaliable logs:\n\(LogServiceTopic.allValuesStrings)" } @@ -106,6 +109,14 @@ extension AppDelegate { private extension AppDelegate { + func configureAppearance() { + let uiConfig = ProgressHUD.UIConfig(radius: CGFloat(42.0.adjustedByWidth), + strokeWidth: CGFloat(8.0.adjustedByWidth), + color: UIColor.nynja.mainRed, + backgroundColor: UIColor.nynja.black.withAlphaComponent(0.4)) + ProgressHUD.updateConfig(uiConfig) + } + func configureWindow() { // TODO: only for demo. While custom theme UI won't be implemented - app should start with dark theme. UserSettingsService.shared.theme = .default diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift index b0dba3178..85e180f74 100644 --- a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift @@ -22,6 +22,8 @@ protocol AuthWireframeProtocol: class { protocol AuthViewProtocol: class where Self: UIViewController { func select(country: Country) func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) + func showLoading() + func hideLoading() } protocol AuthPresenterProtocol: class { diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index e93d54bfe..d172ff77d 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -40,6 +40,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } func loginViaGoogle() { + view?.showLoading() interactor.loginViaGoogle() } @@ -47,6 +48,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, let email = email.trimmed() wireframe.confirmInputData(loginOption: .email(email)) { isConfirmed in if isConfirmed { + self.view?.showLoading() self.interactor.loginViaEmail(email) } } @@ -57,6 +59,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, wireframe.confirmInputData(loginOption: .phoneNumber(numberInfo.displayString)) { isConfirmed in if isConfirmed { + self.view?.showLoading() self.interactor.loginViaPhoneNumber(numberInfo) } } @@ -88,7 +91,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, extension AuthPresenter { func googleAuthWillStart(_ googleAuthService: GoogleAuthService) { - // TODO: hide loading indicators if needed - google auth viewController will be presented. + view?.hideLoading() } func googleAuth(_ googleAuthService: GoogleAuthService, dismiss viewController: UIViewController) { @@ -105,13 +108,13 @@ extension AuthPresenter { extension AuthPresenter { func didAuthenticated(with loginFlow: LoginFlow) { + view?.hideLoading() wireframe?.continueLogin(loginFlow: loginFlow) } func didReceiveAuthenticationFailure(_ error: Error?) { - let actions = [ - UIAlertAction(title: "OK", style: .default, handler: nil) - ] + view?.hideLoading() + let actions = [UIAlertAction(title: "OK", style: .default, handler: nil)] wireframe.presentAlert(title: "Failure", message: error?.localizedDescription ?? "Something went wrong", actions: actions) diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift index 0c99f1c83..92207951a 100644 --- a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit final class AuthViewController: UIViewController, AuthViewProtocol, InitializeInjectable, KeyboardInteractive { @@ -26,6 +27,18 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn return backgroundImageView }() + private lazy var progressHUD: ProgressHUD = { + let progressHUD = ProgressHUD() + + view.addSubview(progressHUD) + progressHUD.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return progressHUD + }() + + // MARK: Scroll Container private lazy var scrollView: UIScrollView = { @@ -249,6 +262,14 @@ extension AuthViewController { phoneNumberTextController.country = autofillPhoneNumberInfo.country phoneNumberLoginView.updatePhone(autofillPhoneNumberInfo) } + + func showLoading() { + progressHUD.startAnimating() + } + + func hideLoading() { + progressHUD.stopAnimating() + } } // MARK: - KeyboardInteractive diff --git a/Podfile b/Podfile index 714ec08b9..cc2b5b403 100644 --- a/Podfile +++ b/Podfile @@ -37,7 +37,7 @@ def commonPodsForNynja pod 'SwiftyJSON', '= 4.0.0' pod 'AutoScrollLabel', '= 0.4.3' - pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' + pod 'JTAppleCalendar', '= 7.1.5' # pod 'NynjaSDK', '= 1.8' @@ -67,7 +67,7 @@ def commonPodsForNynjaTests pod 'GRDBCipher', '= 2.10.0' pod 'AutoScrollLabel', '= 0.4.3' - pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' + pod 'JTAppleCalendar', '= 7.1.5' end @@ -83,6 +83,8 @@ end def commonPodsForNynjaUIKit pod 'SnapKit', '= 4.0.0' + pod 'MaterialComponents/ActivityIndicator', '= 55.3.0' + pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' end target 'Nynja' do diff --git a/Podfile.lock b/Podfile.lock index 70cf9b0bd..585cc024a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -72,14 +72,24 @@ PODS: - Intercom (5.1.6) - JTAppleCalendar (7.1.5) - libPhoneNumber-iOS (0.9.13) + - MaterialComponents/ActivityIndicator (55.3.0): + - MaterialComponents/Palettes + - MaterialComponents/private/Application + - MDFInternationalization + - MotionAnimator (~> 2.0) - MaterialComponents/FlexibleHeader (55.3.0): - MaterialComponents/private/Application - MaterialComponents/private/UIMetrics - MDFTextAccessibility + - MaterialComponents/Palettes (55.3.0) - MaterialComponents/private/Application (55.3.0) - MaterialComponents/private/UIMetrics (55.3.0): - MaterialComponents/private/Application + - MDFInternationalization (1.1.0) - MDFTextAccessibility (1.2.0) + - MotionAnimator (2.8.1): + - MotionInterchange (~> 1.6) + - MotionInterchange (1.6.0) - MulticastDelegateSwift (2.1.1) - NynjaSDK-MultiAcc (0.5.4) - QRCode (2.0) @@ -113,6 +123,7 @@ DEPENDENCIES: - Intercom (= 5.1.6) - JTAppleCalendar (= 7.1.5) - libPhoneNumber-iOS (= 0.9.13) + - MaterialComponents/ActivityIndicator (= 55.3.0) - MaterialComponents/FlexibleHeader (= 55.3.0) - MulticastDelegateSwift (= 2.1.1) - NynjaSDK-MultiAcc (= 0.5.4) @@ -149,7 +160,10 @@ SPEC REPOS: - JTAppleCalendar - libPhoneNumber-iOS - MaterialComponents + - MDFInternationalization - MDFTextAccessibility + - MotionAnimator + - MotionInterchange - MulticastDelegateSwift - QRCode - SDWebImage @@ -198,7 +212,10 @@ SPEC CHECKSUMS: JTAppleCalendar: 2d4f974f9f3c8b4964d51ca1f6e004883c031fbe libPhoneNumber-iOS: e444379ac18bbfbdefad571da735b2cd7e096caa MaterialComponents: 915f4e844400a35db3ea4c710a9af40aa8bcb093 + MDFInternationalization: b5b8626628abf026e630e5ebaeee563037712cbb MDFTextAccessibility: 94098925e0853551c5a311ce7c1ecefbe297cdb6 + MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a + MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 NynjaSDK-MultiAcc: 2747f9a2b306841e013a3cb3ef91eb444015758c QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 @@ -209,6 +226,6 @@ SPEC CHECKSUMS: SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: ef6aa0576323064bab0bc810d9cde845d4814ea9 +PODFILE CHECKSUM: 6bba33c88884addbdbe1126e99066a8bb1f7b911 COCOAPODS: 1.5.3 -- GitLab From bd77720ca87469261acd1f5e85e7c04dc0a0807c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 19 Nov 2018 14:57:04 +0200 Subject: [PATCH 112/138] [NY-5421] Add loading indicator to code confirmation screen. --- .../CodeConfirmationProtocols.swift | 4 +- .../Presenter/CodeConfirmationPresenter.swift | 14 +++---- .../View/CodeConfirmationViewController.swift | 40 +++++++++++++++---- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift index a94a92bf8..be18056d5 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift @@ -17,8 +17,8 @@ protocol CodeConfirmationWireframeProtocol: class { protocol CodeConfirmationViewProtocol: class where Self: UIViewController { func updateTimerLabel(text: String) func showButtons() - func showHUD() - func hideHUD() + func showLoading() + func hideLoading() } protocol CodeConfirmationPresenterProtocol: NavigationProtocol { diff --git a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index 527168487..f9c35e6f1 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -71,12 +71,12 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } func sendConfirmationCode(_ code: String) { - view?.showHUD() + view?.showLoading() interactor.sendConfirmationCode(code) } func resendCode() { - view?.showHUD() + view?.showLoading() interactor?.resendCode() } @@ -84,7 +84,7 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo guard canAskForCall else { return } - view?.showHUD() + view?.showLoading() interactor?.askForCall() } @@ -98,20 +98,20 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo extension CodeConfirmationPresenter { func didResendCode() { - view?.hideHUD() + view?.hideLoading() } func didReceiveResendCodeFailure(_ error: Error) { - view?.hideHUD() + view?.hideLoading() } func didConfirmCode(authenticationType: AuthenticationType) { - view?.hideHUD() + view?.hideLoading() wireframe?.codeValid(with: authenticationType) } func didReceiveCodeConfirmationFailure(_ error: Error) { - view?.hideHUD() + view?.hideLoading() // FIXME: check if it is internal error or real wrong code wireframe?.codeInvalid() } diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift index 4097f5342..2b16cef41 100644 --- a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -7,11 +7,15 @@ // import UIKit +import NynjaUIKit final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, InitializeInjectable { private let viewsFactory: CodeConfirmationViewsFactoryProtocol private let presenter: CodeConfirmationPresenterProtocol + + // MARK: - Views + private lazy var backButton: UIButton = viewsFactory.makeBackButton(on: view, target: self, selector: #selector(back(sender:))) private lazy var welcomeLabel: UILabel = viewsFactory.makeWelcomeLabel(on: view) private lazy var logoImageView: UIImageView = viewsFactory.makeLogoImageView(on: view, top: welcomeLabel) @@ -34,6 +38,20 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi private weak var resendCodeButton: UIButton? private weak var callMeButton: UIButton? + private lazy var progressHUD: ProgressHUD = { + let progressHUD = ProgressHUD() + + view.addSubview(progressHUD) + progressHUD.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return progressHUD + }() + + + // MARK: - Init + init(dependencies: Dependencies) { presenter = dependencies.presenter viewsFactory = dependencies.viewsFactory @@ -46,6 +64,16 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi fatalError("init(coder:) has not been implemented") } + + // MARK: - Appearance + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + + // MARK: - Life Cycle + override func viewDidLoad() { super.viewDidLoad() @@ -69,10 +97,6 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi presenter.viewDidLoad() } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } } // MARK: - CodeConfirmationViewProtocol @@ -91,12 +115,12 @@ extension CodeConfirmationViewController { } } - func showHUD() { - // TODO: implement hud + func showLoading() { + progressHUD.startAnimating() } - func hideHUD() { - // TODO: implement hud + func hideLoading() { + progressHUD.stopAnimating() } } -- GitLab From ac0060f15ce9b1dc3204fc71c67df8ba1de22d02 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 19 Nov 2018 15:22:41 +0200 Subject: [PATCH 113/138] Updated SDK version to 0.5.5. --- Podfile | 2 +- Podfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Podfile b/Podfile index cc2b5b403..8fa111cbf 100644 --- a/Podfile +++ b/Podfile @@ -41,7 +41,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.5' # pod 'NynjaSDK', '= 1.8' - pod 'NynjaSDK-MultiAcc', '= 0.5.4' + pod 'NynjaSDK-MultiAcc', '= 0.5.5' pod 'CryptoSwift', '= 0.10.0' diff --git a/Podfile.lock b/Podfile.lock index 585cc024a..db7723d94 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -91,7 +91,7 @@ PODS: - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.4) + - NynjaSDK-MultiAcc (0.5.5) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -126,7 +126,7 @@ DEPENDENCIES: - MaterialComponents/ActivityIndicator (= 55.3.0) - MaterialComponents/FlexibleHeader (= 55.3.0) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.4) + - NynjaSDK-MultiAcc (= 0.5.5) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.0.0) @@ -217,7 +217,7 @@ SPEC CHECKSUMS: MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: 2747f9a2b306841e013a3cb3ef91eb444015758c + NynjaSDK-MultiAcc: 49d7d927d3fe56b2e7b3c672644db6858e259a0f QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: a42d492c16e80209130a3379f73596c3454b7694 @@ -226,6 +226,6 @@ SPEC CHECKSUMS: SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: 6bba33c88884addbdbe1126e99066a8bb1f7b911 +PODFILE CHECKSUM: e63f87cc0c77a9640951e44c3bb3fe3760247690 COCOAPODS: 1.5.3 -- GitLab From 1103820bbde4ac8fa2a87bcfc7f6efd53c533194 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 19 Nov 2018 15:42:47 +0200 Subject: [PATCH 114/138] Added method to AuthService for google and facebook authentication. --- Nynja/SDK/Auth/AuthService.swift | 59 ++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/Nynja/SDK/Auth/AuthService.swift b/Nynja/SDK/Auth/AuthService.swift index 250659983..6e80e8001 100644 --- a/Nynja/SDK/Auth/AuthService.swift +++ b/Nynja/SDK/Auth/AuthService.swift @@ -22,6 +22,10 @@ protocol AuthService: class { confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) + func loginByFacebook(serverCode: String, completion: @escaping LoginCompletion) + + func loginByGoogle(serverCode: String, completion: @escaping LoginCompletion) + func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) @@ -46,6 +50,8 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog private var loginByPhoneCompletion: LoginCompletion? private var loginByEmailCompletion: LoginCompletion? + private var loginByFacebookCompletion: LoginCompletion? + private var loginByGoogleCompletion: LoginCompletion? private var confirmCodeCompletion: CodeConfirmationCompletion? private var refreshTokenCompletion: RefreshTokenCompletion? @@ -95,7 +101,9 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func login(by email: String, completion: @escaping LoginCompletion) { - loginByEmailCompletion = completion + processingQueue.async { + self.loginByEmailCompletion = completion + } loginManager.sendLogin(byEmail: email, withAppToken: sessionStorage.appToken) } @@ -103,8 +111,9 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) { - loginByPhoneCompletion = completion - + processingQueue.async { + self.loginByPhoneCompletion = completion + } let country = numberInfo.country let numberFormat = "\(country.ISO):\(country.code)\(numberInfo.number)" @@ -113,13 +122,31 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog withSendTokenVia: authConfirmationType.sdkValue) } + func loginByFacebook(serverCode: String, completion: @escaping LoginCompletion) { + processingQueue.async { + self.loginByFacebookCompletion = completion + } + loginManager.sendLoginByFacebook(withAppToken: serverCode) + } + + func loginByGoogle(serverCode: String, completion: @escaping LoginCompletion) { + processingQueue.async { + self.loginByGoogleCompletion = completion + } + loginManager.sendLoginByGooglePlus(withAppToken: serverCode) + } + func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) { - confirmCodeCompletion = completion + processingQueue.async { + self.confirmCodeCompletion = completion + } loginManager.confirmCode(code, withCredential: socialToken) } func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) { - refreshTokenCompletion = completion + processingQueue.async { + self.refreshTokenCompletion = completion + } loginManager.refreshAccessToken(accessToken) } @@ -153,11 +180,29 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func sendLogin(byFacebookDidFinish error: Error?) { - + processingQueue.async { + let completion = self.loginByFacebookCompletion + self.loginByFacebookCompletion = nil + + if let error = error { + completion?(.failure(error)) + } else { + completion?(.success(())) + } + } } func sendLogin(byGooglePlusDidFinish error: Error?) { - + processingQueue.async { + let completion = self.loginByGoogleCompletion + self.loginByGoogleCompletion = nil + + if let error = error { + completion?(.failure(error)) + } else { + completion?(.success(())) + } + } } func confirmCodeDidFinish(withAccoutnId accountId: String, -- GitLab From b7976c90837d90a684286ebfbf856eeee9ce3a72 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 19 Nov 2018 17:07:08 +0200 Subject: [PATCH 115/138] Fixed LoginManager's delegate. --- Nynja/Services/ServiceFactory/ServiceFactory.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 968011c8f..fe1981c1d 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -55,12 +55,17 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return HistoryRequestModelFactory() } - func makeAuthService() -> AuthService { + // Lazy wars is not thread safe, so it must be initialized from single thread + private lazy var authService: AuthService = { let dependencies = AuthServiceImpl.Dependencies(communicator: makeCommunicator(), loginManager: makeLoginManager(), sessionStorage: makeStorageService(), appConfigurationProvider: makeAppConfigurationProvider()) return AuthServiceImpl(dependencies: dependencies) + }() + + func makeAuthService() -> AuthService { + return authService } func makeGoogleAuthService() -> GoogleAuthService { -- GitLab From e02de4e24c8dcb3cabbc2018110a415f497b17a4 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 20 Nov 2018 11:31:46 +0200 Subject: [PATCH 116/138] Implemented requests to google and facebook to SDK. --- .../Interactor/AuthInteractor.swift | 32 ++++++++++++------- .../AuthModule/Presenter/AuthPresenter.swift | 19 +++++------ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 6edc1e0aa..3523b9e40 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -45,24 +45,34 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { } func loginViaFacebook(code: String) { - presenter?.didAuthenticated(with: .facebook) + authService.loginByFacebook(serverCode: code) { [weak self] result in + switch result { + case .success: + self?.presenter?.didAuthenticated(with: .facebook) + case let .failure(error): + self?.presenter?.didReceiveAuthenticationFailure(error) + } + } } func loginViaGoogle() { googleAuthService.signIn { [weak self] result in + guard let `self` = self else { return } + switch result { case let .success(code): - print("Google server auth code: \(code)") - self?.presenter?.didAuthenticated(with: .google) - - case let .failure(error): - switch error { - case let error as GoogleAuthError: - print("Google auth error: \(error.localizedDescription)") - default: - print("Google unknown error: \(error.localizedDescription)") + self.authService.loginByGoogle(serverCode: code) { [weak self] googleResult in + guard let `self` = self else { return } + + switch googleResult { + case .success: + self.presenter?.didAuthenticated(with: .google) + case let .failure(error): + self.presenter?.didReceiveAuthenticationFailure(error) + } } - self?.presenter?.didReceiveAuthenticationFailure(error) + case let .failure(error): + self.presenter?.didReceiveAuthenticationFailure(error) } } } diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index d172ff77d..facb44989 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -30,12 +30,11 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, func loginViaFacebook() { wireframe.showFacebookAuth { [weak self] result in - switch result { - case let .success(code): - self?.interactor?.loginViaFacebook(code: code) - case .failure: - break + guard case let .success(code) = result else { + return } + self?.view?.showLoading() + self?.interactor?.loginViaFacebook(code: code) } } @@ -67,13 +66,11 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, func selectCountry() { wireframe.selectCountry { result in - switch result { - case let .success(country): - self.selectedCountry = country - self.view?.select(country: country) - case .failure: - break + guard case let .success(country) = result else { + return } + self.selectedCountry = country + self.view?.select(country: country) } } -- GitLab From 0b46040ab01f235c5cac86f7f150466e23a02857 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 21 Nov 2018 14:16:10 +0200 Subject: [PATCH 117/138] Implemented AccountService (#1487) * Update SDK version * Implemented base skeleton for AccountService * Added callbacks to AccountService * Implemented AccountService * Rename CoordinatorProtocol to Coordinator * Use single instance of ServiceFactory for auth flow. --- Nynja.xcodeproj/project.pbxproj | 86 +++- Nynja/AppDelegate.swift | 17 +- ...inatorProtocol.swift => Coordinator.swift} | 4 +- .../AccountSettingsCoordinator.swift | 2 +- Nynja/Modules/Auth/AuthCoordinator.swift | 2 +- .../Interactor/AuthInteractor.swift | 2 - .../AuthModule/Presenter/AuthPresenter.swift | 6 +- .../Flows/CameraFlow/CameraCoordinator.swift | 4 +- .../CameraSettingsCoordinator.swift | 2 +- .../GalleryFlow/GalleryCoordinator.swift | 4 +- .../SelectAvatarCoordinator.swift | 4 +- Nynja/SDK/Account/Entities/AccountError.swift | 13 + Nynja/SDK/Account/Entities/AccountInfo.swift | 48 +++ .../SDK/Account/Service/AccountService.swift | 53 +++ .../Account/Service/AccountServiceImpl.swift | 384 ++++++++++++++++++ Nynja/SDK/App/AppBundleCredentials.swift | 1 + Nynja/SDK/App/AppConfigurationProvider.swift | 9 +- .../{ => Entities}/AuthConfirmationType.swift | 0 .../Auth/{ => Entities}/AuthResponse.swift | 0 .../Auth/{ => Entities}/AuthTokenData.swift | 0 .../{ => Entities}/AuthenticationType.swift | 0 .../Auth/{ => Entities}/PhoneNumberInfo.swift | 0 Nynja/SDK/Auth/Service/AuthService.swift | 28 ++ .../AuthServiceImpl.swift} | 83 +--- Nynja/SDK/Session/SessionStorage.swift | 2 +- .../ServiceFactory/ServiceFactory.swift | 3 +- Nynja/StorageService+UserInfo.swift | 4 - Podfile | 2 +- Podfile.lock | 8 +- 29 files changed, 664 insertions(+), 107 deletions(-) rename Nynja/Coordinators/{CoordinatorProtocol.swift => Coordinator.swift} (73%) create mode 100644 Nynja/SDK/Account/Entities/AccountError.swift create mode 100644 Nynja/SDK/Account/Entities/AccountInfo.swift create mode 100644 Nynja/SDK/Account/Service/AccountService.swift create mode 100644 Nynja/SDK/Account/Service/AccountServiceImpl.swift rename Nynja/SDK/Auth/{ => Entities}/AuthConfirmationType.swift (100%) rename Nynja/SDK/Auth/{ => Entities}/AuthResponse.swift (100%) rename Nynja/SDK/Auth/{ => Entities}/AuthTokenData.swift (100%) rename Nynja/SDK/Auth/{ => Entities}/AuthenticationType.swift (100%) rename Nynja/SDK/Auth/{ => Entities}/PhoneNumberInfo.swift (100%) create mode 100644 Nynja/SDK/Auth/Service/AuthService.swift rename Nynja/SDK/Auth/{AuthService.swift => Service/AuthServiceImpl.swift} (76%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index c77ff430b..389526a3a 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -864,6 +864,7 @@ 85057964206D0CE500565C60 /* LocationPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85057963206D0CE500565C60 /* LocationPlaceholderWheelItemModel.swift */; }; 85057966206D17AB00565C60 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85057965206D17AB00565C60 /* ImagePickerHandler.swift */; }; 8506F001206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8506F000206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift */; }; + 8507622421A4735E00E4CEFE /* AccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8507622321A4735E00E4CEFE /* AccountError.swift */; }; 85082DDD2045A873000AE4B2 /* UserSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85082DDC2045A873000AE4B2 /* UserSettingsService.swift */; }; 85082DDF2045A8C2000AE4B2 /* WheelPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85082DDE2045A8C2000AE4B2 /* WheelPosition.swift */; }; 850833DB2037171600587EEF /* FileExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850833DA2037171600587EEF /* FileExtensionView.swift */; }; @@ -1077,7 +1078,7 @@ 855A4E7F2199B4FE00B6E90B /* NynjaImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */; }; 855A4E812199C16F00B6E90B /* RoundNynjaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */; }; 855A4E822199C1A100B6E90B /* RoundNynjaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */; }; - 855A4E9B219B321000B6E90B /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E9A219B321000B6E90B /* AuthService.swift */; }; + 855A4E9B219B321000B6E90B /* AuthServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E9A219B321000B6E90B /* AuthServiceImpl.swift */; }; 855A4E9E219B336000B6E90B /* AppBundleCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E9D219B336000B6E90B /* AppBundleCredentials.swift */; }; 855A4EA0219B35B700B6E90B /* AuthConfirmationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4E9F219B35B700B6E90B /* AuthConfirmationType.swift */; }; 855A4EA2219B3A9400B6E90B /* AuthTokenData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A4EA1219B3A9400B6E90B /* AuthTokenData.swift */; }; @@ -1173,6 +1174,10 @@ 859C429F2056829300AE3797 /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859C429E2056829300AE3797 /* NotificationSettings.swift */; }; 859C42A5205691FB00AE3797 /* Sounds.json in Resources */ = {isa = PBXBuildFile; fileRef = 859C42A4205691FB00AE3797 /* Sounds.json */; }; 859C42AD2056BF9F00AE3797 /* incoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 859C42AC2056BF9F00AE3797 /* incoming_message.mp3 */; }; + 859ECA6621A43A3F003630A0 /* AccountServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859ECA6521A43A3F003630A0 /* AccountServiceImpl.swift */; }; + 859ECA6821A43DC1003630A0 /* AccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859ECA6721A43DC1003630A0 /* AccountInfo.swift */; }; + 859ECA6A21A43FE4003630A0 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859ECA6921A43FE4003630A0 /* AccountService.swift */; }; + 859ECA6C21A441A9003630A0 /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859ECA6B21A441A9003630A0 /* AuthService.swift */; }; 859F9B4C2035CB1E009D017A /* ForwardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859F9B4B2035CB1E009D017A /* ForwardContent.swift */; }; 85A3CA02214129F200E0EDD5 /* KeyboardInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A3CA01214129F200E0EDD5 /* KeyboardInteractive.swift */; }; 85A3CA03214133FD00E0EDD5 /* KeyboardInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A3CA01214129F200E0EDD5 /* KeyboardInteractive.swift */; }; @@ -2143,7 +2148,7 @@ F10B0E2820B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10B0E2720B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift */; }; F10B0E2C20B51CCF00528E7A /* CounterIndicatorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10B0E2B20B51CCF00528E7A /* CounterIndicatorButton.swift */; }; F112B19020E0FBE800B06E3E /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F112B18F20E0FBE800B06E3E /* AsyncBlockOperation.swift */; }; - F11786BB20A8A63F007A9A1B /* CoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786BA20A8A63F007A9A1B /* CoordinatorProtocol.swift */; }; + F11786BB20A8A63F007A9A1B /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786BA20A8A63F007A9A1B /* Coordinator.swift */; }; F11786C920A8E4FD007A9A1B /* CameraVideoPreviewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786BD20A8E4FD007A9A1B /* CameraVideoPreviewProtocols.swift */; }; F11786CA20A8E4FD007A9A1B /* CameraVideoPreviewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786BF20A8E4FD007A9A1B /* CameraVideoPreviewPresenter.swift */; }; F11786CB20A8E4FD007A9A1B /* CameraVideoPreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786C120A8E4FD007A9A1B /* CameraVideoPreviewWireframe.swift */; }; @@ -3199,6 +3204,7 @@ 85057963206D0CE500565C60 /* LocationPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPlaceholderWheelItemModel.swift; sourceTree = ""; }; 85057965206D17AB00565C60 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = ""; }; 8506F000206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemView.swift; sourceTree = ""; }; + 8507622321A4735E00E4CEFE /* AccountError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountError.swift; sourceTree = ""; }; 85082DDC2045A873000AE4B2 /* UserSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsService.swift; sourceTree = ""; }; 85082DDE2045A8C2000AE4B2 /* WheelPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelPosition.swift; sourceTree = ""; }; 850833DA2037171600587EEF /* FileExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtensionView.swift; sourceTree = ""; }; @@ -3374,7 +3380,7 @@ 855A393C213E76E20002B8DC /* LoadingInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoadingInteractive.swift; path = BaseVC/LoadingInteractive.swift; sourceTree = ""; }; 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaImageButton.swift; sourceTree = ""; }; 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundNynjaButton.swift; sourceTree = ""; }; - 855A4E9A219B321000B6E90B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; + 855A4E9A219B321000B6E90B /* AuthServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthServiceImpl.swift; sourceTree = ""; }; 855A4E9D219B336000B6E90B /* AppBundleCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBundleCredentials.swift; sourceTree = ""; }; 855A4E9F219B35B700B6E90B /* AuthConfirmationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationType.swift; sourceTree = ""; }; 855A4EA1219B3A9400B6E90B /* AuthTokenData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTokenData.swift; sourceTree = ""; }; @@ -3467,6 +3473,10 @@ 859C429E2056829300AE3797 /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; 859C42A4205691FB00AE3797 /* Sounds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Sounds.json; sourceTree = ""; }; 859C42AC2056BF9F00AE3797 /* incoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = incoming_message.mp3; sourceTree = ""; }; + 859ECA6521A43A3F003630A0 /* AccountServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountServiceImpl.swift; sourceTree = ""; }; + 859ECA6721A43DC1003630A0 /* AccountInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfo.swift; sourceTree = ""; }; + 859ECA6921A43FE4003630A0 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = ""; }; + 859ECA6B21A441A9003630A0 /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; 859F9B4B2035CB1E009D017A /* ForwardContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardContent.swift; sourceTree = ""; }; 85A3CA01214129F200E0EDD5 /* KeyboardInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardInteractive.swift; sourceTree = ""; }; 85B0013121270DEC000C89FE /* TableOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableOrder.swift; sourceTree = ""; }; @@ -4286,7 +4296,7 @@ F10B0E2720B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPhotoItemCollectionViewCell.swift; sourceTree = ""; }; F10B0E2B20B51CCF00528E7A /* CounterIndicatorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterIndicatorButton.swift; sourceTree = ""; }; F112B18F20E0FBE800B06E3E /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = ""; }; - F11786BA20A8A63F007A9A1B /* CoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorProtocol.swift; sourceTree = ""; }; + F11786BA20A8A63F007A9A1B /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; F11786BD20A8E4FD007A9A1B /* CameraVideoPreviewProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraVideoPreviewProtocols.swift; sourceTree = ""; }; F11786BF20A8E4FD007A9A1B /* CameraVideoPreviewPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraVideoPreviewPresenter.swift; sourceTree = ""; }; F11786C120A8E4FD007A9A1B /* CameraVideoPreviewWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraVideoPreviewWireframe.swift; sourceTree = ""; }; @@ -9174,6 +9184,7 @@ 850B9DA4219C2B7C00EA0CF4 /* App */, 850B9D9D219C11BF00EA0CF4 /* Session */, 855A4E9C219B323200B6E90B /* Auth */, + 859ECA6421A438E8003630A0 /* Account */, ); path = SDK; sourceTree = ""; @@ -9181,12 +9192,8 @@ 855A4E9C219B323200B6E90B /* Auth */ = { isa = PBXGroup; children = ( - 855A4E9A219B321000B6E90B /* AuthService.swift */, - 855A4E9F219B35B700B6E90B /* AuthConfirmationType.swift */, - 855A4EA1219B3A9400B6E90B /* AuthTokenData.swift */, - 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */, - 856A8EFB219C8D7A0004E11E /* AuthenticationType.swift */, - 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */, + 859ECA6D21A441D8003630A0 /* Service */, + 859ECA6E21A441E8003630A0 /* Entities */, ); path = Auth; sourceTree = ""; @@ -9671,6 +9678,54 @@ path = Chat; sourceTree = ""; }; + 859ECA6421A438E8003630A0 /* Account */ = { + isa = PBXGroup; + children = ( + 859ECA6F21A44203003630A0 /* Service */, + 859ECA7021A4420E003630A0 /* Entities */, + ); + path = Account; + sourceTree = ""; + }; + 859ECA6D21A441D8003630A0 /* Service */ = { + isa = PBXGroup; + children = ( + 859ECA6B21A441A9003630A0 /* AuthService.swift */, + 855A4E9A219B321000B6E90B /* AuthServiceImpl.swift */, + ); + path = Service; + sourceTree = ""; + }; + 859ECA6E21A441E8003630A0 /* Entities */ = { + isa = PBXGroup; + children = ( + 855A4E9F219B35B700B6E90B /* AuthConfirmationType.swift */, + 855A4EA1219B3A9400B6E90B /* AuthTokenData.swift */, + 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */, + 856A8EFB219C8D7A0004E11E /* AuthenticationType.swift */, + 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 859ECA6F21A44203003630A0 /* Service */ = { + isa = PBXGroup; + children = ( + 859ECA6921A43FE4003630A0 /* AccountService.swift */, + 859ECA6521A43A3F003630A0 /* AccountServiceImpl.swift */, + ); + path = Service; + sourceTree = ""; + }; + 859ECA7021A4420E003630A0 /* Entities */ = { + isa = PBXGroup; + children = ( + 859ECA6721A43DC1003630A0 /* AccountInfo.swift */, + 8507622321A4735E00E4CEFE /* AccountError.swift */, + ); + path = Entities; + sourceTree = ""; + }; 859F9B4A2035C555009D017A /* Cell */ = { isa = PBXGroup; children = ( @@ -13399,7 +13454,7 @@ F11786B320A8A5EB007A9A1B /* Coordinators */ = { isa = PBXGroup; children = ( - F11786BA20A8A63F007A9A1B /* CoordinatorProtocol.swift */, + F11786BA20A8A63F007A9A1B /* Coordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -15710,6 +15765,7 @@ 26AB1419218775BB00F2BB83 /* ConversionState.swift in Sources */, 9BC9657620FF042E00052AE1 /* CallInProgressProtocols.swift in Sources */, 4B1D7E0D2029DACF00703228 /* ByNumberItemsFactory.swift in Sources */, + 8507622421A4735E00E4CEFE /* AccountError.swift in Sources */, F11DF06C20BEF43A00F3E005 /* ResourceManager.swift in Sources */, F117871920ACF018007A9A1B /* CameraSettingsInteractor.swift in Sources */, 26FA420A2017ADF000E6F6EC /* StarMessageCell.swift in Sources */, @@ -15800,6 +15856,7 @@ 6D6234F81F1E158600EF375F /* HistoryCell.swift in Sources */, FEA655F62167777E00B44029 /* PaymentInteractor.swift in Sources */, E74EC9EF1FC2DE23007268E6 /* MemberTable.swift in Sources */, + 859ECA6821A43DC1003630A0 /* AccountInfo.swift in Sources */, 8514F17220EA219E00883513 /* ContextMenuItemsView.swift in Sources */, 268C3413210688B200F1472A /* TranscribeLongRequestData.swift in Sources */, A46C36312121996000172773 /* DebuggingDetector.swift in Sources */, @@ -16399,6 +16456,7 @@ 6D5157D01F30B36A002A27DB /* ChatView.swift in Sources */, 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */, E735853D1F6C2705003354B5 /* Geometry.swift in Sources */, + 859ECA6621A43A3F003630A0 /* AccountServiceImpl.swift in Sources */, 85B0013421272694000C89FE /* MessageInteractor+History.swift in Sources */, F119E67720D27E990043A532 /* ImagePreviewCVCell.swift in Sources */, 260313A720A0A4BA009AC66D /* ChatLanguageSettingsTableDataSource.swift in Sources */, @@ -16441,7 +16499,7 @@ A94B03A70E016BDA759B0703 /* EditProfileViewController.swift in Sources */, 8562853920D166E5000C9739 /* CollectionPreviewState.swift in Sources */, 4B7C73F2215A5509007924DB /* MotionManager.swift in Sources */, - F11786BB20A8A63F007A9A1B /* CoordinatorProtocol.swift in Sources */, + F11786BB20A8A63F007A9A1B /* Coordinator.swift in Sources */, F105C6BE20A1347E0091786A /* PhotoPreviewInteractor.swift in Sources */, F18AEAFD20C15792004FE01C /* SelectAvatarCoordinator.swift in Sources */, 85D66A1220BD965300FBD803 /* UserMentionTableViewCell.swift in Sources */, @@ -17173,7 +17231,7 @@ 8524C4D92177741A003BF374 /* Service+Construct.swift in Sources */, E77764BD1FBDA9B60042541D /* ImageFullWheelItemView.swift in Sources */, B723C627204D86AF00884FFD /* SettingsDataAndStorageProtocols.swift in Sources */, - 855A4E9B219B321000B6E90B /* AuthService.swift in Sources */, + 855A4E9B219B321000B6E90B /* AuthServiceImpl.swift in Sources */, 260313A120A0A4BA009AC66D /* DirectableActionCell.swift in Sources */, 6547BE911E492D790E0D4390 /* EditGroupNameInteractor.swift in Sources */, 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */, @@ -17230,6 +17288,7 @@ E7C36C3B1FC46B4300740630 /* FeatureExtension.swift in Sources */, B723C630204D9E1500884FFD /* PickableEnum.swift in Sources */, A432CF1F20B44C0000993AFB /* MaterialTextContainer.swift in Sources */, + 859ECA6A21A43FE4003630A0 /* AccountService.swift in Sources */, 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */, 852BB8D22194256600F2E8E4 /* FacebookAuthWireframe.swift in Sources */, A43B259520AB1DFA00FF8107 /* InputContentProtocol.swift in Sources */, @@ -17305,6 +17364,7 @@ FB16E79720EFAF94009FA203 /* Currency.swift in Sources */, 8562853B20D16C61000C9739 /* LongPressClosureRecognizer.swift in Sources */, 852C3DCD216E34FC00447878 /* TypingSenderService.swift in Sources */, + 859ECA6C21A441A9003630A0 /* AuthService.swift in Sources */, 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */, 26DCB24C2064B9CC001EF0AB /* ContactCellModel.swift in Sources */, 856A8EFC219C8D7A0004E11E /* AuthenticationType.swift in Sources */, diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 0591c1ff8..78cfe2b8a 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -34,6 +34,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private let storageService = StorageService.sharedInstance private let antiDebuggingService = AntiDebuggingService() + private let serviceFactory = ServiceFactory() + + // FIXME: need to be removed from here when share extension won't require new mqtt connection. private var appGroupObserver: AppGroupFlagObserver? @@ -128,11 +131,12 @@ private extension AppDelegate { window?.rootViewController = navigation window?.makeKeyAndVisible() - let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) coordinator.start() } func configureDependencies() { + setupNynjaSDK() setupTestFairy() setupCrashlytics() setupGoogleMaps() @@ -179,6 +183,17 @@ private extension AppDelegate { private extension AppDelegate { + func setupNynjaSDK() { + let communicator = serviceFactory.makeCommunicator() + let appConfigurationProvider = serviceFactory.makeAppConfigurationProvider() + + let authConfig = appConfigurationProvider.authServerConfig + communicator.setAuthServerAddress(authConfig.host, andPort: authConfig.port, secure: authConfig.isSecure) + + let accountConfig = appConfigurationProvider.accountServerConfig + communicator.setAccountServerAddress(accountConfig.host, andPort: accountConfig.port, secure: accountConfig.isSecure) + } + func setupTestFairy() { let key = ThirdPartyServicesFactory.testFairy.serviceConfig.key TestFairy.begin(key) diff --git a/Nynja/Coordinators/CoordinatorProtocol.swift b/Nynja/Coordinators/Coordinator.swift similarity index 73% rename from Nynja/Coordinators/CoordinatorProtocol.swift rename to Nynja/Coordinators/Coordinator.swift index 9de25563f..0600b49b7 100644 --- a/Nynja/Coordinators/CoordinatorProtocol.swift +++ b/Nynja/Coordinators/Coordinator.swift @@ -1,5 +1,5 @@ // -// CoordinatorProtocol.swift +// Coordinator.swift // Nynja // // Created by AshCenso on 5/13/18. @@ -8,7 +8,7 @@ import Foundation -protocol CoordinatorProtocol { +protocol Coordinator { func start() func end() } diff --git a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift index a9e84eb31..7114ac476 100644 --- a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift @@ -8,7 +8,7 @@ import Foundation -final class AccountSettingsCoordinator: CoordinatorProtocol, AccountSettingsCoordinatorProtocol { +final class AccountSettingsCoordinator: Coordinator, AccountSettingsCoordinatorProtocol { private let navigation: UINavigationController private let serviceFactory: ServiceFactoryProtocol diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth/AuthCoordinator.swift index 5827a5d14..23a4cdaff 100644 --- a/Nynja/Modules/Auth/AuthCoordinator.swift +++ b/Nynja/Modules/Auth/AuthCoordinator.swift @@ -9,7 +9,7 @@ import Foundation import SDWebImage -final class AuthCoordinator: CoordinatorProtocol, NavigationContainer, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol, CreateProfileCoordinatorProtocol { +final class AuthCoordinator: Coordinator, NavigationContainer, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol, CreateProfileCoordinatorProtocol { private(set) weak var navigation: UINavigationController? diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift index 3523b9e40..c6e6a3ddf 100644 --- a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift @@ -33,8 +33,6 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { authService = dependencies.authService googleAuthService = dependencies.googleAuthService countriesProvider = dependencies.countriesProvider - - authService.initialize() } diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift index facb44989..136ef561f 100644 --- a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift @@ -123,11 +123,11 @@ extension AuthPresenter { extension AuthPresenter: SetInjectable { struct Dependencies { let view: AuthViewProtocol - let interactor: AuthInteractor - let wireframe: AuthWireframe + let interactor: AuthInputInteractorProtocol + let wireframe: AuthWireframeProtocol } - func inject(dependencies: AuthPresenter.Dependencies) { + func inject(dependencies: Dependencies) { view = dependencies.view interactor = dependencies.interactor wireframe = dependencies.wireframe diff --git a/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift b/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift index beff13979..96a2774e7 100644 --- a/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift +++ b/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift @@ -9,7 +9,7 @@ import UIKit -protocol CameraFlowCoordinatorProtocol: CoordinatorProtocol, CameraCoordinatorProtocol, PhotoPreviewCoordinatorProtocol, +protocol CameraFlowCoordinatorProtocol: Coordinator, CameraCoordinatorProtocol, PhotoPreviewCoordinatorProtocol, CameraVideoPreviewCoordinatorProtocol, CameraQRPreviewCoordinatorProtocol {} final class CameraFlowCoordinator: CameraFlowCoordinatorProtocol, SetInjectable { @@ -40,7 +40,7 @@ extension CameraFlowCoordinator { } } -// MARK: - CoordinatorProtocol +// MARK: - Coordinator extension CameraFlowCoordinator { func start() { diff --git a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift index 952845633..8098f41e4 100644 --- a/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift +++ b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettingsCoordinator.swift @@ -8,7 +8,7 @@ import Foundation -protocol CameraSettingsFlowCoordinatorProtocol: CoordinatorProtocol, CameraSettingsCoordinatorProtocol, CameraQualitySettingsCoordinatorProtocol {} +protocol CameraSettingsFlowCoordinatorProtocol: Coordinator, CameraSettingsCoordinatorProtocol, CameraQualitySettingsCoordinatorProtocol {} final class CameraSettingsFlowCoordinator: CameraSettingsFlowCoordinatorProtocol, SetInjectable { private var rootViewController: UIViewController! diff --git a/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift b/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift index 104dc33ad..c66088bd0 100644 --- a/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift +++ b/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift @@ -8,7 +8,7 @@ import Foundation -protocol GalleryFlowCoordinatorProtocol: CoordinatorProtocol, GalleryCoordinatorProtocol, PhotoPreviewCoordinatorProtocol, CameraVideoPreviewCoordinatorProtocol, MultiplePreviewCoordinatorProtocol {} +protocol GalleryFlowCoordinatorProtocol: Coordinator, GalleryCoordinatorProtocol, PhotoPreviewCoordinatorProtocol, CameraVideoPreviewCoordinatorProtocol, MultiplePreviewCoordinatorProtocol {} final class GalleryFlowCoordinator: GalleryFlowCoordinatorProtocol, SetInjectable { private var contact: Contact? @@ -21,7 +21,7 @@ final class GalleryFlowCoordinator: GalleryFlowCoordinatorProtocol, SetInjectabl private var mainFlowVC: UIViewController? } -// MARK: - CoordinatorProtocol +// MARK: - Coordinator extension GalleryFlowCoordinator { func start() { diff --git a/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift b/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift index 70ad9f99c..9e85095dc 100644 --- a/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift +++ b/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift @@ -14,7 +14,7 @@ enum SelectAvatarFlowCoordinatorSource { case gallery } -protocol SelectAvatarFlowCoordinatorProtocol: CoordinatorProtocol, CameraCoordinatorProtocol, GalleryCoordinatorProtocol, +protocol SelectAvatarFlowCoordinatorProtocol: Coordinator, CameraCoordinatorProtocol, GalleryCoordinatorProtocol, PhotoPreviewCoordinatorProtocol {} final class SelectAvatarFlowCoordinator: SelectAvatarFlowCoordinatorProtocol, InitializeInjectable { @@ -41,7 +41,7 @@ final class SelectAvatarFlowCoordinator: SelectAvatarFlowCoordinatorProtocol, In } -// MARK: - CoordinatorProtocol +// MARK: - Coordinator extension SelectAvatarFlowCoordinator { func start() { diff --git a/Nynja/SDK/Account/Entities/AccountError.swift b/Nynja/SDK/Account/Entities/AccountError.swift new file mode 100644 index 000000000..a55ff6a01 --- /dev/null +++ b/Nynja/SDK/Account/Entities/AccountError.swift @@ -0,0 +1,13 @@ +// +// AccountError.swift +// Nynja +// +// Created by Anton Poltoratskyi on 20.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum AccountError: Error { + case invalidResponse +} diff --git a/Nynja/SDK/Account/Entities/AccountInfo.swift b/Nynja/SDK/Account/Entities/AccountInfo.swift new file mode 100644 index 000000000..36e73d196 --- /dev/null +++ b/Nynja/SDK/Account/Entities/AccountInfo.swift @@ -0,0 +1,48 @@ +// +// AccountInfo.swift +// Nynja +// +// Created by Anton Poltoratskyi on 20.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AccountInfo { + let accountId: String + let avatar: String? + let accountMark: String? + let accountName: String? + let firstName: String? + let lastName: String? + let username: String? + let accountStatus: NYNAccountAccessStatus + let roles: NYNAccountRoles? + let qrCode: String? + let birthday: NYNDate? + + init(accountId: String, + avatar: String?, + accountMark: String?, + accountName: String?, + firstName: String?, + lastName: String?, + username: String?, + accountStatus: NYNAccountAccessStatus, + roles: NYNAccountRoles?, + qrCode: String?, + birthday: NYNDate?) { + + self.accountId = accountId + self.avatar = avatar + self.accountMark = accountMark + self.accountName = accountName + self.firstName = firstName + self.lastName = lastName + self.username = username + self.accountStatus = accountStatus + self.roles = roles + self.qrCode = qrCode + self.birthday = birthday + } +} diff --git a/Nynja/SDK/Account/Service/AccountService.swift b/Nynja/SDK/Account/Service/AccountService.swift new file mode 100644 index 000000000..1b89e5222 --- /dev/null +++ b/Nynja/SDK/Account/Service/AccountService.swift @@ -0,0 +1,53 @@ +// +// AccountService.swift +// Nynja +// +// Created by Anton Poltoratskyi on 20.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaSDK + +protocol AccountService: class { + + typealias ProfileCompletion = (Result) -> Void + typealias AccountCompletion = (Result) -> Void + typealias AccountListCompletion = (Result<[NYNAccountDetails]>) -> Void + typealias StatusCompletion = (Result) -> Void + + + // MARK: - Profile (Identity) + + func updateProfile(_ profileId: String, passcode: String?, defaultAccountId: String?, completion: @escaping ProfileCompletion) + + func deleteProfile(_ profileId: String, completion: @escaping StatusCompletion) + + func addAuthenticationProvider(to profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) + + func deleteAuthenticationProvider(from profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) + + + // MARK: - Account + + func createAccount(with authenticationProvider: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) + + func completePendingAccountCreation(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) + + func updateAccount(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) + + func deleteAccount(_ accountId: String, completion: @escaping StatusCompletion) + + func getAccount(by accountId: String, completion: @escaping AccountCompletion) + + func getAccount(by authenticationIdentifier: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) + + func getAllAccounts(by profileId: String, completion: @escaping AccountListCompletion) + + + // MARK: - Account's Contact Info + + func addContactInfo(to accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) + + func deleteContactInfo(from accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) +} diff --git a/Nynja/SDK/Account/Service/AccountServiceImpl.swift b/Nynja/SDK/Account/Service/AccountServiceImpl.swift new file mode 100644 index 000000000..f0b3a5890 --- /dev/null +++ b/Nynja/SDK/Account/Service/AccountServiceImpl.swift @@ -0,0 +1,384 @@ +// +// AccountService.swift +// Nynja +// +// Created by Anton Poltoratskyi on 20.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaSDK + +final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, NYNAccountManagerDelegate { + + // MARK: - Dependencies + + private let accountManager: AccountManager + + private let sessionStorage: SessionStorage + + private let appConfigurationProvider: AppConfigurationProvider + + private let processingQueue: DispatchQueue + + + // MARK: - Handlers + + private var updateProfileCompletion: ProfileCompletion? + private var deleteProfileCompletion: StatusCompletion? + private var addAuthProviderToProfileCompletion: StatusCompletion? + private var deleteAuthProviderFromProfileCompletion: StatusCompletion? + + private var createAccountCompletion: AccountCompletion? + private var completePendingAccountCompletion: AccountCompletion? + private var updateAccountCompletion: AccountCompletion? + private var deleteAccountCompletion: StatusCompletion? + private var getAccountCompletion: AccountCompletion? + private var getAccountByAuthProviderCompletion: AccountCompletion? + private var getAllAccountsCompletion: AccountListCompletion? + + private var addContactInfoCompletion: StatusCompletion? + private var deleteContactInfoCompletion: StatusCompletion? + + private var token: String? { + return sessionStorage.token + } + + + // MARK: - Init + + struct Dependencies { + let accountManager: AccountManager + let sessionStorage: SessionStorage + let appConfigurationProvider: AppConfigurationProvider + let processingQueue: DispatchQueue = .main + } + + init(dependencies: Dependencies) { + accountManager = dependencies.accountManager + sessionStorage = dependencies.sessionStorage + appConfigurationProvider = dependencies.appConfigurationProvider + processingQueue = dependencies.processingQueue + + super.init() + + accountManager.delegate = self + } + + + // MARK: - Profile (Identity) + + func updateProfile(_ profileId: String, passcode: String?, defaultAccountId: String?, completion: @escaping ProfileCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.updateProfileCompletion) + + accountManager.sendUpdateProfile(withProfileId: profileId, + withAccessToken: token, + withPasscode: passcode, + withAccountId: defaultAccountId) + } + + func deleteProfile(_ profileId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteProfileCompletion) + + accountManager.sendDeleteProfile(withProfileId: profileId, withAccessToken: token) + } + + func addAuthenticationProvider(to profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.addAuthProviderToProfileCompletion) + + accountManager.sendAddAuthenticationProviderToProfile(withProfileId: profileId, + withAccessToken: token, + with: authProviderDetails) + } + + func deleteAuthenticationProvider(from profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteAuthProviderFromProfileCompletion) + + accountManager.sendDeleteAuthenticationProviderFromProfile(withProfileId: profileId, + withAccessToken: token, + with: authProviderDetails) + } + + + // MARK: - Account + + func createAccount(with authenticationProvider: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.createAccountCompletion) + + accountManager.sendCreateAccount(withAuthProvider: authenticationProvider, withAccessToken: token, with: authType) + } + + func completePendingAccountCreation(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.completePendingAccountCompletion) + + accountManager.sendCompletePendingAccountCreation(withAccountId: accountInfo.accountId, + withAccessToken: token, + withAvatar: accountInfo.avatar, + withAccountMark: accountInfo.accountMark, + withAccountName: accountInfo.accountName, + withFirstName: accountInfo.firstName, + withLastName: accountInfo.lastName, + withUsername: accountInfo.username, + withQrCode: accountInfo.qrCode, + withAccountStatus: accountInfo.accountStatus, + with: accountInfo.roles) + } + + func updateAccount(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.updateAccountCompletion) + + accountManager.sendUpdateAccount(withAccountId: accountInfo.accountId, + withAccessToken: token, + withAvatar: accountInfo.avatar, + withAccountMark: accountInfo.accountMark, + withAccountName: accountInfo.accountName, + withFirstName: accountInfo.firstName, + withLastName: accountInfo.lastName, + withUsername: accountInfo.username, + withAccountStatus: accountInfo.accountStatus, + with: accountInfo.roles, + withBirthday: accountInfo.birthday) + } + + func deleteAccount(_ accountId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteAccountCompletion) + + accountManager.sendDeleteAccount(withAccountId: accountId, withAccessToken: token) + } + + func getAccount(by accountId: String, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getAccountCompletion) + + accountManager.sendGetAccount(withAccountId: accountId, withAccessToken: token) + } + + func getAccount(by authenticationIdentifier: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getAccountByAuthProviderCompletion) + + accountManager.sendGetAccount(withAuthProvider: authenticationIdentifier, withAccessToken: token, with: authType) + } + + func getAllAccounts(by profileId: String, completion: @escaping AccountListCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getAllAccountsCompletion) + + accountManager.sendGetAllAccounts(withProfileId: profileId, withAccessToken: token) + } + + + // MARK: - Account's Contact Info + + func addContactInfo(to accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.addContactInfoCompletion) + + accountManager.sendAddContactInfoToAccount(withAccountId: accountId, withAccessToken: token, with: contactDetails) + } + + func deleteContactInfo(from accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteContactInfoCompletion) + + accountManager.sendDeleteContactInfoFromAccount(withAccountId: accountId, withAccessToken: token, with: contactDetails) + } + + private func bind(_ value: T, to keyPath: ReferenceWritableKeyPath) { + processingQueue.async { + self[keyPath: keyPath] = value + } + } + + private func handleResponse(_ newValue: T, to keyPath: ReferenceWritableKeyPath, handler: @escaping (T) -> Void) { + processingQueue.async { + let completion = self[keyPath: keyPath] + self[keyPath: keyPath] = newValue + handler(completion) + } + } +} + + +// MARK: - NYNAccountManagerDelegate + +extension AccountServiceImpl { + + // MARK: Profile (Identity) + + public func updateProfileDidFinish(with profileDetails: NYNProfileDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.updateProfileCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + if let profileDetails = profileDetails { + completion?(.success(profileDetails)) + } + completion?(.failure(AccountError.invalidResponse)) + } + } + + public func deleteProfileDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.deleteProfileCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + completion?(.success(status)) + } + } + + public func addAuthenticationProviderToProfileDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.addAuthProviderToProfileCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + completion?(.success(status)) + } + } + + public func deleteAuthenticationProviderFromProfileDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.deleteAuthProviderFromProfileCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + completion?(.success(status)) + } + } + + // MARK: Account + + public func createAccountDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.createAccountCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + if let accountDetails = accountDetails { + completion?(.success(accountDetails)) + } + completion?(.failure(AccountError.invalidResponse)) + } + } + + public func completePendingAccountCreationDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.completePendingAccountCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + if let accountDetails = accountDetails { + completion?(.success(accountDetails)) + } + completion?(.failure(AccountError.invalidResponse)) + } + } + + public func updateAccountDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.updateAccountCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + if let accountDetails = accountDetails { + completion?(.success(accountDetails)) + } + completion?(.failure(AccountError.invalidResponse)) + } + } + + public func deleteAccountDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.deleteAccountCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + completion?(.success(status)) + } + } + + public func getAccountByIdDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getAccountCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + if let accountDetails = accountDetails { + completion?(.success(accountDetails)) + } + completion?(.failure(AccountError.invalidResponse)) + } + } + + public func getAccountByAuthProviderDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getAccountByAuthProviderCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + if let accountDetails = accountDetails { + completion?(.success(accountDetails)) + } + completion?(.failure(AccountError.invalidResponse)) + } + } + + public func getAllAccountsByProfileIdDidFinish(withDetails accountDetailsArray: [Any]?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getAllAccountsCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + if let accountDetailsArray = accountDetailsArray as? [NYNAccountDetails] { + completion?(.success(accountDetailsArray)) + } + completion?(.failure(AccountError.invalidResponse)) + } + } + + // MARK: Account's Contact Info + + public func addContactInfoToAccountDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.addContactInfoCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + completion?(.success(status)) + } + } + + public func deleteContactInfoFromAccountDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.deleteContactInfoCompletion) { completion in + if let error = error { + completion?(.failure(error)) + } + completion?(.success(status)) + } + } +} diff --git a/Nynja/SDK/App/AppBundleCredentials.swift b/Nynja/SDK/App/AppBundleCredentials.swift index 1d32fc1d9..81d2390c5 100644 --- a/Nynja/SDK/App/AppBundleCredentials.swift +++ b/Nynja/SDK/App/AppBundleCredentials.swift @@ -10,4 +10,5 @@ struct AppBundleCredentials { let deviceId: String let instanceId: String let bundleId: String + let appToken: String } diff --git a/Nynja/SDK/App/AppConfigurationProvider.swift b/Nynja/SDK/App/AppConfigurationProvider.swift index 40c12cdda..4e9fe7933 100644 --- a/Nynja/SDK/App/AppConfigurationProvider.swift +++ b/Nynja/SDK/App/AppConfigurationProvider.swift @@ -19,9 +19,12 @@ final class AppConfigurationProviderImpl: AppConfigurationProvider { private let bundle = Bundle.main var sdkCredentials: AppBundleCredentials { - return AppBundleCredentials(deviceId: UIDevice.current.persistentIdentifier, - instanceId: UIDevice.current.persistentIdentifier, - bundleId: bundle.bundleIdentifier) + return AppBundleCredentials( + deviceId: UIDevice.current.persistentIdentifier, + instanceId: UIDevice.current.persistentIdentifier, + bundleId: bundle.bundleIdentifier, + appToken: "" + ) } var authServerConfig: ServerConfig { diff --git a/Nynja/SDK/Auth/AuthConfirmationType.swift b/Nynja/SDK/Auth/Entities/AuthConfirmationType.swift similarity index 100% rename from Nynja/SDK/Auth/AuthConfirmationType.swift rename to Nynja/SDK/Auth/Entities/AuthConfirmationType.swift diff --git a/Nynja/SDK/Auth/AuthResponse.swift b/Nynja/SDK/Auth/Entities/AuthResponse.swift similarity index 100% rename from Nynja/SDK/Auth/AuthResponse.swift rename to Nynja/SDK/Auth/Entities/AuthResponse.swift diff --git a/Nynja/SDK/Auth/AuthTokenData.swift b/Nynja/SDK/Auth/Entities/AuthTokenData.swift similarity index 100% rename from Nynja/SDK/Auth/AuthTokenData.swift rename to Nynja/SDK/Auth/Entities/AuthTokenData.swift diff --git a/Nynja/SDK/Auth/AuthenticationType.swift b/Nynja/SDK/Auth/Entities/AuthenticationType.swift similarity index 100% rename from Nynja/SDK/Auth/AuthenticationType.swift rename to Nynja/SDK/Auth/Entities/AuthenticationType.swift diff --git a/Nynja/SDK/Auth/PhoneNumberInfo.swift b/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift similarity index 100% rename from Nynja/SDK/Auth/PhoneNumberInfo.swift rename to Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift diff --git a/Nynja/SDK/Auth/Service/AuthService.swift b/Nynja/SDK/Auth/Service/AuthService.swift new file mode 100644 index 000000000..20cce9787 --- /dev/null +++ b/Nynja/SDK/Auth/Service/AuthService.swift @@ -0,0 +1,28 @@ +// +// AuthService.swift +// Nynja +// +// Created by Anton Poltoratskyi on 20.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaSDK + +protocol AuthService: class { + typealias LoginCompletion = (Result) -> Void + typealias CodeConfirmationCompletion = (Result) -> Void + typealias RefreshTokenCompletion = (Result) -> Void + + func login(by email: String, completion: @escaping LoginCompletion) + + func login(by phoneNumber: PhoneNumberInfo, confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) + + func loginByFacebook(serverCode: String, completion: @escaping LoginCompletion) + + func loginByGoogle(serverCode: String, completion: @escaping LoginCompletion) + + func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) + + func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) +} diff --git a/Nynja/SDK/Auth/AuthService.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift similarity index 76% rename from Nynja/SDK/Auth/AuthService.swift rename to Nynja/SDK/Auth/Service/AuthServiceImpl.swift index 6e80e8001..1521336af 100644 --- a/Nynja/SDK/Auth/AuthService.swift +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -1,5 +1,5 @@ // -// AuthService.swift +// AuthServiceImpl.swift // Nynja // // Created by Anton Poltoratskyi on 13.11.2018. @@ -9,34 +9,10 @@ import Foundation import NynjaSDK -protocol AuthService: class { - typealias LoginCompletion = (Result) -> Void - typealias CodeConfirmationCompletion = (Result) -> Void - typealias RefreshTokenCompletion = (Result) -> Void - - func initialize() - - func login(by email: String, completion: @escaping LoginCompletion) - - func login(by phoneNumber: PhoneNumberInfo, - confirmVia authConfirmationType: AuthConfirmationType, - completion: @escaping LoginCompletion) - - func loginByFacebook(serverCode: String, completion: @escaping LoginCompletion) - - func loginByGoogle(serverCode: String, completion: @escaping LoginCompletion) - - func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) - - func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) -} - final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLoginManagerDelegate { // MARK: - Dependencies - private let communicator: NynjaCommunicator - private let loginManager: LoginManager private let sessionStorage: SessionStorage @@ -59,7 +35,6 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog // MARK: - Init struct Dependencies { - let communicator: NynjaCommunicator let loginManager: LoginManager let sessionStorage: SessionStorage let appConfigurationProvider: AppConfigurationProvider @@ -67,7 +42,6 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } init(dependencies: Dependencies) { - communicator = dependencies.communicator loginManager = dependencies.loginManager sessionStorage = dependencies.sessionStorage appConfigurationProvider = dependencies.appConfigurationProvider @@ -76,24 +50,15 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog super.init() loginManager.delegate = self + initialize() } // MARK: - API - func initialize() { - setupAuthServer() - initializeSDK() - } - - private func setupAuthServer() { - let config = appConfigurationProvider.authServerConfig - communicator.setAuthServerAddress(config.host, andPort: config.port, secure: config.isSecure) - } - - private func initializeSDK() { + private func initialize() { let credentials = appConfigurationProvider.sdkCredentials - + loginManager.initialize(withDeviceId: credentials.deviceId, withInstanceId: credentials.instanceId, withAppClass: String(describing: type(of: self)), @@ -101,52 +66,40 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func login(by email: String, completion: @escaping LoginCompletion) { - processingQueue.async { - self.loginByEmailCompletion = completion - } - loginManager.sendLogin(byEmail: email, withAppToken: sessionStorage.appToken) + bind(completion, to: \AuthServiceImpl.loginByEmailCompletion) + loginManager.sendLogin(byEmail: email, withAppToken: appConfigurationProvider.sdkCredentials.appToken) } func login(by numberInfo: PhoneNumberInfo, confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) { - - processingQueue.async { - self.loginByPhoneCompletion = completion - } + + bind(completion, to: \AuthServiceImpl.loginByPhoneCompletion) + let country = numberInfo.country let numberFormat = "\(country.ISO):\(country.code)\(numberInfo.number)" + let appToken = appConfigurationProvider.sdkCredentials.appToken - loginManager.sendLogin(byPhone: numberFormat, - withAppToken: sessionStorage.appToken, - withSendTokenVia: authConfirmationType.sdkValue) + loginManager.sendLogin(byPhone: numberFormat, withAppToken: appToken, withSendTokenVia: authConfirmationType.sdkValue) } func loginByFacebook(serverCode: String, completion: @escaping LoginCompletion) { - processingQueue.async { - self.loginByFacebookCompletion = completion - } + bind(completion, to: \AuthServiceImpl.loginByFacebookCompletion) loginManager.sendLoginByFacebook(withAppToken: serverCode) } func loginByGoogle(serverCode: String, completion: @escaping LoginCompletion) { - processingQueue.async { - self.loginByGoogleCompletion = completion - } + bind(completion, to: \AuthServiceImpl.loginByGoogleCompletion) loginManager.sendLoginByGooglePlus(withAppToken: serverCode) } func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) { - processingQueue.async { - self.confirmCodeCompletion = completion - } + bind(completion, to: \AuthServiceImpl.confirmCodeCompletion) loginManager.confirmCode(code, withCredential: socialToken) } func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) { - processingQueue.async { - self.refreshTokenCompletion = completion - } + bind(completion, to: \AuthServiceImpl.refreshTokenCompletion) loginManager.refreshAccessToken(accessToken) } @@ -251,6 +204,12 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog refreshToken: refreshToken, expiration: expiration?.int64Value) } + + private func bind(_ value: T, to keyPath: ReferenceWritableKeyPath) { + processingQueue.async { + self[keyPath: keyPath] = value + } + } } // MARK: - Extensions diff --git a/Nynja/SDK/Session/SessionStorage.swift b/Nynja/SDK/Session/SessionStorage.swift index ddceccc67..1b241edfe 100644 --- a/Nynja/SDK/Session/SessionStorage.swift +++ b/Nynja/SDK/Session/SessionStorage.swift @@ -7,5 +7,5 @@ // protocol SessionStorage: class { - var appToken: String { get } + var token: String? { get set } } diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index fe1981c1d..97aa12b5f 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -57,8 +57,7 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { // Lazy wars is not thread safe, so it must be initialized from single thread private lazy var authService: AuthService = { - let dependencies = AuthServiceImpl.Dependencies(communicator: makeCommunicator(), - loginManager: makeLoginManager(), + let dependencies = AuthServiceImpl.Dependencies(loginManager: makeLoginManager(), sessionStorage: makeStorageService(), appConfigurationProvider: makeAppConfigurationProvider()) return AuthServiceImpl(dependencies: dependencies) diff --git a/Nynja/StorageService+UserInfo.swift b/Nynja/StorageService+UserInfo.swift index 72de8b2c6..d2581a902 100644 --- a/Nynja/StorageService+UserInfo.swift +++ b/Nynja/StorageService+UserInfo.swift @@ -74,10 +74,6 @@ extension StorageService: UserInfo, SessionStorage { set { set(newValue, forId: .wasRun) } } - var appToken: String { - return "" - } - func setupAuth(clientId: String, token: String) { self.clientId = clientId self.token = token diff --git a/Podfile b/Podfile index 8fa111cbf..0a659f175 100644 --- a/Podfile +++ b/Podfile @@ -41,7 +41,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.5' # pod 'NynjaSDK', '= 1.8' - pod 'NynjaSDK-MultiAcc', '= 0.5.5' + pod 'NynjaSDK-MultiAcc', '= 0.5.6' pod 'CryptoSwift', '= 0.10.0' diff --git a/Podfile.lock b/Podfile.lock index db7723d94..41f8cbc57 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -91,7 +91,7 @@ PODS: - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.5) + - NynjaSDK-MultiAcc (0.5.6) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -126,7 +126,7 @@ DEPENDENCIES: - MaterialComponents/ActivityIndicator (= 55.3.0) - MaterialComponents/FlexibleHeader (= 55.3.0) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.5) + - NynjaSDK-MultiAcc (= 0.5.6) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.0.0) @@ -217,7 +217,7 @@ SPEC CHECKSUMS: MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: 49d7d927d3fe56b2e7b3c672644db6858e259a0f + NynjaSDK-MultiAcc: b94c07b446fa1385acb4c33bcd81a9a615b59802 QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: a42d492c16e80209130a3379f73596c3454b7694 @@ -226,6 +226,6 @@ SPEC CHECKSUMS: SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: e63f87cc0c77a9640951e44c3bb3fe3760247690 +PODFILE CHECKSUM: c3950f662ec06efe661ef463decdf2703a18d7f8 COCOAPODS: 1.5.3 -- GitLab From cf63df5303eb2227c44a6f72d10a0b39d4aad8b1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 21 Nov 2018 14:25:17 +0200 Subject: [PATCH 118/138] Update project structure --- Nynja.xcodeproj/project.pbxproj | 12 ++++++------ .../AccountSettings/AccountSettingsProtocols.swift | 0 .../Entities/AddContactCellModel.swift | 0 .../Entities/ContactTVCellModel.swift | 0 .../Entities/DescriptionCellModel.swift | 0 .../Entities/MaterialTextFieldCellModel.swift | 0 .../Entities/SettingsSectionHeader.swift | 0 .../Entities/SettingsSelectorCellModel.swift | 0 .../Entities/SettingsSetAvatarCellModel.swift | 0 .../AccountSettings/Entities/Sizeble.swift | 0 .../AccountSettings/Entities/StatusTimeout.swift | 0 .../AccountSettings/Entities/UserContact.swift | 0 .../AccountSettings/Entities/UserContactAction.swift | 0 .../AccountSettings/Entities/UserProfile.swift | 0 .../AccountSettings/Entities/UserStatus.swift | 0 .../Interactor/AccountSettingsInteractor.swift | 0 .../Presenter/AccountSettingsPresenter.swift | 0 .../View/AccountSettingsViewController.swift | 0 .../AccountSettings/View/Cells/AddContactCell.swift | 0 .../AccountSettings/View/Cells/ContactTVCell.swift | 0 .../View/Cells/DescriptionTVCell.swift | 0 .../View/Cells/MaterialTextFieldTVCell.swift | 0 .../View/Cells/SettingsSelectorTVCell.swift | 0 .../View/Cells/SettingsSetAvatarTVCell.swift | 0 .../View/Header/SettingsSectionHeaderView.swift | 0 .../ViewsFactory/AccountSettingsViewsFactory.swift | 0 .../Wireframe/AccountSettingsWireframe.swift | 0 .../Coordinator/AccountSettingsCoordinator.swift | 0 .../{Auth => Auth Flow}/AuthCoordinator.swift | 0 .../AuthModule/AuthProtocols.swift | 0 .../AuthModule/Entities/EmailTextController.swift | 0 .../AuthModule/Entities/LoginFlow.swift | 0 .../AuthModule/Entities/LoginOption.swift | 0 .../AuthModule/Entities/PhoneNumberFormatter.swift | 0 .../Entities/PhoneNumberTextController.swift | 0 .../AuthModule/Entities/Validator.swift | 0 .../AuthModule/Interactor/AuthInteractor.swift | 0 .../AuthModule/Presenter/AuthPresenter.swift | 0 .../AuthModule/View/AuthViewController.swift | 0 .../AuthModule/View/Subviews/AuthHeaderView.swift | 0 .../AuthModule/View/Subviews/EmailLoginView.swift | 0 .../AuthModule/View/Subviews/LoginOptionsView.swift | 0 .../View/Subviews/PhoneNumberLoginView.swift | 0 .../AuthModule/Wireframe/AuthWireframe.swift | 0 .../CodeConfirmation/CodeConfirmationProtocols.swift | 0 .../CodeConfirmation/Entities/AuthProviderType.swift | 0 .../Interactor/CodeConfirmationInteractor.swift | 0 .../Presenter/CodeConfirmationPresenter.swift | 0 .../View/CodeConfirmationViewController.swift | 0 .../ViewsFactory/CodeConfirmationViewsFactory.swift | 0 .../Wireframe/CodeConfirmationWireframe.swift | 0 .../CreateProfile/CreateProfileProtocols.swift | 0 .../CreateProfile/Entities/ProfileField.swift | 0 .../Interactor/CreateProfileInteractor.swift | 0 .../Presenter/CreateProfilePresenter.swift | 0 .../View/CreateProfileViewController.swift | 0 .../View/Subviews/CreateProfileContentView.swift | 0 .../ViewsFactory/CreateProfileViewsFactory.swift | 0 .../Wireframe/CreateProfileWireframe.swift | 0 .../Facebook/FacebookAuthProtocols.swift | 0 .../Intreractor/FacebookAuthInteractor.swift | 0 .../Facebook/Presenter/FacebookAuthPresenter.swift | 0 .../Facebook/View/FacebookAuthViewController.swift | 0 .../Facebook/Wireframe/FacebookAuthWireframe.swift | 0 .../Login/Interactor/LoginInteractor.swift | 0 .../Login/Interactor/Modelka.swift | 0 .../{Auth => Auth Flow}/Login/LoginProtocols.swift | 0 .../Login/Presenter/LoginPresenter.swift | 0 .../Login/View/LoginViewController.swift | 0 .../Login/View/LoginWheelContainerDataSource.swift | 0 .../Login/View/LoginWheelContainerDelegate.swift | 0 .../Login/WireFrame/LoginWireframe.swift | 0 .../SelectCountry/Entities/CountriesSection.swift | 0 .../SelectCountry/Entities/Country.swift | 0 .../Interactor/SelectCountryInteractor.swift | 0 .../Presenter/SelectCountryPresenter.swift | 0 .../SelectCountry/SelectCountryProtocols.swift | 0 .../View/TableView/Cell/CountryCellModel.swift | 0 .../View/TableView/Cell/CountryTableViewCell.swift | 0 .../TableView/Header/SelectCountryHeaderView.swift | 0 .../ViewController/SelectCountryViewController.swift | 0 .../WireFrame/SelectCountryWireframe.swift | 0 .../Interactor/VerifyNumberInteractor.swift | 0 .../Presenter/VerifyNumberPresenter.swift | 0 .../VerifyNumber/VerifyNumberProtocols.swift | 0 .../View/VerifyNumberViewController.swift | 0 .../Wireframe/VerifyNumberWireFrame.swift | 0 87 files changed, 6 insertions(+), 6 deletions(-) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/AccountSettingsProtocols.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/AddContactCellModel.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/ContactTVCellModel.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/DescriptionCellModel.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/MaterialTextFieldCellModel.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/SettingsSectionHeader.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/SettingsSelectorCellModel.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/SettingsSetAvatarCellModel.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/Sizeble.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/StatusTimeout.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/UserContact.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/UserContactAction.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/UserProfile.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Entities/UserStatus.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Interactor/AccountSettingsInteractor.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Presenter/AccountSettingsPresenter.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/AccountSettingsViewController.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/Cells/AddContactCell.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/Cells/ContactTVCell.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/Cells/DescriptionTVCell.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/Cells/SettingsSelectorTVCell.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/Header/SettingsSectionHeaderView.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/AccountSettings/Wireframe/AccountSettingsWireframe.swift (100%) rename Nynja/Modules/{AccountSettings => Account Flow}/Coordinator/AccountSettingsCoordinator.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthCoordinator.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/AuthProtocols.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Entities/EmailTextController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Entities/LoginFlow.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Entities/LoginOption.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Entities/PhoneNumberFormatter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Entities/PhoneNumberTextController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Entities/Validator.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Interactor/AuthInteractor.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Presenter/AuthPresenter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/View/AuthViewController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/View/Subviews/AuthHeaderView.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/View/Subviews/EmailLoginView.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/View/Subviews/LoginOptionsView.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/View/Subviews/PhoneNumberLoginView.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/AuthModule/Wireframe/AuthWireframe.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CodeConfirmation/CodeConfirmationProtocols.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CodeConfirmation/Entities/AuthProviderType.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CodeConfirmation/View/CodeConfirmationViewController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/CreateProfileProtocols.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/Entities/ProfileField.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/Interactor/CreateProfileInteractor.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/Presenter/CreateProfilePresenter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/View/CreateProfileViewController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/View/Subviews/CreateProfileContentView.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/CreateProfile/Wireframe/CreateProfileWireframe.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Facebook/FacebookAuthProtocols.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Facebook/Intreractor/FacebookAuthInteractor.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Facebook/Presenter/FacebookAuthPresenter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Facebook/View/FacebookAuthViewController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Facebook/Wireframe/FacebookAuthWireframe.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/Interactor/LoginInteractor.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/Interactor/Modelka.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/LoginProtocols.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/Presenter/LoginPresenter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/View/LoginViewController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/View/LoginWheelContainerDataSource.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/View/LoginWheelContainerDelegate.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/Login/WireFrame/LoginWireframe.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/Entities/CountriesSection.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/Entities/Country.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/Interactor/SelectCountryInteractor.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/Presenter/SelectCountryPresenter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/SelectCountryProtocols.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/View/TableView/Cell/CountryCellModel.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/View/ViewController/SelectCountryViewController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/SelectCountry/WireFrame/SelectCountryWireframe.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/VerifyNumber/Interactor/VerifyNumberInteractor.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/VerifyNumber/Presenter/VerifyNumberPresenter.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/VerifyNumber/VerifyNumberProtocols.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/VerifyNumber/View/VerifyNumberViewController.swift (100%) rename Nynja/Modules/{Auth => Auth Flow}/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift (100%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 389526a3a..ebf2735a6 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -6535,8 +6535,8 @@ isa = PBXGroup; children = ( FEA6555D2167777E00B44029 /* Wallet Flows */, - 5EDD454621885EC400C50BC8 /* AccountSettings */, - 4B749F0E214FEFC8002F3A33 /* Auth */, + 4B749F0E214FEFC8002F3A33 /* Auth Flow */, + 5EDD454621885EC400C50BC8 /* Account Flow */, 260531122127407A002E1CF1 /* LogOutput */, 855AC52C208E435700DC2335 /* Stickers */, 2603136720A0A4B9009AC66D /* LanguageSettings */, @@ -6986,7 +6986,7 @@ path = Interactor; sourceTree = ""; }; - 4B749F0E214FEFC8002F3A33 /* Auth */ = { + 4B749F0E214FEFC8002F3A33 /* Auth Flow */ = { isa = PBXGroup; children = ( 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, @@ -6998,7 +6998,7 @@ 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, ); - path = Auth; + path = "Auth Flow"; sourceTree = ""; }; 4B752B4521639F4900E852B9 /* BaseChatCellModel */ = { @@ -7585,13 +7585,13 @@ path = Header; sourceTree = ""; }; - 5EDD454621885EC400C50BC8 /* AccountSettings */ = { + 5EDD454621885EC400C50BC8 /* Account Flow */ = { isa = PBXGroup; children = ( 5EDD454721885EC400C50BC8 /* Coordinator */, 5EDD454821885EC400C50BC8 /* AccountSettings */, ); - path = AccountSettings; + path = "Account Flow"; sourceTree = ""; }; 5EDD454721885EC400C50BC8 /* Coordinator */ = { diff --git a/Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/AccountSettingsProtocols.swift rename to Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/AddContactCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/AddContactCellModel.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/AddContactCellModel.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/AddContactCellModel.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/ContactTVCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/ContactTVCellModel.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/DescriptionCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/DescriptionCellModel.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/MaterialTextFieldCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/MaterialTextFieldCellModel.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSectionHeader.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSectionHeader.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSelectorCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSelectorCellModel.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSetAvatarCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/SettingsSetAvatarCellModel.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/Sizeble.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/Sizeble.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/Sizeble.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/Sizeble.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/StatusTimeout.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/StatusTimeout.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContact.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContact.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContactAction.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContactAction.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/UserContactAction.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/UserContactAction.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserProfile.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/UserProfile.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Entities/UserStatus.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Entities/UserStatus.swift rename to Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Interactor/AccountSettingsInteractor.swift rename to Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Presenter/AccountSettingsPresenter.swift rename to Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/AccountSettingsViewController.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/AddContactCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/AddContactCell.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/Cells/AddContactCell.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/Cells/AddContactCell.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/ContactTVCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/Cells/ContactTVCell.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/DescriptionTVCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/Cells/DescriptionTVCell.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSelectorTVCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSelectorTVCell.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/Header/SettingsSectionHeaderView.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/Header/SettingsSectionHeaderView.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift b/Nynja/Modules/Account Flow/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift rename to Nynja/Modules/Account Flow/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift diff --git a/Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift similarity index 100% rename from Nynja/Modules/AccountSettings/AccountSettings/Wireframe/AccountSettingsWireframe.swift rename to Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift diff --git a/Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift similarity index 100% rename from Nynja/Modules/AccountSettings/Coordinator/AccountSettingsCoordinator.swift rename to Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift diff --git a/Nynja/Modules/Auth/AuthCoordinator.swift b/Nynja/Modules/Auth Flow/AuthCoordinator.swift similarity index 100% rename from Nynja/Modules/Auth/AuthCoordinator.swift rename to Nynja/Modules/Auth Flow/AuthCoordinator.swift diff --git a/Nynja/Modules/Auth/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/AuthProtocols.swift rename to Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift diff --git a/Nynja/Modules/Auth/AuthModule/Entities/EmailTextController.swift b/Nynja/Modules/Auth Flow/AuthModule/Entities/EmailTextController.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Entities/EmailTextController.swift rename to Nynja/Modules/Auth Flow/AuthModule/Entities/EmailTextController.swift diff --git a/Nynja/Modules/Auth/AuthModule/Entities/LoginFlow.swift b/Nynja/Modules/Auth Flow/AuthModule/Entities/LoginFlow.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Entities/LoginFlow.swift rename to Nynja/Modules/Auth Flow/AuthModule/Entities/LoginFlow.swift diff --git a/Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift b/Nynja/Modules/Auth Flow/AuthModule/Entities/LoginOption.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Entities/LoginOption.swift rename to Nynja/Modules/Auth Flow/AuthModule/Entities/LoginOption.swift diff --git a/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberFormatter.swift b/Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberFormatter.swift rename to Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift diff --git a/Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift b/Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Entities/PhoneNumberTextController.swift rename to Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift diff --git a/Nynja/Modules/Auth/AuthModule/Entities/Validator.swift b/Nynja/Modules/Auth Flow/AuthModule/Entities/Validator.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Entities/Validator.swift rename to Nynja/Modules/Auth Flow/AuthModule/Entities/Validator.swift diff --git a/Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Interactor/AuthInteractor.swift rename to Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift diff --git a/Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Presenter/AuthPresenter.swift rename to Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift diff --git a/Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/View/AuthViewController.swift rename to Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/View/Subviews/AuthHeaderView.swift rename to Nynja/Modules/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/View/Subviews/EmailLoginView.swift rename to Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/View/Subviews/LoginOptionsView.swift rename to Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift diff --git a/Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/View/Subviews/PhoneNumberLoginView.swift rename to Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift diff --git a/Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift similarity index 100% rename from Nynja/Modules/Auth/AuthModule/Wireframe/AuthWireframe.swift rename to Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift diff --git a/Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/CodeConfirmation/CodeConfirmationProtocols.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift diff --git a/Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthProviderType.swift similarity index 100% rename from Nynja/Modules/Auth/CodeConfirmation/Entities/AuthProviderType.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthProviderType.swift diff --git a/Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift similarity index 100% rename from Nynja/Modules/Auth/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift diff --git a/Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift similarity index 100% rename from Nynja/Modules/Auth/CodeConfirmation/View/CodeConfirmationViewController.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift diff --git a/Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift similarity index 100% rename from Nynja/Modules/Auth/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift diff --git a/Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift similarity index 100% rename from Nynja/Modules/Auth/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift diff --git a/Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/CreateProfileProtocols.swift rename to Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift diff --git a/Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift b/Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/Entities/ProfileField.swift rename to Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift diff --git a/Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/Interactor/CreateProfileInteractor.swift rename to Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift diff --git a/Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/Presenter/CreateProfilePresenter.swift rename to Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift diff --git a/Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/View/CreateProfileViewController.swift rename to Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift diff --git a/Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/View/Subviews/CreateProfileContentView.swift rename to Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift diff --git a/Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift rename to Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift diff --git a/Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift similarity index 100% rename from Nynja/Modules/Auth/CreateProfile/Wireframe/CreateProfileWireframe.swift rename to Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift diff --git a/Nynja/Modules/Auth/Facebook/FacebookAuthProtocols.swift b/Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/Facebook/FacebookAuthProtocols.swift rename to Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift diff --git a/Nynja/Modules/Auth/Facebook/Intreractor/FacebookAuthInteractor.swift b/Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift similarity index 100% rename from Nynja/Modules/Auth/Facebook/Intreractor/FacebookAuthInteractor.swift rename to Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift diff --git a/Nynja/Modules/Auth/Facebook/Presenter/FacebookAuthPresenter.swift b/Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/Facebook/Presenter/FacebookAuthPresenter.swift rename to Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift diff --git a/Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift b/Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift similarity index 100% rename from Nynja/Modules/Auth/Facebook/View/FacebookAuthViewController.swift rename to Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift diff --git a/Nynja/Modules/Auth/Facebook/Wireframe/FacebookAuthWireframe.swift b/Nynja/Modules/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift similarity index 100% rename from Nynja/Modules/Auth/Facebook/Wireframe/FacebookAuthWireframe.swift rename to Nynja/Modules/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift diff --git a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift b/Nynja/Modules/Auth Flow/Login/Interactor/LoginInteractor.swift similarity index 100% rename from Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift rename to Nynja/Modules/Auth Flow/Login/Interactor/LoginInteractor.swift diff --git a/Nynja/Modules/Auth/Login/Interactor/Modelka.swift b/Nynja/Modules/Auth Flow/Login/Interactor/Modelka.swift similarity index 100% rename from Nynja/Modules/Auth/Login/Interactor/Modelka.swift rename to Nynja/Modules/Auth Flow/Login/Interactor/Modelka.swift diff --git a/Nynja/Modules/Auth/Login/LoginProtocols.swift b/Nynja/Modules/Auth Flow/Login/LoginProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/Login/LoginProtocols.swift rename to Nynja/Modules/Auth Flow/Login/LoginProtocols.swift diff --git a/Nynja/Modules/Auth/Login/Presenter/LoginPresenter.swift b/Nynja/Modules/Auth Flow/Login/Presenter/LoginPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/Login/Presenter/LoginPresenter.swift rename to Nynja/Modules/Auth Flow/Login/Presenter/LoginPresenter.swift diff --git a/Nynja/Modules/Auth/Login/View/LoginViewController.swift b/Nynja/Modules/Auth Flow/Login/View/LoginViewController.swift similarity index 100% rename from Nynja/Modules/Auth/Login/View/LoginViewController.swift rename to Nynja/Modules/Auth Flow/Login/View/LoginViewController.swift diff --git a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift b/Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDataSource.swift similarity index 100% rename from Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift rename to Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDataSource.swift diff --git a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift b/Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDelegate.swift similarity index 100% rename from Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift rename to Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDelegate.swift diff --git a/Nynja/Modules/Auth/Login/WireFrame/LoginWireframe.swift b/Nynja/Modules/Auth Flow/Login/WireFrame/LoginWireframe.swift similarity index 100% rename from Nynja/Modules/Auth/Login/WireFrame/LoginWireframe.swift rename to Nynja/Modules/Auth Flow/Login/WireFrame/LoginWireframe.swift diff --git a/Nynja/Modules/Auth/SelectCountry/Entities/CountriesSection.swift b/Nynja/Modules/Auth Flow/SelectCountry/Entities/CountriesSection.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/Entities/CountriesSection.swift rename to Nynja/Modules/Auth Flow/SelectCountry/Entities/CountriesSection.swift diff --git a/Nynja/Modules/Auth/SelectCountry/Entities/Country.swift b/Nynja/Modules/Auth Flow/SelectCountry/Entities/Country.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/Entities/Country.swift rename to Nynja/Modules/Auth Flow/SelectCountry/Entities/Country.swift diff --git a/Nynja/Modules/Auth/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/Interactor/SelectCountryInteractor.swift rename to Nynja/Modules/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift diff --git a/Nynja/Modules/Auth/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/Presenter/SelectCountryPresenter.swift rename to Nynja/Modules/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift diff --git a/Nynja/Modules/Auth/SelectCountry/SelectCountryProtocols.swift b/Nynja/Modules/Auth Flow/SelectCountry/SelectCountryProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/SelectCountryProtocols.swift rename to Nynja/Modules/Auth Flow/SelectCountry/SelectCountryProtocols.swift diff --git a/Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryCellModel.swift b/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryCellModel.swift rename to Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift diff --git a/Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift b/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift rename to Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift diff --git a/Nynja/Modules/Auth/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift b/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift rename to Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift diff --git a/Nynja/Modules/Auth/SelectCountry/View/ViewController/SelectCountryViewController.swift b/Nynja/Modules/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/View/ViewController/SelectCountryViewController.swift rename to Nynja/Modules/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift diff --git a/Nynja/Modules/Auth/SelectCountry/WireFrame/SelectCountryWireframe.swift b/Nynja/Modules/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift similarity index 100% rename from Nynja/Modules/Auth/SelectCountry/WireFrame/SelectCountryWireframe.swift rename to Nynja/Modules/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift b/Nynja/Modules/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift rename to Nynja/Modules/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift b/Nynja/Modules/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift rename to Nynja/Modules/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift b/Nynja/Modules/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift rename to Nynja/Modules/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift b/Nynja/Modules/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift rename to Nynja/Modules/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift b/Nynja/Modules/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift rename to Nynja/Modules/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift -- GitLab From 63f52b8f71b5479c2753ecfdcf88dfc9ff802905 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 23 Nov 2018 00:31:06 +0200 Subject: [PATCH 119/138] Fixed compile time issues after merge --- .../Views/LoadingIndicator/ProgressHUD.swift | 4 +- Nynja.xcodeproj/project.pbxproj | 141 +++++++++++-- Nynja/AppDelegate.swift | 2 +- Nynja/DB/Models/DBMember.swift | 11 +- Nynja/Library/UI/Alert/AlertDisplayable.swift | 8 +- .../View/AccountSettingsViewController.swift | 4 +- .../CreateProfileViewsFactory.swift | 18 +- .../TableView/Cell/CountryTableViewCell.swift | 2 +- .../Main/Interactor/MainInteractor.swift | 2 +- .../Profile/WireFrame/ProfileWireframe.swift | 2 +- .../HandleServices/TypingHandler.swift | 4 - Nynja/Services/MQTT/MQTTService.swift | 2 +- .../ServiceFactoryProtocol.swift | 19 ++ Nynja/Services/StorageService.swift | 4 +- Nynja/TypingHandlerDelegate.swift | 2 +- Shared/Library/StaticDelegating.swift | 9 +- Shared/Services/Handlers/IoHandler.swift | 188 ------------------ .../Handlers/IoHandler/IoHandler.swift | 26 +-- 18 files changed, 182 insertions(+), 266 deletions(-) delete mode 100644 Shared/Services/Handlers/IoHandler.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift index 45aa19b8e..6d17edd10 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift @@ -77,7 +77,7 @@ public final class ProgressHUD: BaseView { } public func startAnimating() { - superview?.bringSubview(toFront: self) + superview?.bringSubviewToFront(self) isUserInteractionEnabled = true UIView.animate(withDuration: 0.3, delay: 0, options: .beginFromCurrentState, animations: { @@ -94,7 +94,7 @@ public final class ProgressHUD: BaseView { self.backgroundView.alpha = 0 }, completion: { isCompleted in if isCompleted { - self.superview?.sendSubview(toBack: self) + self.superview?.sendSubviewToBack(self) } self.isUserInteractionEnabled = false }) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index c934f0abb..36dbaa697 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -85,7 +85,6 @@ 1325429A6216D23E2E67B6B7 /* EditGroupPhotoWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628BB7CDB18FDAFAAB6FD17D /* EditGroupPhotoWireframe.swift */; }; 16A903BE16E0899FD3E5D232 /* GroupStorageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C986781EE944D55A2B7374C /* GroupStorageInteractor.swift */; }; 188212D3733DB059F2EF5639 /* MainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E9FA97D8C23C864C04C0E /* MainPresenter.swift */; }; - 1A9DFA4A2ED5ACE55035FA17 /* SelectCountryWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B05863F1D1FC27487D496750 /* SelectCountryWireframe.swift */; }; 1CCEA1165C5D016C6768E5DC /* GroupRulesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64CBBFDC7C38F4499D1860E5 /* GroupRulesProtocols.swift */; }; 1D1D5634D125333796D14E10 /* AddParticipantsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7625A2CFF245BC8A47701724 /* AddParticipantsPresenter.swift */; }; 1D31D13E6E53E71F8279C55C /* HistoryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8263B034B7C1F206A1C1A6C /* HistoryProtocols.swift */; }; @@ -454,7 +453,6 @@ 32868DDF1F31CB6D0028B260 /* ChatsListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32868DDE1F31CB6D0028B260 /* ChatsListInteractor.swift */; }; 32868DE11F31CB7D0028B260 /* ChatsListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32868DE01F31CB7D0028B260 /* ChatsListWireframe.swift */; }; 32868DED1F3317C00028B260 /* ChatListTableDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32868DEB1F3317C00028B260 /* ChatListTableDS.swift */; }; - 32E5A25AD25BF752EB3864AB /* SelectCountryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68A1D12FEF0CE24D6B3F6F5 /* SelectCountryInteractor.swift */; }; 3362A56D731AC1411C02D037 /* MyGroupAliasWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977DE2CFBD3AB55AB05CF71 /* MyGroupAliasWireframe.swift */; }; 350A6FCA1F6AFD7E0050C9A8 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 350A6FC91F6AFD7E0050C9A8 /* NotificationView.swift */; }; 357809A91F9765CF00C9680C /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 357809A71F9765CF00C9680C /* MainInterface.storyboard */; }; @@ -862,7 +860,6 @@ 731181233D84FD4F41936981 /* EditGroupPhotoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E427A83589B2A635F99BC0 /* EditGroupPhotoProtocols.swift */; }; 73BFE52F809536A538E6A55E /* ImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8181D695D260804FB2F3102E /* ImagePreviewViewController.swift */; }; 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4177485419FF2E8F7CF8FF98 /* EditPhotoPresenter.swift */; }; - 7C51CDC1260CE191C07EE46C /* SelectCountryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */; }; 82FCF48AA4A8C04CC8B0B5B6 /* FavoritesWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC0DFB96051C50F0FC5B9CA /* FavoritesWireframe.swift */; }; 84BB63C68EA124AA7DD21B30 /* LanguageSettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFBBE8C9FC347038AB74CF43 /* LanguageSettingsProtocols.swift */; }; 85018419204946C900F324A1 /* ThemeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85018418204946C900F324A1 /* ThemeCollectionViewCell.swift */; }; @@ -938,6 +935,16 @@ 850C301F204DA87A00DB26C2 /* PrivacyListWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C301A204DA87A00DB26C2 /* PrivacyListWireFrame.swift */; }; 850C3025204DAC1000DB26C2 /* PrivacyListItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C3024204DAC1000DB26C2 /* PrivacyListItemsFactory.swift */; }; 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850D21FF20D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift */; }; + 850EE2AC21A75E270051F873 /* SelectCountryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE29A21A75E260051F873 /* SelectCountryPresenter.swift */; }; + 850EE2AD21A75E270051F873 /* SelectCountryWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE29C21A75E260051F873 /* SelectCountryWireframe.swift */; }; + 850EE2AE21A75E270051F873 /* CountryCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2A021A75E260051F873 /* CountryCellModel.swift */; }; + 850EE2AF21A75E270051F873 /* CountryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2A121A75E260051F873 /* CountryTableViewCell.swift */; }; + 850EE2B021A75E270051F873 /* SelectCountryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2A321A75E260051F873 /* SelectCountryHeaderView.swift */; }; + 850EE2B121A75E270051F873 /* SelectCountryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2A521A75E260051F873 /* SelectCountryViewController.swift */; }; + 850EE2B221A75E270051F873 /* SelectCountryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2A621A75E260051F873 /* SelectCountryProtocols.swift */; }; + 850EE2B321A75E270051F873 /* SelectCountryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2A821A75E260051F873 /* SelectCountryInteractor.swift */; }; + 850EE2B421A75E270051F873 /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2AA21A75E260051F873 /* Country.swift */; }; + 850EE2B521A75E270051F873 /* CountriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850EE2AB21A75E260051F873 /* CountriesSection.swift */; }; 850FC5EC2032F21E00832D87 /* ForwardSelectorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850FC5EB2032F21E00832D87 /* ForwardSelectorProtocols.swift */; }; 850FC5F22032F33900832D87 /* ForwardSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850FC5F12032F33900832D87 /* ForwardSelectorViewController.swift */; }; 850FC5F42032F4CE00832D87 /* ForwardTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850FC5F32032F4CE00832D87 /* ForwardTargets.swift */; }; @@ -1057,7 +1064,6 @@ 85433F25204D596D00B373A7 /* WebFullScreenInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F20204D596D00B373A7 /* WebFullScreenInteractor.swift */; }; 85433F26204D596D00B373A7 /* WebFullScreenWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F21204D596D00B373A7 /* WebFullScreenWireFrame.swift */; }; 85433F2C204D5AA500B373A7 /* NynjaCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */; }; - 854574CA21931976001D43CF /* CountriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854574C921931976001D43CF /* CountriesSection.swift */; }; 854574CC21933190001D43CF /* NavigableWireframeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854574CB21933190001D43CF /* NavigableWireframeProtocol.swift */; }; 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; 85458CDA212D6FFE00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; @@ -1152,8 +1158,6 @@ 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BD2092368600E4840C /* StickerDataSource.swift */; }; 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */; }; 85739FBD2190AAC3001C4EC8 /* AuthProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */; }; - 8575E5342191A9E70080DD4A /* CountryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8575E5322191A9E70080DD4A /* CountryTableViewCell.swift */; }; - 8575E5352191A9E70080DD4A /* CountryCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8575E5332191A9E70080DD4A /* CountryCellModel.swift */; }; 85788C3C204422FB003600C9 /* BuildNumberProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */; }; 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C412044237B003600C9 /* BuildNumberViewController.swift */; }; 85788C4420442385003600C9 /* BuildNumberPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C4320442385003600C9 /* BuildNumberPresenter.swift */; }; @@ -1341,7 +1345,6 @@ 9BE521222189B2E10070C664 /* ThreeButtonHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE521212189B2E10070C664 /* ThreeButtonHeaderView.swift */; }; 9BFFE61B2178DD00004FE2CA /* BannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BFFE61A2178DCFF004FE2CA /* BannerView.swift */; }; 9E9DD4C7F700872D7CCEE227 /* ProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF6A30BDE89BF7887B67DA0 /* ProfileInteractor.swift */; }; - A1AD6864F4F49D9FC8997D59 /* SelectCountryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5522F1F73FC8C564BF0254BF /* SelectCountryPresenter.swift */; }; A402A1CC20DE694A005BFA20 /* PartialCheckableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A402A1CB20DE694A005BFA20 /* PartialCheckableButton.swift */; }; A402A1CE20DE6B38005BFA20 /* BaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A402A1CD20DE6B38005BFA20 /* BaseButton.swift */; }; A406E396210B34D300435B3E /* QueryFactoryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A406E395210B34D300435B3E /* QueryFactoryTest.swift */; }; @@ -1634,7 +1637,6 @@ A43B25A020AB1DFA00FF8107 /* TextFieldWithPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258320AB1DFA00FF8107 /* TextFieldWithPicker.swift */; }; A43B25A120AB1DFA00FF8107 /* EditFieldLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258420AB1DFA00FF8107 /* EditFieldLayout.swift */; }; A43B25A220AB1DFA00FF8107 /* EditField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258520AB1DFA00FF8107 /* EditField.swift */; }; - A43B25A320AB1DFA00FF8107 /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258620AB1DFA00FF8107 /* Country.swift */; }; A43B25A520AB1DFA00FF8107 /* DrawableAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */; }; A43B25A620AB1DFA00FF8107 /* RecordingAudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258920AB1DFA00FF8107 /* RecordingAudioWaveform.swift */; }; A43B25A720AB1DFA00FF8107 /* ALKeyboardObservingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258B20AB1DFA00FF8107 /* ALKeyboardObservingView.swift */; }; @@ -1989,7 +1991,6 @@ C9B8BEFE204DEBD00018748C /* DataDownloadAndUsageMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */; }; C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */; }; C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */; }; - C9DF574C2023BE92006B990A /* SelectCountryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */; }; CA6AA942773DEBE97BDCFDD6 /* DateTimePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */; }; CC59F623F661C99492F9F415 /* ImagePreviewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD33E34E2376166F3A132271 /* ImagePreviewProtocols.swift */; }; CCF8AA193F15D4191EC99051 /* SplashProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EBF72D0964E3EC64F5B966 /* SplashProtocols.swift */; }; @@ -2328,7 +2329,6 @@ FE58F9B1208F00FE004AFDD3 /* MessageEditActionTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F9B0208F00FE004AFDD3 /* MessageEditActionTable.swift */; }; FE58F9B3208F0583004AFDD3 /* DBMessageEditAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F9B2208F0583004AFDD3 /* DBMessageEditAction.swift */; }; FE9E70D021175DDC0034067A /* ChatScreenAlertFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9E70CF21175DDC0034067A /* ChatScreenAlertFactory.swift */; }; - FEA59F90B93C7B49BAF99F9C /* SelectCountryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7C039B61A0663D43BE5AE5 /* SelectCountryProtocols.swift */; }; FEA6546F2167749D00B44029 /* ContactExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F114620B421AB00F45004 /* ContactExtension.swift */; }; FEA65471216775CD00B44029 /* ServiceWalletExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA65470216775CC00B44029 /* ServiceWalletExtension.swift */; }; FEA65472216775CD00B44029 /* ServiceWalletExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA65470216775CC00B44029 /* ServiceWalletExtension.swift */; }; @@ -3302,6 +3302,16 @@ 850C301A204DA87A00DB26C2 /* PrivacyListWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyListWireFrame.swift; sourceTree = ""; }; 850C3024204DAC1000DB26C2 /* PrivacyListItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyListItemsFactory.swift; sourceTree = ""; }; 850D21FF20D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionFeedbackInteractive.swift; sourceTree = ""; }; + 850EE29A21A75E260051F873 /* SelectCountryPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectCountryPresenter.swift; sourceTree = ""; }; + 850EE29C21A75E260051F873 /* SelectCountryWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectCountryWireframe.swift; sourceTree = ""; }; + 850EE2A021A75E260051F873 /* CountryCellModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryCellModel.swift; sourceTree = ""; }; + 850EE2A121A75E260051F873 /* CountryTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryTableViewCell.swift; sourceTree = ""; }; + 850EE2A321A75E260051F873 /* SelectCountryHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectCountryHeaderView.swift; sourceTree = ""; }; + 850EE2A521A75E260051F873 /* SelectCountryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectCountryViewController.swift; sourceTree = ""; }; + 850EE2A621A75E260051F873 /* SelectCountryProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectCountryProtocols.swift; sourceTree = ""; }; + 850EE2A821A75E260051F873 /* SelectCountryInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectCountryInteractor.swift; sourceTree = ""; }; + 850EE2AA21A75E260051F873 /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; + 850EE2AB21A75E260051F873 /* CountriesSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountriesSection.swift; sourceTree = ""; }; 850FC5EB2032F21E00832D87 /* ForwardSelectorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardSelectorProtocols.swift; sourceTree = ""; }; 850FC5F12032F33900832D87 /* ForwardSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardSelectorViewController.swift; sourceTree = ""; }; 850FC5F32032F4CE00832D87 /* ForwardTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardTargets.swift; sourceTree = ""; }; @@ -7150,8 +7160,8 @@ 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, 5E07BC45216F64DB000E4558 /* CreateProfile */, + 850EE29821A75E260051F873 /* SelectCountry */, 852BB8C7219424EA00F2E8E4 /* Facebook */, - 115A968821FB24FA3C58A6D5 /* SelectCountry */, 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, ); @@ -8682,6 +8692,95 @@ path = HapticFeedback; sourceTree = ""; }; + 850EE29821A75E260051F873 /* SelectCountry */ = { + isa = PBXGroup; + children = ( + 850EE2A621A75E260051F873 /* SelectCountryProtocols.swift */, + 850EE29D21A75E260051F873 /* View */, + 850EE29921A75E260051F873 /* Presenter */, + 850EE2A721A75E260051F873 /* Interactor */, + 850EE29B21A75E260051F873 /* WireFrame */, + 850EE2A921A75E260051F873 /* Entities */, + ); + path = SelectCountry; + sourceTree = ""; + }; + 850EE29921A75E260051F873 /* Presenter */ = { + isa = PBXGroup; + children = ( + 850EE29A21A75E260051F873 /* SelectCountryPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 850EE29B21A75E260051F873 /* WireFrame */ = { + isa = PBXGroup; + children = ( + 850EE29C21A75E260051F873 /* SelectCountryWireframe.swift */, + ); + path = WireFrame; + sourceTree = ""; + }; + 850EE29D21A75E260051F873 /* View */ = { + isa = PBXGroup; + children = ( + 850EE29E21A75E260051F873 /* TableView */, + 850EE2A421A75E260051F873 /* ViewController */, + ); + path = View; + sourceTree = ""; + }; + 850EE29E21A75E260051F873 /* TableView */ = { + isa = PBXGroup; + children = ( + 850EE29F21A75E260051F873 /* Cell */, + 850EE2A221A75E260051F873 /* Header */, + ); + path = TableView; + sourceTree = ""; + }; + 850EE29F21A75E260051F873 /* Cell */ = { + isa = PBXGroup; + children = ( + 850EE2A021A75E260051F873 /* CountryCellModel.swift */, + 850EE2A121A75E260051F873 /* CountryTableViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; + 850EE2A221A75E260051F873 /* Header */ = { + isa = PBXGroup; + children = ( + 850EE2A321A75E260051F873 /* SelectCountryHeaderView.swift */, + ); + path = Header; + sourceTree = ""; + }; + 850EE2A421A75E260051F873 /* ViewController */ = { + isa = PBXGroup; + children = ( + 850EE2A521A75E260051F873 /* SelectCountryViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + 850EE2A721A75E260051F873 /* Interactor */ = { + isa = PBXGroup; + children = ( + 850EE2A821A75E260051F873 /* SelectCountryInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 850EE2A921A75E260051F873 /* Entities */ = { + isa = PBXGroup; + children = ( + 850EE2AA21A75E260051F873 /* Country.swift */, + 850EE2AB21A75E260051F873 /* CountriesSection.swift */, + ); + path = Entities; + sourceTree = ""; + }; 850FC5EA2032F1D900832D87 /* ForwardSelector */ = { isa = PBXGroup; children = ( @@ -16033,6 +16132,7 @@ 26B7CB692178982200C83ED8 /* Locale+Language.swift in Sources */, 265AEA151FE9AFA700AC4806 /* MemberHandler.swift in Sources */, E7C36C311FC4399B00740630 /* DBService.swift in Sources */, + 850EE2B221A75E270051F873 /* SelectCountryProtocols.swift in Sources */, B77C11DE2109242200CCB42E /* AssigningInterpreterInteractor.swift in Sources */, C940514D204C7FAF00D72B04 /* DataAndStorageWireFrame.swift in Sources */, FEA655FE2167777F00B44029 /* TransferDetailsProtocols.swift in Sources */, @@ -16324,7 +16424,6 @@ 264638271FFFE7F9002590E6 /* RepliesPresenter.swift in Sources */, E7C36C391FC46A9E00740630 /* ServiceExtension.swift in Sources */, 8514F17620EA219E00883513 /* ContextMenuControlCell.swift in Sources */, - C9DF574C2023BE92006B990A /* SelectCountryHeaderView.swift in Sources */, E7A3DAB31F9DE9CB00856133 /* ConfigurableCell.swift in Sources */, A49CC1D420E4A9ED00879D41 /* InputBar+ContentType.swift in Sources */, A46C363721219A9000172773 /* DDSysctlMechanism.swift in Sources */, @@ -16345,7 +16444,6 @@ E785EF2A1FB9D99400F0C689 /* PinView.swift in Sources */, A42D51D0206A361400EEB952 /* Star.swift in Sources */, 85C16C3E20D2794500EDB77E /* BubbleImageSizeCalculatable.swift in Sources */, - 8575E5352191A9E70080DD4A /* CountryCellModel.swift in Sources */, A43B25D620AB1EE400FF8107 /* NewChannelPresenter.swift in Sources */, E7598F691FA1D8B90082FBE7 /* ProfileScheduledMesssageCellLayout.swift in Sources */, E7C1D3681F683A7D007D4E1E /* MainNavigationItem.swift in Sources */, @@ -16753,9 +16851,9 @@ 85BA176120BEA7BD001EF8AC /* StickerPreviewContainerView.swift in Sources */, 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */, 855A4E7F2199B4FE00B6E90B /* NynjaImageButton.swift in Sources */, - 8575E5342191A9E70080DD4A /* CountryTableViewCell.swift in Sources */, 00E8513B2021E96E007DC792 /* GApiResponse.swift in Sources */, 850571252050B0AD00EDF794 /* NotificationAlertSoundsWireFrame.swift in Sources */, + 850EE2AC21A75E270051F873 /* SelectCountryPresenter.swift in Sources */, 85D66A0920BD963C00FBD803 /* MentionController.swift in Sources */, F127F3BB20BF03BF007A6F87 /* NumberFormatterExtension.swift in Sources */, 3A1AAFCE1F3DF0470098780A /* DateExtensions.swift in Sources */, @@ -16796,6 +16894,7 @@ F117872920ACF2DB007A9A1B /* CameraSetting.swift in Sources */, E77D58971F98B91600FBE926 /* ProfileTableViewDelegate.swift in Sources */, B79FA02D2107731400F286BF /* MarketplaceProtocols.swift in Sources */, + 850EE2AE21A75E270051F873 /* CountryCellModel.swift in Sources */, B77C11DC2109242200CCB42E /* AssigningInterpreterViewController.swift in Sources */, A45F111420B4218D00F45004 /* SystemCell.swift in Sources */, 26C1A3E92031AAA30009F7F0 /* OtherUserViewController.swift in Sources */, @@ -16862,6 +16961,7 @@ E764919B1F7A5485001E741C /* MainWheelContainerDelegate.swift in Sources */, A4330A6A2109EA850060BD93 /* DatabaseManager.swift in Sources */, 8E6C4BDE1FF40B97009C8374 /* GroupFilesCell.swift in Sources */, + 850EE2B121A75E270051F873 /* SelectCountryViewController.swift in Sources */, 26DCB24E2064B9DC001EF0AB /* ContactsTableDS.swift in Sources */, A45F114820B421AB00F45004 /* Contact+BaseChatModel.swift in Sources */, 6CED2C4CE125011A3A731D62 /* AddContactViaPhoneInteractor.swift in Sources */, @@ -16937,6 +17037,7 @@ E761A0D91F8B8CF000C088E0 /* EditProfileViewControllerLayout.swift in Sources */, E7EB8C511FB1D55F0005A4D9 /* Participant.swift in Sources */, F119E67020D24BCF0043A532 /* MultiplePreviewPresenter.swift in Sources */, + 850EE2B521A75E270051F873 /* CountriesSection.swift in Sources */, 269848CE200FB59800590D6F /* StarMessageTable.swift in Sources */, A42D51C5206A361400EEB952 /* History.swift in Sources */, 850B9DA6219C2B9500EA0CF4 /* AppConfigurationProvider.swift in Sources */, @@ -17180,6 +17281,7 @@ B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */, 26FA420E201812D600E6F6EC /* StarTableDS.swift in Sources */, F10B0E2820B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift in Sources */, + 850EE2B321A75E270051F873 /* SelectCountryInteractor.swift in Sources */, E7C36C351FC448CB00740630 /* DBFeature.swift in Sources */, 4B71AC4521622AA700E4583B /* AppNotificationsProviding.swift in Sources */, 4B06D3122028A4CF003B275B /* GroupChatsItemsFactory.swift in Sources */, @@ -17251,6 +17353,7 @@ 26C1A3EB2031AAD20009F7F0 /* OtherUserInteractor.swift in Sources */, 5E7D5D3F218C5A12009B5D8D /* StatusTimeout.swift in Sources */, 26D8317520EA65200067C5B4 /* TranslationInfo.swift in Sources */, + 850EE2B021A75E270051F873 /* SelectCountryHeaderView.swift in Sources */, A4868F3A2121E22C001F624E /* AntiDebuggingService.swift in Sources */, 85D66A0020BD963C00FBD803 /* InputTextMessage.swift in Sources */, A4868F312121D360001F624E /* DebuggingPreventing.swift in Sources */, @@ -17271,11 +17374,11 @@ 4BE2C5E32142EB0F00A73DD9 /* AudioManagerDelegate.swift in Sources */, 85D66A2320BD970400FBD803 /* BBTagBuilder.swift in Sources */, A981ECC08BCDAF26E135B20D /* VideoPreviewViewController.swift in Sources */, + 850EE2AD21A75E270051F873 /* SelectCountryWireframe.swift in Sources */, A45F113B20B4218D00F45004 /* MessageInteractor+Fetch.swift in Sources */, 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */, 3A8045DA1F60E18E00AED866 /* Queue.swift in Sources */, A42D51A3206A361400EEB952 /* Test.swift in Sources */, - 854574CA21931976001D43CF /* CountriesSection.swift in Sources */, A45F113220B4218D00F45004 /* BaseChatCellLayout.swift in Sources */, A411D95C20AC3A5A009D107C /* ConversationsProvider.swift in Sources */, 859F9B4C2035CB1E009D017A /* ForwardContent.swift in Sources */, @@ -17368,7 +17471,6 @@ 9BB33F3E2146A14B009FB252 /* HoldToSpeakView.swift in Sources */, 850C0B5420E0369E003341D0 /* ChatListMessageCellModelDelegate.swift in Sources */, A432CF1220B4347D00993AFB /* InputInfoProvider.swift in Sources */, - A43B25A320AB1DFA00FF8107 /* Country.swift in Sources */, A43B25A820AB1DFA00FF8107 /* ALTextInputBarDelegate.swift in Sources */, FEA655F92167777F00B44029 /* PaymentModel.swift in Sources */, A42D51C8206A361400EEB952 /* iterator.swift in Sources */, @@ -17462,6 +17564,7 @@ 4BF090BB21635B6600DCCA5C /* LogServiceTopic.swift in Sources */, 8514F17420EA219E00883513 /* ContextMenuNextCell.swift in Sources */, A42D52D7206A53AB00EEB952 /* History_Spec.swift in Sources */, + 850EE2AF21A75E270051F873 /* CountryTableViewCell.swift in Sources */, F127F3BC20BF03BF007A6F87 /* WeakDays.swift in Sources */, 8E96019F1FF303DF00E0C21D /* GroupAudiosListVC.swift in Sources */, B723C629204D86AF00884FFD /* SettingsDataAndStorageWireFrame.swift in Sources */, @@ -17598,21 +17701,16 @@ 8514F17A20EA219F00883513 /* ContextMenuArrowView.swift in Sources */, B3D0F59E1E7BDB7E485AE662 /* GroupStorageWireframe.swift in Sources */, A45F114120B4218D00F45004 /* MessageInteractor+StorageSubscriber.swift in Sources */, - FEA59F90B93C7B49BAF99F9C /* SelectCountryProtocols.swift in Sources */, 5E7D5D3D218C59F1009B5D8D /* UserStatus.swift in Sources */, 26ABCA3E21189DA400EA4782 /* Aps.swift in Sources */, 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */, 260313AB20A0A4BA009AC66D /* ChatLanguageSettingsInteractor.swift in Sources */, - 7C51CDC1260CE191C07EE46C /* SelectCountryViewController.swift in Sources */, 8596CEF22048A763006FC65D /* ThemeCellModel.swift in Sources */, 4B030F3B2195CF8100F293B7 /* Host.swift in Sources */, 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */, 5E7D5D5A21901BC6009B5D8D /* DescriptionTVCell.swift in Sources */, - A1AD6864F4F49D9FC8997D59 /* SelectCountryPresenter.swift in Sources */, - 32E5A25AD25BF752EB3864AB /* SelectCountryInteractor.swift in Sources */, A42D52DB206A53AB00EEB952 /* messageEvent_Spec.swift in Sources */, 4B749F08214FEE4F002F3A33 /* VerifyNumberInteractor.swift in Sources */, - 1A9DFA4A2ED5ACE55035FA17 /* SelectCountryWireframe.swift in Sources */, A42D51CA206A361400EEB952 /* ExtendedStar.swift in Sources */, 01AA377709C2831ACE2F08D0 /* AddContactByUsernameProtocols.swift in Sources */, 9BFFE61B2178DD00004FE2CA /* BannerView.swift in Sources */, @@ -17627,6 +17725,7 @@ 859ECA6C21A441A9003630A0 /* AuthService.swift in Sources */, 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */, 26DCB24C2064B9CC001EF0AB /* ContactCellModel.swift in Sources */, + 850EE2B421A75E270051F873 /* Country.swift in Sources */, 856A8EFC219C8D7A0004E11E /* AuthenticationType.swift in Sources */, 4B749F06214FEE4F002F3A33 /* VerifyNumberViewController.swift in Sources */, A42D52AF206A53AA00EEB952 /* Room_Spec.swift in Sources */, diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 7bd3cfe9a..d9015612b 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -71,7 +71,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return NynjaJoinByLinkService.shared().handleJoin(by: url) } - func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { return GIDSignIn.sharedInstance().handle(url, sourceApplication: options[.sourceApplication] as? String, annotation: options[.annotation]) diff --git a/Nynja/DB/Models/DBMember.swift b/Nynja/DB/Models/DBMember.swift index 5f5f66468..6999cd152 100644 --- a/Nynja/DB/Models/DBMember.swift +++ b/Nynja/DB/Models/DBMember.swift @@ -197,8 +197,7 @@ final class DBMember: Record, DBModel { // MARK: - Requests - static private func requestMember(roomId: String, phoneIds: [String], selection: String = "*") -> AnyTypedRequest { - + static private func requestMember(roomId: String, phoneIds: [String]) -> AnyTypedRequest { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name @@ -207,7 +206,7 @@ final class DBMember: Record, DBModel { .joinedByComma() let sql = """ - SELECT \(memberTable).\(selection) + SELECT \(memberTable).* FROM \(roomMemberTable) LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = \(memberTable).\(MemberTable.Column.id.title) @@ -215,10 +214,10 @@ final class DBMember: Record, DBModel { AND \(MemberTable.Column.phoneId.title) in (\(phoneIds)) """ - return sql + return SQLRequest(sql).asRequest(of: DBMember.self) } - static private func sqlAdmin(roomId: String, isAdmin: Bool) -> String { + static private func requestMember(roomId: String, isAdmin: Bool) -> AnyTypedRequest { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name @@ -231,6 +230,6 @@ final class DBMember: Record, DBModel { AND \(RoomMemberTable.Column.isAdmin.title) = \(isAdmin ? 1 : 0) """ - return sql + return SQLRequest(sql).asRequest(of: DBMember.self) } } diff --git a/Nynja/Library/UI/Alert/AlertDisplayable.swift b/Nynja/Library/UI/Alert/AlertDisplayable.swift index 434595160..cc8d4901a 100644 --- a/Nynja/Library/UI/Alert/AlertDisplayable.swift +++ b/Nynja/Library/UI/Alert/AlertDisplayable.swift @@ -9,10 +9,10 @@ import Foundation protocol AlertDisplayable: class { - func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?, completion: (() -> Void)?) + func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?, completion: (() -> Void)?) func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?, completion: (() -> Void)?) - func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?) + func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?) func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?) } @@ -22,7 +22,7 @@ extension AlertDisplayable { presentAlert(title: title, message: message, style: .alert, actions: actions, completion: completion) } - func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?) { + func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?) { presentAlert(title: title, message: message, style: style, actions: actions, completion: nil) } @@ -36,7 +36,7 @@ protocol NavigationContainer: AlertDisplayable { } extension NavigationContainer { - func presentAlert(title: String?, message: String?, style: UIAlertControllerStyle, actions: [UIAlertAction]?, completion: (() -> Void)?) { + func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?, completion: (() -> Void)?) { let alert = UIAlertController(title: title, message: message, preferredStyle: style) actions?.forEach { alert.addAction($0) } diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift index 5f0095db8..45c26eb36 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift @@ -40,7 +40,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView }() private lazy var tableView: UITableView = { - let table = UITableView(frame: CGRect.zero, style: UITableViewStyle.grouped) + let table = UITableView(frame: CGRect.zero, style: .grouped) view.addSubview(table) table.delegate = self @@ -163,7 +163,7 @@ extension AccountSettingsViewController { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { guard let item = presenter.item(for: indexPath.section, row: indexPath.row) as? VerticalSizeble else { - return UITableViewAutomaticDimension + return UITableView.automaticDimension } return CGFloat(item.height) diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift index dae66ada8..73af9b4e2 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift @@ -249,14 +249,16 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { textView.isEditable = false let beginOfStr = NSMutableAttributedString(string: "I agree at".localized) - beginOfStr.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.nynja.dustyGray, - NSAttributedStringKey.font: FontFamily.NotoSans.regular.font(size: 14)], range: NSMakeRange(0, beginOfStr.length)) - - let attributes = [NSAttributedStringKey.link : "https://landing.nynja.io/terms-of-use", - NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue, - NSAttributedStringKey.underlineColor: UIColor.nynja.blue, - NSAttributedStringKey.foregroundColor: UIColor.nynja.blue, - NSAttributedStringKey.font: FontFamily.NotoSans.regular.font(size: 14)] as [NSAttributedStringKey : Any] + beginOfStr.addAttributes([.foregroundColor : UIColor.nynja.dustyGray, + .font: FontFamily.NotoSans.regular.font(size: 14)], range: NSMakeRange(0, beginOfStr.length)) + + let attributes: [NSAttributedString.Key : Any] = [ + .link : "https://landing.nynja.io/terms-of-use", + .underlineStyle: NSUnderlineStyle.single.rawValue, + .underlineColor: UIColor.nynja.blue, + .foregroundColor: UIColor.nynja.blue, + .font: FontFamily.NotoSans.regular.font(size: 14) + ] let termsOfUseStr = NSMutableAttributedString(string: String.localizable.termsOfUse) termsOfUseStr.addAttributes(attributes, range: NSMakeRange(0, termsOfUseStr.length)) diff --git a/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift b/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift index b7d65995d..8ad772fa5 100644 --- a/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift +++ b/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift @@ -73,7 +73,7 @@ final class CountryTableViewCell: UITableViewCell { // MARK: - Init - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setup() } diff --git a/Nynja/Modules/Main/Interactor/MainInteractor.swift b/Nynja/Modules/Main/Interactor/MainInteractor.swift index 244af25b0..a44c1b25b 100644 --- a/Nynja/Modules/Main/Interactor/MainInteractor.swift +++ b/Nynja/Modules/Main/Interactor/MainInteractor.swift @@ -38,7 +38,7 @@ final class MainInteractor: BaseInteractor, MainInteractorInputProtocol, EditPho override func loadData() { super.loadData() - ProfileHandler.delegate = self + ProfileHandler.shared.delegate = self pushService.registerForNotifications() mqttService.addSubscriber(self) setupIntercom() diff --git a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift index 7ece87679..ff6696f83 100644 --- a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift +++ b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift @@ -28,7 +28,7 @@ class ProfileWireFrame: ProfileWireFrameProtocol { presenter: presenter, homeDataProvider: serviceFactory.makeHomeDataProvider(limit: 5), contactsProvider: serviceFactory.makeContactsProvider(), - typingProvider: serviceFactory.makeTypingProvider() + typingProvider: serviceFactory.makeTypingProvider(), mqttService: serviceFactory.makeMQTTService())) presenter.inject( dependencies: .init( diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index 6e21a8e87..08714daaa 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -8,10 +8,6 @@ import Foundation -protocol TypingHandlerDelegate: class { - func didReceiveTyping(_ typing: Typing) -} - final class TypingHandler: BaseHandler, Observable { // MARK: - Singleton diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index 9272ea851..c088dcb40 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -178,7 +178,7 @@ final class MQTTService: NSObject, MQTTServiceProtocol, CocoaMQTTDelegate, Conne LogService.log(topic: .db) { return "Clear storage: bad username" } storageService.clearStorage() - IoHandler.delegate { $0.sessionNotFound() } + IoHandler.shared.delegate { $0.sessionNotFound() } notifySubscribers { (delegate) in delegate.mqttServiceDidReceiveAuthenticationFailure(self) } diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift index 25e2ce876..91d0c858a 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -38,6 +38,7 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtoc func makeWalletService() -> WalletService func makeSyncFileManager() -> SyncFileManager + func makeSyncFileManager(with kind: FileDownloaderKind) -> SyncFileManager func makeMuteChatService() -> MuteChatServiceProtocol @@ -51,4 +52,22 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtoc func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol func makeAudioSessionManager() -> AudioSessionManager + + func makeHomeDataProvider(limit: Int) -> HomeDataProvider + + func makePushService() -> PushService + + func makeNynjaCommunicatorService() -> NynjaCommunicatorService + + func makeBadgeNumberService() -> BadgeNumberServiceProtocol + + func makeNotificationManager() -> NotificationManager + + func makeUserSettingsService() -> UserSettingsService + + func makeReachabilityService() -> ReachabilityService + + func makeUserInfo() -> UserInfo + + func makeDatabaseManager() -> DBManagerProtocol } diff --git a/Nynja/Services/StorageService.swift b/Nynja/Services/StorageService.swift index dd3b86460..fe9b0754b 100644 --- a/Nynja/Services/StorageService.swift +++ b/Nynja/Services/StorageService.swift @@ -160,9 +160,9 @@ extension StorageService: DBManagerProtocol { #endif -// MARK: - UserInfo +// MARK: - UserInfo + SessionStorage -extension StorageService: UserInfo { +extension StorageService: UserInfo, SessionStorage { var token: String? { get { return userInfo.token } diff --git a/Nynja/TypingHandlerDelegate.swift b/Nynja/TypingHandlerDelegate.swift index de1844d5f..4b7822d53 100644 --- a/Nynja/TypingHandlerDelegate.swift +++ b/Nynja/TypingHandlerDelegate.swift @@ -9,5 +9,5 @@ import Foundation protocol TypingHandlerDelegate: class { - func getTyping(typing: Typing) + func didReceiveTyping(_ typing: Typing) } diff --git a/Shared/Library/StaticDelegating.swift b/Shared/Library/StaticDelegating.swift index 39173973e..13e5d29d6 100644 --- a/Shared/Library/StaticDelegating.swift +++ b/Shared/Library/StaticDelegating.swift @@ -11,18 +11,17 @@ import Foundation protocol StaticDelegating { associatedtype Delegate - static var delegate: Delegate? { get set } + var delegate: Delegate? { get set } - static func delegate(on queue: DispatchQueue, closure: @escaping (Delegate) -> Void) + func delegate(on queue: DispatchQueue, closure: @escaping (Delegate) -> Void) } extension StaticDelegating { - static func delegate(on queue: DispatchQueue = .main, closure: @escaping (Delegate) -> Void) { - guard let delegate = self.delegate else { + func delegate(on queue: DispatchQueue = .main, closure: @escaping (Delegate) -> Void) { + guard let delegate = delegate else { return } - queue.async { closure(delegate) } diff --git a/Shared/Services/Handlers/IoHandler.swift b/Shared/Services/Handlers/IoHandler.swift deleted file mode 100644 index 478cce95e..000000000 --- a/Shared/Services/Handlers/IoHandler.swift +++ /dev/null @@ -1,188 +0,0 @@ -// -// IoHandler.swift -// Nynja -// -// Created by Anton Makarov on 14.06.2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol IoHandlerDelegate: class { - func smsSent() - func logined() - func wrongCode() - func mismatchUserData() - func sessionNotFound() - func attemptsExpired() - func notAuthorized() - func added() - func invalidData() - func callInProgress() - func logout() - func numberNotAllowed() - func sessionsCleared() - func sessionDeleted() - - func getContactSuccess(contact: Contact) - func getContactQRSuccess(contact: Contact) - func getContactByUsernameSucces(contact: Contact) - func getContactsSuccess(contacts: [Contact]) - func contactNotFound() - func contactsNotFound() - func contactQRNotFound() - func contactByUsernameNotFound() - func usernameIsBusy() - -} - -extension IoHandlerDelegate { - func smsSent() {} - func logined() {} - func wrongCode() {} - func mismatchUserData() {} - func sessionNotFound() {} - func attemptsExpired() {} - func notAuthorized() {} - func added() {} - func invalidData() {} - func callInProgress() {} - func logout() {} - func sessionsCleared() {} - func sessionDeleted() {} - - func getContactSuccess(contact: Contact) {} - func getContactQRSuccess(contact: Contact) {} - func getContactByUsernameSucces(contact: Contact) {} - func getContactsSuccess(contacts: [Contact]) {} - func contactNotFound() {} - func contactsNotFound() {} - func contactQRNotFound() {} - func contactByUsernameNotFound() {} - - func usernameIsBusy() {} - func numberNotAllowed() {} - - -} - -final class IoHandler: BaseHandler { - - static let shared = IoHandler() - - private init() {} - - weak var delegate: IoHandlerDelegate? - - var storageService: StorageService { - return .sharedInstance - } - - var mqttService: MQTTService { - return .sharedInstance - } - - var keychainService: KeychainService { - return .standard - } - - func executeHandle(data: BertTuple) { - if let IO = get_io().parse(bert: data) as? io { - var code: String? = nil - if let value = ((IO.code as? ok)?.code as? StringAtom)?.string { - code = value - } - if let value = ((IO.code as? error)?.code as? StringAtom)?.string { - code = value - } - if let value = (IO.code as? ok)?.code as? String { - code = value - } - if let value = (IO.code as? error2)?.code?.string { - code = value - } - if let ok2 = IO.code as? ok2 { - code = ok2.code?.string - if let src = (IO.code as? ok2)?.src as? [AnyObject] { - if src.count > 1 { - if let clientId = src[0] as? String, let token = src[1] as? String { - storageService.setupAuth(clientId: clientId, token: token) - } - } - } - } - if let action = code { - switch action { - case "deleted": - self.delegate?.sessionDeleted() - case "cleared": - self.delegate?.sessionsCleared() - case "sms_sent": - self.delegate?.smsSent() - case "invalid_data": - self.delegate?.invalidData() - case "session_not_found": - self.delegate?.sessionNotFound() - case "call_in_progress": - self.delegate?.callInProgress() - case "login": - StorageService.sharedInstance.wasLogined = true - case "mismatch_user_data": - self.delegate?.mismatchUserData() - case "invalid_sms_code": - self.delegate?.wrongCode() - case "attempts_expired": - self.delegate?.attemptsExpired() - case "number_not_allowed": - self.delegate?.numberNotAllowed() - case "logout": - LogService.log(topic: .db) { return "Clear storage: IoHandler" } - storageService.clearStorage() - - mqttService.state = .notAuthenticated(isLoggedOutFromServer: true) - - mqttService.disconnect() - mqttService.reconnect() - self.delegate?.logout() - case "phone": - if let roster = IO.data as? Roster { - if let contact = roster.userlist?.first { - self.delegate?.getContactSuccess(contact: contact) - } else { - self.delegate?.contactNotFound() - } - } - case "phonebook": - if let roster = IO.data as? Roster { - if let contacts = roster.userlist { - self.delegate?.getContactsSuccess(contacts: contacts) - } else { - self.delegate?.contactsNotFound() - } - } - case "qrcode": - if let roster = IO.data as? Roster { - if let contact = roster.userlist?.first { - self.delegate?.getContactQRSuccess(contact: contact) - } else { - self.delegate?.contactQRNotFound() - } - } - case "nick": - self.delegate?.usernameIsBusy() - case "username": - if let roster = IO.data as? Roster { - if let contact = roster.userlist?.first { - self.delegate?.getContactByUsernameSucces(contact: contact) - } else { - self.delegate?.contactByUsernameNotFound() - } - } - default: - break - } - } - } - } - -} diff --git a/Shared/Services/Handlers/IoHandler/IoHandler.swift b/Shared/Services/Handlers/IoHandler/IoHandler.swift index 58a1b1f52..ad5abc7b7 100644 --- a/Shared/Services/Handlers/IoHandler/IoHandler.swift +++ b/Shared/Services/Handlers/IoHandler/IoHandler.swift @@ -10,17 +10,17 @@ import Foundation final class IoHandler: BaseHandler, StaticDelegating { - static weak var delegate: IoHandlerDelegate? + weak var delegate: IoHandlerDelegate? - static var storageService: StorageService { - return .sharedInstance - } + private let storageService = StorageService.sharedInstance - static var mqttService: MQTTService { - return .sharedInstance - } + private let mqttService = MQTTService.sharedInstance + + static let shared = IoHandler() + + private init() {} - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { if let IO = get_io().parse(bert: data) as? io { var code: String? = nil if let value = ((IO.code as? ok)?.code as? StringAtom)?.string { @@ -109,14 +109,4 @@ final class IoHandler: BaseHandler, StaticDelegating { } } } - - static func delegate(_ closure: @escaping (IoHandlerDelegate) -> Void) { - guard let delegate = self.delegate else { - return - } - - dispatchAsyncMain { - closure(delegate) - } - } } -- GitLab From f8abac475bad5afc789bcf7b84dc96299f01c003 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 23 Nov 2018 13:06:06 +0200 Subject: [PATCH 120/138] Update SDK version --- Nynja.xcodeproj/project.pbxproj | 15 ++++++++------- Podfile | 2 +- Podfile.lock | 8 ++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 36dbaa697..a820bfa00 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -18273,8 +18273,8 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -18287,7 +18287,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "f6fa31cc-ee3a-42a3-ae54-af6e54db9f3e"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_AdHocExt; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -18456,8 +18456,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; @@ -18470,7 +18470,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "6001ef39-7fcd-4abe-8f6d-2de8430809d4"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_adhoc; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; @@ -18561,6 +18561,7 @@ SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; + SWIFT_VERSION = 4.2; SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -19429,7 +19430,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; diff --git a/Podfile b/Podfile index 3ebf00ea9..b7ad47031 100644 --- a/Podfile +++ b/Podfile @@ -40,7 +40,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.6' # pod 'NynjaSDK', '= 1.8' - pod 'NynjaSDK-MultiAcc', '= 0.5.6' + pod 'NynjaSDK-MultiAcc', '= 0.5.6.1' pod 'CryptoSwift', '= 0.13.0' diff --git a/Podfile.lock b/Podfile.lock index b4e22fe05..c49b8ebe3 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -90,7 +90,7 @@ PODS: - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.6) + - NynjaSDK-MultiAcc (0.5.6.1) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -124,7 +124,7 @@ DEPENDENCIES: - MaterialComponents/ActivityIndicator (= 55.3.0) - MaterialComponents/FlexibleHeader (= 55.3.0) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.6) + - NynjaSDK-MultiAcc (= 0.5.6.1) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.2.0) @@ -214,7 +214,7 @@ SPEC CHECKSUMS: MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: b94c07b446fa1385acb4c33bcd81a9a615b59802 + NynjaSDK-MultiAcc: 036c00a535be1b4e3f44bb9703a785a7bb63b18d QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a @@ -222,6 +222,6 @@ SPEC CHECKSUMS: SwiftyJSON: c4bcba26dd9ec7a027fc8eade48e2c911f229e96 TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: 6adc2e25d07ddb1a0b77f9eb89477ff8ae96ae69 +PODFILE CHECKSUM: 8efd1d6c055d050f06d3dd21496decb8758b29f7 COCOAPODS: 1.5.3 -- GitLab From 154364ec248dc21055a9e3ed80f20ffe53906b16 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 23 Nov 2018 15:55:20 +0200 Subject: [PATCH 121/138] Fixed compile errors --- Nynja.xcodeproj/project.pbxproj | 135 ++++++++++++++++++ .../Account/Service/AccountServiceImpl.swift | 4 +- 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index a820bfa00..abbd97ecd 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -18079,9 +18079,21 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -18463,9 +18475,60 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/AWSCore\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/AWSS3\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/AutoScrollLabel\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CocoaMQTT\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseAuth\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseStorage\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GRDBCipher\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GTMOAuth2\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GoogleToolboxForMac\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/JTAppleCalendar\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MDFInternationalization\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MDFTextAccessibility\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MaterialComponents-cfb03c44\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MotionAnimator\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MotionInterchange\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/MulticastDelegateSwift\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/QRCode\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SQLCipher\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SnapKit\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/libPhoneNumber-iOS\"", + "\"${PODS_ROOT}/Crashlytics/iOS\"", + "\"${PODS_ROOT}/Fabric/iOS\"", + "\"${PODS_ROOT}/GoogleMaps/Base/Frameworks\"", + "\"${PODS_ROOT}/GoogleMaps/Maps/Frameworks\"", + "\"${PODS_ROOT}/GooglePlaces/Frameworks\"", + "\"${PODS_ROOT}/GoogleSignIn/Frameworks\"", + "\"${PODS_ROOT}/Intercom/Intercom\"", + "\"${PODS_ROOT}/NynjaSDK-MultiAcc\"", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\""; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -18496,9 +18559,21 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "1971b115-2b2c-48b6-a20f-3686249e0a88"; @@ -18583,9 +18658,21 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -18763,9 +18850,21 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -19030,9 +19129,21 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -19211,9 +19322,21 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "e9cc21bd-73cb-4b39-92ab-097127d12162"; @@ -19405,9 +19528,21 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/Firebase/CoreOnly/Sources", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Public/TestFairy\"", + ); INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PODS_ROOT)/TestFairy\"", + ); MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Nynja/SDK/Account/Service/AccountServiceImpl.swift b/Nynja/SDK/Account/Service/AccountServiceImpl.swift index f0b3a5890..c587d0318 100644 --- a/Nynja/SDK/Account/Service/AccountServiceImpl.swift +++ b/Nynja/SDK/Account/Service/AccountServiceImpl.swift @@ -350,12 +350,12 @@ extension AccountServiceImpl { } } - public func getAllAccountsByProfileIdDidFinish(withDetails accountDetailsArray: [Any]?, withError error: Error?) { + public func getAllAccountsByProfileIdDidFinish(withDetails accountDetailsArray: [NYNAccountDetails]?, withError error: Error?) { handleResponse(nil, to: \AccountServiceImpl.getAllAccountsCompletion) { completion in if let error = error { completion?(.failure(error)) } - if let accountDetailsArray = accountDetailsArray as? [NYNAccountDetails] { + if let accountDetailsArray = accountDetailsArray { completion?(.success(accountDetailsArray)) } completion?(.failure(AccountError.invalidResponse)) -- GitLab From 9416d534c5b6c60d4efaa5078b59fc10f8389c30 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 5 Dec 2018 13:05:12 +0200 Subject: [PATCH 122/138] [Multiple accounts] Login options (#1500) * [NY-3855] Add login options module skeleton. * [NY-3855] Implemented base UI for login options. * [NY-3855] Added alerts. * [NY-3855] Added LoginOption model. Minor changes. * Fixed compile issues. * [NY-3855] Fixed screen title * Fixed switch * [NY-5519] Add AuthProvider module from template * Minor renaming changes. * Remove unused references from project file * Added transition to 'add auth provider' screen. * Implemented base UI for add auth provider. * Fixed UI * Fixed UI hierarchy. * Fixed UI components reusability from auth screen. --- Nynja.xcodeproj/project.pbxproj | 358 ++++++++++++------ Nynja/Coordinators/NavigationError.swift | 13 + Nynja/Generated/AssetsConstants.swift | 2 + Nynja/Generated/LocalizableConstants.swift | 22 ++ .../UI/Buttons/NynjaButton/NynjaSwitch.swift | 47 +++ .../AuthProvider/AuthProviderProtocols.swift | 44 +++ .../AuthProvider/Entities/AuthProvider.swift | 12 + .../AuthProviderUIConfiguration.swift | 18 + .../Interactor/AuthProviderInteractor.swift | 42 ++ .../Presenter/AuthProviderPresenter.swift | 144 +++++++ .../View/AuthProviderViewController.swift | 213 +++++++++++ .../Subviews/SearchAvailabilityView.swift | 153 ++++++++ .../Wireframe/AuthProviderWireframe.swift | 59 +++ .../Coordinator/LoginOptionsCoordinator.swift | 92 +++++ .../LoginOptions/Entities/LoginOption.swift | 32 ++ .../Interactor/LoginOptionsInteractor.swift | 59 +++ .../LoginOptions/LoginOptionsProtocols.swift | 38 ++ .../Presenter/LoginOptionsPresenter.swift | 181 +++++++++ .../Forms/FieldRowItem/AnyFieldRowItem.swift | 51 +++ .../Forms/FieldRowItem/FieldRowItem.swift | 27 ++ .../LoginOptions/View/Forms/Form.swift | 32 ++ .../View/Forms/Items/ActionRowItemView.swift | 150 ++++++++ .../View/Forms/Items/SwitchRowItemView.swift | 123 ++++++ .../Forms/Items/TextFieldRowItemView.swift | 111 ++++++ .../View/Forms/Items/TextRowItemView.swift | 109 ++++++ .../View/LoginOptionsViewController.swift | 121 ++++++ .../LoginOptionSwitchRowItemView.swift | 140 +++++++ .../Wireframe/LoginOptionsWireframe.swift | 54 +++ Nynja/Modules/Auth Flow/AuthCoordinator.swift | 27 +- .../Auth Flow/AuthModule/AuthProtocols.swift | 10 +- ...ginOption.swift => PlainLoginOption.swift} | 4 +- .../Interactor/AuthInteractor.swift | 8 +- .../AuthModule/Presenter/AuthPresenter.swift | 8 +- .../AuthModule/View/AuthViewController.swift | 70 ++-- .../View/Subviews/EmailLoginView.swift | 63 +-- .../View/Subviews/LoginContainerView.swift | 68 ++++ .../View/Subviews/LoginOptionsView.swift | 8 +- .../View/Subviews/PhoneNumberLoginView.swift | 28 +- .../AuthModule/Wireframe/AuthWireframe.swift | 4 +- .../CodeConfirmationProtocols.swift | 2 +- ...rType.swift => AuthConfirmationData.swift} | 4 +- .../CodeConfirmationInteractor.swift | 12 +- .../Presenter/CodeConfirmationPresenter.swift | 6 +- .../Wireframe/CodeConfirmationWireframe.swift | 2 +- .../Main/WireFrame/MainWireframe.swift | 12 +- .../ic_add.imageset/Contents.json | 11 +- .../ic_phone.imageset/Contents.json | 12 + .../ic_phone.imageset/ic_phone.pdf | Bin 0 -> 6136 bytes Nynja/Resources/en.lproj/Localizable.strings | 20 + 49 files changed, 2547 insertions(+), 279 deletions(-) create mode 100644 Nynja/Coordinators/NavigationError.swift create mode 100644 Nynja/Library/UI/Buttons/NynjaButton/NynjaSwitch.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProvider.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift create mode 100644 Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift create mode 100644 Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/Entities/LoginOption.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Forms/Form.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/LoginOptionsViewController.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift create mode 100644 Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift rename Nynja/Modules/Auth Flow/AuthModule/Entities/{LoginOption.swift => PlainLoginOption.swift} (78%) create mode 100644 Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift rename Nynja/Modules/Auth Flow/CodeConfirmation/Entities/{AuthProviderType.swift => AuthConfirmationData.swift} (78%) create mode 100644 Nynja/Resources/Assets.xcassets/ic_phone.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/ic_phone.imageset/ic_phone.pdf diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index abbd97ecd..9b0344e10 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -482,6 +482,10 @@ 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */; }; 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0281F61F53794800206871 /* UIViewExtenstions.swift */; }; 3A1146681ED6F047006BA132 /* ring.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 3A1146671ED6F047006BA132 /* ring.mp3 */; }; + 3A14D83821ABEC41009CD23A /* AuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83721ABEC41009CD23A /* AuthProvider.swift */; }; + 3A14D83B21AC03A3009CD23A /* SearchAvailabilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */; }; + 3A14D83D21AC136F009CD23A /* AuthProviderUIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */; }; + 3A14D83F21AC1F07009CD23A /* NavigationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83E21AC1F07009CD23A /* NavigationError.swift */; }; 3A19FEAD1F3B7F1D00ACE750 /* MessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A19FEAC1F3B7F1D00ACE750 /* MessageHandler.swift */; }; 3A1AAFCE1F3DF0470098780A /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1AAFCD1F3DF0470098780A /* DateExtensions.swift */; }; 3A1C87421F6101A50029B0BC /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1C87411F6101A50029B0BC /* Reachability.swift */; }; @@ -514,6 +518,12 @@ 3A8045D61F60C93D00AED866 /* MQTTServiceChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045D51F60C93D00AED866 /* MQTTServiceChat.swift */; }; 3A8045D81F60C98200AED866 /* MQTTService+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045D71F60C98200AED866 /* MQTTService+Helper.swift */; }; 3A8045DA1F60E18E00AED866 /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045D91F60E18E00AED866 /* Queue.swift */; }; + 3A80BF9721A864220016285E /* AuthProviderPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9221A864220016285E /* AuthProviderPresenter.swift */; }; + 3A80BF9821A864220016285E /* AuthProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9321A864220016285E /* AuthProviderViewController.swift */; }; + 3A80BF9921A864220016285E /* AuthProviderProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9421A864220016285E /* AuthProviderProtocols.swift */; }; + 3A80BF9A21A864220016285E /* AuthProviderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9521A864220016285E /* AuthProviderInteractor.swift */; }; + 3A80BF9B21A864220016285E /* AuthProviderWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9621A864220016285E /* AuthProviderWireframe.swift */; }; + 3A9635EB21AC4EE300ABC2C5 /* LoginContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */; }; 3AA13C761F2252F900BE5D8F /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA13C751F2252F900BE5D8F /* SearchModel.swift */; }; 3AA4E6ACDBCB060172A7A279 /* FavoritesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 462440AD41D807CE8957FDD9 /* FavoritesProtocols.swift */; }; 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */; }; @@ -924,7 +934,7 @@ 850B9DA6219C2B9500EA0CF4 /* AppConfigurationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DA5219C2B9500EA0CF4 /* AppConfigurationProvider.swift */; }; 850B9DA8219C324B00EA0CF4 /* ServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DA7219C324B00EA0CF4 /* ServerConfig.swift */; }; 850B9DAB219C6EE900EA0CF4 /* PhoneNumberInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */; }; - 850B9DAD219C7ADA00EA0CF4 /* LoginOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DAC219C7ADA00EA0CF4 /* LoginOption.swift */; }; + 850B9DAD219C7ADA00EA0CF4 /* PlainLoginOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850B9DAC219C7ADA00EA0CF4 /* PlainLoginOption.swift */; }; 850C0B2620E00C3E003341D0 /* UIScreen+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C0B2520E00C3E003341D0 /* UIScreen+Keyboard.swift */; }; 850C0B2820E01F7F003341D0 /* NotificationCenter+WheelNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C0B2720E01F7F003341D0 /* NotificationCenter+WheelNotifications.swift */; }; 850C0B5420E0369E003341D0 /* ChatListMessageCellModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */; }; @@ -959,6 +969,14 @@ 8513F06C218D053F003B901B /* BERTEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8513F06B218D053F003B901B /* BERTEncodable.swift */; }; 8513F071218D0753003B901B /* BERTEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8513F06B218D053F003B901B /* BERTEncodable.swift */; }; 8513F072218D0753003B901B /* BERTEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8513F06B218D053F003B901B /* BERTEncodable.swift */; }; + 851452A321A5865100DF10A6 /* LoginOptionsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452A221A5865100DF10A6 /* LoginOptionsCoordinator.swift */; }; + 851452AA21A586E900DF10A6 /* LoginOptionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452A521A586E900DF10A6 /* LoginOptionsPresenter.swift */; }; + 851452AB21A586E900DF10A6 /* LoginOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452A621A586E900DF10A6 /* LoginOptionsViewController.swift */; }; + 851452AC21A586E900DF10A6 /* LoginOptionsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452A721A586E900DF10A6 /* LoginOptionsProtocols.swift */; }; + 851452AD21A586EA00DF10A6 /* LoginOptionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452A821A586E900DF10A6 /* LoginOptionsInteractor.swift */; }; + 851452AE21A586EA00DF10A6 /* LoginOptionsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452A921A586E900DF10A6 /* LoginOptionsWireframe.swift */; }; + 851452B621A5A2E100DF10A6 /* ActionRowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452B521A5A2E100DF10A6 /* ActionRowItemView.swift */; }; + 851452B921A5A91E00DF10A6 /* FieldRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851452B821A5A91E00DF10A6 /* FieldRowItem.swift */; }; 8514D52220EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D52120EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift */; }; 8514D52420EE48A30002378A /* NynjaContextMenuItemsFactory+Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D52320EE48A30002378A /* NynjaContextMenuItemsFactory+Messages.swift */; }; 8514DE892136A50100718DD8 /* DBStarAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514DE882136A50100718DD8 /* DBStarAction.swift */; }; @@ -998,6 +1016,10 @@ 8520040920D4F9B4007C0036 /* MessageStickerRepliedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520040820D4F9B4007C0036 /* MessageStickerRepliedView.swift */; }; 8520040B20D4FB06007C0036 /* ReplyInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520040A20D4FB06007C0036 /* ReplyInfoView.swift */; }; 8520040D20D513B8007C0036 /* OpponentMessageStickerRepliedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520040C20D513B8007C0036 /* OpponentMessageStickerRepliedView.swift */; }; + 852037E621A5AD4A0085CF1F /* TextRowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852037E521A5AD4A0085CF1F /* TextRowItemView.swift */; }; + 852037E821A5B1E00085CF1F /* SwitchRowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852037E721A5B1E00085CF1F /* SwitchRowItemView.swift */; }; + 852037EA21A5B4230085CF1F /* TextFieldRowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852037E921A5B4230085CF1F /* TextFieldRowItemView.swift */; }; + 852037ED21A5BD380085CF1F /* LoginOptionSwitchRowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852037EC21A5BD380085CF1F /* LoginOptionSwitchRowItemView.swift */; }; 85249D322045B1F800B43007 /* WheelPositionItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85249D312045B1F800B43007 /* WheelPositionItemsFactory.swift */; }; 8524C4D22177713C003BF374 /* Member+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8524C4D12177713C003BF374 /* Member+Status.swift */; }; 8524C4D321777157003BF374 /* Member+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8524C4D12177713C003BF374 /* Member+Status.swift */; }; @@ -1024,6 +1046,8 @@ 852E847121345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852E847021345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift */; }; 852E8473213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852E8472213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift */; }; 852E8475213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852E8474213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift */; }; + 853567BB21A6B00100AAEEF9 /* Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853567BA21A6B00100AAEEF9 /* Form.swift */; }; + 853567BD21A6B76600AAEEF9 /* AnyFieldRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853567BC21A6B76600AAEEF9 /* AnyFieldRowItem.swift */; }; 853801242052C848002C6960 /* TextCheckmarkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853801232052C848002C6960 /* TextCheckmarkTableViewCell.swift */; }; 853801262052C853002C6960 /* TextCheckmarkCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853801252052C853002C6960 /* TextCheckmarkCellModel.swift */; }; 853801282052CCAD002C6960 /* SoundCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853801272052CCAD002C6960 /* SoundCellModel.swift */; }; @@ -1058,6 +1082,8 @@ 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B811218879B100A286E5 /* TypingDisplayModel.swift */; }; + 8542FBF321A6ECC100CC295B /* NynjaSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542FBF221A6ECC100CC295B /* NynjaSwitch.swift */; }; + 8542FBF821A6FFE200CC295B /* LoginOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542FBF721A6FFE200CC295B /* LoginOption.swift */; }; 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */; }; 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */; }; 85433F24204D596D00B373A7 /* WebFullScreenProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */; }; @@ -1157,7 +1183,7 @@ 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */; }; 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BD2092368600E4840C /* StickerDataSource.swift */; }; 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */; }; - 85739FBD2190AAC3001C4EC8 /* AuthProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */; }; + 85739FBD2190AAC3001C4EC8 /* AuthConfirmationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* AuthConfirmationData.swift */; }; 85788C3C204422FB003600C9 /* BuildNumberProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */; }; 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C412044237B003600C9 /* BuildNumberViewController.swift */; }; 85788C4420442385003600C9 /* BuildNumberPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C4320442385003600C9 /* BuildNumberPresenter.swift */; }; @@ -2875,6 +2901,10 @@ 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerViewController.swift; sourceTree = ""; }; 3A0281F61F53794800206871 /* UIViewExtenstions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtenstions.swift; sourceTree = ""; }; 3A1146671ED6F047006BA132 /* ring.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ring.mp3; sourceTree = ""; }; + 3A14D83721ABEC41009CD23A /* AuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProvider.swift; sourceTree = ""; }; + 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAvailabilityView.swift; sourceTree = ""; }; + 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderUIConfiguration.swift; sourceTree = ""; }; + 3A14D83E21AC1F07009CD23A /* NavigationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationError.swift; sourceTree = ""; }; 3A19FEAC1F3B7F1D00ACE750 /* MessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageHandler.swift; path = Services/HandleServices/MessageHandler.swift; sourceTree = ""; }; 3A1AAFCD1F3DF0470098780A /* DateExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; 3A1C87411F6101A50029B0BC /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Reachability.swift; path = Library/Reachability.swift; sourceTree = ""; }; @@ -2908,7 +2938,13 @@ 3A8045D51F60C93D00AED866 /* MQTTServiceChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTServiceChat.swift; sourceTree = ""; }; 3A8045D71F60C98200AED866 /* MQTTService+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MQTTService+Helper.swift"; sourceTree = ""; }; 3A8045D91F60E18E00AED866 /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Queue.swift; path = Library/Queue.swift; sourceTree = ""; }; + 3A80BF9221A864220016285E /* AuthProviderPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderPresenter.swift; sourceTree = ""; }; + 3A80BF9321A864220016285E /* AuthProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderViewController.swift; sourceTree = ""; }; + 3A80BF9421A864220016285E /* AuthProviderProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderProtocols.swift; sourceTree = ""; }; + 3A80BF9521A864220016285E /* AuthProviderInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderInteractor.swift; sourceTree = ""; }; + 3A80BF9621A864220016285E /* AuthProviderWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderWireframe.swift; sourceTree = ""; }; 3A8C12DDFD0D831F21959665 /* Pods-Nynja-Share.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.devautotests.xcconfig"; sourceTree = ""; }; + 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContainerView.swift; sourceTree = ""; }; 3AA13C751F2252F900BE5D8F /* SearchModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; 3ABCE8ED1EC9330D00A80B15 /* Nynja.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nynja.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -3110,7 +3146,6 @@ 4BF2C3FB218AFE9D00E59F6C /* FullNameRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullNameRepresentable.swift; sourceTree = ""; }; 4CDA2BE900351F21464CE687 /* DateTimePickerInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerInteractor.swift; sourceTree = ""; }; 4D247CBC45C1C1267BBBB289 /* QRCodeReaderInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeReaderInteractor.swift; sourceTree = ""; }; - 4F7C039B61A0663D43BE5AE5 /* SelectCountryProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryProtocols.swift; sourceTree = ""; }; 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactViaPhoneViewController.swift; sourceTree = ""; }; 505C687860C446A37E2FE4FF /* AddContactByUsernamePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactByUsernamePresenter.swift; sourceTree = ""; }; 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScheduleMessageInteractor.swift; sourceTree = ""; }; @@ -3119,7 +3154,6 @@ 5245254E61C6EB3C6ACF4D2C /* ScheduleMessageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScheduleMessageViewController.swift; sourceTree = ""; }; 53C4A4171CFEB4A5B1E55109 /* Pods-NynjaUnitTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.dev.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.dev.xcconfig"; sourceTree = ""; }; 549BC324D11F1DDF87DAAEB7 /* AddContactByUsernameViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactByUsernameViewController.swift; sourceTree = ""; }; - 5522F1F73FC8C564BF0254BF /* SelectCountryPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryPresenter.swift; sourceTree = ""; }; 553988DBF434D09AB77837DF /* SplashViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; 55EC130CCF07D992BC6DD435 /* MapSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapSearchPresenter.swift; sourceTree = ""; }; 5957BF589EEC24E6799EB4CF /* TimeZoneSelectorViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimeZoneSelectorViewController.swift; sourceTree = ""; }; @@ -3229,7 +3263,6 @@ 7ADCB0C891B31AF691307B4F /* Pods-Nynja.spotify.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.spotify.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.spotify.xcconfig"; sourceTree = ""; }; 7C19AFE8E64821851F4112EE /* ProfileProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileProtocols.swift; sourceTree = ""; }; 7C2CBB5F32D209160D00F744 /* CreateGroupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupViewController.swift; sourceTree = ""; }; - 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryViewController.swift; sourceTree = ""; }; 7F0193413D570F0548B2E55F /* VideoPreviewPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VideoPreviewPresenter.swift; sourceTree = ""; }; 7F5541C91FE7845F3E5C7EB2 /* VideoPreviewWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VideoPreviewWireframe.swift; sourceTree = ""; }; 7F7FC209C7703E3E7617D782 /* Pods-Nynja-Share.spotify.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.spotify.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.spotify.xcconfig"; sourceTree = ""; }; @@ -3291,7 +3324,7 @@ 850B9DA5219C2B9500EA0CF4 /* AppConfigurationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationProvider.swift; sourceTree = ""; }; 850B9DA7219C324B00EA0CF4 /* ServerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfig.swift; sourceTree = ""; }; 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberInfo.swift; sourceTree = ""; }; - 850B9DAC219C7ADA00EA0CF4 /* LoginOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOption.swift; sourceTree = ""; }; + 850B9DAC219C7ADA00EA0CF4 /* PlainLoginOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainLoginOption.swift; sourceTree = ""; }; 850C0B2520E00C3E003341D0 /* UIScreen+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScreen+Keyboard.swift"; sourceTree = ""; }; 850C0B2720E01F7F003341D0 /* NotificationCenter+WheelNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+WheelNotifications.swift"; sourceTree = ""; }; 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageCellModelDelegate.swift; sourceTree = ""; }; @@ -3324,6 +3357,14 @@ 8511D3732034596E00B2A620 /* Collection+ViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+ViewLayout.swift"; sourceTree = ""; }; 8512349121221B9E000129A2 /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; 8513F06B218D053F003B901B /* BERTEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BERTEncodable.swift; sourceTree = ""; }; + 851452A221A5865100DF10A6 /* LoginOptionsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsCoordinator.swift; sourceTree = ""; }; + 851452A521A586E900DF10A6 /* LoginOptionsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsPresenter.swift; sourceTree = ""; }; + 851452A621A586E900DF10A6 /* LoginOptionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsViewController.swift; sourceTree = ""; }; + 851452A721A586E900DF10A6 /* LoginOptionsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsProtocols.swift; sourceTree = ""; }; + 851452A821A586E900DF10A6 /* LoginOptionsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsInteractor.swift; sourceTree = ""; }; + 851452A921A586E900DF10A6 /* LoginOptionsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionsWireframe.swift; sourceTree = ""; }; + 851452B521A5A2E100DF10A6 /* ActionRowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRowItemView.swift; sourceTree = ""; }; + 851452B821A5A91E00DF10A6 /* FieldRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRowItem.swift; sourceTree = ""; }; 8514D52120EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NynjaContextMenuItemsFactory+Design.swift"; sourceTree = ""; }; 8514D52320EE48A30002378A /* NynjaContextMenuItemsFactory+Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NynjaContextMenuItemsFactory+Messages.swift"; sourceTree = ""; }; 8514DE882136A50100718DD8 /* DBStarAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBStarAction.swift; sourceTree = ""; }; @@ -3359,6 +3400,10 @@ 8520040820D4F9B4007C0036 /* MessageStickerRepliedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStickerRepliedView.swift; sourceTree = ""; }; 8520040A20D4FB06007C0036 /* ReplyInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyInfoView.swift; sourceTree = ""; }; 8520040C20D513B8007C0036 /* OpponentMessageStickerRepliedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpponentMessageStickerRepliedView.swift; sourceTree = ""; }; + 852037E521A5AD4A0085CF1F /* TextRowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRowItemView.swift; sourceTree = ""; }; + 852037E721A5B1E00085CF1F /* SwitchRowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchRowItemView.swift; sourceTree = ""; }; + 852037E921A5B4230085CF1F /* TextFieldRowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldRowItemView.swift; sourceTree = ""; }; + 852037EC21A5BD380085CF1F /* LoginOptionSwitchRowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOptionSwitchRowItemView.swift; sourceTree = ""; }; 85249D312045B1F800B43007 /* WheelPositionItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelPositionItemsFactory.swift; sourceTree = ""; }; 8524C4D12177713C003BF374 /* Member+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Member+Status.swift"; sourceTree = ""; }; 8524C4D5217772C8003BF374 /* Member+Construct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Member+Construct.swift"; sourceTree = ""; }; @@ -3381,6 +3426,8 @@ 852E847021345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewLayoutAttributes.swift; sourceTree = ""; }; 852E8472213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversedMessageCollectionViewLayoutAttributes.swift; sourceTree = ""; }; 852E8474213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversedMessageCollectionViewLayout.swift; sourceTree = ""; }; + 853567BA21A6B00100AAEEF9 /* Form.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Form.swift; sourceTree = ""; }; + 853567BC21A6B76600AAEEF9 /* AnyFieldRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyFieldRowItem.swift; sourceTree = ""; }; 853801232052C848002C6960 /* TextCheckmarkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCheckmarkTableViewCell.swift; sourceTree = ""; }; 853801252052C853002C6960 /* TextCheckmarkCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCheckmarkCellModel.swift; sourceTree = ""; }; 853801272052CCAD002C6960 /* SoundCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundCellModel.swift; sourceTree = ""; }; @@ -3412,13 +3459,14 @@ 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; 8542B811218879B100A286E5 /* TypingDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingDisplayModel.swift; sourceTree = ""; }; + 8542FBF221A6ECC100CC295B /* NynjaSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaSwitch.swift; sourceTree = ""; }; + 8542FBF721A6FFE200CC295B /* LoginOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOption.swift; sourceTree = ""; }; 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenPresenter.swift; sourceTree = ""; }; 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenViewController.swift; sourceTree = ""; }; 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenProtocols.swift; sourceTree = ""; }; 85433F20204D596D00B373A7 /* WebFullScreenInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenInteractor.swift; sourceTree = ""; }; 85433F21204D596D00B373A7 /* WebFullScreenWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenWireFrame.swift; sourceTree = ""; }; 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaCloseButton.swift; sourceTree = ""; }; - 854574C921931976001D43CF /* CountriesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountriesSection.swift; sourceTree = ""; }; 854574CB21933190001D43CF /* NavigableWireframeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableWireframeProtocol.swift; sourceTree = ""; }; 85458CD8212D6FED00BA8814 /* String+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = ""; }; 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIdentifiers.swift; sourceTree = ""; }; @@ -3487,9 +3535,7 @@ 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionDataSource.swift; sourceTree = ""; }; 8572C3BD2092368600E4840C /* StickerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDataSource.swift; sourceTree = ""; }; 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileField.swift; sourceTree = ""; }; - 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderType.swift; sourceTree = ""; }; - 8575E5322191A9E70080DD4A /* CountryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryTableViewCell.swift; sourceTree = ""; }; - 8575E5332191A9E70080DD4A /* CountryCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryCellModel.swift; sourceTree = ""; }; + 85739FBC2190AAC3001C4EC8 /* AuthConfirmationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationData.swift; sourceTree = ""; }; 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberProtocols.swift; sourceTree = ""; }; 85788C412044237B003600C9 /* BuildNumberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberViewController.swift; sourceTree = ""; }; 85788C4320442385003600C9 /* BuildNumberPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberPresenter.swift; sourceTree = ""; }; @@ -3854,7 +3900,6 @@ A43B258320AB1DFA00FF8107 /* TextFieldWithPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldWithPicker.swift; sourceTree = ""; }; A43B258420AB1DFA00FF8107 /* EditFieldLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditFieldLayout.swift; sourceTree = ""; }; A43B258520AB1DFA00FF8107 /* EditField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditField.swift; sourceTree = ""; }; - A43B258620AB1DFA00FF8107 /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; A43B258820AB1DFA00FF8107 /* DrawableAudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawableAudioWaveform.swift; sourceTree = ""; }; A43B258920AB1DFA00FF8107 /* RecordingAudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingAudioWaveform.swift; sourceTree = ""; }; A43B258B20AB1DFA00FF8107 /* ALKeyboardObservingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ALKeyboardObservingView.swift; sourceTree = ""; }; @@ -4082,7 +4127,6 @@ AFA5ECB1F18C92E648BC93B0 /* FavoritesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = ""; }; AFC76E2B3DD0BCA0A622A5CD /* CreateGroupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupInteractor.swift; sourceTree = ""; }; B051231EAD6BB435200B4C74 /* LoginPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoginPresenter.swift; sourceTree = ""; }; - B05863F1D1FC27487D496750 /* SelectCountryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryWireframe.swift; sourceTree = ""; }; B0E0429CA4EF8A228D791BED /* HistoryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryWireframe.swift; sourceTree = ""; }; B15F3B55EC2BF6FB5D7A2FAF /* LoginInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoginInteractor.swift; sourceTree = ""; }; B28416F302A40E1E56041080 /* TimeZoneSelectorProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimeZoneSelectorProtocols.swift; sourceTree = ""; }; @@ -4161,7 +4205,6 @@ C22259D46BE5732B494C4C7D /* SplashWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SplashWireframe.swift; sourceTree = ""; }; C3E427A83589B2A635F99BC0 /* EditGroupPhotoProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupPhotoProtocols.swift; sourceTree = ""; }; C45F64793E8126ABF4E69F7B /* QRCodeGeneratorWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeGeneratorWireframe.swift; sourceTree = ""; }; - C68A1D12FEF0CE24D6B3F6F5 /* SelectCountryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryInteractor.swift; sourceTree = ""; }; C8263B034B7C1F206A1C1A6C /* HistoryProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryProtocols.swift; sourceTree = ""; }; C90E6A9A20558C0300D733E0 /* FileSizeFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSizeFormatter.swift; sourceTree = ""; }; C921738120BADAFC00519A2D /* TextInputValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputValidationService.swift; sourceTree = ""; }; @@ -4181,7 +4224,6 @@ C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDownloadAndUsageMode.swift; sourceTree = ""; }; C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideAnimatedTransitioning.swift; sourceTree = ""; }; C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeBackHelper.swift; sourceTree = ""; }; - C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryHeaderView.swift; sourceTree = ""; }; CA78C91DFDF5884E382D38FA /* EditGroupNamePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupNamePresenter.swift; sourceTree = ""; }; CA8C095C6950E26F3BC48A1C /* EditPhotoInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditPhotoInteractor.swift; sourceTree = ""; }; CB70AD73977CD00AD11C287C /* Pods-NynjaUnitTests.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.devautotests.xcconfig"; sourceTree = ""; }; @@ -4794,14 +4836,6 @@ path = Interactor; sourceTree = ""; }; - 0AD119947B4A6FA309A1060E /* Interactor */ = { - isa = PBXGroup; - children = ( - C68A1D12FEF0CE24D6B3F6F5 /* SelectCountryInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; 0CAFBBC1CE7BB9EBD7BDAABB /* WireFrame */ = { isa = PBXGroup; children = ( @@ -4834,19 +4868,6 @@ path = View; sourceTree = ""; }; - 115A968821FB24FA3C58A6D5 /* SelectCountry */ = { - isa = PBXGroup; - children = ( - 4F7C039B61A0663D43BE5AE5 /* SelectCountryProtocols.swift */, - 43D5323E27F49A5C95BBB6D6 /* View */, - 337A8E299DCF438AD28A7043 /* Presenter */, - 0AD119947B4A6FA309A1060E /* Interactor */, - CADE0A8BB5BE972F51CE1E2F /* WireFrame */, - 854574C821931945001D43CF /* Entities */, - ); - path = SelectCountry; - sourceTree = ""; - }; 12396B05D93D1CA3A8410766 /* Interactor */ = { isa = PBXGroup; children = ( @@ -6087,14 +6108,6 @@ name = WireFrame; sourceTree = ""; }; - 337A8E299DCF438AD28A7043 /* Presenter */ = { - isa = PBXGroup; - children = ( - 5522F1F73FC8C564BF0254BF /* SelectCountryPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; 351B7F3065DD333AFEA34D24 /* View */ = { isa = PBXGroup; children = ( @@ -6165,6 +6178,23 @@ path = Model; sourceTree = ""; }; + 3A14D83621ABEC25009CD23A /* Entities */ = { + isa = PBXGroup; + children = ( + 3A14D83721ABEC41009CD23A /* AuthProvider.swift */, + 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 3A14D83921AC037B009CD23A /* Subviews */ = { + isa = PBXGroup; + children = ( + 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; 3A1DC7371EF151B6006A8E9F /* Handlers */ = { isa = PBXGroup; children = ( @@ -6312,6 +6342,52 @@ path = Services/MQTT; sourceTree = ""; }; + 3A80BF9121A8637F0016285E /* AuthProvider */ = { + isa = PBXGroup; + children = ( + 3A80BF9421A864220016285E /* AuthProviderProtocols.swift */, + 3A80BF9D21A8642E0016285E /* View */, + 3A80BF9C21A864280016285E /* Presenter */, + 3A80BF9F21A864430016285E /* Interactor */, + 3A80BFA021A864490016285E /* Wireframe */, + 3A14D83621ABEC25009CD23A /* Entities */, + ); + path = AuthProvider; + sourceTree = ""; + }; + 3A80BF9C21A864280016285E /* Presenter */ = { + isa = PBXGroup; + children = ( + 3A80BF9221A864220016285E /* AuthProviderPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 3A80BF9D21A8642E0016285E /* View */ = { + isa = PBXGroup; + children = ( + 3A14D83921AC037B009CD23A /* Subviews */, + 3A80BF9321A864220016285E /* AuthProviderViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 3A80BF9F21A864430016285E /* Interactor */ = { + isa = PBXGroup; + children = ( + 3A80BF9521A864220016285E /* AuthProviderInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 3A80BFA021A864490016285E /* Wireframe */ = { + isa = PBXGroup; + children = ( + 3A80BF9621A864220016285E /* AuthProviderWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; 3A82187C1EDEEDF400337B05 /* UI */ = { isa = PBXGroup; children = ( @@ -6579,15 +6655,6 @@ path = WireFrame; sourceTree = ""; }; - 43D5323E27F49A5C95BBB6D6 /* View */ = { - isa = PBXGroup; - children = ( - 854574CD21933D47001D43CF /* ViewController */, - C9C695062022318500A57297 /* TableView */, - ); - path = View; - sourceTree = ""; - }; 4645720B5E0E5A8B5B0B0F39 /* WireFrame */ = { isa = PBXGroup; children = ( @@ -6661,7 +6728,6 @@ 14929D916183E29FEAFA6221 /* QRCodeReader */, 264638181FFFC537002590E6 /* Replies */, E61C394BD0E94E3DCF853D4F /* ScheduleMessage */, - 115A968821FB24FA3C58A6D5 /* SelectCountry */, 859B86352048224B003272B2 /* Settings */, 267BE27D1FDE900900C47E18 /* SettingsGroup */, 4FBB666690A18EEA5438EAB7 /* Splash */, @@ -7802,6 +7868,8 @@ children = ( 5EDD454721885EC400C50BC8 /* Coordinator */, 5EDD454821885EC400C50BC8 /* AccountSettings */, + 851452A421A5865C00DF10A6 /* LoginOptions */, + 3A80BF9121A8637F0016285E /* AuthProvider */, ); path = "Account Flow"; sourceTree = ""; @@ -7810,6 +7878,7 @@ isa = PBXGroup; children = ( 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */, + 851452A221A5865100DF10A6 /* LoginOptionsCoordinator.swift */, ); path = Coordinator; sourceTree = ""; @@ -7817,12 +7886,12 @@ 5EDD454821885EC400C50BC8 /* AccountSettings */ = { isa = PBXGroup; children = ( + 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */, 5EDD454921885EC400C50BC8 /* Presenter */, 5EDD454A21885EC400C50BC8 /* Wireframe */, 5EDD454B21885EC400C50BC8 /* View */, 5EDD454C21885EC400C50BC8 /* Interactor */, 5EDD454D21885EC400C50BC8 /* Entities */, - 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */, ); path = AccountSettings; sourceTree = ""; @@ -7939,7 +8008,7 @@ 5EEB73B0216046EA00D8ECE6 /* Entities */ = { isa = PBXGroup; children = ( - 85739FBC2190AAC3001C4EC8 /* AuthProviderType.swift */, + 85739FBC2190AAC3001C4EC8 /* AuthConfirmationData.swift */, ); path = Entities; sourceTree = ""; @@ -8003,7 +8072,7 @@ isa = PBXGroup; children = ( 5EEB73D12161CEA100D8ECE6 /* LoginFlow.swift */, - 850B9DAC219C7ADA00EA0CF4 /* LoginOption.swift */, + 850B9DAC219C7ADA00EA0CF4 /* PlainLoginOption.swift */, 851FFA67219EAFBF0015F073 /* Validator.swift */, 851FFA65219EAF980015F073 /* EmailTextController.swift */, 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */, @@ -8017,6 +8086,7 @@ children = ( 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */, 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */, + 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */, 5EEB73D52161DBF100D8ECE6 /* EmailLoginView.swift */, 5EEB73D72162227B00D8ECE6 /* PhoneNumberLoginView.swift */, ); @@ -8896,6 +8966,61 @@ path = Collection; sourceTree = ""; }; + 851452A421A5865C00DF10A6 /* LoginOptions */ = { + isa = PBXGroup; + children = ( + 851452A721A586E900DF10A6 /* LoginOptionsProtocols.swift */, + 851452AF21A586F100DF10A6 /* View */, + 851452B021A586F700DF10A6 /* Presenter */, + 851452B121A5870400DF10A6 /* Interactor */, + 851452B221A5870B00DF10A6 /* Wireframe */, + 8542FBF621A6F0D100CC295B /* Entities */, + ); + path = LoginOptions; + sourceTree = ""; + }; + 851452AF21A586F100DF10A6 /* View */ = { + isa = PBXGroup; + children = ( + 852037EB21A5BD100085CF1F /* Forms */, + 851452B721A5A90400DF10A6 /* Subviews */, + 851452A621A586E900DF10A6 /* LoginOptionsViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 851452B021A586F700DF10A6 /* Presenter */ = { + isa = PBXGroup; + children = ( + 851452A521A586E900DF10A6 /* LoginOptionsPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 851452B121A5870400DF10A6 /* Interactor */ = { + isa = PBXGroup; + children = ( + 851452A821A586E900DF10A6 /* LoginOptionsInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 851452B221A5870B00DF10A6 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 851452A921A586E900DF10A6 /* LoginOptionsWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 851452B721A5A90400DF10A6 /* Subviews */ = { + isa = PBXGroup; + children = ( + 852037EC21A5BD380085CF1F /* LoginOptionSwitchRowItemView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; 8514D52020EE48750002378A /* ContextMenu */ = { isa = PBXGroup; children = ( @@ -8998,6 +9123,16 @@ path = Reply; sourceTree = ""; }; + 852037EB21A5BD100085CF1F /* Forms */ = { + isa = PBXGroup; + children = ( + 853567BA21A6B00100AAEEF9 /* Form.swift */, + 8542FBF421A6EDD400CC295B /* FieldRowItem */, + 8542FBF521A6EDE800CC295B /* Items */, + ); + path = Forms; + sourceTree = ""; + }; 8524C4D42177715E003BF374 /* Member */ = { isa = PBXGroup; children = ( @@ -9305,6 +9440,34 @@ path = Placeholders; sourceTree = ""; }; + 8542FBF421A6EDD400CC295B /* FieldRowItem */ = { + isa = PBXGroup; + children = ( + 851452B821A5A91E00DF10A6 /* FieldRowItem.swift */, + 853567BC21A6B76600AAEEF9 /* AnyFieldRowItem.swift */, + ); + path = FieldRowItem; + sourceTree = ""; + }; + 8542FBF521A6EDE800CC295B /* Items */ = { + isa = PBXGroup; + children = ( + 851452B521A5A2E100DF10A6 /* ActionRowItemView.swift */, + 852037E521A5AD4A0085CF1F /* TextRowItemView.swift */, + 852037E721A5B1E00085CF1F /* SwitchRowItemView.swift */, + 852037E921A5B4230085CF1F /* TextFieldRowItemView.swift */, + ); + path = Items; + sourceTree = ""; + }; + 8542FBF621A6F0D100CC295B /* Entities */ = { + isa = PBXGroup; + children = ( + 8542FBF721A6FFE200CC295B /* LoginOption.swift */, + ); + path = Entities; + sourceTree = ""; + }; 85433F1C204D593100B373A7 /* WebFullScreen */ = { isa = PBXGroup; children = ( @@ -9349,23 +9512,6 @@ path = WireFrame; sourceTree = ""; }; - 854574C821931945001D43CF /* Entities */ = { - isa = PBXGroup; - children = ( - A43B258620AB1DFA00FF8107 /* Country.swift */, - 854574C921931976001D43CF /* CountriesSection.swift */, - ); - path = Entities; - sourceTree = ""; - }; - 854574CD21933D47001D43CF /* ViewController */ = { - isa = PBXGroup; - children = ( - 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */, - ); - path = ViewController; - sourceTree = ""; - }; 85482841204E912600DCBEC8 /* ViewController */ = { isa = PBXGroup; children = ( @@ -9648,23 +9794,6 @@ path = Entities; sourceTree = ""; }; - 8575E5362191B22E0080DD4A /* Cell */ = { - isa = PBXGroup; - children = ( - 8575E5322191A9E70080DD4A /* CountryTableViewCell.swift */, - 8575E5332191A9E70080DD4A /* CountryCellModel.swift */, - ); - path = Cell; - sourceTree = ""; - }; - 8575E5372191B5F90080DD4A /* Header */ = { - isa = PBXGroup; - children = ( - C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */, - ); - path = Header; - sourceTree = ""; - }; 85788C3A20442263003600C9 /* BuildNumber */ = { isa = PBXGroup; children = ( @@ -10624,6 +10753,7 @@ 8503B51820503683006F0593 /* NynjaCellButton.swift */, 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */, 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */, + 8542FBF221A6ECC100CC295B /* NynjaSwitch.swift */, ); path = NynjaButton; sourceTree = ""; @@ -12604,15 +12734,6 @@ path = SwipeBackHelper; sourceTree = ""; }; - C9C695062022318500A57297 /* TableView */ = { - isa = PBXGroup; - children = ( - 8575E5372191B5F90080DD4A /* Header */, - 8575E5362191B22E0080DD4A /* Cell */, - ); - path = TableView; - sourceTree = ""; - }; CAB5F1F6E675E93CA821CC51 /* Presenter */ = { isa = PBXGroup; children = ( @@ -12637,14 +12758,6 @@ path = WireFrame; sourceTree = ""; }; - CADE0A8BB5BE972F51CE1E2F /* WireFrame */ = { - isa = PBXGroup; - children = ( - B05863F1D1FC27487D496750 /* SelectCountryWireframe.swift */, - ); - path = WireFrame; - sourceTree = ""; - }; CE9D96E59FD1D607EFA72FCE /* View */ = { isa = PBXGroup; children = ( @@ -13777,6 +13890,7 @@ isa = PBXGroup; children = ( F11786BA20A8A63F007A9A1B /* Coordinator.swift */, + 3A14D83E21AC1F07009CD23A /* NavigationError.swift */, ); path = Coordinators; sourceTree = ""; @@ -15900,6 +16014,7 @@ 8586CACB203338F6009F2A75 /* ForwardTarget.swift in Sources */, C90E6A9B20558C0300D733E0 /* FileSizeFormatter.swift in Sources */, 6D6234F61F1E150600EF375F /* HistoryTableDS.swift in Sources */, + 8542FBF321A6ECC100CC295B /* NynjaSwitch.swift in Sources */, FBE3885D2118849000149721 /* AlertActionWrapper.swift in Sources */, F119E66E20D24BBF0043A532 /* MultiplePreviewWireframe.swift in Sources */, 2611CEF72182090900FFD4DD /* LogWriterProtocol.swift in Sources */, @@ -15981,7 +16096,7 @@ 3A27B0A71EF307A900B4B3CB /* DeleteUserModel.swift in Sources */, 3A1F74FA1F5ED344009A11E4 /* PushService.swift in Sources */, FEA656042167777F00B44029 /* WalletBalancesInteractor.swift in Sources */, - 85739FBD2190AAC3001C4EC8 /* AuthProviderType.swift in Sources */, + 85739FBD2190AAC3001C4EC8 /* AuthConfirmationData.swift in Sources */, 261F2E2E200EB0AD007D0813 /* RepliesVC+CellDelegate.swift in Sources */, 4B0CC1FD2195B52000E0BA61 /* IoHandlerDelegate.swift in Sources */, A45F110620B4218D00F45004 /* MessageConfiguration.swift in Sources */, @@ -15991,6 +16106,7 @@ F1AC0DE3207252E1001C68F7 /* Testable.swift in Sources */, A408A0BD20C174040029F54B /* ChannelsListInteractor.swift in Sources */, A458FABD20EB8B320075D55E /* MessageChannelActionsProtocol.swift in Sources */, + 3A9635EB21AC4EE300ABC2C5 /* LoginContainerView.swift in Sources */, 4BE2C5D92142EAC500A73DD9 /* AudioPlayer.swift in Sources */, F11786EE20AC39E9007A9A1B /* Job_Spec.swift in Sources */, 0008E92420347A8E003E316E /* DBJobMessage.swift in Sources */, @@ -16088,6 +16204,7 @@ 2657BE532012405600F21935 /* ImageActionItemModel.swift in Sources */, E74EC9F21FC2E30F007268E6 /* RoomMemberTable.swift in Sources */, 26A0CFE12005138C006F6617 /* MemberExtension+BERT.swift in Sources */, + 3A80BF9721A864220016285E /* AuthProviderPresenter.swift in Sources */, FBD885792147F9640099B8C3 /* LocalizableConstants.swift in Sources */, 2652D6181FA85B28005E62C7 /* ImageSelector.swift in Sources */, 26DE8D9120FE1AF500C41096 /* ChatCellFooterView.swift in Sources */, @@ -16208,10 +16325,12 @@ B763DD9320AA1C3400A30B63 /* ContactCellLayout.swift in Sources */, A45F110820B4218D00F45004 /* ChatCheckpoint.swift in Sources */, 26245F3E204EF58E00C8D3DD /* BasePresenterProtocol.swift in Sources */, + 852037EA21A5B4230085CF1F /* TextFieldRowItemView.swift in Sources */, 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */, 5BC1D37B20D3B4A8002A44B3 /* GroupAddParticipantsCollectionViewCell.swift in Sources */, 269D9DF01FC3AF0D00324263 /* CGSizeExtension.swift in Sources */, 4BB0EFBD2151347900704136 /* AlertManager.swift in Sources */, + 851452B621A5A2E100DF10A6 /* ActionRowItemView.swift in Sources */, D30EB73829E48C0B1C1FD1C9 /* LoginViewController.swift in Sources */, FEA65471216775CD00B44029 /* ServiceWalletExtension.swift in Sources */, FEA655E82167777E00B44029 /* TransferHistoryProtocols.swift in Sources */, @@ -16247,6 +16366,7 @@ E791178A1F97874D00462D68 /* GradientView.swift in Sources */, FEA655EE2167777E00B44029 /* TransferHistoryCell.swift in Sources */, E79117901F97A3BC00462D68 /* ProfileDetailsView.swift in Sources */, + 851452AC21A586E900DF10A6 /* LoginOptionsProtocols.swift in Sources */, 265F5D24209B6987008ACCC8 /* Place.swift in Sources */, E734831A1F9F39400090A4DB /* CellModel.swift in Sources */, B74BB00321076AFA0049CD27 /* CircleMenuItem.swift in Sources */, @@ -16384,6 +16504,7 @@ 26588E6720A20E49000D3E1A /* Customizable.swift in Sources */, 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */, F11786DE20A9ED65007A9A1B /* DownloadOperation.swift in Sources */, + 851452AA21A586E900DF10A6 /* LoginOptionsPresenter.swift in Sources */, 852E8473213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift in Sources */, 005886CD2030F41700FE2E89 /* NynjaTimeAmPmDelegate.swift in Sources */, 8514F17820EA219F00883513 /* ContextMenu.swift in Sources */, @@ -16443,6 +16564,7 @@ 8E6C4BE61FF83D93009C8374 /* IntExtensions.swift in Sources */, E785EF2A1FB9D99400F0C689 /* PinView.swift in Sources */, A42D51D0206A361400EEB952 /* Star.swift in Sources */, + 3A14D83D21AC136F009CD23A /* AuthProviderUIConfiguration.swift in Sources */, 85C16C3E20D2794500EDB77E /* BubbleImageSizeCalculatable.swift in Sources */, A43B25D620AB1EE400FF8107 /* NewChannelPresenter.swift in Sources */, E7598F691FA1D8B90082FBE7 /* ProfileScheduledMesssageCellLayout.swift in Sources */, @@ -16503,6 +16625,7 @@ 2683F751203F34470003181A /* ChatBaseFactory.swift in Sources */, F117871620ACF018007A9A1B /* SwitchedTableCell.swift in Sources */, 85E1DD2520BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift in Sources */, + 852037E621A5AD4A0085CF1F /* TextRowItemView.swift in Sources */, 266AD07A20F51AAE00EA275F /* TranscriptionInfo.swift in Sources */, 8E9601951FF2A04E00E0C21D /* ItemSelectorCell.swift in Sources */, FEA655F72167777E00B44029 /* PaymentViewControllerModel.swift in Sources */, @@ -16700,6 +16823,7 @@ 26342CA020ECAA0700D2196B /* TranscribeNetworkRouter.swift in Sources */, A42D51C1206A361400EEB952 /* boundaryEvent.swift in Sources */, 8ED0F3D01FBC5CF2004916AB /* GroupsListTableDS.swift in Sources */, + 8542FBF821A6FFE200CC295B /* LoginOption.swift in Sources */, 260225DD20F379EF004FC238 /* MessageConvertionView.swift in Sources */, A45F111F20B4218D00F45004 /* InfoView.swift in Sources */, E76462891FCD64790091FC2E /* DBModel.swift in Sources */, @@ -16747,6 +16871,7 @@ A45F111620B4218D00F45004 /* BaseChatCellModel.swift in Sources */, 4B06D31C2028A6A1003B275B /* ChatItemsFactory.swift in Sources */, 4B6F860521A41C8E00727A90 /* ParticipantsInput.swift in Sources */, + 851452A321A5865100DF10A6 /* LoginOptionsCoordinator.swift in Sources */, 267D465920AB4C1500D42242 /* Feature+DB.swift in Sources */, 9E9DD4C7F700872D7CCEE227 /* ProfileInteractor.swift in Sources */, F10B0E2C20B51CCF00528E7A /* CounterIndicatorButton.swift in Sources */, @@ -16870,6 +16995,7 @@ A4330A6E2109EBA70060BD93 /* CountriesProvider.swift in Sources */, 26142B1320473BFD004E5FE4 /* DBMessageLink.swift in Sources */, 68B66BDEEFD73CDC331AC840 /* EditProfilePresenter.swift in Sources */, + 853567BD21A6B76600AAEEF9 /* AnyFieldRowItem.swift in Sources */, 4B06D3082028A200003B275B /* WCItemsFactory.swift in Sources */, E7AE41681FCC596300C3ED5D /* DBRoomMember.swift in Sources */, 2648C4102069B52100863614 /* ChangeNumberStep3Wireframe.swift in Sources */, @@ -16931,7 +17057,7 @@ 8580BAF020BD9AAE00239D9D /* ConstraintMaker+Extensions.swift in Sources */, 850A2BB0203584B000D68FDF /* SearchActionsView.swift in Sources */, 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */, - 850B9DAD219C7ADA00EA0CF4 /* LoginOption.swift in Sources */, + 850B9DAD219C7ADA00EA0CF4 /* PlainLoginOption.swift in Sources */, A4679B8920B2DA550021FE9C /* Array+ChannelSubscriber.swift in Sources */, A4ED79AC20C7056C00A41F67 /* AllChannelsItemsFactory.swift in Sources */, 5E7D5D4C218C6239009B5D8D /* SettingsSetAvatarTVCell.swift in Sources */, @@ -16988,6 +17114,7 @@ 3A237BC91F30AB0F00C42B6E /* EditProfileVC.swift in Sources */, A406E39A210B457300435B3E /* DictionaryExtension.swift in Sources */, 2661D1331F373D5900F3E125 /* WheelConfiguration.swift in Sources */, + 852037ED21A5BD380085CF1F /* LoginOptionSwitchRowItemView.swift in Sources */, 4B749F05214FEE4F002F3A33 /* VerifyNumberPresenter.swift in Sources */, 263C04EB2132E56E00B8F0BE /* TranscribeOperation.swift in Sources */, A42CE5AD20692EDB000889CC /* StringAtom.swift in Sources */, @@ -17069,6 +17196,7 @@ A411D95A20AC39C6009D107C /* ConversationsProviding.swift in Sources */, 267BE90A20693F4800153FB8 /* ProfileDAOProtocol.swift in Sources */, 4BE2C5DC2142EAC500A73DD9 /* SoundBundle.swift in Sources */, + 851452AE21A586EA00DF10A6 /* LoginOptionsWireframe.swift in Sources */, 4B8996CD204ED33400DCB183 /* StarDAOProtocol.swift in Sources */, 85D669EC20BD962800FBD803 /* MessageVCLayout.swift in Sources */, 8EDDB08A200529C6000B7EC2 /* GroupStorageCollectionVC.swift in Sources */, @@ -17105,6 +17233,7 @@ A42D5207206A4C6F00EEB952 /* UIView+Gradient.swift in Sources */, 188212D3733DB059F2EF5639 /* MainPresenter.swift in Sources */, 8557989D209368E7007050B8 /* StickerPackHeaderModel.swift in Sources */, + 853567BB21A6B00100AAEEF9 /* Form.swift in Sources */, A42D52C1206A53AA00EEB952 /* p2p_Spec.swift in Sources */, 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */, A45F113A20B4218D00F45004 /* AvatarView.swift in Sources */, @@ -17172,6 +17301,7 @@ 8502DB542061030100613C8C /* WheelPositionPickerViewController.swift in Sources */, 4B752B612163A5C900E852B9 /* DescExtension.swift in Sources */, A45F116120B422AF00F45004 /* Message+System.swift in Sources */, + 851452AD21A586EA00DF10A6 /* LoginOptionsInteractor.swift in Sources */, 4B06D30C2028A25D003B275B /* HomeItemsFactory.swift in Sources */, 266AE8C3203496B60096A12C /* AsyncOperation.swift in Sources */, FEA655DE2167777E00B44029 /* WalletDetailsInteractor.swift in Sources */, @@ -17251,6 +17381,7 @@ FEA655F82167777F00B44029 /* PaymentTableCellModel.swift in Sources */, 8509FC852158F7D100734D93 /* AppGroupFlagContainer.swift in Sources */, 26342CAF20ECD16A00D2196B /* TranscribeResponseData.swift in Sources */, + 3A80BF9921A864220016285E /* AuthProviderProtocols.swift in Sources */, 4B6D20EB2164D4AB003ADB29 /* MessageType.swift in Sources */, E743B5881FB08F0F00F72F92 /* ParticipantsAvatarCell.swift in Sources */, D883A2CBD629A340B27997EF /* SplashViewController.swift in Sources */, @@ -17261,6 +17392,7 @@ 9B9670A02152356D0058E98F /* LeaveVoiceMessageProtocols.swift in Sources */, A481BD1F20EE73BD008FFED8 /* InfoInjectableConstants.swift in Sources */, 8528E50C2072724600A8644A /* StarDateConverter.swift in Sources */, + 3A14D83821ABEC41009CD23A /* AuthProvider.swift in Sources */, E791178C1F978ACF00462D68 /* ImagePreviewViewControllerLayout.swift in Sources */, E7302A971FC8642F002892F8 /* MucTable.swift in Sources */, A43B25D820AB1EE400FF8107 /* NewChannelViewController.swift in Sources */, @@ -17414,6 +17546,7 @@ 853E595920D711B1007799B9 /* StickerPackTable.swift in Sources */, 8514DE8C2136A5FD00718DD8 /* StarActionTable.swift in Sources */, 4B1D7E092029D86600703228 /* CreateGroupItemsFactory.swift in Sources */, + 3A80BF9B21A864220016285E /* AuthProviderWireframe.swift in Sources */, 85052E5720D1A90D00BCC386 /* StickerImagePreviewView.swift in Sources */, 8580BACC20BD984500239D9D /* MessageEditInfo.swift in Sources */, 4B877133219314D50014AD09 /* TypedRequestExtension.swift in Sources */, @@ -17490,6 +17623,7 @@ FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, E79061B61FBF1C8C009FD83A /* DescTable.swift in Sources */, + 3A14D83F21AC1F07009CD23A /* NavigationError.swift in Sources */, 5EDD454F21885ED200C50BC8 /* AccountSettingsCoordinator.swift in Sources */, 5EEB73D62161DBF100D8ECE6 /* EmailLoginView.swift in Sources */, 5EEB73B4216047E000D8ECE6 /* CodeConfirmationViewController.swift in Sources */, @@ -17503,6 +17637,7 @@ A43B25A920AB1DFA00FF8107 /* ALTextInputBar.swift in Sources */, 2BE42DBEDF331A9C7E6FCBE1 /* TopUpAccountViewController.swift in Sources */, A43B25AF20AB1DFA00FF8107 /* ImagePlaceholderField.swift in Sources */, + 852037E821A5B1E00085CF1F /* SwitchRowItemView.swift in Sources */, E7CC5AD01FD99697002746F6 /* ImageCellModel.swift in Sources */, A43B25D520AB1EE400FF8107 /* NewChannelProtocols.swift in Sources */, A42D51BD206A361400EEB952 /* CDR.swift in Sources */, @@ -17581,6 +17716,7 @@ 855EF423202CC85300541BE3 /* MQTTServiceStars.swift in Sources */, 3362A56D731AC1411C02D037 /* MyGroupAliasWireframe.swift in Sources */, 260313A020A0A4BA009AC66D /* SwitchableActionCell.swift in Sources */, + 3A80BF9821A864220016285E /* AuthProviderViewController.swift in Sources */, 4B055C37219C313A001FE077 /* FileDownloaderFactory.swift in Sources */, 8ED0F3D11FBC5CF2004916AB /* GroupsListViewController.swift in Sources */, BBF46945EB64E07C58817ACA /* EditGroupNameProtocols.swift in Sources */, @@ -17595,6 +17731,7 @@ 855A4E9B219B321000B6E90B /* AuthServiceImpl.swift in Sources */, 260313A120A0A4BA009AC66D /* DirectableActionCell.swift in Sources */, 6547BE911E492D790E0D4390 /* EditGroupNameInteractor.swift in Sources */, + 3A14D83B21AC03A3009CD23A /* SearchAvailabilityView.swift in Sources */, 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */, 263A60AE1FB51C22006F9D52 /* MemberExtension.swift in Sources */, E709383F1FBEE41D006CCDC6 /* Describable.swift in Sources */, @@ -17640,6 +17777,7 @@ 268C341921074D6C00F1472A /* TranscribeLongOperationResponseData.swift in Sources */, 8511D3712034427F00B2A620 /* UIView+SafeArea.swift in Sources */, A42D52DF206A53AB00EEB952 /* Roster_Spec.swift in Sources */, + 3A80BF9A21A864220016285E /* AuthProviderInteractor.swift in Sources */, E785F1591FF3E0C8006C52D9 /* UIFontExtension.swift in Sources */, F11786E320AACEBF007A9A1B /* DescInfo.swift in Sources */, C493782D4488E45CB1D67DE4 /* CreateGroupViewController.swift in Sources */, @@ -17809,6 +17947,7 @@ F13EACDB20B86B8C007104D6 /* Wireframe.swift in Sources */, E27620AE3F571711EE70C0C8 /* LanguageSettingsPresenter.swift in Sources */, BC1BA70218B40F3F64841848 /* LanguageSettingsInteractor.swift in Sources */, + 851452AB21A586E900DF10A6 /* LoginOptionsViewController.swift in Sources */, F922EF38E4C1662D54CE533D /* LanguageSettingsWireframe.swift in Sources */, 26A421CF217E541800120542 /* SnackBarLayoutGuide.swift in Sources */, E8AFC57E49EED25C3F5001B7 /* SecurityProtocols.swift in Sources */, @@ -17818,6 +17957,7 @@ 4BF090C521635E8600DCCA5C /* Message+LinkedId.swift in Sources */, C6B308C6734EFB77892832A0 /* SecurityPresenter.swift in Sources */, A42D52B4206A53AA00EEB952 /* ok_Spec.swift in Sources */, + 851452B921A5A91E00DF10A6 /* FieldRowItem.swift in Sources */, B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */, 8E54E93EA25B11D417A6100E /* SecurityInteractor.swift in Sources */, A43B259420AB1DFA00FF8107 /* InputBar.swift in Sources */, diff --git a/Nynja/Coordinators/NavigationError.swift b/Nynja/Coordinators/NavigationError.swift new file mode 100644 index 000000000..4eb4b567f --- /dev/null +++ b/Nynja/Coordinators/NavigationError.swift @@ -0,0 +1,13 @@ +// +// NavigationError.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum NavigationError: Error { + case dismissed +} diff --git a/Nynja/Generated/AssetsConstants.swift b/Nynja/Generated/AssetsConstants.swift index cfc1d8229..81028dd21 100644 --- a/Nynja/Generated/AssetsConstants.swift +++ b/Nynja/Generated/AssetsConstants.swift @@ -746,6 +746,8 @@ internal extension Image { static var icMuteMember: ImageAsset { return ImageAsset(name: "ic_mute_member") } /// "ic_participants_search" static var icParticipantsSearch: ImageAsset { return ImageAsset(name: "ic_participants_search") } + /// "ic_phone" + static var icPhone: ImageAsset { return ImageAsset(name: "ic_phone") } /// "ic_phone_storage" static var icPhoneStorage: ImageAsset { return ImageAsset(name: "ic_phone_storage") } /// "ic_photo_placeholder" diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index aabb6dc53..ed73ea544 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1522,6 +1522,14 @@ internal extension String { static var authPopupConfirmPhoneTitle: String { return localizable.tr("Localizable", "auth.popup.confirm_phone_title") } /// Modify static var authPopupModifyAction: String { return localizable.tr("Localizable", "auth.popup.modify_action") } + /// Available for search + static var authProviderAvailableForSearch: String { return localizable.tr("Localizable", "auth_provider.available_for_search") } + /// ADD EMAIL + static var authProviderEmailScreenTitle: String { return localizable.tr("Localizable", "auth_provider.email_screen_title") } + /// ADD PHONE NUMBER + static var authProviderPhoneScreenTitle: String { return localizable.tr("Localizable", "auth_provider.phone_screen_title") } + /// When turned on, other people can search you by this and add you as a contact. + static var authProviderSearchFlagDescription: String { return localizable.tr("Localizable", "auth_provider.search_flag_description") } /// Call me static var codeConfirmationCall: String { return localizable.tr("Localizable", "code_confirmation.call") } /// We've sent code to your email @@ -1546,6 +1554,20 @@ internal extension String { } /// Welcome to static var codeConfirmationWelcome: String { return localizable.tr("Localizable", "code_confirmation.welcome") } + /// Add Login Provider + static var loginOptionsAddAlertTitle: String { return localizable.tr("Localizable", "login_options.add_alert_title") } + /// Add Login Option + static var loginOptionsAddLabel: String { return localizable.tr("Localizable", "login_options.add_label") } + /// Delete this phone number? + static var loginOptionsDeleteOptionAlertMessage: String { return localizable.tr("Localizable", "login_options.delete_option_alert_message") } + /// You can have up to 3 alternative login options. In addition, you can choose which login options other users can search you by in NYNJA. When on, users can find you by this option. When off, it's private information for you only. + static var loginOptionsDescriptionLabel: String { return localizable.tr("Localizable", "login_options.description_label") } + /// Email + static var loginOptionsEmailOptionActionText: String { return localizable.tr("Localizable", "login_options.email_option_action_text") } + /// Phone Number + static var loginOptionsPhoneNumberOptionActionText: String { return localizable.tr("Localizable", "login_options.phone_number_option_action_text") } + /// LOGIN OPTIONS + static var loginOptionsScreenTitle: String { return localizable.tr("Localizable", "login_options.screen_title") } } } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/NynjaSwitch.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaSwitch.swift new file mode 100644 index 000000000..0f379272f --- /dev/null +++ b/Nynja/Library/UI/Buttons/NynjaButton/NynjaSwitch.swift @@ -0,0 +1,47 @@ +// +// NynjaSwitch.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class NynjaSwitch: UISwitch { + + var offTintColor: UIColor = UIColor.nynja.mercury { + didSet { + backgroundColor = offTintColor + } + } + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + baseSetup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + baseSetup() + } + + + // MARK: - Setup + + func baseSetup() { + backgroundColor = offTintColor + onTintColor = UIColor.nynja.mainRed + tintColor = UIColor.nynja.clear + } + + + // MARK: - Layout + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = 16 + } +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift new file mode 100644 index 000000000..c2a1b0fbc --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift @@ -0,0 +1,44 @@ +// +// AuthProviderProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/23/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol AuthProviderWireframeProtocol: class { + func dismiss() + func selectCountry(completion: @escaping (Result) -> Void) +} + +protocol AuthProviderViewInput: class where Self: UIViewController { + + var screenTitle: String? { get set } + + func setupContentView(with configuration: AuthProviderUIConfiguration) + func setNextActionEnabled(_ isEnabled: Bool) + + func select(country: Country) + func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) +} + +protocol AuthProviderPresenterProtocol: BasePresenterProtocol, NavigationProtocol { + + var authProvider: AuthProvider { get } + + func viewDidLoad() + + func setAvailableForSearch(_ isAvailable: Bool) + func selectCountry() + func next(inputText: String) +} + +protocol AuthProviderInteractorInput: class { + func fetchDefaultCountry() -> Country + func fetchCountry(by code: String) -> Country? +} + +protocol AuthProviderInteractorOutput: class { +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProvider.swift b/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProvider.swift new file mode 100644 index 000000000..d1b637df4 --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProvider.swift @@ -0,0 +1,12 @@ +// +// AuthProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum AuthProvider { + case phoneNumber + case email +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift b/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift new file mode 100644 index 000000000..f1fe853fc --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift @@ -0,0 +1,18 @@ +// +// AuthProviderUIConfiguration.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct AuthProviderUIConfiguration { + + enum Content { + case email(controller: EmailTextController) + case phoneNumber(controller: PhoneNumberTextController, country: Country, selectionHandler: () -> Void) + } + + let content: Content + let isAvailableForSearch: Bool +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift b/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift new file mode 100644 index 000000000..4b576718b --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift @@ -0,0 +1,42 @@ +// +// AuthProviderInteractor.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/23/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AuthProviderInteractor: BaseInteractor, AuthProviderInteractorInput { + + private weak var presenter: AuthProviderInteractorOutput? + + // MARK: - Services + + private let countriesProvider: CountriesProviding + + + // MARK: - Init + + struct Dependencies { + let presenter: AuthProviderInteractorOutput + let countriesProvider: CountriesProviding + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + countriesProvider = dependencies.countriesProvider + } + + + // MARK: - Interactor Input + + func fetchDefaultCountry() -> Country { + return countriesProvider.fetchDefaultCountry() + } + + func fetchCountry(by code: String) -> Country? { + return countriesProvider.fetchCountries().first { $0.code == code } + } +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift b/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift new file mode 100644 index 000000000..7789fe32c --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift @@ -0,0 +1,144 @@ +// +// AuthProviderPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/23/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, AuthProviderInteractorOutput { + + override var itemsFactory: WCItemsFactory? { + return nil + } + + private weak var view: AuthProviderViewInput? + private var interactor: AuthProviderInteractorInput! + private var wireframe: AuthProviderWireframeProtocol! + + let authProvider: AuthProvider + + private var isAvailableForSearch: Bool = true + + private var phoneNumberController: PhoneNumberTextController? + + + // MARK: - Init + + init(authProvider: AuthProvider) { + self.authProvider = authProvider + } + + + // MARK: - Presenter + + func viewDidLoad() { + let screenTitle: String + let content: AuthProviderUIConfiguration.Content + + switch authProvider { + case .email: + screenTitle = String.localizable.authProviderEmailScreenTitle + + let controller = makeEmailController() + + content = .email(controller: controller) + + case .phoneNumber: + screenTitle = String.localizable.authProviderPhoneScreenTitle + + let country = interactor.fetchDefaultCountry() + let controller = makePhoneNumberController(with: country) + + self.phoneNumberController = controller + + content = .phoneNumber(controller: controller, country: country, selectionHandler: { [weak self] in + self?.selectCountry() + }) + } + + let config = AuthProviderUIConfiguration(content: content, isAvailableForSearch: isAvailableForSearch) + + view?.screenTitle = screenTitle + view?.setupContentView(with: config) + } + + private func makePhoneNumberController(with country: Country) -> PhoneNumberTextController { + let controller = PhoneNumberTextController(country: country) + + controller.validationAction = { [weak view] result in + view?.setNextActionEnabled(result) + } + + controller.autofillHandler = { [weak self] autofillInfo in + self?.processPhoneAutoFillInfo(autofillInfo) + } + + return controller + } + + private func makeEmailController() -> EmailTextController { + return EmailTextController(validator: EmailValidator()) { [weak view] result in + view?.setNextActionEnabled(result) + } + } + + func setAvailableForSearch(_ isAvailable: Bool) { + isAvailableForSearch = isAvailable + } + + func selectCountry() { + wireframe.selectCountry { [weak self] result in + guard case let .success(country) = result else { + return + } + self?.phoneNumberController?.country = country + self?.view?.select(country: country) + } + } + + func next(inputText: String) { + switch authProvider { + case .email: + break + case .phoneNumber: + break + } + } + + func back() { + wireframe.dismiss() + } + + private func processPhoneAutoFillInfo(_ autofillInfo: PhoneAutoFillInfo) { + guard let country = interactor.fetchCountry(by: autofillInfo.countryCode) else { + return + } + phoneNumberController?.country = country + + let phoneNumberInfo = PhoneNumberInfo(country: country, number: autofillInfo.phoneNumber) + view?.update(phone: phoneNumberInfo) + } + + + // MARK: - Interactor Output +} + +// MARK: - Injection + +extension AuthProviderPresenter: SetInjectable { + + struct Dependencies { + let view: AuthProviderViewInput + let interactor: AuthProviderInteractorInput + let wireframe: AuthProviderWireframeProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift b/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift new file mode 100644 index 000000000..765b6f600 --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift @@ -0,0 +1,213 @@ +// +// AuthProviderViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/23/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class AuthProviderViewController: BaseVC, AuthProviderViewInput { + + private var presenter: AuthProviderPresenterProtocol + + // MARK: - Views + + private lazy var formContainer: UIView = { + let top = Constraints.formContainer.top.adjustedByWidth + + let container = UIView() + + view.addSubview(container) + container.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom).offset(top) + maker.left.right.equalToSuperview() + } + + return container + }() + + private lazy var searchAvailabilityView: SearchAvailabilityView = { + let top = Constraints.searchContainer.top.adjustedByWidth + let horizontalInset = Constraints.horizontal.adjustedByWidth + + let searchView = SearchAvailabilityView( + title: String.localizable.authProviderAvailableForSearch, + text: String.localizable.authProviderSearchFlagDescription) { [weak self] sender in + + self?.presenter.setAvailableForSearch(sender.isOn) + } + + view.addSubview(searchView) + searchView.snp.makeConstraints { maker in + maker.top.equalTo(formContainer.snp.bottom).offset(top) + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + } + + return searchView + }() + + private lazy var nextButton: UIButton = { + let horizontalInset = Constraints.horizontal.adjustedByWidth + let height = Constraints.nextButton.height.adjustedByWidth + let bottom = Constraints.nextButton.bottom.adjustedByWidth + + let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, + labelHeight: Constraints.nextButton.fontHeight) + + button.textColor = UIColor.nynja.white + button.setTitle(String.localizable.next.uppercased(), for: .normal) + button.addTarget(self, action: #selector(next(sender:)), for: .touchUpInside) + + view.addSubview(button) + button.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + maker.bottom.equalTo(view.keyboardLayoutGuide.snp.top).offset(-bottom) + maker.height.equalTo(height) + } + + return button + }() + + private var phoneNumberView: PhoneNumberLoginView? + + private var emailView: EmailLoginView? + + + // MARK: - Init + + struct Dependencies { + let presenter: AuthProviderPresenterProtocol + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + super.init(nibName: nil, bundle: nil) + _presenter = presenter + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Life Cycle + + override func initialize() { + super.initialize() + setupUI() + presenter.viewDidLoad() + } + + + // MARK: - UI Setup + + private func setupUI() { + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) + formContainer.isHidden = false + searchAvailabilityView.isHidden = false + nextButton.isHidden = false + nextButton.isEnabled = false + } + + + // MARK: - Actions + + @objc private func next(sender: UIButton) { + let inputText: String + + switch presenter.authProvider { + case .email: + inputText = emailView?.inputField.text ?? "" + + case .phoneNumber: + inputText = phoneNumberView?.phoneNumberTextField.text ?? "" + } + presenter.next(inputText: inputText) + } + + override func prepareForDissappear() { + super.prepareForDissappear() + endEditing() + } + + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + endEditing() + } + + + // MARK: - View Input + + func setNextActionEnabled(_ isEnabled: Bool) { + nextButton.isEnabled = isEnabled + } + + func setupContentView(with configuration: AuthProviderUIConfiguration) { + let contentView: UIView + + switch configuration.content { + case let .email(controller): + let emailView = EmailLoginView(textController: controller) + self.emailView = emailView + + contentView = emailView + + case let .phoneNumber(controller, country, countrySelectionHandler): + let phoneNumberView = PhoneNumberLoginView(textController: controller) + self.phoneNumberView = phoneNumberView + + phoneNumberView.configure(config: .init(country: country, countrySelectorAction: countrySelectionHandler)) + + contentView = phoneNumberView + } + + formContainer.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + searchAvailabilityView.isEnabled = configuration.isAvailableForSearch + } + + func select(country: Country) { + phoneNumberView?.selectCountry(country) + } + + func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) { + phoneNumberView?.updatePhone(autofillPhoneNumberInfo) + } +} + +// MARK: - Layout + +private extension AuthProviderViewController { + + enum Constraints { + static let horizontal: CGFloat = 16 + + enum formContainer { + static let top: CGFloat = 32 + } + + enum searchContainer { + static let top: CGFloat = 16 + } + + enum nextButton { + static let fontHeight: CGFloat = 22 + static let height: CGFloat = 44 + static let bottom: CGFloat = 28 + } + } +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift b/Nynja/Modules/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift new file mode 100644 index 000000000..ae7cde9bf --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift @@ -0,0 +1,153 @@ +// +// SearchAvailabilityView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +final class SearchAvailabilityView: BaseView { + + private let title: String + private let text: String + private let switchAction: (UISwitch) -> Void + + var isEnabled: Bool = true { + didSet { + switchView.setOn(isEnabled, animated: false) + } + } + + + // MARK: - Views + + override var activatedViews: [UIView] { + return [actionContainer, separatorView, titleLabel, switchView, descriptionLabel] + } + + private lazy var actionContainer: UIView = { + let height = Constraints.actionContainer.height.adjustedByWidth + + let container = UIView() + + addSubview(container) + container.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + maker.height.equalTo(height) + } + + return container + }() + + private lazy var separatorView: SeparatorView = { + let separatorView = SeparatorView() + + actionContainer.addSubview(separatorView) + separatorView.snp.makeConstraints { maker in + maker.left.right.bottom.equalToSuperview() + } + + return separatorView + }() + + private lazy var titleLabel: UILabel = { + let fontHeight = Constraints.titleLabel.fontHeight + + let label = UILabel(height: fontHeight, color: UIColor.nynja.white, font: FontFamily.NotoSans.medium) + label.text = title + + actionContainer.addSubview(label) + label.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview() + } + + return label + }() + + private lazy var switchView: NynjaSwitch = { + let switchView = NynjaSwitch() + switchView.setOn(isEnabled, animated: false) + + actionContainer.addSubview(switchView) + switchView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalTo(titleLabel.snp.right) + maker.right.equalToSuperview() + } + + return switchView + }() + + private lazy var descriptionLabel: UILabel = { + let fontHeight = Constraints.descriptionLabel.fontHeight + let top = Constraints.descriptionLabel.top.adjustedByWidth + + let label = UILabel(height: fontHeight, color: UIColor.nynja.manatee, font: FontFamily.NotoSans.regular) + label.text = text + label.numberOfLines = 0 + label.setContentHuggingPriority(.required, for: .vertical) + + addSubview(label) + label.snp.makeConstraints { maker in + maker.top.equalTo(actionContainer.snp.bottom).offset(top) + maker.left.right.equalToSuperview() + maker.bottom.equalToSuperview() + } + + return label + }() + + + // MARK: - Init + + init(title: String, text: String, switchAction: @escaping (UISwitch) -> Void) { + self.title = title + self.text = text + self.switchAction = switchAction + super.init(frame: .zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + switchView.addTarget(self, action: #selector(actionSwitchToggled(sender:)), for: .valueChanged) + } + + + // MARK: - Actions + + @objc private func actionSwitchToggled(sender: UISwitch) { + switchAction(sender) + } + + + // MARK: - Layout + + private enum Constraints { + + enum actionContainer { + static let height: CGFloat = 44 + } + + enum titleLabel { + static let fontHeight: CGFloat = 22 + } + + enum descriptionLabel { + static let fontHeight: CGFloat = 20 + static let top: CGFloat = 16 + } + } +} diff --git a/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift b/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift new file mode 100644 index 000000000..dd7ad777c --- /dev/null +++ b/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift @@ -0,0 +1,59 @@ +// +// AuthProviderWireframe.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/23/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import UIKit.UIViewController + +protocol AuthProviderCoordinatorProtocol: class { + func wireframe(_ wireframe: AuthProviderWireframe, didEndWithState state: AuthProviderWireframe.State) +} + +final class AuthProviderWireframe: Wireframe, AuthProviderWireframeProtocol { + + private let coordinator: AuthProviderCoordinatorProtocol + + init(coordinator: AuthProviderCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let authProvider: AuthProvider + } + + struct Dependencies { + let countriesProvider: CountriesProviding + } + + enum State { + case dismiss + case selectCountry(callback: (Result) -> Void) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = AuthProviderPresenter(authProvider: parameters.authProvider) + + let view = AuthProviderViewController(dependencies: .init(presenter: presenter)) + + let interactor = AuthProviderInteractor(dependencies: .init( + presenter: presenter, + countriesProvider: dependencies.countriesProvider) + ) + + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + + return view + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } + + func selectCountry(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) + } +} diff --git a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift new file mode 100644 index 000000000..4e7248689 --- /dev/null +++ b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift @@ -0,0 +1,92 @@ +// +// LoginOptionsCoordinator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class LoginOptionsCoordinator: Coordinator, NavigationContainer { + + private(set) weak var navigation: UINavigationController? + + private let serviceFactory: ServiceFactoryProtocol + + private var selectCountryCallback: ((Result) -> Void)? + + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { + self.navigation = navigation + self.serviceFactory = serviceFactory + } + + func start() { + let wireframe = LoginOptionsWireframe(coordinator: self) + let view = wireframe.prepareModule() + navigation?.pushViewController(view, animated: true) + } + + func end() { + navigation?.popViewController(animated: true) + } +} + +// MARK: - Login Options + +extension LoginOptionsCoordinator: LoginOptionsCoordinatorProtocol { + + func wireframe(_ wireframe: LoginOptionsWireframe, didEndWithState state: LoginOptionsWireframe.State) { + switch state { + case .dismiss: + end() + case let .addAuthProvider(provider): + let wireframe = AuthProviderWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(authProvider: provider), + dependencies: .init(countriesProvider: serviceFactory.makeCountriesProvider()) + ) + navigation?.pushViewController(view, animated: true) + } + } +} + +// MARK: - Add Auth Provider + +extension LoginOptionsCoordinator: AuthProviderCoordinatorProtocol { + + func wireframe(_ wireframe: AuthProviderWireframe, didEndWithState state: AuthProviderWireframe.State) { + switch state { + case .dismiss: + navigation?.popViewController(animated: true) + + case let .selectCountry(callback): + selectCountryCallback = callback + + let wireframe = SelectCountryWireFrame(coordinator: self) + let view = wireframe.prepareModule( + dependencies: SelectCountryWireFrame.Dependencies( + countriesProvider: serviceFactory.makeCountriesProvider() + ) + ) + + navigation?.pushViewController(view, animated: true) + } + } +} + +// MARK: - Country Selector + +extension LoginOptionsCoordinator: CountrySelectorCoordinatorProtocol { + + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { + switch state { + case .selected(let country): + selectCountryCallback?(.success(country)) + case .dismiss: + selectCountryCallback?(.failure(NavigationError.dismissed)) + } + selectCountryCallback = nil + navigation?.popViewController(animated: true) + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/Entities/LoginOption.swift b/Nynja/Modules/Account Flow/LoginOptions/Entities/LoginOption.swift new file mode 100644 index 000000000..151cadf2f --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/Entities/LoginOption.swift @@ -0,0 +1,32 @@ +// +// LoginOption.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class LoginOption { + + enum Provider { + case phoneNumber + case email + case facebook + case google + case twitter + } + + let provider: Provider + let identifier: String + let displayInfo: String + var isAvailableForSearch: Bool + + init(provider: Provider, identifier: String, displayInfo: String, isAvailableForSearch: Bool = true) { + self.provider = provider + self.identifier = identifier + self.displayInfo = displayInfo + self.isAvailableForSearch = isAvailableForSearch + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift b/Nynja/Modules/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift new file mode 100644 index 000000000..823b755c7 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift @@ -0,0 +1,59 @@ +// +// LoginOptionsInteractor.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class LoginOptionsInteractor: BaseInteractor, LoginOptionsInteractorInput { + + private weak var presenter: LoginOptionsInteractorOutput? + + let maxAvailableLoginOptionsCount = 3 + + + // MARK: - Services + + // Declare services here + + + // MARK: - Init + + struct Dependencies { + let presenter: LoginOptionsInteractorOutput + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + } + + + // MARK: - Interactor Input + + func fetchLoginOptions() -> [LoginOption] { + return [ + LoginOption(provider: .phoneNumber, + identifier: "+380971112233", + displayInfo: "+380 97 111 2233", + isAvailableForSearch: false), + + LoginOption(provider: .email, + identifier: "test@test.com", + displayInfo: "test@test.com", + isAvailableForSearch: true) + ] + } + + func delete(_ loginOption: LoginOption) { + presenter?.didDelete(loginOption) + } + + func update(_ loginOption: LoginOption, isAvailableForSearch: Bool) { + loginOption.isAvailableForSearch = isAvailableForSearch + presenter?.didUpdate(loginOption) + } +} + diff --git a/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift b/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift new file mode 100644 index 000000000..cba5aac94 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift @@ -0,0 +1,38 @@ +// +// LoginOptionsProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol LoginOptionsWireframeProtocol: class { + func dismiss() + func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]) + func addAuthProvider(ofType provider: AuthProvider) +} + +protocol LoginOptionsViewInput: class where Self: UIViewController { + func setup(form: Form) + func removeItem(at index: Int) +} + +protocol LoginOptionsPresenterProtocol: BasePresenterProtocol, NavigationProtocol { +} + +protocol LoginOptionsInteractorInput: class { + + var maxAvailableLoginOptionsCount: Int { get } + + func fetchLoginOptions() -> [LoginOption] + + func delete(_ loginOption: LoginOption) + func update(_ loginOption: LoginOption, isAvailableForSearch: Bool) +} + +protocol LoginOptionsInteractorOutput: class { + func didDelete(_ loginOption: LoginOption) + func didUpdate(_ loginOption: LoginOption) +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift b/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift new file mode 100644 index 000000000..a67cddee1 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift @@ -0,0 +1,181 @@ +// +// LoginOptionsPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class LoginOptionsPresenter: BasePresenter, LoginOptionsPresenterProtocol, LoginOptionsInteractorOutput { + + override var itemsFactory: WCItemsFactory? { + return nil + } + + private weak var view: LoginOptionsViewInput? + private var interactor: LoginOptionsInteractorInput! + private var wireframe: LoginOptionsWireframeProtocol! + + private var rowHeight: CGFloat { + return CGFloat(44.0) + } + + private lazy var addLoginOptionItem: ActionRowItem = { + return ActionRowItem( + text: String.localizable.loginOptionsAddLabel, + font: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 22)!, + textColor: UIColor.nynja.white, + icon: UIImage.nynja.icAdd.image, + height: rowHeight, + action: { [weak self] _ in self?.addLoginOption() } + ) + }() + + private lazy var textDescriptionItem: TextRowItem = { + return TextRowItem(text: String.localizable.loginOptionsDescriptionLabel, + font: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 20)!, + textColor: UIColor.nynja.manatee, + backgroundColor: UIColor.nynja.clear, + edges: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)) + }() + + private var loginOptionsItems: [LoginOptionSwitchRowItem]? + + private var items: [AnyFieldRowItem] { + var rows: [AnyFieldRowItem] = [ + addLoginOptionItem, + textDescriptionItem + ] + + if let options = loginOptionsItems { + rows.append(contentsOf: options) + } + + return rows + } + + + // MARK: - Presenter + + override func screenLoaded() { + super.screenLoaded() + + let options = interactor.fetchLoginOptions() + self.loginOptionsItems = makeRowItems(for: options) + + addLoginOptionItem.isEnabled = options.count < interactor.maxAvailableLoginOptionsCount + + let form = Form(rows: items) + view?.setup(form: form) + } + + func back() { + wireframe.dismiss() + } + + private func makeRowItems(for loginOptions: [LoginOption]) -> [LoginOptionSwitchRowItem] { + let font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 22)! + let textColor = UIColor.nynja.white + let icon = UIImage.nynja.icPhone.image + + return loginOptions.map { option in + LoginOptionSwitchRowItem( + option: option, + font: font, + textColor: textColor, + icon: icon, + height: rowHeight, + longTapHandler: { [weak self] _ in + self?.handleLongPress(on: option) + }, + switchHandler: { [weak self] switchItemView in + self?.toggle(option, isOn: switchItemView.switchView.isOn) + } + ) + } + } + + private func handleLongPress(on loginOption: LoginOption) { + let actions: [UIAlertAction] = [ + UIAlertAction(title: String.localizable.delete, style: .destructive) { [weak self] _ in + self?.delete(loginOption) + }, + UIAlertAction(title: String.localizable.cancel, style: .cancel, handler: nil) + ] + wireframe.presentAlert(title: nil, message: nil, style: .actionSheet, actions: actions) + } + + + // MARK: - Actions + + private func addLoginOption() { + let actions: [UIAlertAction] = [ + UIAlertAction(title: String.localizable.loginOptionsPhoneNumberOptionActionText, style: .default) { [weak self] _ in + self?.wireframe.addAuthProvider(ofType: .phoneNumber) + }, + UIAlertAction(title: String.localizable.loginOptionsEmailOptionActionText, style: .default) { [weak self] _ in + self?.wireframe.addAuthProvider(ofType: .email) + }, + UIAlertAction(title: String.localizable.cancel, style: .cancel, handler: nil) + ] + wireframe.presentAlert(title: String.localizable.loginOptionsAddAlertTitle, + message: nil, + style: .actionSheet, + actions: actions) + } + + private func toggle(_ loginOption: LoginOption, isOn: Bool) { + interactor.update(loginOption, isAvailableForSearch: isOn) + } + + private func delete(_ loginOption: LoginOption) { + let actions: [UIAlertAction] = [ + UIAlertAction(title: String.localizable.no, style: .default, handler: nil), + UIAlertAction(title: String.localizable.yes, style: .default) { [weak self] _ in + self?.interactor.delete(loginOption) + } + ] + wireframe.presentAlert(title: nil, + message: String.localizable.loginOptionsDeleteOptionAlertMessage, + style: .alert, + actions: actions) + } + + + // MARK: - Interactor Output + + func didUpdate(_ loginOption: LoginOption) { + loginOptionsItems?.forEach { + if $0.option.identifier == loginOption.identifier { + $0.option = loginOption + } + } + } + + func didDelete(_ loginOption: LoginOption) { + loginOptionsItems?.enumerated().forEach { (index, item) in + if item.option.identifier == loginOption.identifier { + view?.removeItem(at: index) + } + } + } +} + +// MARK: - Injection + +extension LoginOptionsPresenter: SetInjectable { + + struct Dependencies { + let view: LoginOptionsViewInput + let interactor: LoginOptionsInteractorInput + let wireframe: LoginOptionsWireframeProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift new file mode 100644 index 000000000..6a09e753a --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift @@ -0,0 +1,51 @@ +// +// AnyFieldRowItem.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AnyFieldRowItem: class { + typealias ResponderAction = (Bool) -> Void + + var height: CGFloat? { get } + + var nextResponder: AnyFieldRowItem? { get } + var responderAction: ResponderAction? { get } + + func becomeFirstResponder() + func resignFirstResponder() + + func makeView() -> UIView + func configure(_ view: UIView) +} + +extension AnyFieldRowItem { + + var height: CGFloat? { + return nil + } + + var nextResponder: AnyFieldRowItem? { + return nil + } + + var responderAction: ResponderAction? { + return nil + } + + var hasNextResponder: Bool { + return nextResponder != nil + } + + func becomeFirstResponder() { + responderAction?(true) + } + + func resignFirstResponder() { + responderAction?(false) + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift new file mode 100644 index 000000000..94fcfeab6 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift @@ -0,0 +1,27 @@ +// +// FieldRowItem.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol FieldRowItem: AnyFieldRowItem { + associatedtype View: UIView + + func makeView() -> View + func configure(_ view: View) +} + +extension FieldRowItem { + + func configure(_ view: UIView) { + configure(view as! View) + } + + func makeView() -> UIView { + return makeView() as View + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Form.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Form.swift new file mode 100644 index 000000000..e0af5c120 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Form.swift @@ -0,0 +1,32 @@ +// +// Form.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class Form { + + final class Section { + var header: AnyFieldRowItem? + var rows: [AnyFieldRowItem] = [] + + init(header: AnyFieldRowItem? = nil, rows: [AnyFieldRowItem]) { + self.rows = rows + } + } + + var sections: [Section] + + init(sections: [Section]) { + self.sections = sections + } + + convenience init(rows: [AnyFieldRowItem]) { + let section = Section(rows: rows) + self.init(sections: [section]) + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift new file mode 100644 index 000000000..f2fd87954 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift @@ -0,0 +1,150 @@ +// +// ActionRowItemView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +// MARK: - Model + +final class ActionRowItem: FieldRowItem { + + typealias View = ActionRowItemView + typealias Action = (View) -> Void + + let text: String + let font: UIFont + let textColor: UIColor + let icon: UIImage + let height: CGFloat? + + var isEnabled: Bool = true { + didSet { + view?.isEnabled = isEnabled + } + } + let action: Action? + + private weak var view: View? + + init(text: String, font: UIFont, textColor: UIColor, icon: UIImage, height: CGFloat, action: Action?) { + self.text = text + self.font = font + self.textColor = textColor + self.icon = icon + self.height = height + self.action = action + } + + func makeView() -> View { + let view = View() + + self.view = view + configure(view) + + return view + } + + func configure(_ view: View) { + view.isEnabled = isEnabled + view.titleLabel.font = font + view.titleLabel.text = text + view.titleLabel.textColor = textColor + view.actionButton.setImage(icon, for: .normal) + view.actionHandler = action + } +} + +// MARK: - View - + +final class ActionRowItemView: BaseView { + + var isEnabled: Bool = true { + didSet { + // TODO: set enabled / disabled + } + } + + var actionHandler: ((ActionRowItemView) -> Void)? + + + // MARK: - Views + + private(set) lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + + addSubview(titleLabel) + titleLabel.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(Constraints.titleLabel.left) + } + + return titleLabel + }() + + private(set) lazy var actionButton: UIButton = { + let button = UIButton() + + addSubview(button) + button.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalTo(titleLabel.snp.right).offset(Constraints.actionButton.left) + maker.right.equalToSuperview().inset(Constraints.actionButton.right) + } + + return button + }() + + private lazy var separatorView: SeparatorView = { + let view = SeparatorView() + + addSubview(view) + view.snp.makeConstraints { maker in + maker.horizontalInset(Constraints.separator.horizontal.adjustedByWidth) + maker.bottom.equalToSuperview() + } + + return view + }() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + backgroundColor = UIColor.nynja.clear + separatorView.isHidden = false + actionButton.addTarget(self, action: #selector(actionTapped(sender:)), for: .touchUpInside) + } + + + // MARK: - Actions + + @objc private func actionTapped(sender: UIButton){ + actionHandler?(self) + } + + + // MARK: - Layout + + private enum Constraints { + + enum separator { + static let horizontal: CGFloat = 16 + } + + enum titleLabel { + static let left: CGFloat = 16 + } + + enum actionButton { + static let left: CGFloat = 16 + static let right: CGFloat = 8 + } + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift new file mode 100644 index 000000000..42d9d86d0 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift @@ -0,0 +1,123 @@ +// +// SwitchRowItemView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +// MARK: - Model + +class SwitchRowItem { + typealias SwitchHandler = (SwitchRowItemView) -> Void + + var isEnabled: Bool + let height: CGFloat? + let switchHandler: SwitchHandler? + + init(isEnabled: Bool, height: CGFloat, switchHandler: SwitchHandler?) { + self.isEnabled = isEnabled + self.height = height + self.switchHandler = switchHandler + } + + func configure(_ view: SwitchRowItemView) { + view.switchHandler = switchHandler + view.isEnabled = isEnabled + } +} + +// MARK: - View - + +class SwitchRowItemView: BaseView { + + var isEnabled: Bool = true { + didSet { + switchView.isOn = isEnabled + } + } + + var switchHandler: ((SwitchRowItemView) -> Void)? + + + // MARK: - Views + + private(set) lazy var contentView: UIView = { + let contentView = UIView() + + addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(Constraints.contentView.left) + } + + return contentView + }() + + private(set) lazy var switchView: NynjaSwitch = { + let switchView = NynjaSwitch() + + addSubview(switchView) + switchView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalTo(contentView.snp.right).offset(Constraints.switchView.left) + maker.right.equalToSuperview().inset(Constraints.switchView.right) + } + + return switchView + }() + + private lazy var separatorView: SeparatorView = { + let view = SeparatorView() + + addSubview(view) + view.snp.makeConstraints { maker in + maker.horizontalInset(Constraints.separator.horizontal.adjustedByWidth) + maker.bottom.equalToSuperview() + } + + return view + }() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + backgroundColor = UIColor.nynja.clear + separatorView.isHidden = false + contentView.isHidden = false + switchView.isHidden = false + switchView.addTarget(self, action: #selector(actionSwitchValueChanged(sender:)), for: .valueChanged) + } + + + // MARK: - Actions + + @objc private func actionSwitchValueChanged(sender: UISwitch) { + switchHandler?(self) + } + + + // MARK: - Layout + + private enum Constraints { + + enum separator { + static let horizontal: CGFloat = 16 + } + + enum contentView { + static let left: CGFloat = 16 + } + + enum switchView { + static let left: CGFloat = 16 + static let right: CGFloat = 8 + } + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift new file mode 100644 index 000000000..634439e2d --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift @@ -0,0 +1,111 @@ +// +// TextFieldRowItemView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +// MARK: - Model + +final class TextFieldRowItem: FieldRowItem { + + typealias View = TextFieldRowItemView + typealias TextChangeAction = (View) -> Void + + struct FieldConfig { + var edges: UIEdgeInsets + var font: UIFont + var text: String? + var placeholder: String? = nil + var textColor: UIColor? + var placeholderColor: UIColor? = .black + var keyboardType: UIKeyboardType = .default + var keyboardAppearance: UIKeyboardAppearance = .default + var returnKeyType: UIReturnKeyType = .default + var textContentType: UITextContentType? = nil + var autocapitalizationType: UITextAutocapitalizationType = .sentences + var isSecureTextEntry: Bool = false + } + + let validator: Validator? + let config: FieldConfig + let height: CGFloat? + let textChangeAction: TextChangeAction? + + weak var nextResponder: AnyFieldRowItem? + + private(set) var responderAction: ResponderAction? + + private weak var view: View? + + init(validator: Validator?, config: FieldConfig, height: CGFloat, textChangeAction: TextChangeAction?) { + self.validator = validator + self.config = config + self.height = height + self.textChangeAction = textChangeAction + } + + func makeView() -> View { + let view = View() + + self.view = view + configure(view) + + return view + } + + func configure(_ view: View) { + responderAction = { [weak view] isFirstResponder in + if isFirstResponder { + _ = view?.textField.becomeFirstResponder() + } else { + _ = view?.textField.resignFirstResponder() + } + } + + view.textField.snp.updateConstraints { maker in + maker.edges.equalToSuperview().inset(config.edges) + } + + view.textField.returnKeyType = hasNextResponder ? .next : .done + + view.textField.returnHandler = { [weak self] _ in + if let responder = self?.nextResponder { + responder.becomeFirstResponder() + } else { + _ = self?.resignFirstResponder() + } + return false + } + } +} + +// MARK: - View - + +final class TextFieldRowItemView: BaseView { + + private(set) lazy var textField: MaterialTextField = { + let textField = MaterialTextField() + + addSubview(textField) + textField.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return textField + }() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + backgroundColor = UIColor.nynja.clear + textField.isHidden = false + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift new file mode 100644 index 000000000..30ed44013 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift @@ -0,0 +1,109 @@ +// +// TextRowItemView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +// MARK: - Model + +final class TextRowItem: FieldRowItem { + + typealias View = TextRowItemView + + let text: String + let font: UIFont + let textColor: UIColor + let backgroundColor: UIColor + let edges: UIEdgeInsets + + private weak var view: View? + + init(text: String, font: UIFont, textColor: UIColor, backgroundColor: UIColor, edges: UIEdgeInsets) { + self.text = text + self.font = font + self.textColor = textColor + self.backgroundColor = backgroundColor + self.edges = edges + } + + func makeView() -> View { + let view = View() + + self.view = view + configure(view) + + return view + } + + func configure(_ view: View) { + view.textView.text = text + view.textView.font = font + view.textView.textColor = textColor + view.textView.textContainerInset = edges + view.backgroundColor = backgroundColor + } +} + +// MARK: - View - + +final class TextRowItemView: BaseView { + + // MARK: - Views + + private(set) lazy var textView: UITextView = { + let textView = UITextView() + + textView.isScrollEnabled = false + textView.isEditable = false + textView.isSelectable = false + + textView.backgroundColor = UIColor.nynja.clear + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + + addSubview(textView) + textView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return textView + }() + + private lazy var separatorView: SeparatorView = { + let view = SeparatorView() + + addSubview(view) + view.snp.makeConstraints { maker in + maker.horizontalInset(Constraints.separator.horizontal.adjustedByWidth) + maker.bottom.equalToSuperview() + } + + return view + }() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + backgroundColor = UIColor.nynja.clear + textView.isHidden = false + separatorView.isHidden = true + } + + + // MARK: - Layout + + private enum Constraints { + + enum separator { + static let horizontal: CGFloat = 16 + } + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/LoginOptionsViewController.swift b/Nynja/Modules/Account Flow/LoginOptions/View/LoginOptionsViewController.swift new file mode 100644 index 000000000..4ac8b049b --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/LoginOptionsViewController.swift @@ -0,0 +1,121 @@ +// +// LoginOptionsViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class LoginOptionsViewController: BaseVC, LoginOptionsViewInput { + + private var presenter: LoginOptionsPresenterProtocol + + // MARK: - Views + + private(set) lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + + view.addSubview(scrollView) + scrollView.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom) + maker.bottom.left.right.equalToSuperview() + } + + return scrollView + }() + + private lazy var contentView: UIView = { + let contentView = UIView() + + scrollView.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + maker.width.equalToSuperview() + } + + return contentView + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + + contentView.addSubview(stackView) + stackView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return stackView + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: LoginOptionsPresenterProtocol + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + super.init(nibName: nil, bundle: nil) + _presenter = presenter + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Life Cycle + + override func initialize() { + super.initialize() + setupUI() + } + + + // MARK: - UI Setup + + private func setupUI() { + screenTitle = String.localizable.loginOptionsScreenTitle + + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) + + scrollView.alwaysBounceVertical = false + contentView.isHidden = false + } + + + // MARK: - View Input + + func setup(form: Form) { + stackView.arrangedSubviews.forEach { stackView.removeArrangedSubview($0) } + + for section in form.sections { + for row in section.rows { + let itemView = row.makeView() + + if let height = row.height { + itemView.snp.makeConstraints { maker in + maker.height.equalTo(height) + } + } + + stackView.addArrangedSubview(itemView) + } + } + } + + func removeItem(at index: Int) { +// stackView.removeArrangedSubview(<#T##view: UIView##UIView#>) + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift b/Nynja/Modules/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift new file mode 100644 index 000000000..5100623da --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift @@ -0,0 +1,140 @@ +// +// LoginOptionSwitchRowItemView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +// MARK: - Model + +final class LoginOptionSwitchRowItem: SwitchRowItem, FieldRowItem { + + typealias View = LoginOptionSwitchRowItemView + + typealias LongTapHandler = (View) -> Void + + var option: LoginOption { + didSet { + isEnabled = option.isAvailableForSearch + view.flatMap(configure) + } + } + + let font: UIFont + let textColor: UIColor + let icon: UIImage + let longTapHandler: LongTapHandler? + + private weak var view: View? + + init(option: LoginOption, + font: UIFont, + textColor: UIColor, + icon: UIImage, + height: CGFloat, + longTapHandler: LongTapHandler?, + switchHandler: SwitchHandler?) { + self.option = option + self.font = font + self.textColor = textColor + self.icon = icon + self.longTapHandler = longTapHandler + super.init(isEnabled: option.isAvailableForSearch, height: height, switchHandler: switchHandler) + } + + func makeView() -> View { + let view = View() + + self.view = view + configure(view) + + return view + } + + func configure(_ view: View) { + super.configure(view) + view.iconImageView.image = icon + view.textLabel.text = option.displayInfo + view.textLabel.font = font + view.textLabel.textColor = textColor + view.longTapHandler = longTapHandler + } +} + +// MARK: - View - + +final class LoginOptionSwitchRowItemView: SwitchRowItemView { + + fileprivate(set) var longTapHandler: ((LoginOptionSwitchRowItemView) -> Void)? + + private(set) lazy var iconImageView: UIImageView = { + let iconImageView = UIImageView() + + contentView.addSubview(iconImageView) + iconImageView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview() + maker.width.height.equalTo(Constraints.iconImageView.size.adjustedByWidth) + } + + return iconImageView + }() + + private(set) lazy var textLabel: UILabel = { + let horizontalInset = Constraints.textLabel.horizontal.adjustedByWidth + + let label = UILabel() + + contentView.addSubview(label) + label.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalTo(iconImageView.snp.right).offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + } + + return label + }() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + backgroundColor = UIColor.nynja.clear + textLabel.isHidden = false + + let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(actionLongPressRecognized(recognizer:))) + addGestureRecognizer(recognizer) + } + + + // MARK: - Gestures + + @objc private func actionLongPressRecognized(recognizer: UILongPressGestureRecognizer) { + switch recognizer.state { + case .began: + longTapHandler?(self) + default: + break + } + } + + + // MARK: - Layout + + private enum Constraints { + + enum iconImageView { + static let size: CGFloat = 24 + } + + enum textLabel { + static let horizontal: CGFloat = 16 + } + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift b/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift new file mode 100644 index 000000000..64299f920 --- /dev/null +++ b/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift @@ -0,0 +1,54 @@ +// +// LoginOptionsWireframe.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import UIKit.UIViewController + +protocol LoginOptionsCoordinatorProtocol: AlertDisplayable { + func wireframe(_ wireframe: LoginOptionsWireframe, didEndWithState state: LoginOptionsWireframe.State) +} + +final class LoginOptionsWireframe: Wireframe, LoginOptionsWireframeProtocol { + + private let coordinator: LoginOptionsCoordinatorProtocol + + init(coordinator: LoginOptionsCoordinatorProtocol) { + self.coordinator = coordinator + } + + enum State { + case dismiss + case addAuthProvider(AuthProvider) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = LoginOptionsPresenter() + + let view = LoginOptionsViewController(dependencies: .init(presenter: presenter)) + + let interactor = LoginOptionsInteractor(dependencies: .init( + presenter: presenter) + ) + + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + + return view + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } + + func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]) { + coordinator.presentAlert(title: title, message: message, style: style, actions: actions) + } + + func addAuthProvider(ofType provider: AuthProvider) { + coordinator.wireframe(self, didEndWithState: .addAuthProvider(provider)) + } +} diff --git a/Nynja/Modules/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Auth Flow/AuthCoordinator.swift index 23a4cdaff..00366a5d9 100644 --- a/Nynja/Modules/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AuthCoordinator.swift @@ -27,15 +27,15 @@ final class AuthCoordinator: Coordinator, NavigationContainer, CountrySelectorCo } func start() { -// SplashWireFrame().presentSplash(navigation: navigation!) + SplashWireFrame().presentSplash(navigation: navigation!) - let wireframe = AuthWireframe(coordinator: self) - let view = wireframe.prepareModule( - dependencies: .init(authService: serviceFactory.makeAuthService(), - googleAuthService: serviceFactory.makeGoogleAuthService(), - countriesProvider: serviceFactory.makeCountriesProvider()) - ) - navigation?.pushViewController(view, animated: true) +// let wireframe = AuthWireframe(coordinator: self) +// let view = wireframe.prepareModule( +// dependencies: .init(authService: serviceFactory.makeAuthService(), +// googleAuthService: serviceFactory.makeGoogleAuthService(), +// countriesProvider: serviceFactory.makeCountriesProvider()) +// ) +// navigation?.pushViewController(view, animated: true) } func end() { @@ -51,8 +51,9 @@ extension AuthCoordinator { case .selected(let country): selectCountryCallback?(.success(country)) case .dismiss: - break + selectCountryCallback?(.failure(NavigationError.dismissed)) } + selectCountryCallback = nil navigation?.popViewController(animated: true) } } @@ -148,7 +149,7 @@ extension AuthCoordinator { } } - private func showConfirmationPopup(loginOption: LoginOption) { + private func showConfirmationPopup(loginOption: PlainLoginOption) { let popup = UIAlertController(title: titleForPopup(loginOption: loginOption), message: messageForPopup(loginOption: loginOption), preferredStyle: .alert) @@ -167,7 +168,7 @@ extension AuthCoordinator { navigation?.present(popup, animated: true, completion: nil) } - private func titleForPopup(loginOption: LoginOption) -> String { + private func titleForPopup(loginOption: PlainLoginOption) -> String { switch loginOption { case .email: return String.localizable.authPopupConfirmEmailTitle @@ -176,7 +177,7 @@ extension AuthCoordinator { } } - private func messageForPopup(loginOption: LoginOption) -> String { + private func messageForPopup(loginOption: PlainLoginOption) -> String { switch loginOption { case let .email(email): return email @@ -228,7 +229,7 @@ extension AuthCoordinator: FacebookAuthCoordinatorProtocol { case let .authenticated(code): facebookAuthCodeCallback?(.success(code)) case .dismiss: - break + facebookAuthCodeCallback?(.failure(NavigationError.dismissed)) } facebookAuthCodeCallback = nil navigation?.popViewController(animated: true) diff --git a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift index 85e180f74..10b3673ea 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift @@ -10,7 +10,7 @@ import Foundation protocol AuthWireframeProtocol: class { func selectCountry(completion: @escaping (Result) -> Void) - func confirmInputData(loginOption: LoginOption, confirmationHandler: @escaping (Bool) -> Void) + func confirmInputData(loginOption: PlainLoginOption, confirmationHandler: @escaping (Bool) -> Void) func continueLogin(loginFlow: LoginFlow) func showFacebookAuth(completion: @escaping (Result) -> Void) @@ -27,7 +27,7 @@ protocol AuthViewProtocol: class where Self: UIViewController { } protocol AuthPresenterProtocol: class { - var loginOption: LoginOption { get } + var loginOption: PlainLoginOption { get } var selectedCountry: Country { get } @@ -43,13 +43,13 @@ protocol AuthPresenterProtocol: class { } protocol AuthInputInteractorProtocol: class { + func fetchDefaultCountry() -> Country + func fetchCountry(by code: String) -> Country? + func loginViaFacebook(code: String) func loginViaGoogle() func loginViaEmail(_ email: String) func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) - - func fetchDefaultCountry() -> Country - func fetchCountry(by code: String) -> Country? } protocol AuthOutputInteractorProtocol: class { diff --git a/Nynja/Modules/Auth Flow/AuthModule/Entities/LoginOption.swift b/Nynja/Modules/Auth Flow/AuthModule/Entities/PlainLoginOption.swift similarity index 78% rename from Nynja/Modules/Auth Flow/AuthModule/Entities/LoginOption.swift rename to Nynja/Modules/Auth Flow/AuthModule/Entities/PlainLoginOption.swift index d1d020191..25718767d 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Entities/LoginOption.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Entities/PlainLoginOption.swift @@ -1,12 +1,12 @@ // -// LoginOption.swift +// PlainLoginOption.swift // Nynja // // Created by Anton Poltoratskyi on 14.11.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -enum LoginOption { +enum PlainLoginOption { case phoneNumber(String) case email(String) } diff --git a/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift index c6e6a3ddf..de54105c2 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift @@ -42,6 +42,10 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { return countriesProvider.fetchDefaultCountry() } + func fetchCountry(by code: String) -> Country? { + return countriesProvider.fetchCountries().first { $0.code == code } + } + func loginViaFacebook(code: String) { authService.loginByFacebook(serverCode: code) { [weak self] result in switch result { @@ -96,8 +100,4 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { } } } - - func fetchCountry(by code: String) -> Country? { - return countriesProvider.fetchCountries().first { $0.code == code } - } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift index 136ef561f..6f69f9031 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift @@ -13,7 +13,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, private var interactor: AuthInputInteractorProtocol! private var wireframe: AuthWireframeProtocol! - private(set) var loginOption: LoginOption = .phoneNumber("") + private(set) var loginOption: PlainLoginOption = .phoneNumber("") private(set) lazy var selectedCountry: Country = { return interactor.fetchDefaultCountry() @@ -65,12 +65,12 @@ final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, } func selectCountry() { - wireframe.selectCountry { result in + wireframe.selectCountry { [weak self] result in guard case let .success(country) = result else { return } - self.selectedCountry = country - self.view?.select(country: country) + self?.selectedCountry = country + self?.view?.select(country: country) } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift index 92207951a..27159247a 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift @@ -81,9 +81,6 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn // MARK: Center Content - private lazy var emailLoginView = makeEmailLoginView(on: loginContainerView) - private lazy var phoneNumberLoginView = makePhoneNumberLoginView(on: loginContainerView, country: presenter.selectedCountry) - private lazy var loginContainerView: UIView = { let containerView = UIView() @@ -95,7 +92,19 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn return containerView }() - + + private lazy var emailContainerView = makeEmailLoginView(on: loginContainerView) + + private lazy var phoneContainerView = makePhoneNumberLoginView(on: loginContainerView, country: presenter.selectedCountry) + + private var emailLoginView: EmailLoginView { + return emailContainerView.contentView + } + + private var phoneNumberLoginView: PhoneNumberLoginView { + return phoneContainerView.contentView + } + private(set) lazy var nextButton: UIButton = { let button = NynjaImageButton(fontName: FontFamily.NotoSans.medium.name, labelHeight: 22) @@ -140,7 +149,7 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn bottomView.configure(config: LoginOptionsView.Config( loginOption: presenter.loginOption, - switchLoginAction: { [unowned self] () -> LoginOption in + switchLoginAction: { [unowned self] () -> PlainLoginOption in self.presenter.switchLoginOption() let loginOption = self.presenter.loginOption @@ -231,8 +240,8 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn showPhoneNumberLogin(animated: false) - emailLoginView.snp.makeConstraints { maker in - maker.height.equalTo(phoneNumberLoginView) + emailContainerView.snp.makeConstraints { maker in + maker.height.equalTo(phoneContainerView) } enableKeyboardHidingWhenTappedAround() @@ -319,54 +328,55 @@ private extension AuthViewController { func showPhoneNumberLogin(animated: Bool) { if animated { let nextButtonEnabled = phoneNumberTextController.isValid - animateChangingViews(first: emailLoginView, second: phoneNumberLoginView, isNextActionEnabled: nextButtonEnabled) + animateChangingViews(first: emailContainerView, second: phoneContainerView, isNextActionEnabled: nextButtonEnabled) } else { - emailLoginView.isHidden = true - phoneNumberLoginView.isHidden = false + emailContainerView.isHidden = true + phoneContainerView.isHidden = false } } func showEmailLogin(animated: Bool) { if animated { let nextButtonEnabled = emailTextController.isValid - animateChangingViews(first: phoneNumberLoginView, second: emailLoginView, isNextActionEnabled: nextButtonEnabled) + animateChangingViews(first: phoneContainerView, second: emailContainerView, isNextActionEnabled: nextButtonEnabled) } else { - emailLoginView.isHidden = false - phoneNumberLoginView.isHidden = true + emailContainerView.isHidden = false + phoneContainerView.isHidden = true } } - func makeEmailLoginView(on view: UIView) -> EmailLoginView { + func makeEmailLoginView(on view: UIView) -> LoginContainerView { let loginView = EmailLoginView(textController: emailTextController) - view.addSubview(loginView) - loginView.configure() + let container = LoginContainerView(contentView: loginView) + container.detailsLabel.text = String.localizable.authEnterEmailAddressComment - loginView.snp.makeConstraints { maker in - maker.top.left.right.equalToSuperview() - maker.bottom.lessThanOrEqualToSuperview() - maker.bottom.equalToSuperview().priority(.high) + view.addSubview(container) + container.snp.makeConstraints { maker in + maker.edges.equalToSuperview() } - return loginView + return container } - func makePhoneNumberLoginView(on view: UIView, country: Country) -> PhoneNumberLoginView { + func makePhoneNumberLoginView(on view: UIView, country: Country) -> LoginContainerView { let loginView = PhoneNumberLoginView(textController: phoneNumberTextController) - view.addSubview(loginView) - loginView.configure(config: PhoneNumberLoginView.Config( + let container = LoginContainerView(contentView: loginView) + container.detailsLabel.text = String.localizable.authEnterPhoneNumberComment + + view.addSubview(container) + container.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + loginView.configure(config: .init( country: country, countrySelectorAction: { [weak presenter] in presenter?.selectCountry() } )) - loginView.snp.makeConstraints { maker in - maker.top.left.right.equalToSuperview() - maker.bottom.equalToSuperview() - } - - return loginView + return container } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift index ea0c63a4d..008a7f6fd 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift @@ -8,13 +8,11 @@ import UIKit -final class EmailLoginView: UIView, Configurable { +final class EmailLoginView: UIView { // MARK: - Views - private lazy var inputFieldContainer = makeInputFieldContainer() private(set) lazy var inputField = makeInputField() - private(set) lazy var detailsLabel = makeDetailsLabel() private let textController: EmailTextController @@ -27,18 +25,18 @@ final class EmailLoginView: UIView, Configurable { super.init(frame: .zero) inputField.isHidden = false + + setup() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } -} - -// MARK: - Configurable - -extension EmailLoginView { - func configure(config: Config) { + + // MARK: - Setup + + private func setup() { inputField.textChanged = { [weak textController] textInput in textController?.textDidChange(textInput) } @@ -46,8 +44,6 @@ extension EmailLoginView { inputField.returnHandler = { [weak textController] textInput in return textController?.textInputShouldReturn(textInput) ?? false } - - _ = [inputFieldContainer, inputField, detailsLabel] } } @@ -55,21 +51,6 @@ extension EmailLoginView { private extension EmailLoginView { - func makeInputFieldContainer() -> UIView { - let container = UIView() - container.backgroundColor = UIColor.nynja.clear - - addSubview(container) - container.snp.makeConstraints { maker in - maker.top.equalToSuperview().offset(16) - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) - maker.height.equalTo(44) - } - - return container - } - func makeInputField() -> MaterialTextField { let textField = MaterialTextField() @@ -86,36 +67,14 @@ private extension EmailLoginView { textField.keyboardType = .emailAddress textField.returnKeyType = .done - inputFieldContainer.addSubview(textField) + addSubview(textField) textField.snp.makeConstraints { maker in - maker.centerY.equalToSuperview() - maker.left.equalToSuperview() - maker.right.equalToSuperview() - } - - return textField - } - - func makeDetailsLabel() -> UILabel { - let label = UILabel() - - label.text = String.localizable.authEnterEmailAddressComment - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - - label.setContentCompressionResistancePriority(.required, for: .vertical) - label.setContentHuggingPriority(.required, for: .vertical) - - label.numberOfLines = 0 - - addSubview(label) - label.snp.makeConstraints { maker in - maker.top.equalTo(inputFieldContainer.snp.bottom) + maker.top.bottom.equalToSuperview() maker.left.equalToSuperview().offset(16) maker.right.equalToSuperview().inset(16) - maker.bottom.lessThanOrEqualToSuperview() + maker.height.equalTo(64) } - return label + return textField } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift new file mode 100644 index 000000000..c05f11b85 --- /dev/null +++ b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift @@ -0,0 +1,68 @@ +// +// LoginContainerView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +final class LoginContainerView: BaseView { + + let contentView: T + + private(set) var detailsLabel: UILabel! + + + // MARK: - Init + + init(contentView: T) { + self.contentView = contentView + super.init(frame: .zero) + setup() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Setup + + private func setup() { + addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + } + + detailsLabel = makeDetailsLabel() + } + + + // MARK: - Layout + + private func makeDetailsLabel() -> UILabel { + let label = UILabel() + + label.font = FontFamily.NotoSans.regular.font(size: 14) + label.textColor = UIColor.nynja.dustyGray + label.numberOfLines = 0 + + label.setContentHuggingPriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .vertical) + + addSubview(label) + label.snp.makeConstraints { maker in + maker.top.equalTo(contentView.snp.bottom) + maker.left.equalToSuperview().offset(16) + maker.right.equalToSuperview().inset(16) + maker.bottom.lessThanOrEqualToSuperview() + maker.bottom.equalToSuperview().priority(.high) + } + + return label + } +} diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift index b506867c0..f32f5b657 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift @@ -16,7 +16,7 @@ final class LoginOptionsView: UIView, Configurable { private lazy var loginWithFacebook = makeLoginWithFacebookButton() private lazy var loginWithGoogle = makeLoginWithGoogleButton() - private var switchLoginAction: (() -> LoginOption)? + private var switchLoginAction: (() -> PlainLoginOption)? private var facebookLoginAction: (() -> Void)? private var googleLoginAction: (() -> Void)? @@ -37,8 +37,8 @@ final class LoginOptionsView: UIView, Configurable { extension LoginOptionsView { struct Config { - let loginOption: LoginOption - let switchLoginAction: () -> LoginOption + let loginOption: PlainLoginOption + let switchLoginAction: () -> PlainLoginOption let facebookLoginAction: () -> Void let googleLoginAction: () -> Void } @@ -74,7 +74,7 @@ private extension LoginOptionsView { googleLoginAction?() } - func updateSwitchButton(loginOption: LoginOption) { + func updateSwitchButton(loginOption: PlainLoginOption) { switch loginOption { case .email: switchLoginButton.setTitle(String.localizable.authLoginWithPhoneNumber.uppercased(), for: .normal) diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift index 9f752e923..528e739bb 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -21,8 +21,6 @@ final class PhoneNumberLoginView: UIView, Configurable { private lazy var phoneNumberContainer = makePhoneNumberContainer() private(set) lazy var phoneNumberTextField = makePhoneNumberTextField() - private(set) lazy var detailsLabel = makeDetailsNumberLabel() - private let textController: PhoneNumberTextController private var country: Country? @@ -55,7 +53,7 @@ extension PhoneNumberLoginView { phoneNumberTextField.delegate = textController selectCountry(config.country) - _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField, detailsLabel] + _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField] } } @@ -132,7 +130,7 @@ private extension PhoneNumberLoginView { addSubview(container) container.snp.makeConstraints { maker in maker.top.equalTo(countrySelector.snp.bottom) - maker.left.equalToSuperview() + maker.left.bottom.equalToSuperview() maker.width.equalTo(100) maker.height.equalTo(64) } @@ -201,26 +199,4 @@ private extension PhoneNumberLoginView { return textField } - - func makeDetailsNumberLabel() -> UILabel { - let label = UILabel() - - label.text = String.localizable.authEnterPhoneNumberComment - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - label.numberOfLines = 0 - - label.setContentHuggingPriority(.required, for: .vertical) - label.setContentCompressionResistancePriority(.required, for: .vertical) - - addSubview(label) - label.snp.makeConstraints { maker in - maker.top.equalTo(countryCodeContainer.snp.bottom) - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) - maker.bottom.equalToSuperview() - } - - return label - } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift index 8918e0911..9ad671d96 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift @@ -29,7 +29,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { enum State { case selectCountry(callback: (Result) -> Void) - case confirmInputData(loginOption: LoginOption, confirmationHandler: (Bool) -> Void) + case confirmInputData(loginOption: PlainLoginOption, confirmationHandler: (Bool) -> Void) case continueLogin(loginFlow: LoginFlow) case showFacebookAuth(callback: (Result) -> Void) case present(UIViewController) @@ -57,7 +57,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) } - func confirmInputData(loginOption: LoginOption, confirmationHandler: @escaping (Bool) -> Void) { + func confirmInputData(loginOption: PlainLoginOption, confirmationHandler: @escaping (Bool) -> Void) { coordinator.wireframe(self, didEndWithState: .confirmInputData(loginOption: loginOption, confirmationHandler: confirmationHandler)) } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift index be18056d5..cdbc17ceb 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift @@ -34,7 +34,7 @@ protocol CodeConfirmationPresenterProtocol: NavigationProtocol { protocol CodeConfirmationInputInteractorProtocol: class { var address: String { get } - var authProviderType: AuthProviderType { get } + var confirmationData: AuthConfirmationData { get } func sendConfirmationCode(_ code: String) func resendCode() diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthProviderType.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthConfirmationData.swift similarity index 78% rename from Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthProviderType.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthConfirmationData.swift index 989ef8d5a..cb434c68b 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthProviderType.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthConfirmationData.swift @@ -1,5 +1,5 @@ // -// AuthProviderType.swift +// AuthConfirmationData.swift // Nynja // // Created by Anton Poltoratskyi on 05.11.2018. @@ -8,7 +8,7 @@ import Foundation -enum AuthProviderType { +enum AuthConfirmationData { case email(String) case phoneNumber(PhoneNumberInfo) } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index e3e0f99ea..0bd640ce0 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -12,10 +12,10 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, private weak var presenter: CodeConfirmationOutputInteractorProtocol? - let authProviderType: AuthProviderType + let confirmationData: AuthConfirmationData var address: String { - switch authProviderType { + switch confirmationData { case let .email(email): return email case let .phoneNumber(phoneNumberInfo): @@ -33,13 +33,13 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, struct Dependencies { let presenter: CodeConfirmationOutputInteractorProtocol - let authProviderType: AuthProviderType + let authProviderType: AuthConfirmationData let authService: AuthService } init(dependencies: Dependencies) { self.presenter = dependencies.presenter - self.authProviderType = dependencies.authProviderType + self.confirmationData = dependencies.authProviderType self.authService = dependencies.authService } @@ -58,7 +58,7 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, } func resendCode() { - switch authProviderType { + switch confirmationData { case let .email(email): authService.login(by: email) { [weak self] result in self?.handleResendCodeResponse(result) @@ -71,7 +71,7 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, } func askForCall() { - guard case let .phoneNumber(phoneNumberInfo) = authProviderType else { + guard case let .phoneNumber(phoneNumberInfo) = confirmationData else { return } authService.login(by: phoneNumberInfo, confirmVia: .call) { [weak self] result in diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index f9c35e6f1..d54bef64a 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -38,7 +38,7 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo private var timer: Timer? var canAskForCall: Bool { - if case .phoneNumber = interactor.authProviderType { + if case .phoneNumber = interactor.confirmationData { return true } return false @@ -49,7 +49,7 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } var descriptionText: String { - switch interactor.authProviderType { + switch interactor.confirmationData { case .phoneNumber: return String.localizable.codeConfirmationCodeSentToPhone case .email: @@ -58,7 +58,7 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } func viewDidLoad() { - switch interactor.authProviderType { + switch interactor.confirmationData { case .email: timerValue = 15 * 60 case .phoneNumber: diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index 535168fb5..780fa900b 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -21,7 +21,7 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto } struct Parameters { - let authType: AuthProviderType + let authType: AuthConfirmationData } struct Dependencies { diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 62a1b7a30..89026f3ac 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -18,8 +18,6 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega weak var messageinteractor: MessageInteractor? weak var external: EditParticipantsDelegate? = nil - - private var accountSettingsCoordinator: AccountSettingsCoordinator? func presentMain(navigation: UINavigationController, isRegistered: Bool, checkSession: Bool = false) { let serviceFactory = ServiceFactory() @@ -464,12 +462,16 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega func showAccountSettings() { guard let navigation = navigation else { return } - accountSettingsCoordinator = AccountSettingsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) - accountSettingsCoordinator?.start() + + let coordinator = AccountSettingsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.start() } func showLoginOptions() { - print(#function) + guard let navigation = navigation else { return } + + let coordinator = LoginOptionsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.start() } func updateAvatar(image: UIImage) { diff --git a/Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json index cbc434672..277429129 100644 --- a/Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_add.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "ic_add.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/ic_phone.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_phone.imageset/Contents.json new file mode 100644 index 000000000..24bb65c00 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/ic_phone.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_phone.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_phone.imageset/ic_phone.pdf b/Nynja/Resources/Assets.xcassets/ic_phone.imageset/ic_phone.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ae5e51006eabd2e112caef0b5a2358b9ad9daaf0 GIT binary patch literal 6136 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 L1_)F}sL=c$=zL*H literal 0 HcmV?d00001 diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index b2ad6d34c..a513239c3 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -1030,3 +1030,23 @@ // MARK: Create Profile "terms_of_use" = "terms of use"; + + +// MARK: - Account Edit Flow + +// MARK: Login Options + +"login_options.screen_title" = "LOGIN OPTIONS"; +"login_options.add_label" = "Add Login Option"; +"login_options.description_label" = "You can have up to 3 alternative login options. In addition, you can choose which login options other users can search you by in NYNJA. When on, users can find you by this option. When off, it's private information for you only."; +"login_options.delete_option_alert_message" = "Delete this phone number?"; +"login_options.add_alert_title" = "Add Login Provider"; +"login_options.email_option_action_text" = "Email"; +"login_options.phone_number_option_action_text" = "Phone Number"; + +// MARK: Auth Provider + +"auth_provider.email_screen_title" = "ADD EMAIL"; +"auth_provider.phone_screen_title" = "ADD PHONE NUMBER"; +"auth_provider.available_for_search" = "Available for search"; +"auth_provider.search_flag_description" = "When turned on, other people can search you by this and add you as a contact."; -- GitLab From 75154893e89eb35e5f31bde50eec2d32041fb8b9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 5 Dec 2018 13:16:29 +0200 Subject: [PATCH 123/138] [Multiple accounts] SMS autofill (#1501) * [NY-3855] Add login options module skeleton. * [NY-3855] Implemented base UI for login options. * [NY-3855] Added alerts. * [NY-3855] Added LoginOption model. Minor changes. * Fixed compile issues. * [NY-3855] Fixed screen title * Fixed switch * [NY-5519] Add AuthProvider module from template * Minor renaming changes. * Remove unused references from project file * Added transition to 'add auth provider' screen. * Implemented base UI for add auth provider. * Fixed UI * Fixed UI hierarchy. * Fixed UI components reusability from auth screen. * Implemented code confirmation input as a separate view. * Add new files * Remove unused code * Minor refactor. * Fixed accessibility and fonts construction. * Present code confirmation * Present code confirmation * Add adjust on code confirmation screen. * Remove unused files. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 52 +++- .../TextInput/TextInputUtils.swift | 0 .../Extensions/TextInput/UITextInput.swift | 20 +- .../{ => Button}/UnderlinedButton.swift | 0 .../SecureCode/SecureCodeContainerView.swift | 169 +++++++++++ .../SecureCode/SecureCodeInputView.swift | 282 ++++++++++++++++++ .../Controls/{ => TextInput}/TextField.swift | 0 .../{ => TextInput}/UnderlinedTextField.swift | 29 +- Nynja.xcodeproj/project.pbxproj | 12 +- .../UI/BaseVC/LoadingInteractive.swift | 35 ++- .../UI/Extensions/UI/UIFontExtension.swift | 8 + .../UI/TextInput/InputBar/InputBar.swift | 1 + .../AuthProvider/AuthProviderProtocols.swift | 8 +- .../Interactor/AuthProviderInteractor.swift | 8 + .../Presenter/AuthProviderPresenter.swift | 32 +- .../View/AuthProviderViewController.swift | 8 +- .../Wireframe/AuthProviderWireframe.swift | 5 + .../Coordinator/LoginOptionsCoordinator.swift | 24 ++ .../Presenter/LoginOptionsPresenter.swift | 12 +- .../AddContactByUsernameProtocols.swift | 4 +- .../AddContactByUsernameInteractor.swift | 6 +- .../AddContactByUsernamePresenter.swift | 4 +- .../AddContactViaPhoneProtocols.swift | 4 +- .../AddContactViaPhoneInteractor.swift | 6 +- .../AddContactViaPhonePresenter.swift | 4 +- Nynja/Modules/Auth Flow/AuthCoordinator.swift | 11 +- .../Auth Flow/AuthModule/AuthProtocols.swift | 4 +- .../AuthModule/View/AuthViewController.swift | 21 +- .../View/Subviews/PhoneNumberLoginView.swift | 4 +- .../CodeConfirmationProtocols.swift | 2 +- ...ationData.swift => ConfirmationData.swift} | 4 +- .../CodeConfirmationInteractor.swift | 6 +- .../View/CodeConfirmationViewController.swift | 143 ++++----- .../CodeConfirmationViewsFactory.swift | 144 +-------- .../Wireframe/CodeConfirmationWireframe.swift | 4 +- 35 files changed, 769 insertions(+), 307 deletions(-) rename Frameworks/NynjaUIKit/NynjaUIKit/Core/{ => Extensions}/TextInput/TextInputUtils.swift (100%) rename Nynja/Library/UI/TextInput/UITextInput+Cursor.swift => Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/TextInput/UITextInput.swift (57%) rename Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/{ => Button}/UnderlinedButton.swift (100%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeContainerView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeInputView.swift rename Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/{ => TextInput}/TextField.swift (100%) rename Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/{ => TextInput}/UnderlinedTextField.swift (80%) rename Nynja/Modules/Auth Flow/CodeConfirmation/Entities/{AuthConfirmationData.swift => ConfirmationData.swift} (78%) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 24eade12d..8779c566e 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ 297D4FB3B2A977EBC50F9621 /* Pods_NynjaUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */; }; + 3A07C5B421AED3FC00AE3429 /* SecureCodeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B221AED3FC00AE3429 /* SecureCodeContainerView.swift */; }; + 3A07C5B521AED3FC00AE3429 /* SecureCodeInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B321AED3FC00AE3429 /* SecureCodeInputView.swift */; }; + 3A07C5B921AEDF7800AE3429 /* UITextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B821AEDF7800AE3429 /* UITextInput.swift */; }; 850A2E9E219F37AD00C784D9 /* TextInputUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */; }; 8514D4C620EE27080002378A /* NynjaUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 8514D4C420EE27080002378A /* NynjaUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8514D4DF20EE2D970002378A /* InteractiveCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D4CF20EE2D970002378A /* InteractiveCellViewModel.swift */; }; @@ -52,6 +55,9 @@ 1820AD65D6897E3E306C16A2 /* Pods-NynjaUIKit.translate.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.translate.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.translate.xcconfig"; sourceTree = ""; }; 18558C18EB6A4113B623AEDE /* Pods-NynjaUIKit.prereleasedebug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.prereleasedebug.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.prereleasedebug.xcconfig"; sourceTree = ""; }; 1D38BAE43B22E6C930DB3980 /* Pods-NynjaUIKit.release-debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.release-debug.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.release-debug.xcconfig"; sourceTree = ""; }; + 3A07C5B221AED3FC00AE3429 /* SecureCodeContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureCodeContainerView.swift; sourceTree = ""; }; + 3A07C5B321AED3FC00AE3429 /* SecureCodeInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureCodeInputView.swift; sourceTree = ""; }; + 3A07C5B821AEDF7800AE3429 /* UITextInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextInput.swift; sourceTree = ""; }; 600B9308D041B8E4DE0DBEF1 /* Pods-NynjaUIKit.loaddb.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.loaddb.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.loaddb.xcconfig"; sourceTree = ""; }; 7336042AC840197E622730FD /* Pods-NynjaUIKit.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.prerelease.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.prerelease.xcconfig"; sourceTree = ""; }; 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputUtils.swift; sourceTree = ""; }; @@ -114,6 +120,41 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3A07C5AD21AED2F800AE3429 /* Button */ = { + isa = PBXGroup; + children = ( + 855A4E91219B0C5600B6E90B /* UnderlinedButton.swift */, + ); + path = Button; + sourceTree = ""; + }; + 3A07C5AE21AED30300AE3429 /* TextInput */ = { + isa = PBXGroup; + children = ( + 855A4E87219AFB0F00B6E90B /* TextField.swift */, + 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */, + ); + path = TextInput; + sourceTree = ""; + }; + 3A07C5B121AED3FC00AE3429 /* SecureCode */ = { + isa = PBXGroup; + children = ( + 3A07C5B221AED3FC00AE3429 /* SecureCodeContainerView.swift */, + 3A07C5B321AED3FC00AE3429 /* SecureCodeInputView.swift */, + ); + path = SecureCode; + sourceTree = ""; + }; + 3A07C5BA21AF00C900AE3429 /* TextInput */ = { + isa = PBXGroup; + children = ( + 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */, + 3A07C5B821AEDF7800AE3429 /* UITextInput.swift */, + ); + path = TextInput; + sourceTree = ""; + }; 703C0F257F98BCEAD4D25D95 /* Pods */ = { isa = PBXGroup; children = ( @@ -303,6 +344,7 @@ children = ( 8540A00D2181ED10003A010F /* CoreAnimation */, 8514D51F20EE47350002378A /* UIWindow */, + 3A07C5BA21AF00C900AE3429 /* TextInput */, ); path = Extensions; sourceTree = ""; @@ -375,9 +417,9 @@ 855A4E84219AFA6C00B6E90B /* Controls */ = { isa = PBXGroup; children = ( - 855A4E87219AFB0F00B6E90B /* TextField.swift */, - 855A4E85219AFA8200B6E90B /* UnderlinedTextField.swift */, - 855A4E91219B0C5600B6E90B /* UnderlinedButton.swift */, + 3A07C5B121AED3FC00AE3429 /* SecureCode */, + 3A07C5AE21AED30300AE3429 /* TextInput */, + 3A07C5AD21AED2F800AE3429 /* Button */, ); path = Controls; sourceTree = ""; @@ -387,7 +429,6 @@ children = ( 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */, 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */, - 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */, ); path = TextInput; sourceTree = ""; @@ -513,12 +554,14 @@ files = ( 8514D4DF20EE2D970002378A /* InteractiveCellViewModel.swift in Sources */, 8541995C21A2C2B7004009F7 /* ProgressHUD.swift in Sources */, + 3A07C5B921AEDF7800AE3429 /* UITextInput.swift in Sources */, 8514D51720EE40540002378A /* NynjaContextMenu.swift in Sources */, 8514D4E720EE2D970002378A /* XibInitializable.swift in Sources */, 855A4E8A219AFB9D00B6E90B /* InputsCachePolicy.swift in Sources */, 8514D4E820EE2D970002378A /* UITableView+ViewModels.swift in Sources */, 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */, 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, + 3A07C5B521AED3FC00AE3429 /* SecureCodeInputView.swift in Sources */, 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, 855A4E86219AFA8200B6E90B /* UnderlinedTextField.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, @@ -547,6 +590,7 @@ 85EB37F621832D41003A2D6F /* RecordingIndicatorView.swift in Sources */, 8514D51520EE40540002378A /* NynjaContextMenuItemCollectionViewCell.swift in Sources */, 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */, + 3A07C5B421AED3FC00AE3429 /* SecureCodeContainerView.swift in Sources */, 8514D4E120EE2D970002378A /* SelectableCellViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/TextInputUtils.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/TextInput/TextInputUtils.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/TextInputUtils.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/TextInput/TextInputUtils.swift diff --git a/Nynja/Library/UI/TextInput/UITextInput+Cursor.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/TextInput/UITextInput.swift similarity index 57% rename from Nynja/Library/UI/TextInput/UITextInput+Cursor.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/TextInput/UITextInput.swift index 89d0c94a7..98f1591bc 100644 --- a/Nynja/Library/UI/TextInput/UITextInput+Cursor.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/TextInput/UITextInput.swift @@ -1,8 +1,8 @@ // -// UITextInput+Cursor.swift -// Nynja +// UITextInput.swift +// NynjaUIKit // -// Created by Anton Poltoratskyi on 29.04.2018. +// Created by Anton Poltoratskyi on 11/28/18. // Copyright © 2018 TecSynt Solutions. All rights reserved. // @@ -10,7 +10,7 @@ import UIKit extension UITextInput { - var cursorPosition: Int? { + public var cursorPosition: Int? { get { guard let selectedRange = selectedTextRange else { return nil @@ -27,4 +27,16 @@ extension UITextInput { } } } + + public func moveCursorToStart() { + cursorPosition = 0 + } + + public func moveCursorToEnd() { + guard let range = textRange(from: beginningOfDocument, to: endOfDocument), let text = self.text(in: range) else { + cursorPosition = 0 + return + } + cursorPosition = text.isEmpty ? 0 : text.endIndex.encodedOffset + } } diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedButton.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/Button/UnderlinedButton.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedButton.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/Button/UnderlinedButton.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeContainerView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeContainerView.swift new file mode 100644 index 000000000..159e09873 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeContainerView.swift @@ -0,0 +1,169 @@ +// +// SecureCodeContainerView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +public final class SecureCodeContainerView: BaseView { + + public var appearance: Appearance = .default { + didSet { + setupAppearance() + } + } + + public var fullFillHandler: ((String, Bool) -> Void)? { + didSet { + codeInputView.fullFillHandler = fullFillHandler + } + } + + public var currentInputText: String { + return codeInputView.inputText + } + + + // MARK: - Views + + public private(set) lazy var titleLabel: UILabel = { + let label = UILabel() + addSubview(label) + return label + }() + + private lazy var codeInputView: SecureCodeInputView = { + let inputView = SecureCodeInputView() + addSubview(inputView) + return inputView + }() + + public private(set) lazy var descriptionLabel: UILabel = { + let label = UILabel() + addSubview(label) + return label + }() + + + // MARK: - Setup + + public override func baseSetup() { + super.baseSetup() + setupAppearance() + setupLayout() + } + + private func setupLayout() { + titleLabel.textAlignment = .center + titleLabel.setContentHuggingPriority(.required, for: .vertical) + titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + titleLabel.snp.makeConstraints { maker in + maker.top.equalToSuperview() + maker.left.right.equalToSuperview() + } + + codeInputView.snp.makeConstraints { maker in + let top = Constraints.codeInputView.top.adjustedByWidth + let height = Constraints.codeInputView.height.adjustedByWidth + + maker.top.equalTo(titleLabel.snp.bottom).offset(top) + maker.left.greaterThanOrEqualToSuperview() + maker.right.lessThanOrEqualToSuperview() + maker.centerX.equalToSuperview() + maker.height.equalTo(height) + } + + descriptionLabel.numberOfLines = 0 + descriptionLabel.textAlignment = .center + descriptionLabel.setContentHuggingPriority(.required, for: .vertical) + descriptionLabel.setContentCompressionResistancePriority(.required, for: .vertical) + descriptionLabel.snp.makeConstraints { maker in + let top = Constraints.codeInputView.bottom.adjustedByWidth + + maker.top.equalTo(codeInputView.snp.bottom).offset(top) + maker.left.right.bottom.equalToSuperview() + } + } + + private func setupAppearance() { + titleLabel.font = appearance.titleFont + titleLabel.textColor = appearance.titleColor + codeInputView.tintColor = appearance.tintColor + codeInputView.textColor = appearance.textColor + codeInputView.font = appearance.textFont + descriptionLabel.font = appearance.descriptionFont + descriptionLabel.textColor = appearance.descriptionColor + } + + + // MARK: - Actions + + public func beginEditing() { + codeInputView.beginEditing() + } + + + // MARK: - Layout + + private enum Constraints { + + enum codeInputView { + static let top = 16.0 + static let height = 64.0 + static let bottom = 8.0 + } + } +} + +// TODO: move Testable protocol to NynjaUIKit +extension SecureCodeContainerView { + + func setupTestingKeys() { + titleLabel.accessibilityIdentifier = "code_input_title_label" + codeInputView.accessibilityIdentifier = "code_input_container" + descriptionLabel.accessibilityIdentifier = "code_input_description_label" + } +} + +extension SecureCodeContainerView { + + public struct Appearance { + public let tintColor: UIColor + public let titleFont: UIFont + public let titleColor: UIColor + public let textFont: UIFont + public let textColor: UIColor + public let descriptionFont: UIFont + public let descriptionColor: UIColor + + public init(tintColor: UIColor, + titleFont: UIFont, + titleColor: UIColor, + textFont: UIFont, + textColor: UIColor, + descriptionFont: UIFont, + descriptionColor: UIColor) { + self.tintColor = tintColor + self.titleFont = titleFont + self.titleColor = titleColor + self.textFont = textFont + self.textColor = textColor + self.descriptionFont = descriptionFont + self.descriptionColor = descriptionColor + } + + static var `default`: Appearance { + return Appearance(tintColor: .red, + titleFont: .systemFont(ofSize: 16, weight: .medium), + titleColor: .white, + textFont: .systemFont(ofSize: 16), + textColor: .white, + descriptionFont: .systemFont(ofSize: 16, weight: .medium), + descriptionColor: .lightGray) + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeInputView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeInputView.swift new file mode 100644 index 000000000..5205967cc --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeInputView.swift @@ -0,0 +1,282 @@ +// +// SecureCodeInputView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class SecureCodeInputView: BaseView { + + var font: UIFont = UIFont.systemFont(ofSize: 16) { + didSet { + setupFont() + } + } + + var textColor: UIColor = UIColor.white { + didSet { + setupTextColor() + } + } + + var spacing: CGFloat = CGFloat(8.0).adjustedByWidth { + didSet { + stackContainer.spacing = spacing + } + } + + var itemsCount: Int = 6 { + didSet { + stackContainer.arrangedSubviews.forEach { + stackContainer.removeArrangedSubview($0) + } + setupItems() + } + } + + var fullFillHandler: ((String, Bool) -> Void)? { + didSet { + textController.allFieldsFilledAction = fullFillHandler + } + } + + var inputText: String { + return textController.fullText + } + + override var tintColor: UIColor! { + didSet { + setupTintColor() + } + } + + // MARK: - Views + + private lazy var stackContainer: UIStackView = { + let stackContainer = UIStackView() + stackContainer.axis = .horizontal + stackContainer.distribution = .fillEqually + stackContainer.spacing = spacing + + addSubview(stackContainer) + stackContainer.snp.makeConstraints { maker in + maker.left.right.equalToSuperview() + maker.top.greaterThanOrEqualToSuperview() + maker.bottom.lessThanOrEqualToSuperview() + maker.centerY.equalToSuperview() + } + + return stackContainer + }() + + private var inputFields: [UnderlinedTextField] = [] + + private let textController = TextFieldsController() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + setupItems() + setupTintColor() + setupFont() + setupTextColor() + } + + private func setupItems() { + for index in 0.. UnderlinedTextField { + let field = UnderlinedTextField() + + field.delegate = textController + + // Security rules + field.shouldResetAfterBackground = true + field.shouldCacheInputs = false + + // Appearance + field.lineWidth = Constraints.lineWidth + field.lineOffset = Constraints.lineOffset + field.underlineColor = tintColor + field.highlighedUnderlineColor = tintColor + field.keyboardType = .numberPad + field.textAlignment = .center + + return field + } + + + // MARK: - Actions + + func beginEditing() { + _ = inputFields.first?.textField.becomeFirstResponder() + } + + + // MARK: - Layout + + private enum Constraints { + static let itemWidth: CGFloat = 44.0 + static let lineWidth: CGFloat = 2.0 + static let lineOffset: CGFloat = 1.0 + } +} + +// MARK: - UITextFieldDelegate + +extension SecureCodeInputView { + + final class TextFieldsController: NSObject, UITextFieldDelegate { + + private var fields: [UITextField] = [] + + var allFieldsFilledAction: ((_ code: String, _ fullfilled: Bool) -> Void)? + + var fullText: String { + return fields.compactMap { $0.text }.joined() + } + + func add(fields: [UITextField]) { + self.fields.forEach { $0.delegate = nil } + self.fields = fields + self.fields.forEach { $0.delegate = self } + } + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + return true + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + textField.moveCursorToEnd() + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + defer { notify() } + + // 'string' can be empty only on deleting. + if string.isEmpty { + handleDelete(on: textField, string: string) + return false + } + + // check paste or autofill + if string.count > 1 { + handlePasted(text: string, on: textField) + return false + } + + let currentText = textField.text ?? "" + let result = (currentText as NSString).replacingCharacters(in: range, with: string) + + if result.isEmpty { + handleDelete(on: textField, string: string) + + } else if result.count == 1 { + handleReplace(on: textField, string: string) + + } else { + // 'textField' is filled, so need to move to the next field. + handleNextInput(on: textField, string: string) + } + + return false + } + + private func handlePasted(text: String, on currentField: UITextField) { + let skipCount = max(fields.count - text.count, 0) + + for (textField, character) in zip(fields.dropFirst(skipCount), text) { + textField.text = String(character) + } + } + + private func handleDelete(on textField: UITextField, string: String) { + textField.text = string + fields.previous(before: textField)?.becomeFirstResponder() + } + + private func handleReplace(on textField: UITextField, string: String) { + textField.text = string + } + + private func handleNextInput(on textField: UITextField, string: String) { + guard let nextField = fields.next(after: textField) else { + return + } + nextField.text = string + nextField.becomeFirstResponder() + } + + private func notify() { + let isAllFieldsFilled = !fields.contains { $0.text == nil || $0.text!.isEmpty } + allFieldsFilledAction?(fullText, isAllFieldsFilled) + } + } +} + +// TODO: move to Core +fileprivate extension Array where Element: Equatable { + + func next(after element: Element) -> Element? { + guard let indexOfCurrent = index(of: element) else { + return nil + } + + let indexForNewElement = index(after: indexOfCurrent) + + if indexForNewElement <= count - 1 { + return self[indexForNewElement] + } else { + return nil + } + } + + func previous(before element: Element) -> Element? { + guard let indexOfCurrent = index(of: element) else { + return nil + } + + let indexForNewElement = index(before: indexOfCurrent) + + if indexForNewElement >= 0 { + return self[indexForNewElement] + } else { + return nil + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/TextField.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextField.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/TextField.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/UnderlinedTextField.swift similarity index 80% rename from Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/UnderlinedTextField.swift index 7a99724af..038be962e 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/UnderlinedTextField.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/UnderlinedTextField.swift @@ -9,7 +9,7 @@ import UIKit import SnapKit -open class UnderlinedTextField: BaseView { +open class UnderlinedTextField: BaseView, InputsCachePolicy { public var underlineColor: UIColor = .lightGray { didSet { @@ -35,6 +35,11 @@ open class UnderlinedTextField: BaseView { } } + open override var tintColor: UIColor! { + get { return textField.tintColor } + set { textField.tintColor = newValue } + } + public var text: String? { get { return textField.text } set { textField.text = newValue } @@ -50,6 +55,11 @@ open class UnderlinedTextField: BaseView { set { textField.font = newValue } } + public var textAlignment: NSTextAlignment { + get { return textField.textAlignment } + set { textField.textAlignment = newValue } + } + public var placeholder: String? { get { return textField.placeholder } set { textField.placeholder = newValue } @@ -60,12 +70,17 @@ open class UnderlinedTextField: BaseView { set { textField.attributedPlaceholder = newValue } } + public var autocorrectionType: UITextAutocorrectionType { + get { return textField.autocorrectionType } + set { textField.autocorrectionType = newValue } + } + public var textContentType: UITextContentType! { get { return textField.textContentType } set { textField.textContentType = newValue } } - public var keyboardType: UIKeyboardType! { + public var keyboardType: UIKeyboardType { get { return textField.keyboardType } set { textField.keyboardType = newValue } } @@ -80,10 +95,16 @@ open class UnderlinedTextField: BaseView { set { textField.prohibitedOptions = newValue } } + public var shouldResetAfterBackground: Bool { + get { return textField.shouldResetAfterBackground } + set { textField.shouldResetAfterBackground = newValue } + } + + // MARK: - Views - public private(set) lazy var textField: TextField = { - let textField = TextField() + public private(set) lazy var textField: T = { + let textField = T() textField.setContentHuggingPriority(.required, for: .vertical) textField.setContentCompressionResistancePriority(.required, for: .vertical) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 9b0344e10..b35a1f107 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1183,7 +1183,7 @@ 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */; }; 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BD2092368600E4840C /* StickerDataSource.swift */; }; 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */; }; - 85739FBD2190AAC3001C4EC8 /* AuthConfirmationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* AuthConfirmationData.swift */; }; + 85739FBD2190AAC3001C4EC8 /* ConfirmationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* ConfirmationData.swift */; }; 85788C3C204422FB003600C9 /* BuildNumberProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */; }; 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C412044237B003600C9 /* BuildNumberViewController.swift */; }; 85788C4420442385003600C9 /* BuildNumberPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C4320442385003600C9 /* BuildNumberPresenter.swift */; }; @@ -1211,7 +1211,6 @@ 8580BADE20BD997600239D9D /* MentionCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BADD20BD997500239D9D /* MentionCounterView.swift */; }; 8580BAE120BD99D200239D9D /* InputBar+Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BADF20BD99D100239D9D /* InputBar+Mentions.swift */; }; 8580BAE220BD99D200239D9D /* InputContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE020BD99D200239D9D /* InputContent.swift */; }; - 8580BAE420BD99DD00239D9D /* UITextInput+Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE320BD99DC00239D9D /* UITextInput+Cursor.swift */; }; 8580BAE720BD9A5600239D9D /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; 8580BAEC20BD9A7100239D9D /* LinkRecognizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE920BD9A7100239D9D /* LinkRecognizable.swift */; }; 8580BAED20BD9A7100239D9D /* LinkLongPressGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAEA20BD9A7100239D9D /* LinkLongPressGestureRecognizer.swift */; }; @@ -3535,7 +3534,7 @@ 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionDataSource.swift; sourceTree = ""; }; 8572C3BD2092368600E4840C /* StickerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDataSource.swift; sourceTree = ""; }; 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileField.swift; sourceTree = ""; }; - 85739FBC2190AAC3001C4EC8 /* AuthConfirmationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationData.swift; sourceTree = ""; }; + 85739FBC2190AAC3001C4EC8 /* ConfirmationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationData.swift; sourceTree = ""; }; 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberProtocols.swift; sourceTree = ""; }; 85788C412044237B003600C9 /* BuildNumberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberViewController.swift; sourceTree = ""; }; 85788C4320442385003600C9 /* BuildNumberPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberPresenter.swift; sourceTree = ""; }; @@ -3563,7 +3562,6 @@ 8580BADD20BD997500239D9D /* MentionCounterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionCounterView.swift; sourceTree = ""; }; 8580BADF20BD99D100239D9D /* InputBar+Mentions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "InputBar+Mentions.swift"; sourceTree = ""; }; 8580BAE020BD99D200239D9D /* InputContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputContent.swift; sourceTree = ""; }; - 8580BAE320BD99DC00239D9D /* UITextInput+Cursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextInput+Cursor.swift"; sourceTree = ""; }; 8580BAE620BD9A5600239D9D /* SeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; }; 8580BAE920BD9A7100239D9D /* LinkRecognizable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkRecognizable.swift; sourceTree = ""; }; 8580BAEA20BD9A7100239D9D /* LinkLongPressGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkLongPressGestureRecognizer.swift; sourceTree = ""; }; @@ -8008,7 +8006,7 @@ 5EEB73B0216046EA00D8ECE6 /* Entities */ = { isa = PBXGroup; children = ( - 85739FBC2190AAC3001C4EC8 /* AuthConfirmationData.swift */, + 85739FBC2190AAC3001C4EC8 /* ConfirmationData.swift */, ); path = Entities; sourceTree = ""; @@ -11198,7 +11196,6 @@ A43B257120AB1DFA00FF8107 /* TextInput */ = { isa = PBXGroup; children = ( - 8580BAE320BD99DC00239D9D /* UITextInput+Cursor.swift */, A432CF0320B4347C00993AFB /* Material */, A43B257220AB1DFA00FF8107 /* InputBar */, A43B257920AB1DFA00FF8107 /* TextView */, @@ -16096,7 +16093,7 @@ 3A27B0A71EF307A900B4B3CB /* DeleteUserModel.swift in Sources */, 3A1F74FA1F5ED344009A11E4 /* PushService.swift in Sources */, FEA656042167777F00B44029 /* WalletBalancesInteractor.swift in Sources */, - 85739FBD2190AAC3001C4EC8 /* AuthConfirmationData.swift in Sources */, + 85739FBD2190AAC3001C4EC8 /* ConfirmationData.swift in Sources */, 261F2E2E200EB0AD007D0813 /* RepliesVC+CellDelegate.swift in Sources */, 4B0CC1FD2195B52000E0BA61 /* IoHandlerDelegate.swift in Sources */, A45F110620B4218D00F45004 /* MessageConfiguration.swift in Sources */, @@ -16752,7 +16749,6 @@ E7A3DAB51F9DEAC400856133 /* ProfileSectionModel.swift in Sources */, 2603139F20A0A4BA009AC66D /* ChatLanguageSettingsWireframe.swift in Sources */, E7C9CECA1FCC27A30090C2E0 /* FeedProtocol.swift in Sources */, - 8580BAE420BD99DD00239D9D /* UITextInput+Cursor.swift in Sources */, A4F3DAA32084935400FF71C7 /* Constants.swift in Sources */, E74FD69D1FC5D06200656611 /* TransactionObserverExtension.swift in Sources */, B7121EB8205045F300AABBE6 /* MediaDownloadManager.swift in Sources */, diff --git a/Nynja/Library/UI/BaseVC/LoadingInteractive.swift b/Nynja/Library/UI/BaseVC/LoadingInteractive.swift index 23e6d7fd1..69ad07c9a 100644 --- a/Nynja/Library/UI/BaseVC/LoadingInteractive.swift +++ b/Nynja/Library/UI/BaseVC/LoadingInteractive.swift @@ -6,12 +6,35 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol LoadingInteractiveView: class { - func showSpinner() - func hideSpinner() -} +import NynjaUIKit protocol LoadingInteractive: class { - func showHUD() - func hideHUD() + func showLoading() + func hideLoading() +} + +protocol LoadingDisplayable: LoadingInteractive { + var progressHUD: ProgressHUD { get } +} + +extension LoadingDisplayable { + + func showLoading() { + progressHUD.startAnimating() + } + + func hideLoading() { + progressHUD.stopAnimating() + } + + func makeProgressHUD(on container: UIView) -> ProgressHUD { + let progressHUD = ProgressHUD() + + container.addSubview(progressHUD) + progressHUD.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return progressHUD + } } diff --git a/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift b/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift index abec040d4..2a941388d 100644 --- a/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift +++ b/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift @@ -17,6 +17,14 @@ extension UIFont { private static var fontsCache: [FontInitialValues: UIFont] = [:] + static func makeFont(with fontName: String, width: Int = 100, height: Int) -> UIFont? { + return makeFont(with: fontName, width: CGFloat(width), height: CGFloat(height)) + } + + static func makeFont(with fontName: String, width: Double = 100, height: Double) -> UIFont? { + return makeFont(with: fontName, width: CGFloat(width), height: CGFloat(height)) + } + static func makeFont(with fontName: String, width: CGFloat = 100, height: CGFloat) -> UIFont? { let values = FontInitialValues(fontName: fontName, height: height, width: width) diff --git a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift index b9e408445..ddb2d984a 100644 --- a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift +++ b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift @@ -7,6 +7,7 @@ // import Foundation import SnapKit +import NynjaUIKit protocol InputBarDelegate: class { func didPlayTapped(_ model: AudioPlayable) diff --git a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift index c2a1b0fbc..fa2d1583d 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift @@ -11,9 +11,10 @@ import UIKit protocol AuthProviderWireframeProtocol: class { func dismiss() func selectCountry(completion: @escaping (Result) -> Void) + func confirm(data: ConfirmationData) } -protocol AuthProviderViewInput: class where Self: UIViewController { +protocol AuthProviderViewInput: LoadingInteractive where Self: UIViewController { var screenTitle: String? { get set } @@ -38,7 +39,12 @@ protocol AuthProviderPresenterProtocol: BasePresenterProtocol, NavigationProtoco protocol AuthProviderInteractorInput: class { func fetchDefaultCountry() -> Country func fetchCountry(by code: String) -> Country? + + func addEmailProvider(_ email: String) + func addPhoneNumberProvider(_ phoneNumber: PhoneNumberInfo) } protocol AuthProviderInteractorOutput: class { + func didAddAuthProvider(with confirmationData: ConfirmationData) + func didReceiveFailure(_ error: Error?) } diff --git a/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift b/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift index 4b576718b..8697c14f8 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift @@ -39,4 +39,12 @@ final class AuthProviderInteractor: BaseInteractor, AuthProviderInteractorInput func fetchCountry(by code: String) -> Country? { return countriesProvider.fetchCountries().first { $0.code == code } } + + func addEmailProvider(_ email: String) { + presenter?.didAddAuthProvider(with: .email(email)) + } + + func addPhoneNumberProvider(_ phoneNumber: PhoneNumberInfo) { + presenter?.didAddAuthProvider(with: .phoneNumber(phoneNumber)) + } } diff --git a/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift b/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift index 7789fe32c..d2e41c0da 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift @@ -24,6 +24,10 @@ final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, private var phoneNumberController: PhoneNumberTextController? + private var selectedCountry: Country? { + return phoneNumberController?.country + } + // MARK: - Init @@ -100,11 +104,22 @@ final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, } func next(inputText: String) { + let inputText = inputText.trimmed() + switch authProvider { case .email: - break + view?.showLoading() + interactor.addEmailProvider(inputText) + case .phoneNumber: - break + guard let country = selectedCountry else { + return + } + view?.showLoading() + + let rawNumber = inputText.replacingOccurrences(of: " ", with: "") + let phoneNumber = PhoneNumberInfo(country: country, number: rawNumber) + interactor.addPhoneNumberProvider(phoneNumber) } } @@ -118,12 +133,21 @@ final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, } phoneNumberController?.country = country - let phoneNumberInfo = PhoneNumberInfo(country: country, number: autofillInfo.phoneNumber) - view?.update(phone: phoneNumberInfo) + let phoneNumber = PhoneNumberInfo(country: country, number: autofillInfo.phoneNumber) + view?.update(phone: phoneNumber) } // MARK: - Interactor Output + + func didAddAuthProvider(with confirmationData: ConfirmationData) { + view?.hideLoading() + wireframe.confirm(data: confirmationData) + } + + func didReceiveFailure(_ error: Error?) { + view?.hideLoading() + } } // MARK: - Injection diff --git a/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift b/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift index 765b6f600..658781f33 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift @@ -8,12 +8,16 @@ import UIKit import SnapKit +import NynjaUIKit -final class AuthProviderViewController: BaseVC, AuthProviderViewInput { +final class AuthProviderViewController: BaseVC, AuthProviderViewInput, LoadingDisplayable { + + private let presenter: AuthProviderPresenterProtocol - private var presenter: AuthProviderPresenterProtocol // MARK: - Views + + private(set) lazy var progressHUD = makeProgressHUD(on: view) private lazy var formContainer: UIView = { let top = Constraints.formContainer.top.adjustedByWidth diff --git a/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift b/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift index dd7ad777c..7fc8fd59e 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift @@ -32,6 +32,7 @@ final class AuthProviderWireframe: Wireframe, AuthProviderWireframeProtocol { enum State { case dismiss case selectCountry(callback: (Result) -> Void) + case confirmProvider(ConfirmationData) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -56,4 +57,8 @@ final class AuthProviderWireframe: Wireframe, AuthProviderWireframeProtocol { func selectCountry(completion: @escaping (Result) -> Void) { coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) } + + func confirm(data: ConfirmationData) { + coordinator.wireframe(self, didEndWithState: .confirmProvider(data)) + } } diff --git a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift index 4e7248689..f7fced653 100644 --- a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift +++ b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift @@ -71,6 +71,14 @@ extension LoginOptionsCoordinator: AuthProviderCoordinatorProtocol { ) navigation?.pushViewController(view, animated: true) + + case let .confirmProvider(confirmationData): + let wireframe = CodeConfirmationWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(confirmationData: confirmationData), + dependencies: .init(authService: serviceFactory.makeAuthService()) + ) + navigation?.pushViewController(view, animated: true) } } } @@ -90,3 +98,19 @@ extension LoginOptionsCoordinator: CountrySelectorCoordinatorProtocol { navigation?.popViewController(animated: true) } } + +// MARK: - Code Confirmation + +extension LoginOptionsCoordinator: CodeConfirmationCoordinatorProtocol { + + func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { + switch state { + case .back: + navigation?.popViewController(animated: true) + case let .validCode(type): + break + case .invalidCode: + break + } + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift b/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift index a67cddee1..fcdfa46be 100644 --- a/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift +++ b/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift @@ -23,9 +23,11 @@ final class LoginOptionsPresenter: BasePresenter, LoginOptionsPresenterProtocol, } private lazy var addLoginOptionItem: ActionRowItem = { + let fontHeight = CGFloat(22).adjustedByWidth + return ActionRowItem( text: String.localizable.loginOptionsAddLabel, - font: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 22)!, + font: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: fontHeight)!, textColor: UIColor.nynja.white, icon: UIImage.nynja.icAdd.image, height: rowHeight, @@ -34,8 +36,10 @@ final class LoginOptionsPresenter: BasePresenter, LoginOptionsPresenterProtocol, }() private lazy var textDescriptionItem: TextRowItem = { + let fontHeight = CGFloat(20).adjustedByWidth + return TextRowItem(text: String.localizable.loginOptionsDescriptionLabel, - font: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 20)!, + font: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: fontHeight)!, textColor: UIColor.nynja.manatee, backgroundColor: UIColor.nynja.clear, edges: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)) @@ -76,7 +80,9 @@ final class LoginOptionsPresenter: BasePresenter, LoginOptionsPresenterProtocol, } private func makeRowItems(for loginOptions: [LoginOption]) -> [LoginOptionSwitchRowItem] { - let font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 22)! + let fontHeight = CGFloat(22).adjustedByWidth + + let font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: fontHeight)! let textColor = UIColor.nynja.white let icon = UIImage.nynja.icPhone.image diff --git a/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift b/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift index b455f0e6a..f92e615bd 100644 --- a/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift +++ b/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift @@ -20,13 +20,15 @@ protocol AddContactByUsernameWireFrameProtocol: class { func showMyProfile() } -protocol AddContactByUsernameViewProtocol: LoadingInteractiveView { +protocol AddContactByUsernameViewProtocol: class { var presenter: AddContactByUsernamePresenterProtocol! { get set } /** * Add here your methods for communication PRESENTER -> VIEW */ + func showSpinner() + func hideSpinner() } protocol AddContactByUsernamePresenterProtocol: BasePresenterProtocol { diff --git a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift index b2dca91d3..28f8c0d0f 100644 --- a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift +++ b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift @@ -78,14 +78,14 @@ final class AddContactByUsernameInteractor: AddContactByUsernameInteractorInputP return } if mqttService.isConnectedSuccess { - presenter.showHUD() + presenter.showLoading() } searchAction() } private func finishSearch() { searchAction = nil - presenter.hideHUD() + presenter.hideLoading() } @@ -99,7 +99,7 @@ final class AddContactByUsernameInteractor: AddContactByUsernameInteractorInputP func mqttServiceDidDisconnect(_ mqttService: MQTTService) { DispatchQueue.main.async { - self.presenter.hideHUD() + self.presenter.hideLoading() } } } diff --git a/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift b/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift index b8ae4b2e3..e93c77965 100644 --- a/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift +++ b/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift @@ -32,11 +32,11 @@ final class AddContactByUsernamePresenter: BasePresenter, AddContactByUsernamePr wireFrame.showMyProfile() } - func showHUD() { + func showLoading() { view.showSpinner() } - func hideHUD() { + func hideLoading() { view.hideSpinner() } diff --git a/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift b/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift index ffe2ec8d4..87fec6d64 100644 --- a/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift +++ b/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift @@ -25,7 +25,7 @@ protocol AddContactViaPhoneWireFrameOutput: class { func didSelectCountry(_ country: Country) } -protocol AddContactViaPhoneViewProtocol: LoadingInteractiveView { +protocol AddContactViaPhoneViewProtocol: class { var presenter: AddContactViaPhonePresenterProtocol! { get set } @@ -34,6 +34,8 @@ protocol AddContactViaPhoneViewProtocol: LoadingInteractiveView { */ func setupSelectedCountry(_ country: Country) + func showSpinner() + func hideSpinner() } protocol AddContactViaPhonePresenterProtocol: BasePresenterProtocol { diff --git a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift index 59f21af37..032292783 100644 --- a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift +++ b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift @@ -79,14 +79,14 @@ final class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProto return } if mqttService.isConnectedSuccess { - presenter.showHUD() + presenter.showLoading() } searchAction() } private func finishSearch() { searchAction = nil - presenter.hideHUD() + presenter.hideLoading() } @@ -100,7 +100,7 @@ final class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProto func mqttServiceDidDisconnect(_ mqttService: MQTTService) { DispatchQueue.main.async { - self.presenter.hideHUD() + self.presenter.hideLoading() } } } diff --git a/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift b/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift index cf87b2270..94cb8f25d 100644 --- a/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift +++ b/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift @@ -36,11 +36,11 @@ final class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresen self.wireFrame.showSelectCountry() } - func showHUD() { + func showLoading() { view.showSpinner() } - func hideHUD() { + func hideLoading() { view.hideSpinner() } diff --git a/Nynja/Modules/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Auth Flow/AuthCoordinator.swift index 00366a5d9..9d0735ac2 100644 --- a/Nynja/Modules/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AuthCoordinator.swift @@ -35,6 +35,13 @@ final class AuthCoordinator: Coordinator, NavigationContainer, CountrySelectorCo // googleAuthService: serviceFactory.makeGoogleAuthService(), // countriesProvider: serviceFactory.makeCountriesProvider()) // ) + +// let wireframe = CodeConfirmationWireframe(coordinator: self) +// let view = wireframe.prepareModule( +// parameters: .init(confirmationData: .email("anton.poltoratskyi@gmail.com")), +// dependencies: .init(authService: serviceFactory.makeAuthService()) +// ) +// // navigation?.pushViewController(view, animated: true) } @@ -128,7 +135,7 @@ extension AuthCoordinator { case let .email(email): let wireframe = CodeConfirmationWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: .init(authType: .email(email)), + parameters: .init(confirmationData: .email(email)), dependencies: .init(authService: serviceFactory.makeAuthService()) ) navigation?.pushViewController(view, animated: true) @@ -136,7 +143,7 @@ extension AuthCoordinator { case let .phoneNumber(numberInfo): let wireframe = CodeConfirmationWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: .init(authType: .phoneNumber(numberInfo)), + parameters: .init(confirmationData: .phoneNumber(numberInfo)), dependencies: .init(authService: serviceFactory.makeAuthService()) ) navigation?.pushViewController(view, animated: true) diff --git a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift index 10b3673ea..dc8517348 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift @@ -19,11 +19,9 @@ protocol AuthWireframeProtocol: class { func presentAlert(title: String, message: String, actions: [UIAlertAction]) } -protocol AuthViewProtocol: class where Self: UIViewController { +protocol AuthViewProtocol: LoadingInteractive where Self: UIViewController { func select(country: Country) func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) - func showLoading() - func hideLoading() } protocol AuthPresenterProtocol: class { diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift index 27159247a..a408e4ce3 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift @@ -9,7 +9,7 @@ import UIKit import NynjaUIKit -final class AuthViewController: UIViewController, AuthViewProtocol, InitializeInjectable, KeyboardInteractive { +final class AuthViewController: UIViewController, AuthViewProtocol, InitializeInjectable, KeyboardInteractive, LoadingDisplayable { private let presenter: AuthPresenterProtocol @@ -27,16 +27,7 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn return backgroundImageView }() - private lazy var progressHUD: ProgressHUD = { - let progressHUD = ProgressHUD() - - view.addSubview(progressHUD) - progressHUD.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - return progressHUD - }() + private(set) lazy var progressHUD = makeProgressHUD(on: view) // MARK: Scroll Container @@ -271,14 +262,6 @@ extension AuthViewController { phoneNumberTextController.country = autofillPhoneNumberInfo.country phoneNumberLoginView.updatePhone(autofillPhoneNumberInfo) } - - func showLoading() { - progressHUD.startAnimating() - } - - func hideLoading() { - progressHUD.stopAnimating() - } } // MARK: - KeyboardInteractive diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift index 528e739bb..0bec7f77a 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -177,8 +177,8 @@ private extension PhoneNumberLoginView { return container } - func makePhoneNumberTextField() -> UnderlinedTextField { - let textField = UnderlinedTextField() + func makePhoneNumberTextField() -> UnderlinedTextField { + let textField = UnderlinedTextField() textField.prohibitedOptions = .all textField.underlineColor = UIColor.nynja.dustyGray diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift index cdbc17ceb..70a95c52c 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift @@ -34,7 +34,7 @@ protocol CodeConfirmationPresenterProtocol: NavigationProtocol { protocol CodeConfirmationInputInteractorProtocol: class { var address: String { get } - var confirmationData: AuthConfirmationData { get } + var confirmationData: ConfirmationData { get } func sendConfirmationCode(_ code: String) func resendCode() diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthConfirmationData.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift similarity index 78% rename from Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthConfirmationData.swift rename to Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift index cb434c68b..8e0b5ad41 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/AuthConfirmationData.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift @@ -1,5 +1,5 @@ // -// AuthConfirmationData.swift +// ConfirmationData.swift // Nynja // // Created by Anton Poltoratskyi on 05.11.2018. @@ -8,7 +8,7 @@ import Foundation -enum AuthConfirmationData { +enum ConfirmationData { case email(String) case phoneNumber(PhoneNumberInfo) } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index 0bd640ce0..f22df1f79 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -12,7 +12,7 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, private weak var presenter: CodeConfirmationOutputInteractorProtocol? - let confirmationData: AuthConfirmationData + let confirmationData: ConfirmationData var address: String { switch confirmationData { @@ -33,13 +33,13 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, struct Dependencies { let presenter: CodeConfirmationOutputInteractorProtocol - let authProviderType: AuthConfirmationData + let confirmationData: ConfirmationData let authService: AuthService } init(dependencies: Dependencies) { self.presenter = dependencies.presenter - self.confirmationData = dependencies.authProviderType + self.confirmationData = dependencies.confirmationData self.authService = dependencies.authService } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift index 2b16cef41..9c05c8622 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -10,28 +10,55 @@ import UIKit import NynjaUIKit final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, InitializeInjectable { + private let viewsFactory: CodeConfirmationViewsFactoryProtocol + private let presenter: CodeConfirmationPresenterProtocol // MARK: - Views private lazy var backButton: UIButton = viewsFactory.makeBackButton(on: view, target: self, selector: #selector(back(sender:))) - private lazy var welcomeLabel: UILabel = viewsFactory.makeWelcomeLabel(on: view) - private lazy var logoImageView: UIImageView = viewsFactory.makeLogoImageView(on: view, top: welcomeLabel) - private lazy var addressLabel: UILabel = viewsFactory.makeAddressLabel(on: view, top: logoImageView) - - private lazy var textFieldsContainer: UIView = viewsFactory.makeTextFieldsContainer(on: view, top: addressLabel) - private let textFieldsController: TextFieldsController - private lazy var textField1: UITextField = viewsFactory.makeFirstTextField(on: textFieldsContainer) - private lazy var textField2: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField1) - private lazy var textField3: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField2) - private lazy var textField4: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField3) - private lazy var textField5: UITextField = viewsFactory.makeMiddleTextField(on: textFieldsContainer, left: textField4) - private lazy var textField6: UITextField = viewsFactory.makeLastTextField(on: textFieldsContainer, left: textField5) + private lazy var headerView: AuthHeaderView = { + let headerView = AuthHeaderView() + headerView.configure() + + view.addSubview(headerView) + headerView.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + } + + return headerView + }() - private lazy var descriptionLabel: UILabel = viewsFactory.makeDescriptionLabel(on: view, top: textFieldsContainer) + private lazy var codeInputView: SecureCodeContainerView = { + + let titleFontHeight = Constraints.titleLabel.fontHeight.adjustedByWidth + let textFontHeight = Constraints.codeInputView.fontHeight.adjustedByWidth + let descriptionFontHeight = Constraints.descriptionLabel.fontHeight.adjustedByWidth + + let codeInputView = SecureCodeContainerView() + codeInputView.appearance = SecureCodeContainerView.Appearance( + tintColor: UIColor.nynja.mainRed, + titleFont: UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: titleFontHeight)!, + titleColor: UIColor.nynja.white, + textFont: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: textFontHeight)!, + textColor: UIColor.nynja.white, + descriptionFont: UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: descriptionFontHeight)!, + descriptionColor: UIColor.nynja.manatee + ) + + view.addSubview(codeInputView) + codeInputView.snp.makeConstraints { maker in + maker.top.equalTo(headerView.snp.bottom).offset(Constraints.codeInputView.top.adjustedByWidth) + maker.centerX.equalToSuperview() + maker.left.greaterThanOrEqualToSuperview() + maker.right.lessThanOrEqualToSuperview() + } + + return codeInputView + }() private lazy var timerLabel: UILabel = viewsFactory.makeTimerLabel(on: view) @@ -55,7 +82,6 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi init(dependencies: Dependencies) { presenter = dependencies.presenter viewsFactory = dependencies.viewsFactory - textFieldsController = TextFieldsController() super.init(nibName: nil, bundle: nil) } @@ -77,23 +103,18 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi override func viewDidLoad() { super.viewDidLoad() - _ = [backButton, welcomeLabel, logoImageView, addressLabel, textFieldsContainer, textField1, textField2, textField3, textField4, textField5, textField6, descriptionLabel, timerLabel, resendCodeButton, callMeButton] + _ = [backButton, headerView, codeInputView, timerLabel, resendCodeButton, callMeButton] view.backgroundColor = UIColor.nynja.backgroundColor - addressLabel.text = presenter.address - descriptionLabel.text = presenter.descriptionText - - textField1.becomeFirstResponder() - - view.layoutIfNeeded() - - [textField1, textField2, textField3, textField4, textField5, textField6] - .forEach { $0.appendBottomBorder(color: UIColor.nynja.mainRed, width: 2) } - - textFieldsController.add(textFields: [textField1, textField2, textField3, textField4, textField5, textField6]) - textFieldsController.allFieldsFilledAction = { [weak self] code in - self?.presenter.sendConfirmationCode(code) + codeInputView.titleLabel.text = presenter.address + codeInputView.descriptionLabel.text = presenter.descriptionText + + codeInputView.fullFillHandler = { [weak self] code, isFullfilled in + guard let self = self, isFullfilled else { return } + self.presenter.sendConfirmationCode(code) } + + codeInputView.beginEditing() presenter.viewDidLoad() } @@ -149,69 +170,23 @@ extension CodeConfirmationViewController { } } -// MARK: - Text field controller +// MARK: - Layout private extension CodeConfirmationViewController { - - final class TextFieldsController: NSObject, UITextFieldDelegate { - - private var textFields: [UITextField] = [] - - var allFieldsFilledAction: ((_ code: String) -> Void)? - - func add(textFields: [UITextField]) { - self.textFields.forEach { $0.delegate = nil } - self.textFields = textFields - self.textFields.forEach { $0.delegate = self } - } + + enum Constraints { - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if string.isEmpty { - textFields.previous(before: textField)?.becomeFirstResponder() - textField.text = string - return false - } - - var isNew = false - - if range.location > 0 || range.length > 0 { - if let newTextField = textFields.next(after: textField) { - newTextField.becomeFirstResponder() - newTextField.text = string - isNew = true - } else { - if isAllFieldsFilled() { - allFieldsFilledAction?(allValues()) - } - - textField.text = String(string.last ?? Character("")) - - return false - } - } - - if isAllFieldsFilled() { - allFieldsFilledAction?(allValues()) - } - - if string.count == 1 && !isNew { - textField.text = string - } - - return false + enum titleLabel { + static let fontHeight: CGFloat = 22.0 } - private func isAllFieldsFilled() -> Bool { - return textFields - .filter { ($0.text ?? "").count == 0 } - .count < 1 + enum descriptionLabel { + static let fontHeight: CGFloat = 20.0 } - private func allValues() -> String { - return textFields - .map { $0.text } - .compactMap { $0 } - .joined() + enum codeInputView { + static let top: CGFloat = 10.0 + static let fontHeight: CGFloat = 22.0 } } } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift index 5153d414b..2395c1756 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift @@ -10,14 +10,6 @@ import UIKit protocol CodeConfirmationViewsFactoryProtocol { func makeBackButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeWelcomeLabel(on view: UIView) -> UILabel - func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView - func makeAddressLabel(on view: UIView, top: UIView) -> UILabel - func makeTextFieldsContainer(on view: UIView, top: UIView) -> UIView - func makeFirstTextField(on view: UIView) -> UITextField - func makeMiddleTextField(on view: UIView, left: UIView) -> UITextField - func makeLastTextField(on view: UIView, left: UIView) -> UITextField - func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel func makeTimerLabel(on view: UIView) -> UILabel func makeResendCodeButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton func makeCallMeButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton @@ -40,142 +32,13 @@ final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { return button } - func makeWelcomeLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white - - label.text = String.localizable.codeConfirmationWelcome - - label.snp.makeConstraints { (make) in - make.top.equalToSuperview().offset(70) - make.centerX.equalToSuperview() - } - - return label - } - - func makeLogoImageView(on view: UIView, top: UIView) -> UIImageView { - let imageView = UIImageView() - view.addSubview(imageView) - - imageView.contentMode = .scaleAspectFill - imageView.image = UIImage.nynja.logo2.image - - imageView.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom).offset(16) - make.centerX.equalToSuperview() - make.width.equalToSuperview().multipliedBy(9/20) - } - - return imageView - } - - func makeAddressLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.medium.font(size: 16) - label.textColor = UIColor.nynja.white - - label.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom).offset(42) - make.centerX.equalToSuperview() - } - - return label - } - - func makeTextFieldsContainer(on view: UIView, top: UIView) -> UIView { - let container = UIView() - view.addSubview(container) - container.snp.makeConstraints { (make) in - make.height.equalTo(64) - make.centerX.equalToSuperview() - make.top.equalTo(top.snp.bottom).offset(16) - } - - return container - } - - func makeFirstTextField(on view: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.keyboardType = .numberPad - textField.tintColor = UIColor.nynja.mainRed - textField.textAlignment = .center - textField.textColor = UIColor.nynja.white - - textField.snp.makeConstraints { (make) in - make.left.equalToSuperview() - make.centerY.equalToSuperview() - make.width.equalTo(36) - } - - return textField - } - - func makeMiddleTextField(on view: UIView, left: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.keyboardType = .numberPad - textField.tintColor = UIColor.nynja.mainRed - textField.textAlignment = .center - textField.textColor = UIColor.nynja.white - - textField.snp.makeConstraints { (make) in - make.left.equalTo(left.snp.right).offset(5) - make.centerY.equalToSuperview() - make.width.equalTo(36) - } - - return textField - } - - func makeLastTextField(on view: UIView, left: UIView) -> UITextField { - let textField = UITextField() - view.addSubview(textField) - - textField.keyboardType = .numberPad - textField.tintColor = UIColor.nynja.mainRed - textField.textAlignment = .center - textField.textColor = UIColor.nynja.white - - textField.snp.makeConstraints { (make) in - make.left.equalTo(left.snp.right).offset(5) - make.centerY.equalToSuperview() - make.width.equalTo(36) - make.right.equalToSuperview() - } - - return textField - } - - func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.manatee - - label.snp.makeConstraints { (make) in - make.top.equalTo(top.snp.bottom) - make.centerX.equalToSuperview() - } - return label - } - func makeTimerLabel(on view: UIView) -> UILabel { let label = UILabel() view.addSubview(label) label.textAlignment = .center label.textColor = UIColor.nynja.white - label.font = FontFamily.NotoSans.regular.font(size: 16) + label.font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 16.0.adjustedByWidth) label.snp.makeConstraints { (make) in make.centerX.equalToSuperview() @@ -193,7 +56,7 @@ final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { button.setTitle(String.localizable.codeConfirmationResendCode, for: .normal) button.setTitleColor(UIColor.nynja.mainRed, for: .normal) button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) button.addTarget(target, action: selector, for: .touchUpInside) button.snp.makeConstraints { (make) in @@ -211,13 +74,12 @@ final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { button.setTitle(String.localizable.codeConfirmationCall, for: .normal) button.setTitleColor(UIColor.nynja.mainRed, for: .normal) button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) button.addTarget(target, action: selector, for: .touchUpInside) button.snp.makeConstraints { (make) in make.centerX.equalToSuperview() make.top.equalTo(top.snp.bottom).offset(10) - } return button diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index 780fa900b..c333d947f 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -21,7 +21,7 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto } struct Parameters { - let authType: AuthConfirmationData + let confirmationData: ConfirmationData } struct Dependencies { @@ -44,7 +44,7 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto let interactor = CodeConfirmationInteractor(dependencies: .init( presenter: presenter, - authProviderType: parameters.authType, + confirmationData: parameters.confirmationData, authService: dependencies.authService) ) -- GitLab From 03b8939595c5054ef9a1052d6ad8fd07c83c56bc Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 5 Dec 2018 13:22:24 +0200 Subject: [PATCH 124/138] [Multiple accounts] DBAccount + profile mock. (#1502) * [NY-3855] Add login options module skeleton. * [NY-3855] Implemented base UI for login options. * [NY-3855] Added alerts. * [NY-3855] Added LoginOption model. Minor changes. * Fixed compile issues. * [NY-3855] Fixed screen title * Fixed switch * [NY-5519] Add AuthProvider module from template * Minor renaming changes. * Remove unused references from project file * Added transition to 'add auth provider' screen. * Implemented base UI for add auth provider. * Fixed UI * Fixed UI hierarchy. * Fixed UI components reusability from auth screen. * Implemented code confirmation input as a separate view. * Add new files * Remove unused code * Minor refactor. * Fixed accessibility and fonts construction. * Present code confirmation * Present code confirmation * Add adjust on code confirmation screen. * [NY-5508] Added profile mock. * [NY-5508] Minot refactoring in AuthService * [NY-5508] Implemented profile mock provider. * [NY-5508] Save auth token in AuthService. Update test target. * [NY-5508] Make userInfo not lazy in StorageService. * [NY-5508] Added DBModels. * Added AccountTable * Fixed compile issues. * [NY-5508] Added ContactInfoTable * [NY-5508] Added db migration for account and contact info tables. Pass AccountService to CreateProfile module. * [NY-5508] Updated code confirmation flow * [NY-5508] Remove code confirmation views factory. * [NY-5508] Upload avatar logic * [NY-5508] Add loading indicator on Create profile screen. * [NY-5508] Added AppCoordinator and try to present MainWireframe but receive crash. * [NY-5508] Open profile screen with mocked data. * [NY-5508] Setup database * [NY-5508] Refactoring * [NY-5508] Merge DBProfile and DBAccount --- Nynja.xcodeproj/project.pbxproj | 58 ++++-- Nynja/AppDelegate.swift | 5 +- Nynja/DB/Models/DBAccount.swift | 159 ++++++++++++++++ Nynja/DB/Models/DBContactInfo.swift | 77 ++++++++ Nynja/DB/Models/DBProfile.swift | 8 +- Nynja/DB/Tables/AccountTable.swift | 61 ++++++ Nynja/DB/Tables/ContactInfoTable.swift | 39 ++++ Nynja/DatabaseManager.swift | 5 +- .../Extensions/Models/AccountExtension.swift | 19 ++ .../Extensions/Models/ProfileExtension.swift | 5 - Nynja/Improvements/StorageObserver.swift | 4 +- .../UI/TextInput/TextView/TextView.swift | 1 + Nynja/MigrationManager.swift | 25 ++- .../AuthProvider/AuthProviderProtocols.swift | 1 + .../Wireframe/AuthProviderWireframe.swift | 2 +- .../Coordinator/LoginOptionsCoordinator.swift | 13 +- Nynja/Modules/Auth Flow/AppCoordinator.swift | 42 +++++ Nynja/Modules/Auth Flow/AuthCoordinator.swift | 175 ++++++++++-------- .../Interactor/AuthInteractor.swift | 4 +- .../CodeConfirmationProtocols.swift | 26 ++- .../Entities/ConfirmationData.swift | 2 - .../CodeConfirmationInteractor.swift | 30 ++- .../Presenter/CodeConfirmationPresenter.swift | 43 ++++- .../View/CodeConfirmationViewController.swift | 119 ++++++++---- .../CodeConfirmationViewsFactory.swift | 87 --------- .../Wireframe/CodeConfirmationWireframe.swift | 24 +-- .../CreateProfileProtocols.swift | 7 +- .../Interactor/CreateProfileInteractor.swift | 109 ++++++++++- .../Presenter/CreateProfilePresenter.swift | 20 +- .../View/CreateProfileViewController.swift | 4 +- .../Wireframe/CreateProfileWireframe.swift | 32 +++- Nynja/NotificationManager.swift | 4 +- Nynja/Resources/profile.bert | Bin 0 -> 8873 bytes .../Account/Service/AccountServiceImpl.swift | 25 ++- Nynja/SDK/Auth/Entities/AuthResponse.swift | 1 - Nynja/SDK/Auth/ProfileMockProvider.swift | 41 ++++ Nynja/SDK/Auth/Service/AuthService.swift | 2 + Nynja/SDK/Auth/Service/AuthServiceImpl.swift | 93 ++++++---- Nynja/SDK/Session/SessionStorage.swift | 10 +- .../HandleServices/ProfileHandler.swift | 2 +- .../MQTT/Extensions/MQTTService+Helper.swift | 2 +- .../MobileSDKFactoryProtocol.swift | 1 + .../ServiceFactory/ServiceFactory.swift | 20 +- .../ServiceFactoryProtocol.swift | 2 + Nynja/Services/StorageService.swift | 44 ++++- Nynja/TransferManager.swift | 5 +- Nynja/UserInfo.swift | 27 ++- Nynja/UserInfoImpl.swift | 30 ++- .../InputsCachePolicyTest.swift | 1 + .../InputsCachePolicy/TextFieldTest.swift | 1 + .../Services/UserInfo/UserInfoTest.swift | 39 +++- Podfile | 3 +- Podfile.lock | 8 +- 53 files changed, 1190 insertions(+), 377 deletions(-) create mode 100644 Nynja/DB/Models/DBAccount.swift create mode 100644 Nynja/DB/Models/DBContactInfo.swift create mode 100644 Nynja/DB/Tables/AccountTable.swift create mode 100644 Nynja/DB/Tables/ContactInfoTable.swift create mode 100644 Nynja/Extensions/Models/AccountExtension.swift create mode 100644 Nynja/Modules/Auth Flow/AppCoordinator.swift delete mode 100644 Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift create mode 100644 Nynja/Resources/profile.bert create mode 100644 Nynja/SDK/Auth/ProfileMockProvider.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b35a1f107..a9eff9928 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -481,6 +481,13 @@ 38182BD2C2E0C783796C8AA1 /* QRCodeReaderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D247CBC45C1C1267BBBB289 /* QRCodeReaderInteractor.swift */; }; 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */; }; 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0281F61F53794800206871 /* UIViewExtenstions.swift */; }; + 3A0AEA6A21AFF0FE0066CBBA /* profile.bert in Resources */ = {isa = PBXBuildFile; fileRef = 3A0AEA6921AFF0FD0066CBBA /* profile.bert */; }; + 3A0AEA6D21AFF3FE0066CBBA /* ProfileMockProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA6C21AFF3FE0066CBBA /* ProfileMockProvider.swift */; }; + 3A0AEA7121B018380066CBBA /* DBAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA7021B018380066CBBA /* DBAccount.swift */; }; + 3A0AEA7321B01EC50066CBBA /* DBContactInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA7221B01EC50066CBBA /* DBContactInfo.swift */; }; + 3A0AEA7521B028120066CBBA /* AccountTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA7421B028120066CBBA /* AccountTable.swift */; }; + 3A0E865921B130DC00BAF80B /* ContactInfoTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E865821B130DC00BAF80B /* ContactInfoTable.swift */; }; + 3A0E865C21B14ECB00BAF80B /* AccountExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E865B21B14ECB00BAF80B /* AccountExtension.swift */; }; 3A1146681ED6F047006BA132 /* ring.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 3A1146671ED6F047006BA132 /* ring.mp3 */; }; 3A14D83821ABEC41009CD23A /* AuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83721ABEC41009CD23A /* AuthProvider.swift */; }; 3A14D83B21AC03A3009CD23A /* SearchAvailabilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */; }; @@ -526,6 +533,7 @@ 3A9635EB21AC4EE300ABC2C5 /* LoginContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */; }; 3AA13C761F2252F900BE5D8F /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA13C751F2252F900BE5D8F /* SearchModel.swift */; }; 3AA4E6ACDBCB060172A7A279 /* FavoritesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 462440AD41D807CE8957FDD9 /* FavoritesProtocols.swift */; }; + 3AAA92AE21B1A6C800EF5F1E /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */; }; 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */; }; 3ABCE9061EC9357900A80B15 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */; }; 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC07E3B1F055B3F00ADBE26 /* DoubleExtensions.swift */; }; @@ -781,7 +789,6 @@ 5BC1D38420D3B670002A44B3 /* CallCreatorMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */; }; 5C468A609C445962C0D19DD3 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D6900257B1AB2CA0BC834EB /* HistoryViewController.swift */; }; 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */; }; - 5E07BC40216E09F0000E4558 /* CodeConfirmationViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */; }; 5E07BC4D216F64EC000E4558 /* CreateProfileProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */; }; 5E07BC4F216F659E000E4558 /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */; }; 5E07BC51216F6617000E4558 /* CreateProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */; }; @@ -2899,6 +2906,13 @@ 35F2DA601F73CAD400777920 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerViewController.swift; sourceTree = ""; }; 3A0281F61F53794800206871 /* UIViewExtenstions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtenstions.swift; sourceTree = ""; }; + 3A0AEA6921AFF0FD0066CBBA /* profile.bert */ = {isa = PBXFileReference; lastKnownFileType = file; path = profile.bert; sourceTree = ""; }; + 3A0AEA6C21AFF3FE0066CBBA /* ProfileMockProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileMockProvider.swift; sourceTree = ""; }; + 3A0AEA7021B018380066CBBA /* DBAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBAccount.swift; sourceTree = ""; }; + 3A0AEA7221B01EC50066CBBA /* DBContactInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBContactInfo.swift; sourceTree = ""; }; + 3A0AEA7421B028120066CBBA /* AccountTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTable.swift; sourceTree = ""; }; + 3A0E865821B130DC00BAF80B /* ContactInfoTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoTable.swift; sourceTree = ""; }; + 3A0E865B21B14ECB00BAF80B /* AccountExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExtension.swift; sourceTree = ""; }; 3A1146671ED6F047006BA132 /* ring.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ring.mp3; sourceTree = ""; }; 3A14D83721ABEC41009CD23A /* AuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProvider.swift; sourceTree = ""; }; 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAvailabilityView.swift; sourceTree = ""; }; @@ -2945,6 +2959,7 @@ 3A8C12DDFD0D831F21959665 /* Pods-Nynja-Share.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.devautotests.xcconfig"; sourceTree = ""; }; 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContainerView.swift; sourceTree = ""; }; 3AA13C751F2252F900BE5D8F /* SearchModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; + 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 3ABCE8ED1EC9330D00A80B15 /* Nynja.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nynja.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; @@ -3169,7 +3184,6 @@ 5BC1D38020D3B54B002A44B3 /* CallInfoViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInfoViewLayout.swift; sourceTree = ""; }; 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallCreatorMediator.swift; sourceTree = ""; }; 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryInteractor.swift; sourceTree = ""; }; - 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationViewsFactory.swift; sourceTree = ""; }; 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileProtocols.swift; sourceTree = ""; }; 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = ""; }; 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileInteractor.swift; sourceTree = ""; }; @@ -6502,6 +6516,7 @@ 3ABCE9021EC9357900A80B15 /* Resources */ = { isa = PBXGroup; children = ( + 3A0AEA6921AFF0FD0066CBBA /* profile.bert */, F127E92020A44F7B006C03CF /* Nynja.entitlements */, A4F3DAA22084935400FF71C7 /* Constants.swift */, 00F7B347202B316F00E443E1 /* timezones.json */, @@ -7220,6 +7235,7 @@ 4B749F0E214FEFC8002F3A33 /* Auth Flow */ = { isa = PBXGroup; children = ( + 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */, 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, @@ -7769,14 +7785,6 @@ path = View; sourceTree = ""; }; - 5E07BC3E216E09DF000E4558 /* ViewsFactory */ = { - isa = PBXGroup; - children = ( - 5E07BC3F216E09F0000E4558 /* CodeConfirmationViewsFactory.swift */, - ); - path = ViewsFactory; - sourceTree = ""; - }; 5E07BC45216F64DB000E4558 /* CreateProfile */ = { isa = PBXGroup; children = ( @@ -7989,7 +7997,6 @@ 5EEB73AE216046EA00D8ECE6 /* View */ = { isa = PBXGroup; children = ( - 5E07BC3E216E09DF000E4558 /* ViewsFactory */, 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */, ); path = View; @@ -9648,6 +9655,7 @@ children = ( 859ECA6D21A441D8003630A0 /* Service */, 859ECA6E21A441E8003630A0 /* Entities */, + 3A0AEA6C21AFF3FE0066CBBA /* ProfileMockProvider.swift */, ); path = Auth; sourceTree = ""; @@ -13044,6 +13052,8 @@ E79385861FC32ACC00744CB0 /* DBProfile.swift */, E7C36C301FC4399B00740630 /* DBService.swift */, E7C36C321FC441B900740630 /* DBRoster.swift */, + 3A0AEA7021B018380066CBBA /* DBAccount.swift */, + 3A0AEA7221B01EC50066CBBA /* DBContactInfo.swift */, E7C36C341FC448CB00740630 /* DBFeature.swift */, E74FD69E1FC5DEAA00656611 /* DBContact.swift */, 2686D3261FC640440079CB75 /* DBSyncFile.swift */, @@ -13090,6 +13100,8 @@ children = ( E79061B21FBF1057009FD83A /* Base */, E70938361FBEDA2B006CCDC6 /* ProfileTable.swift */, + 3A0AEA7421B028120066CBBA /* AccountTable.swift */, + 3A0E865821B130DC00BAF80B /* ContactInfoTable.swift */, E709383C1FBEE176006CCDC6 /* ServiceTable.swift */, E79061B71FBF2243009FD83A /* FeatureTable.swift */, E79061B51FBF1C8C009FD83A /* DescTable.swift */, @@ -13498,6 +13510,7 @@ 263529142075729400DC6FBD /* Job+DB.swift */, 00E9825D205FDB1A008BF03D /* AuthExtension.swift */, A438DB9120763AFB00AA86A2 /* Contact+Desc.swift */, + 3A0E865B21B14ECB00BAF80B /* AccountExtension.swift */, ); path = Models; sourceTree = ""; @@ -15342,6 +15355,7 @@ 85D70CF62056C655000925EA /* notification.mp3 in Resources */, 3A22662C1EFEF31E00D6A867 /* Assets.xcassets in Resources */, 3A1D03031F0B29CF005F5F18 /* countries.txt in Resources */, + 3A0AEA6A21AFF0FE0066CBBA /* profile.bert in Resources */, A4B544F820EFC0AD00EB7B0F /* StatusCodes.strings in Resources */, 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */, E77B9B7C1FDEC6E20035CA12 /* NotoSans-Bold.ttf in Resources */, @@ -15485,9 +15499,9 @@ "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", "${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework", - "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", - "${BUILT_PRODUCTS_DIR}/MaterialComponents-cfb03c44/MaterialComponents.framework", "${BUILT_PRODUCTS_DIR}/MDFInternationalization/MDFInternationalization.framework", + "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", + "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", "${BUILT_PRODUCTS_DIR}/MotionAnimator/MotionAnimator.framework", "${BUILT_PRODUCTS_DIR}/MotionInterchange/MotionInterchange.framework", ); @@ -15514,9 +15528,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFInternationalization.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFInternationalization.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionAnimator.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionInterchange.framework", ); @@ -15548,8 +15562,11 @@ "${BUILT_PRODUCTS_DIR}/SQLCipher/SQLCipher.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", "${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework", + "${BUILT_PRODUCTS_DIR}/MDFInternationalization/MDFInternationalization.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", - "${BUILT_PRODUCTS_DIR}/MaterialComponents-efec52f1/MaterialComponents.framework", + "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", + "${BUILT_PRODUCTS_DIR}/MotionAnimator/MotionAnimator.framework", + "${BUILT_PRODUCTS_DIR}/MotionInterchange/MotionInterchange.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( @@ -15569,8 +15586,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SQLCipher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFInternationalization.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionAnimator.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionInterchange.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -16461,6 +16481,7 @@ A4CB15232103735200C3B68B /* JDFileBasedMechanism.swift in Sources */, 855A4EA0219B35B700B6E90B /* AuthConfirmationType.swift in Sources */, 3A771CAA1F191B38008D968A /* ProfileHandler.swift in Sources */, + 3A0E865C21B14ECB00BAF80B /* AccountExtension.swift in Sources */, B74BAFFC21076AFA0049CD27 /* SectionView.swift in Sources */, E78EFB871FC867A900C44975 /* DBP2p.swift in Sources */, 8ED0F3CF1FBC5CF2004916AB /* GroupsListPresenter.swift in Sources */, @@ -16775,6 +16796,7 @@ 005886CB2030F3F900FE2E89 /* NynjaTimeMinsDelegate.swift in Sources */, 0F409A888929B0CC8EDF6656 /* QRCodeReaderPresenter.swift in Sources */, 2646381E1FFFC5CB002590E6 /* RepliesProtocols.swift in Sources */, + 3A0AEA6D21AFF3FE0066CBBA /* ProfileMockProvider.swift in Sources */, 38182BD2C2E0C783796C8AA1 /* QRCodeReaderInteractor.swift in Sources */, 2603139520A0A4B9009AC66D /* LanguageSelectorWireframe.swift in Sources */, A42D51CF206A361400EEB952 /* Contact.swift in Sources */, @@ -17201,7 +17223,6 @@ 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */, - 5E07BC40216E09F0000E4558 /* CodeConfirmationViewsFactory.swift in Sources */, F10AFEBC20F7B1D200C7CE83 /* WheelPreviewFactory.swift in Sources */, 4B3F055F2043F871002E0F54 /* ScheduleMessageConfiguration.swift in Sources */, A42D51A1206A361400EEB952 /* Friend.swift in Sources */, @@ -17312,6 +17333,7 @@ FEA655CB2167777E00B44029 /* SeedVerificationWalletPresenter.swift in Sources */, 260313A420A0A4BA009AC66D /* DirectableActionCellViewModel.swift in Sources */, 859773232087965700B03B4A /* NynjaControlContainerView.swift in Sources */, + 3A0AEA7321B01EC50066CBBA /* DBContactInfo.swift in Sources */, E743B58A1FB0911200F72F92 /* ParticipantsContactCell.swift in Sources */, A46679F120F10B5900DBC6B4 /* LinkModelFactory.swift in Sources */, F119E66C20D24BAF0043A532 /* MultiplePreviewViewController.swift in Sources */, @@ -17320,6 +17342,7 @@ A43B259820AB1DFA00FF8107 /* TextInputContent.swift in Sources */, E723FD221F9E59A600E0B602 /* ProfileSection.swift in Sources */, 4B030F2D2195BFF300F293B7 /* AuthHandlerDelegate.swift in Sources */, + 3A0E865921B130DC00BAF80B /* ContactInfoTable.swift in Sources */, 4B752B652163A64300E852B9 /* Desc+Place.swift in Sources */, 8524C4D22177713C003BF374 /* Member+Status.swift in Sources */, E77D58A01F98C38100FBE926 /* ProfileSectionHeaderView.swift in Sources */, @@ -17398,6 +17421,7 @@ 85788C4620442392003600C9 /* BuildNumberInteractor.swift in Sources */, 8541995221A2B003004009F7 /* PhoneNumberFormatter.swift in Sources */, 2605312921298BEF002E1CF1 /* Logoutputcell.swift in Sources */, + 3AAA92AE21B1A6C800EF5F1E /* AppCoordinator.swift in Sources */, 6B3D349607A18D5650BF47E6 /* SplashInteractor.swift in Sources */, 859B863720485F01003272B2 /* CarouselPickerViewController.swift in Sources */, 8566771E20C1579C00DD4204 /* StorageSubscriberReference.swift in Sources */, @@ -17717,6 +17741,7 @@ 8ED0F3D11FBC5CF2004916AB /* GroupsListViewController.swift in Sources */, BBF46945EB64E07C58817ACA /* EditGroupNameProtocols.swift in Sources */, 8520040020D466CE007C0036 /* StickerPack_Spec.swift in Sources */, + 3A0AEA7521B028120066CBBA /* AccountTable.swift in Sources */, 1E615EDDA6522EF693319BA5 /* EditGroupNameViewController.swift in Sources */, 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */, 43F333D298934DCBAC8D8192 /* EditGroupNamePresenter.swift in Sources */, @@ -17871,6 +17896,7 @@ 853FB06A2049B193000996C5 /* SupportWireFrame.swift in Sources */, 855A4EA2219B3A9400B6E90B /* AuthTokenData.swift in Sources */, 853D0F9A20C0514E008C3684 /* UICollectionViewFlowLayout+ItemSize.swift in Sources */, + 3A0AEA7121B018380066CBBA /* DBAccount.swift in Sources */, A45F113920B4218D00F45004 /* AvatarViewLayout.swift in Sources */, B16EC832C763628A2EBBD383 /* MapSearchProtocols.swift in Sources */, A43B25A720AB1DFA00FF8107 /* ALKeyboardObservingView.swift in Sources */, diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index d9015612b..945520a31 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -34,6 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private let storageService = StorageService.sharedInstance private let antiDebuggingService = AntiDebuggingService() + private var appCoordinator: AppCoordinator! private let serviceFactory = ServiceFactory() @@ -131,8 +132,8 @@ private extension AppDelegate { window?.rootViewController = navigation window?.makeKeyAndVisible() - let coordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) - coordinator.start() + appCoordinator = AppCoordinator(navigation: navigation, serviceFactory: serviceFactory) + appCoordinator.start() } func configureDependencies() { diff --git a/Nynja/DB/Models/DBAccount.swift b/Nynja/DB/Models/DBAccount.swift new file mode 100644 index 000000000..4f7be8009 --- /dev/null +++ b/Nynja/DB/Models/DBAccount.swift @@ -0,0 +1,159 @@ +// +// DBAccount.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/29/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher +import NynjaSDK + +/// Database representation of `NYNAccountDetails`. +final class DBAccount: Record, DBModel { + + enum Role: String { + case user = "user" + case admin = "admin" + + init?(role: NYNAccountRole) { + switch role { + case .user: + self = .user + case .admin: + self = .admin + case .unknown: + return nil + } + } + } + + let accountId: String + let profileId: String + let authenticationIdentifier: String + let authenticationType: String + let avatar: String + let accountMark: String + let accountName: String + let firstName: String + let lastName: String + let username: String + let status: Int + let qrCode: String? + private(set) var contactInfo: [DBContactInfo]? + let birthday: Int64? + let roles: [Role]? + let created: Int64? + let updated: Int64? + + init(account: NYNAccountDetails) { + accountId = account.accountId + profileId = account.profileId + authenticationIdentifier = account.authenticationIdentifier + authenticationType = account.authenticationType + avatar = account.avatar + accountMark = account.accountMark + accountName = account.accountName + firstName = account.firstName + lastName = account.lastName + username = account.username + status = account.accountStatus.rawValue + qrCode = account.qrCode + contactInfo = account.contactsInfo?.compactMap { + DBContactInfo(accountId: account.accountId, contactInfo: $0) + } + birthday = account.birthday.flatMap { + let calendar = Calendar.current + let dateComponents = DateComponents(calendar: calendar, + year: $0.year.intValue, + month: $0.month.intValue, + day: $0.day.intValue) + + return calendar.date(from: dateComponents).flatMap { Int64($0.timeIntervalSince1970) } + } + roles = account.roles.flatMap { roles -> [Role] in + return (0.. [DBAccount] { + let profileIdColumn = Column(AccountTable.Column.profileId.title) + + let accounts = try DBAccount.filter(profileIdColumn == profileId).fetchAll(db) + try accounts.forEach { try $0.construct(db) } + + return accounts + } +} diff --git a/Nynja/DB/Models/DBContactInfo.swift b/Nynja/DB/Models/DBContactInfo.swift new file mode 100644 index 000000000..48cd60ca2 --- /dev/null +++ b/Nynja/DB/Models/DBContactInfo.swift @@ -0,0 +1,77 @@ +// +// DBContactInfo.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/29/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher +import NynjaSDK + +/// Database representation of `NYNContactDetails`. +final class DBContactInfo: Record, DBModel { + + enum InfoType: String { + case phone = "phone" + case email = "email" + case facebook = "facebook" + case google = "google" + case twitter = "twitter" + + init?(type: NYNAccountContactType) { + switch type { + case .phone: + self = .phone + case .email: + self = .email + case .facebook: + self = .facebook + case .googlePlus: + self = .google + case .twitter: + self = .twitter + case .missing: + return nil + } + } + } + + let accountId: String + let type: InfoType + let value: String + let label: String? + + init?(accountId: String, contactInfo: NYNContactDetails) { + guard let type = InfoType(type: contactInfo.type) else { + return nil + } + self.accountId = accountId + self.type = type + self.value = contactInfo.value + self.label = contactInfo.label + super.init() + } + + + // MARK: - Record + + override class var databaseTableName: String { + return ContactInfoTable.name + } + + required init(row: Row) { + accountId = row[ContactInfoTable.Column.accountId.title] + type = InfoType(rawValue: row[ContactInfoTable.Column.type.title])! + value = row[ContactInfoTable.Column.value.title] + label = row[ContactInfoTable.Column.label.title] + super.init() + } + + override func encode(to container: inout PersistenceContainer) { + container[ContactInfoTable.Column.accountId.title] = accountId + container[ContactInfoTable.Column.type.title] = type.rawValue + container[ContactInfoTable.Column.value.title] = value + container[ContactInfoTable.Column.label.title] = label + } +} diff --git a/Nynja/DB/Models/DBProfile.swift b/Nynja/DB/Models/DBProfile.swift index 5c89cf9ff..46c08fe5b 100644 --- a/Nynja/DB/Models/DBProfile.swift +++ b/Nynja/DB/Models/DBProfile.swift @@ -18,6 +18,7 @@ final class DBProfile: Record, DBModel { var services: [DBService] = [] var rosters: [DBRoster] = [] + var accounts: [DBAccount] = [] var features: [DBFeature] = [] // MARK: - Mapping @@ -70,6 +71,7 @@ final class DBProfile: Record, DBModel { try self.save(db) try services.forEach { try $0.save(db) } try rosters.forEach { try $0.saveAggregate(db) } + try accounts.forEach { try $0.saveAggregate(db) } try features.forEach { try $0.save(db) } } @@ -80,6 +82,7 @@ final class DBProfile: Record, DBModel { dbProfile.services = try DBService.request(targetId: dbProfile.phone, targetType: .profile).fetchAll(db) dbProfile.rosters = try DBRoster.rosters(from: db, profileId: dbProfile.phone) + dbProfile.accounts = try DBAccount.accounts(from: db, profileId: dbProfile.phone) dbProfile.features = try DBFeature.request(targetId: dbProfile.phone, targetType: .profile).fetchAll(db) return dbProfile @@ -89,7 +92,10 @@ final class DBProfile: Record, DBModel { func deleteAggregate(_ db: Database) throws -> Bool { try DBFeature.deleteAll(db, targetId: phone, targetType: .profile) try DBService.deleteAll(db, targetId: phone, targetType: .profile) - + + try rosters.forEach { try $0.deleteAggregate(db) } + try accounts.forEach { try $0.deleteAggregate(db) } + return try delete(db) } diff --git a/Nynja/DB/Tables/AccountTable.swift b/Nynja/DB/Tables/AccountTable.swift new file mode 100644 index 000000000..5f7843b25 --- /dev/null +++ b/Nynja/DB/Tables/AccountTable.swift @@ -0,0 +1,61 @@ +// +// AccountTable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/29/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +final class AccountTable: Table { + + class var name: String { + return "account" + } + + static func create(in db: Database) throws { + try db.create(self) { t in + t.column(Column.accountId, .text).notNull().primaryKey() + + // FIXME: must be primary key + t.column(Column.profileId, .text).notNull() + t.column(Column.authenticationIdentifier, .text) + t.column(Column.authenticationType, .text) + t.column(Column.avatar, .text) + t.column(Column.accountMark, .text) + t.column(Column.accountName, .text) + t.column(Column.firstName, .text) + t.column(Column.lastName, .text) + t.column(Column.username, .text) + t.column(Column.status, .text) + t.column(Column.qrCode, .text) + t.column(Column.birthday, .integer) + t.column(Column.roles, .text) + t.column(Column.created, .integer) + t.column(Column.updated, .integer) + } + } +} + +extension AccountTable { + + enum Column: Int, Describable { + case accountId + case profileId + case authenticationIdentifier + case authenticationType + case avatar + case accountMark + case accountName + case firstName + case lastName + case username + case status + case qrCode + case birthday + case roles + case created + case updated + } +} diff --git a/Nynja/DB/Tables/ContactInfoTable.swift b/Nynja/DB/Tables/ContactInfoTable.swift new file mode 100644 index 000000000..975d7d6fa --- /dev/null +++ b/Nynja/DB/Tables/ContactInfoTable.swift @@ -0,0 +1,39 @@ +// +// ContactInfoTable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +final class ContactInfoTable: Table { + + class var name: String { + return "contact_info" + } + + static func create(in db: Database) throws { + try db.create(self) { t in + t.primaryKey([Column.accountId.title, Column.type.title, Column.value.title], onConflict: .replace) + + t.column(Column.accountId, .text).notNull() + .references(AccountTable.name, column: AccountTable.Column.accountId.title, onDelete: .cascade) + + t.column(Column.type, .text).notNull() + t.column(Column.value, .text).notNull() + t.column(Column.label, .text) + } + } +} + +extension ContactInfoTable { + + enum Column: Int, Describable { + case accountId + case type + case value + case label + } +} diff --git a/Nynja/DatabaseManager.swift b/Nynja/DatabaseManager.swift index 83c0dc9d7..d65ad7b1b 100644 --- a/Nynja/DatabaseManager.swift +++ b/Nynja/DatabaseManager.swift @@ -262,7 +262,10 @@ extension DatabaseManager { StickerPackTable.self, ConvertMessageTable.self, - StarActionTable.self + StarActionTable.self, + + AccountTable.self, + ContactInfoTable.self ] } diff --git a/Nynja/Extensions/Models/AccountExtension.swift b/Nynja/Extensions/Models/AccountExtension.swift new file mode 100644 index 000000000..fad0c3c1c --- /dev/null +++ b/Nynja/Extensions/Models/AccountExtension.swift @@ -0,0 +1,19 @@ +// +// AccountExtension.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaSDK + +typealias Account = NYNAccountDetails + +extension Account: DBModelConvertible { + + var databaseModel: DBModel? { + return DBAccount(account: self) + } +} diff --git a/Nynja/Extensions/Models/ProfileExtension.swift b/Nynja/Extensions/Models/ProfileExtension.swift index 6b633fe57..5062ea4e2 100644 --- a/Nynja/Extensions/Models/ProfileExtension.swift +++ b/Nynja/Extensions/Models/ProfileExtension.swift @@ -25,11 +25,6 @@ extension Profile { var hasPhone: Bool { return phone != nil && phone != "" } - - var phoneId: String? { - return (rosters?.first as? Roster)?.phoneId - } - } extension Profile: DBModelConvertible { diff --git a/Nynja/Improvements/StorageObserver.swift b/Nynja/Improvements/StorageObserver.swift index 299312bc0..806b3a145 100644 --- a/Nynja/Improvements/StorageObserver.swift +++ b/Nynja/Improvements/StorageObserver.swift @@ -22,7 +22,7 @@ extension StorageObserver { func register(subscriber: StorageSubscriber, type: SubscribeType) { isolationQueue.async { [weak self] in - guard let `self` = self else { + guard let self = self else { return } @@ -44,7 +44,7 @@ extension StorageObserver { func unregister(subscriber: StorageSubscriber, type: SubscribeType) { isolationQueue.async { [weak self] in - guard let `self` = self else { + guard let self = self else { return } diff --git a/Nynja/Library/UI/TextInput/TextView/TextView.swift b/Nynja/Library/UI/TextInput/TextView/TextView.swift index cf9a427da..c32a752fc 100644 --- a/Nynja/Library/UI/TextInput/TextView/TextView.swift +++ b/Nynja/Library/UI/TextInput/TextView/TextView.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit class TextView: UITextView { diff --git a/Nynja/MigrationManager.swift b/Nynja/MigrationManager.swift index ed69ff78b..b5c64abb5 100644 --- a/Nynja/MigrationManager.swift +++ b/Nynja/MigrationManager.swift @@ -8,24 +8,16 @@ import GRDBCipher -enum Migration: Int, Describable { - case addSeenByColumnToMessage = 0 +enum Migration: CaseIterable, Describable { + case addSeenByColumnToMessage case updateDBFeatureTargetTypes case addAutoColumnToConvertMessage case correctMessageIdTypeInStarTable + case addAccountAndContactInfoTables - static var allTitles: [String] = { - var i = 0 - - var strings: [String] = [] - - while let migration = Migration(rawValue: i) { - i += 1 - strings.append(migration.title) - } - - return strings - }() + static var allTitles: [String] { + return allCases.map { $0.title } + } } final class MigrationManager { @@ -100,6 +92,11 @@ final class MigrationManager { try db.drop(table: tempTableName) } + + migrator.registerMigration(.addAccountAndContactInfoTables) { db in + try AccountTable.createIfNotExists(in: db) + try ContactInfoTable.createIfNotExists(in: db) + } } } diff --git a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift index fa2d1583d..9fb0771f5 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift @@ -37,6 +37,7 @@ protocol AuthProviderPresenterProtocol: BasePresenterProtocol, NavigationProtoco } protocol AuthProviderInteractorInput: class { + func fetchDefaultCountry() -> Country func fetchCountry(by code: String) -> Country? diff --git a/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift b/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift index 7fc8fd59e..15894cb25 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift @@ -57,7 +57,7 @@ final class AuthProviderWireframe: Wireframe, AuthProviderWireframeProtocol { func selectCountry(completion: @escaping (Result) -> Void) { coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) } - + func confirm(data: ConfirmationData) { coordinator.wireframe(self, didEndWithState: .confirmProvider(data)) } diff --git a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift index f7fced653..c6324b452 100644 --- a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift +++ b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift @@ -71,13 +71,16 @@ extension LoginOptionsCoordinator: AuthProviderCoordinatorProtocol { ) navigation?.pushViewController(view, animated: true) - + case let .confirmProvider(confirmationData): let wireframe = CodeConfirmationWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: .init(confirmationData: confirmationData), - dependencies: .init(authService: serviceFactory.makeAuthService()) + parameters: .init(confirmationData: confirmationData, isLogoVisible: false), + dependencies: .init(storageService: serviceFactory.makeStorageService(), + authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService()) ) + navigation?.pushViewController(view, animated: true) } } @@ -107,9 +110,9 @@ extension LoginOptionsCoordinator: CodeConfirmationCoordinatorProtocol { switch state { case .back: navigation?.popViewController(animated: true) - case let .validCode(type): + case .loggedIn: break - case .invalidCode: + case .registered: break } } diff --git a/Nynja/Modules/Auth Flow/AppCoordinator.swift b/Nynja/Modules/Auth Flow/AppCoordinator.swift new file mode 100644 index 000000000..583bf8dba --- /dev/null +++ b/Nynja/Modules/Auth Flow/AppCoordinator.swift @@ -0,0 +1,42 @@ +// +// AppCoordinator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AppCoordinator: Coordinator { + + private unowned let navigation: UINavigationController + + private let serviceFactory: ServiceFactoryProtocol + + + // MARK: - Init + + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { + self.navigation = navigation + self.serviceFactory = serviceFactory + } + + func start() { + let authCoordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) + authCoordinator.delegate = self + authCoordinator.start() + } + + func end() { + } +} + +// MARK: - Auth Delegate + +extension AppCoordinator: AuthCoordinatorDelegate { + + func authCoordinatorDidFinish(_ coordinator: AuthCoordinator) { + MainWireFrame().presentMain(navigation: navigation, isRegistered: true) + } +} diff --git a/Nynja/Modules/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Auth Flow/AuthCoordinator.swift index 9d0735ac2..ea8a5f33d 100644 --- a/Nynja/Modules/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AuthCoordinator.swift @@ -9,7 +9,13 @@ import Foundation import SDWebImage -final class AuthCoordinator: Coordinator, NavigationContainer, CountrySelectorCoordinatorProtocol, CodeConfirmationCoordinatorProtocol, AuthCoordinatorProtocol, CreateProfileCoordinatorProtocol { +protocol AuthCoordinatorDelegate: class { + func authCoordinatorDidFinish(_ coordinator: AuthCoordinator) +} + +final class AuthCoordinator: Coordinator, NavigationContainer { + + weak var delegate: AuthCoordinatorDelegate? private(set) weak var navigation: UINavigationController? @@ -27,73 +33,32 @@ final class AuthCoordinator: Coordinator, NavigationContainer, CountrySelectorCo } func start() { - SplashWireFrame().presentSplash(navigation: navigation!) +// SplashWireFrame().presentSplash(navigation: navigation!) -// let wireframe = AuthWireframe(coordinator: self) -// let view = wireframe.prepareModule( -// dependencies: .init(authService: serviceFactory.makeAuthService(), -// googleAuthService: serviceFactory.makeGoogleAuthService(), -// countriesProvider: serviceFactory.makeCountriesProvider()) -// ) + let wireframe = AuthWireframe(coordinator: self) + let view = wireframe.prepareModule( + dependencies: .init(authService: serviceFactory.makeAuthService(), + googleAuthService: serviceFactory.makeGoogleAuthService(), + countriesProvider: serviceFactory.makeCountriesProvider()) + ) // let wireframe = CodeConfirmationWireframe(coordinator: self) // let view = wireframe.prepareModule( // parameters: .init(confirmationData: .email("anton.poltoratskyi@gmail.com")), // dependencies: .init(authService: serviceFactory.makeAuthService()) // ) -// -// navigation?.pushViewController(view, animated: true) - } - - func end() { - - } -} - -// MARK: - CountrySelectorCoordinatorProtocol -extension AuthCoordinator { - func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { - switch state { - case .selected(let country): - selectCountryCallback?(.success(country)) - case .dismiss: - selectCountryCallback?(.failure(NavigationError.dismissed)) - } - selectCountryCallback = nil - navigation?.popViewController(animated: true) - } -} - -// MARK: - CodeConfirmationCoordinatorProtocol - -extension AuthCoordinator { - func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { - switch state { - case .back: - navigation?.popViewController(animated: true) - case .invalidCode: - let actions = [UIAlertAction(title: "OK", style: .default, handler: nil)] - presentAlert(title: "Failure", message: "Code is invalid or internal error", actions: actions) - case .validCode(let type): - handleType(type) - } + navigation?.pushViewController(view, animated: true) } - private func handleType(_ type: AuthenticationType) { - switch type { - case .register: - let view = CreateProfileWireframe(coordinator: self).prepareModule(dependencies: CreateProfileWireframe.Dependencies()) - navigation?.pushViewController(view, animated: true) - case .login: - break - } + func end() { + delegate?.authCoordinatorDidFinish(self) } } -// MARK: - AuthCoordinatorProtocol +// MARK: - Auth -extension AuthCoordinator { +extension AuthCoordinator: AuthCoordinatorProtocol { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) { switch state { case let .confirmInputData(loginOption, confirmationHandler): @@ -108,7 +73,7 @@ extension AuthCoordinator { let wireframe = SelectCountryWireFrame(coordinator: self) let view = wireframe.prepareModule( - dependencies: SelectCountryWireFrame.Dependencies( + dependencies: .init( countriesProvider: serviceFactory.makeCountriesProvider() ) ) @@ -135,16 +100,24 @@ extension AuthCoordinator { case let .email(email): let wireframe = CodeConfirmationWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: .init(confirmationData: .email(email)), - dependencies: .init(authService: serviceFactory.makeAuthService()) + parameters: .init(confirmationData: .email(email), isLogoVisible: true), + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService() + ) ) navigation?.pushViewController(view, animated: true) case let .phoneNumber(numberInfo): let wireframe = CodeConfirmationWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: .init(confirmationData: .phoneNumber(numberInfo)), - dependencies: .init(authService: serviceFactory.makeAuthService()) + parameters: .init(confirmationData: .phoneNumber(numberInfo), isLogoVisible: true), + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService() + ) ) navigation?.pushViewController(view, animated: true) @@ -194,14 +167,74 @@ extension AuthCoordinator { } } -// MARK: - CreateProfileCoordinatorProtocol +// MARK: - Facebook Auth -extension AuthCoordinator { - func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) { +extension AuthCoordinator: FacebookAuthCoordinatorProtocol { + + func wireframe(_ wireframe: FacebookAuthWireframe, didEndWithState state: FacebookAuthWireframe.State) { + switch state { + case let .authenticated(code): + facebookAuthCodeCallback?(.success(code)) + case .dismiss: + facebookAuthCodeCallback?(.failure(NavigationError.dismissed)) + } + facebookAuthCodeCallback = nil + navigation?.popViewController(animated: true) + } +} + +// MARK: - Country Selector + +extension AuthCoordinator: CountrySelectorCoordinatorProtocol { + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { + switch state { + case .selected(let country): + selectCountryCallback?(.success(country)) + case .dismiss: + selectCountryCallback?(.failure(NavigationError.dismissed)) + } + selectCountryCallback = nil + navigation?.popViewController(animated: true) + } +} + +// MARK: - Code Confirmation + +extension AuthCoordinator: CodeConfirmationCoordinatorProtocol { + func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { switch state { case .back: navigation?.popViewController(animated: true) - + case .loggedIn: + end() + case let .registered(accountId): + let wireframe = CreateProfileWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(accountId: accountId), + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + resourceManager: serviceFactory.makeResourceManager(), + transferManager: serviceFactory.makeTransferManager(), + authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService() + ) + ) + navigation?.pushViewController(view, animated: true) + } + } +} + +// MARK: - Create Profile + +extension AuthCoordinator: CreateProfileCoordinatorProtocol { + func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) { + switch state { + case .back: + if let authViewController = navigation?.viewControllers.first(where: { $0 is AuthViewController }) { + navigation?.popToViewController(authViewController, animated: true) + } else { + navigation?.popViewController(animated: true) + } case .next: end() @@ -226,19 +259,3 @@ extension AuthCoordinator { } } } - -// MARK: - FacebookAuthCoordinatorProtocol - -extension AuthCoordinator: FacebookAuthCoordinatorProtocol { - - func wireframe(_ wireframe: FacebookAuthWireframe, didEndWithState state: FacebookAuthWireframe.State) { - switch state { - case let .authenticated(code): - facebookAuthCodeCallback?(.success(code)) - case .dismiss: - facebookAuthCodeCallback?(.failure(NavigationError.dismissed)) - } - facebookAuthCodeCallback = nil - navigation?.popViewController(animated: true) - } -} diff --git a/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift index de54105c2..d37a2c6e1 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift @@ -59,12 +59,12 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { func loginViaGoogle() { googleAuthService.signIn { [weak self] result in - guard let `self` = self else { return } + guard let self = self else { return } switch result { case let .success(code): self.authService.loginByGoogle(serverCode: code) { [weak self] googleResult in - guard let `self` = self else { return } + guard let self = self else { return } switch googleResult { case .success: diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift index 70a95c52c..ad6c4a8c4 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift @@ -8,43 +8,55 @@ import Foundation +// MARK: - Wireframe + protocol CodeConfirmationWireframeProtocol: class { - func codeValid(with type: AuthenticationType) - func codeInvalid() + func continueSignUpFlow(with accountId: String) + func continueSuccessAuthentication() func back() } -protocol CodeConfirmationViewProtocol: class where Self: UIViewController { +// MARK: - View + +protocol CodeConfirmationViewProtocol: LoadingInteractive where Self: UIViewController { func updateTimerLabel(text: String) func showButtons() - func showLoading() - func hideLoading() } +// MARK: - Presenter + protocol CodeConfirmationPresenterProtocol: NavigationProtocol { var canAskForCall: Bool { get } var address: String { get } var descriptionText: String { get } func viewDidLoad() + func viewDidDisappear() func sendConfirmationCode(_ code: String) func resendCode() func askForCall() } +// MARK: - Interactor + +// MARK: Input protocol CodeConfirmationInputInteractorProtocol: class { var address: String { get } var confirmationData: ConfirmationData { get } func sendConfirmationCode(_ code: String) + func loadAccount(by accountId: String) + func resendCode() func askForCall() } +// MARK: Output protocol CodeConfirmationOutputInteractorProtocol: class { func didResendCode() func didReceiveResendCodeFailure(_ error: Error) - func didConfirmCode(authenticationType: AuthenticationType) - func didReceiveCodeConfirmationFailure(_ error: Error) + func didConfirmCode(response: AuthResponse) + func didSaveAccount() + func didReceiveFailure(_ error: Error) } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift index 8e0b5ad41..5b2853ba8 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift @@ -6,8 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - enum ConfirmationData { case email(String) case phoneNumber(PhoneNumberInfo) diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index f22df1f79..93f6a59f0 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -26,21 +26,29 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, // MARK: - Services + private let storageService: StorageService + private let authService: AuthService + private let accountService: AccountService + // MARK: - Init struct Dependencies { let presenter: CodeConfirmationOutputInteractorProtocol let confirmationData: ConfirmationData + let storageService: StorageService let authService: AuthService + let accountService: AccountService } init(dependencies: Dependencies) { self.presenter = dependencies.presenter self.confirmationData = dependencies.confirmationData + self.storageService = dependencies.storageService self.authService = dependencies.authService + self.accountService = dependencies.accountService } @@ -50,9 +58,27 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, authService.confirm(code: code, with: "") { [weak self] result in switch result { case let .success(response): - self?.presenter?.didConfirmCode(authenticationType: response.authenticationType) + self?.presenter?.didConfirmCode(response: response) + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + } + + func loadAccount(by accountId: String) { + accountService.getAccount(by: accountId) { [weak self] result in + guard let self = self else { return } + + switch result { + case let .success(account): + do { + try self.authService.processAuthenticatedAccount(account) + self.presenter?.didSaveAccount() + } catch { + self.presenter?.didReceiveFailure(error) + } case let .failure(error): - self?.presenter?.didReceiveCodeConfirmationFailure(error) + self.presenter?.didReceiveFailure(error) } } } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index d54bef64a..f47b78fed 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -14,6 +14,8 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo private var interactor: CodeConfirmationInputInteractorProtocol! private var wireframe: CodeConfirmationWireframe! + private var timer: Timer? + private var timerValue = 0 { didSet { if timerValue > 60 { @@ -29,14 +31,11 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo if timerValue == 0 { view?.showButtons() - timer?.invalidate() - timer = nil + invalidateTimer() } } } - private var timer: Timer? - var canAskForCall: Bool { if case .phoneNumber = interactor.confirmationData { return true @@ -57,6 +56,16 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo } } + + deinit { + invalidateTimer() + } + + private func invalidateTimer() { + timer?.invalidate() + timer = nil + } + func viewDidLoad() { switch interactor.confirmationData { case .email: @@ -65,11 +74,15 @@ final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeCo timerValue = 60 } timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in - guard let `self` = self else { return } + guard let self = self else { return } self.timerValue = self.timerValue - 1 } } + func viewDidDisappear() { + invalidateTimer() + } + func sendConfirmationCode(_ code: String) { view?.showLoading() interactor.sendConfirmationCode(code) @@ -105,15 +118,27 @@ extension CodeConfirmationPresenter { view?.hideLoading() } - func didConfirmCode(authenticationType: AuthenticationType) { + func didConfirmCode(response: AuthResponse) { + switch response.authenticationType { + case .login: + interactor.loadAccount(by: response.accountId) + case .register: + view?.hideLoading() + wireframe.continueSignUpFlow(with: response.accountId) + } + } + + func didSaveAccount() { view?.hideLoading() - wireframe?.codeValid(with: authenticationType) + wireframe.continueSuccessAuthentication() } - func didReceiveCodeConfirmationFailure(_ error: Error) { + func didReceiveFailure(_ error: Error) { view?.hideLoading() + +// let actions = [UIAlertAction(title: "OK", style: .default, handler: nil)] +// presentAlert(title: "Failure", message: "Code is invalid or internal error", actions: actions) // FIXME: check if it is internal error or real wrong code - wireframe?.codeInvalid() } } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift index 9c05c8622..d3e114b66 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -9,16 +9,16 @@ import UIKit import NynjaUIKit -final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, InitializeInjectable { - - private let viewsFactory: CodeConfirmationViewsFactoryProtocol +final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, LoadingDisplayable, InitializeInjectable { private let presenter: CodeConfirmationPresenterProtocol + private let isLogoVisible: Bool + // MARK: - Views - private lazy var backButton: UIButton = viewsFactory.makeBackButton(on: view, target: self, selector: #selector(back(sender:))) + private lazy var backButton = makeBackButton(on: view, target: self, selector: #selector(back(sender:))) private lazy var headerView: AuthHeaderView = { let headerView = AuthHeaderView() @@ -33,7 +33,6 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi }() private lazy var codeInputView: SecureCodeContainerView = { - let titleFontHeight = Constraints.titleLabel.fontHeight.adjustedByWidth let textFontHeight = Constraints.codeInputView.fontHeight.adjustedByWidth let descriptionFontHeight = Constraints.descriptionLabel.fontHeight.adjustedByWidth @@ -60,29 +59,19 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi return codeInputView }() - private lazy var timerLabel: UILabel = viewsFactory.makeTimerLabel(on: view) + private lazy var timerLabel = makeTimerLabel(on: view) private weak var resendCodeButton: UIButton? private weak var callMeButton: UIButton? - private lazy var progressHUD: ProgressHUD = { - let progressHUD = ProgressHUD() - - view.addSubview(progressHUD) - progressHUD.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - return progressHUD - }() + private(set) lazy var progressHUD = makeProgressHUD(on: view) // MARK: - Init init(dependencies: Dependencies) { presenter = dependencies.presenter - viewsFactory = dependencies.viewsFactory - + isLogoVisible = dependencies.isLogoVisible super.init(nibName: nil, bundle: nil) } @@ -103,7 +92,7 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi override func viewDidLoad() { super.viewDidLoad() - _ = [backButton, headerView, codeInputView, timerLabel, resendCodeButton, callMeButton] + _ = [headerView, codeInputView, timerLabel, resendCodeButton, callMeButton, backButton] view.backgroundColor = UIColor.nynja.backgroundColor codeInputView.titleLabel.text = presenter.address @@ -118,36 +107,35 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi presenter.viewDidLoad() } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + presenter.viewDidDisappear() + } } // MARK: - CodeConfirmationViewProtocol extension CodeConfirmationViewController { + func updateTimerLabel(text: String) { timerLabel.text = text } func showButtons() { timerLabel.isHidden = true - resendCodeButton = viewsFactory.makeResendCodeButton(on: view, target: self, selector: #selector(resendCode(sender:))) + resendCodeButton = makeResendCodeButton(on: view, target: self, selector: #selector(resendCode(sender:))) if presenter.canAskForCall { - callMeButton = viewsFactory.makeCallMeButton(on: view, top: resendCodeButton!, target: self, selector: #selector(callMe(sender:))) + callMeButton = makeCallMeButton(on: view, top: resendCodeButton!, target: self, selector: #selector(callMe(sender:))) } } - - func showLoading() { - progressHUD.startAnimating() - } - - func hideLoading() { - progressHUD.stopAnimating() - } } // MARK: - Actions -extension CodeConfirmationViewController { +private extension CodeConfirmationViewController { + @objc func back(sender: UIButton) { presenter.back() } @@ -166,7 +154,7 @@ extension CodeConfirmationViewController { extension CodeConfirmationViewController { struct Dependencies { let presenter: CodeConfirmationPresenterProtocol - let viewsFactory: CodeConfirmationViewsFactoryProtocol + let isLogoVisible: Bool } } @@ -174,6 +162,75 @@ extension CodeConfirmationViewController { private extension CodeConfirmationViewController { + func makeBackButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setImage(UIImage.nynja.icBackNavigation.image, for: .normal) + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.left.equalToSuperview().offset(8) + make.top.equalToSuperview().offset(37) + make.width.height.equalTo(24) + } + + return button + } + + func makeTimerLabel(on view: UIView) -> UILabel { + let label = UILabel() + view.addSubview(label) + + label.textAlignment = .center + label.textColor = UIColor.nynja.white + label.font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 16.0.adjustedByWidth) + + label.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-300) + make.width.lessThanOrEqualToSuperview() + } + + return label + } + + func makeResendCodeButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setTitle(String.localizable.codeConfirmationResendCode, for: .normal) + button.setTitleColor(UIColor.nynja.mainRed, for: .normal) + button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-300) + } + + return button + } + + func makeCallMeButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { + let button = UIButton() + view.addSubview(button) + + button.setTitle(String.localizable.codeConfirmationCall, for: .normal) + button.setTitleColor(UIColor.nynja.mainRed, for: .normal) + button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) + button.addTarget(target, action: selector, for: .touchUpInside) + + button.snp.makeConstraints { (make) in + make.centerX.equalToSuperview() + make.top.equalTo(top.snp.bottom).offset(10) + } + + return button + } + enum Constraints { enum titleLabel { diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift deleted file mode 100644 index 2395c1756..000000000 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/View/ViewsFactory/CodeConfirmationViewsFactory.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// CodeConfirmationViewsFactory.swift -// Nynja -// -// Created by Ash on 10/10/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -protocol CodeConfirmationViewsFactoryProtocol { - func makeBackButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeTimerLabel(on view: UIView) -> UILabel - func makeResendCodeButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeCallMeButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton -} - -final class CodeConfirmationViewsFactory: CodeConfirmationViewsFactoryProtocol { - func makeBackButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setImage(UIImage.nynja.icBackNavigation.image, for: .normal) - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.left.equalToSuperview().offset(8) - make.top.equalToSuperview().offset(37) - make.width.height.equalTo(24) - } - - return button - } - - func makeTimerLabel(on view: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.textAlignment = .center - label.textColor = UIColor.nynja.white - label.font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 16.0.adjustedByWidth) - - label.snp.makeConstraints { (make) in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-300) - make.width.lessThanOrEqualToSuperview() - } - - return label - } - - func makeResendCodeButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setTitle(String.localizable.codeConfirmationResendCode, for: .normal) - button.setTitleColor(UIColor.nynja.mainRed, for: .normal) - button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-300) - } - - return button - } - - func makeCallMeButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setTitle(String.localizable.codeConfirmationCall, for: .normal) - button.setTitleColor(UIColor.nynja.mainRed, for: .normal) - button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.centerX.equalToSuperview() - make.top.equalTo(top.snp.bottom).offset(10) - } - - return button - } -} diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index c333d947f..002b65588 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -22,30 +22,32 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto struct Parameters { let confirmationData: ConfirmationData + let isLogoVisible: Bool } struct Dependencies { + let storageService: StorageService let authService: AuthService + let accountService: AccountService } enum State { - case validCode(type: AuthenticationType) - case invalidCode + case registered(accountId: String) + case loggedIn case back } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = CodeConfirmationPresenter() - let view = CodeConfirmationViewController(dependencies: .init( - presenter: presenter, - viewsFactory: CodeConfirmationViewsFactory()) - ) + let view = CodeConfirmationViewController(dependencies: .init(presenter: presenter, isLogoVisible: true)) let interactor = CodeConfirmationInteractor(dependencies: .init( presenter: presenter, confirmationData: parameters.confirmationData, - authService: dependencies.authService) + storageService: dependencies.storageService, + authService: dependencies.authService, + accountService: dependencies.accountService) ) presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) @@ -53,12 +55,12 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto return view } - func codeValid(with type: AuthenticationType) { - coordinator.wireframe(self, didEndWith: .validCode(type: type)) + func continueSignUpFlow(with accountId: String) { + coordinator.wireframe(self, didEndWith: .registered(accountId: accountId)) } - func codeInvalid() { - coordinator.wireframe(self, didEndWith: .invalidCode) + func continueSuccessAuthentication() { + coordinator.wireframe(self, didEndWith: .loggedIn) } func back() { diff --git a/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift index 7a6d2c0f4..3d842f384 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift @@ -15,7 +15,7 @@ protocol CreateProfileWireframeProtocol: class { func open(url: URL) } -protocol CreateProfileViewProtocol: class where Self: UIViewController { +protocol CreateProfileViewProtocol: LoadingInteractive where Self: UIViewController { func updateProfileField(_ field: ProfileField, value: String) func setCreateEnabled(_ enabled: Bool) } @@ -34,9 +34,14 @@ protocol CreateProfileInputInteractorProtocol: class { func checkTermsOfUse() -> Bool func setAvatar(image: UIImage?) func isValidValue(_ value: String, for field: ProfileField) -> Result + func createAccount() } protocol CreateProfileOutputInteractorProtocol: class { + func minimalRequirementsAreSatisfied(_ satisfied: Bool) func profileFieldUpdated(_ profileField: ProfileField, value: String) + + func didReceiveCreatedAccount() + func didReceiveFailure(_ error: Error?) } diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift index 92a713154..b2c5667f8 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -10,10 +10,17 @@ import Foundation final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, InitializeInjectable { + private enum UploadingError: Error { + case imageCouldNotBeSaved + case uploadFailed + } + private weak var presenter: CreateProfileOutputInteractorProtocol? // MARK: - Fields + private let accountId: String + private var avatar: UIImage? = nil private var firstName: String = "" { @@ -53,19 +60,104 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, Initi } + // MARK: - Services + + private let storageService: StorageService + + private let resourceManager: ResourceManagerProtocol + + private let transferManager: TransferManager + + private let authService: AuthService + + private let accountService: AccountService + + // MARK: - Init struct Dependencies { let presenter: CreateProfileOutputInteractorProtocol + let accountId: String + let storageService: StorageService + let resourceManager: ResourceManagerProtocol + let transferManager: TransferManager + let authService: AuthService + let accountService: AccountService } init(dependencies: CreateProfileInteractor.Dependencies) { presenter = dependencies.presenter + accountId = dependencies.accountId + storageService = dependencies.storageService + resourceManager = dependencies.resourceManager + transferManager = dependencies.transferManager + authService = dependencies.authService + accountService = dependencies.accountService } // MARK: - CreateProfileInputInteractorProtocol + func createAccount() { + if let avatar = avatar { + uploadAvatar(avatar) { [weak self] result in + switch result { + case let .success(avatarURL): + self?.createAccount(withAvatar: avatarURL) + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + } else { + createAccount(withAvatar: nil) + } + } + + private func createAccount(withAvatar avatarURL: URL?) { + let accountInfo = AccountInfo(accountId: accountId, + avatar: avatarURL?.absoluteString, + accountMark: nil, + accountName: accountName, + firstName: firstName, + lastName: lastName, + username: userName, + accountStatus: .enabled, + roles: nil, + qrCode: nil, + birthday: nil) + + accountService.completePendingAccountCreation(accountInfo) { [weak self] result in + guard let self = self else { return } + + switch result { + case let .success(account): + do { + try self.authService.processAuthenticatedAccount(account) + self.presenter?.didReceiveCreatedAccount() + } catch { + self.presenter?.didReceiveFailure(error) + } + case let .failure(error): + self.presenter?.didReceiveFailure(error) + } + } + } + + private func uploadAvatar(_ image: UIImage, completion: @escaping (Result) -> Void) { + guard let localURL = resourceManager.savePhotoAsFile(image: image, setting: .highest) else { + completion(.failure(UploadingError.imageCouldNotBeSaved)) + return + } + transferManager.upload(url: localURL, listener: nil) { result in + switch result { + case let .success(remoteURL, _): + completion(.success(remoteURL)) + case let .error(error): + completion(.failure(error ?? UploadingError.uploadFailed)) + } + } + } + func checkTermsOfUse() -> Bool { checkTermsOfUsage = !checkTermsOfUsage @@ -78,11 +170,16 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, Initi func setProfileField(_ field: ProfileField, value: String) { switch field { - case .firstName: firstName = value - case .lastName: lastName = value - case .accountName: accountName = value - case .userName: userName = value - case .profileMessage: break + case .firstName: + firstName = value + case .lastName: + lastName = value + case .accountName: + accountName = value + case .userName: + userName = value + case .profileMessage: + break } } @@ -91,7 +188,7 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, Initi case somethingWentWrong } - let predicate = NSPredicate(format:"SELF MATCHES %@", field.validationRule) + let predicate = NSPredicate(format: "SELF MATCHES %@", field.validationRule) return predicate.evaluate(with: value) ? .success(()) diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift index 3886a7387..d2861eece 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -15,10 +15,11 @@ final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, Creat private var wireframe: CreateProfileWireframeProtocol! - // MARK: - CreateProfilePresenterProtocol + // MARK: - Presenter func createAccount() { - wireframe?.end() + view?.showLoading() + interactor.createAccount() } func isValidValue(_ value: String, for field: ProfileField) -> Result { @@ -30,10 +31,10 @@ final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, Creat } func chooseAvatar(completion: @escaping (UIImage?) -> Void) { - wireframe.chooseAvatar(completion: { (image) in + wireframe.chooseAvatar { image in completion(image) self.interactor?.setAvatar(image: image) - }) + } } func profileFieldUpdated(_ profileField: ProfileField, value: String) { @@ -49,7 +50,7 @@ final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, Creat } - // MARK: - CreateProfileOutputInteractorProtocol + // MARK: - Interactor Output func minimalRequirementsAreSatisfied(_ satisfied: Bool) { view?.setCreateEnabled(satisfied) @@ -58,6 +59,15 @@ final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, Creat func back() { wireframe.back() } + + func didReceiveCreatedAccount() { + view?.hideLoading() + wireframe?.end() + } + + func didReceiveFailure(_ error: Error?) { + view?.hideLoading() + } } // MARK: - SetInjectable diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift index b196db657..265e39018 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift @@ -8,10 +8,12 @@ import Foundation -final class CreateProfileViewController: UIViewController, CreateProfileViewProtocol, InitializeInjectable, KeyboardInteractive { +final class CreateProfileViewController: UIViewController, CreateProfileViewProtocol, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { private let presenter: CreateProfilePresenterProtocol private let viewsFactory: CreateProfileViewsFactoryProtocol + private(set) lazy var progressHUD = makeProgressHUD(on: view) + private lazy var topHeaderLayoutGuide: UILayoutGuide = viewsFactory.makeTopLayoutGuide(on: view) private lazy var headerView: NavigationView = viewsFactory.makeHeaderView(on: view, topLayoutGuide: topHeaderLayoutGuide, navigationHandler: presenter) private lazy var createButton: UIButton = viewsFactory.makeCreateButton(on: view, target: self, selector: #selector(createAccount(sender:))) diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift index cf46ca174..752d2b452 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -21,6 +21,18 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { case openTerms(url: URL) } + struct Parameters { + let accountId: String + } + + struct Dependencies { + let storageService: StorageService + let resourceManager: ResourceManagerProtocol + let transferManager: TransferManager + let authService: AuthService + let accountService: AccountService + } + private let coordinator: CreateProfileCoordinatorProtocol init(coordinator: CreateProfileCoordinatorProtocol) { @@ -30,14 +42,22 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = CreateProfilePresenter() - let viewDependencies = CreateProfileViewController.Dependencies(presenter: presenter, viewsFactory: CreateProfileViewsFactory()) - let view = CreateProfileViewController(dependencies: viewDependencies) + let view = CreateProfileViewController(dependencies: .init( + presenter: presenter, + viewsFactory: CreateProfileViewsFactory()) + ) - let interactorDependencies = CreateProfileInteractor.Dependencies(presenter: presenter) - let interactor = CreateProfileInteractor(dependencies: interactorDependencies) + let interactor = CreateProfileInteractor(dependencies: .init( + presenter: presenter, + accountId: parameters.accountId, + storageService: dependencies.storageService, + resourceManager: dependencies.resourceManager, + transferManager: dependencies.transferManager, + authService: dependencies.authService, + accountService: dependencies.accountService) + ) - let presenterDependencies = CreateProfilePresenter.Dependencies(wireframe: self, interactor: interactor, view: view) - presenter.inject(dependencies: presenterDependencies) + presenter.inject(dependencies: .init(wireframe: self, interactor: interactor, view: view)) return view } diff --git a/Nynja/NotificationManager.swift b/Nynja/NotificationManager.swift index 51c91018d..1a780e98c 100644 --- a/Nynja/NotificationManager.swift +++ b/Nynja/NotificationManager.swift @@ -147,7 +147,7 @@ final class NotificationManager { case .message: if let vc = self.getMessageVC { dispatchAsyncMain { [weak self] in - guard let `self` = self, self.isFromPush else { + guard let self = self, self.isFromPush else { return } @@ -369,7 +369,7 @@ final class NotificationManager { private func showNotificationView(view: NotificationView) { dispatchAsyncMain { [weak self] in - guard let `self` = self else { + guard let self = self else { return } diff --git a/Nynja/Resources/profile.bert b/Nynja/Resources/profile.bert new file mode 100644 index 0000000000000000000000000000000000000000..983836056d2b3aa85186efa00a1044a5b00ab979 GIT binary patch literal 8873 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 E-)w>e>;M1& literal 0 HcmV?d00001 diff --git a/Nynja/SDK/Account/Service/AccountServiceImpl.swift b/Nynja/SDK/Account/Service/AccountServiceImpl.swift index c587d0318..ddb8cc3be 100644 --- a/Nynja/SDK/Account/Service/AccountServiceImpl.swift +++ b/Nynja/SDK/Account/Service/AccountServiceImpl.swift @@ -10,14 +10,12 @@ import Foundation import NynjaSDK final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, NYNAccountManagerDelegate { - + // MARK: - Dependencies private let accountManager: AccountManager - private let sessionStorage: SessionStorage - - private let appConfigurationProvider: AppConfigurationProvider + private let storage: SessionStorage private let processingQueue: DispatchQueue @@ -41,7 +39,7 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, private var deleteContactInfoCompletion: StatusCompletion? private var token: String? { - return sessionStorage.token + return storage.token } @@ -49,15 +47,13 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, struct Dependencies { let accountManager: AccountManager - let sessionStorage: SessionStorage - let appConfigurationProvider: AppConfigurationProvider + let storage: SessionStorage let processingQueue: DispatchQueue = .main } init(dependencies: Dependencies) { accountManager = dependencies.accountManager - sessionStorage = dependencies.sessionStorage - appConfigurationProvider = dependencies.appConfigurationProvider + storage = dependencies.storage processingQueue = dependencies.processingQueue super.init() @@ -381,4 +377,15 @@ extension AccountServiceImpl { completion?(.success(status)) } } + + + // MARK: Search + + func searchByPhoneNumberDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + + } + + func searchByQrCodeDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + + } } diff --git a/Nynja/SDK/Auth/Entities/AuthResponse.swift b/Nynja/SDK/Auth/Entities/AuthResponse.swift index c3c318ebb..b14b8a90d 100644 --- a/Nynja/SDK/Auth/Entities/AuthResponse.swift +++ b/Nynja/SDK/Auth/Entities/AuthResponse.swift @@ -8,6 +8,5 @@ struct AuthResponse { let accountId: String - let tokenData: AuthTokenData let authenticationType: AuthenticationType } diff --git a/Nynja/SDK/Auth/ProfileMockProvider.swift b/Nynja/SDK/Auth/ProfileMockProvider.swift new file mode 100644 index 000000000..de81844fe --- /dev/null +++ b/Nynja/SDK/Auth/ProfileMockProvider.swift @@ -0,0 +1,41 @@ +// +// ProfileMockProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/29/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class ProfileMockProvider { + + private let mockFileName = "profile.bert" + + func makeProfile(id: String) -> DBProfile { + do { + guard let url = Bundle.main.url(forResource: "profile", withExtension: "bert") else { + fatalError("\(#file), \(#function): file not found") + } + let data = try Data(contentsOf: url) + let bertObject = try Bert.decode(data: data as NSData) + + let profile = get_Profile().parse(bert: bertObject) as! Profile + + profile.phone = id + + return DBProfile(profile: profile)! + } catch { + fatalError("\(#file), \(#function): \(error)") + } + } + + func saveProfile(data: BertObject) throws { + let data = try Bert.encode(object: data) + let url = try FileManager.default + .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + .appendingPathComponent(mockFileName) + + data.write(to: url, atomically: true) + } +} diff --git a/Nynja/SDK/Auth/Service/AuthService.swift b/Nynja/SDK/Auth/Service/AuthService.swift index 20cce9787..48aa62561 100644 --- a/Nynja/SDK/Auth/Service/AuthService.swift +++ b/Nynja/SDK/Auth/Service/AuthService.swift @@ -25,4 +25,6 @@ protocol AuthService: class { func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) + + func processAuthenticatedAccount(_ account: Account) throws } diff --git a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift index 1521336af..52d4e4752 100644 --- a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -15,7 +15,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog private let loginManager: LoginManager - private let sessionStorage: SessionStorage + private let storage: StorageService private let appConfigurationProvider: AppConfigurationProvider @@ -36,14 +36,14 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog struct Dependencies { let loginManager: LoginManager - let sessionStorage: SessionStorage + let storage: StorageService let appConfigurationProvider: AppConfigurationProvider let processingQueue: DispatchQueue = .main } init(dependencies: Dependencies) { loginManager = dependencies.loginManager - sessionStorage = dependencies.sessionStorage + storage = dependencies.storage appConfigurationProvider = dependencies.appConfigurationProvider processingQueue = dependencies.processingQueue @@ -103,14 +103,36 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog loginManager.refreshAccessToken(accessToken) } + func processAuthenticatedAccount(_ account: Account) throws { + let account = DBAccount(account: account) + + if case let identityId = account.profileId, !identityId.isEmpty { + LogService.log(topic: .db) { return "Setup DB: Prifile Handler" } + storage.setupDatabase(with: identityId, application: UIApplication.shared) + } else { + assertionFailure("Unable to setup database") + } + + storage.identityId = account.profileId + storage.accountId = account.accountId + + // FIXME: must be removed + let profile = ProfileMockProvider().makeProfile(id: account.profileId) + profile.accounts = [account] + + let roster = profile.rosters.first! + storage.phone = roster.phone + storage.rosterId = roster.id + storage.clientId = UIDevice.current.persistentIdentifier + + try storage.perform(action: .save, with: profile) + } + // MARK: - Delegate func sendLogin(byEmailDidFinish error: Error?) { - processingQueue.async { - let completion = self.loginByEmailCompletion - self.loginByEmailCompletion = nil - + handleResponse(nil, to: \AuthServiceImpl.loginByEmailCompletion) { completion in if let error = error { completion?(.failure(error)) } else { @@ -120,10 +142,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func sendLogin(byPhoneDidFinish error: Error?) { - processingQueue.async { - let completion = self.loginByPhoneCompletion - self.loginByPhoneCompletion = nil - + handleResponse(nil, to: \AuthServiceImpl.loginByPhoneCompletion) { completion in if let error = error { completion?(.failure(error)) } else { @@ -133,10 +152,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func sendLogin(byFacebookDidFinish error: Error?) { - processingQueue.async { - let completion = self.loginByFacebookCompletion - self.loginByFacebookCompletion = nil - + handleResponse(nil, to: \AuthServiceImpl.loginByFacebookCompletion) { completion in if let error = error { completion?(.failure(error)) } else { @@ -146,10 +162,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func sendLogin(byGooglePlusDidFinish error: Error?) { - processingQueue.async { - let completion = self.loginByGoogleCompletion - self.loginByGoogleCompletion = nil - + handleResponse(nil, to: \AuthServiceImpl.loginByGoogleCompletion) { completion in if let error = error { completion?(.failure(error)) } else { @@ -164,21 +177,17 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog withExpiration expiration: NSNumber?, isPendingAccount pending: Bool, withError error: Error?) { - processingQueue.async { - let completion = self.confirmCodeCompletion - self.confirmCodeCompletion = nil - + handleResponse(nil, to: \AuthServiceImpl.confirmCodeCompletion) { completion in if let error = error { + self.storage.clearToken() completion?(.failure(error)) - } else { - let tokenData = self.makeTokenData(accessToken: accessToken, refreshToken: refreshToken, expiration: expiration) - - let response = AuthResponse(accountId: accountId, - tokenData: tokenData, - authenticationType: pending ? .register : .login) - - completion?(.success(response)) + return } + self.storage.save(accessToken: accessToken, refreshToken: refreshToken) + + let response = AuthResponse(accountId: accountId, authenticationType: pending ? .register : .login) + + completion?(.success(response)) } } @@ -186,16 +195,16 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog withRefreshToken refreshToken: String, withExpiration expiration: NSNumber?, withError error: Error?) { - processingQueue.async { - let completion = self.refreshTokenCompletion - self.refreshTokenCompletion = nil - + handleResponse(nil, to: \AuthServiceImpl.refreshTokenCompletion) { completion in if let error = error { + self.storage.clearToken() completion?(.failure(error)) - } else { - let tokenData = self.makeTokenData(accessToken: accessToken, refreshToken: refreshToken, expiration: expiration) - completion?(.success(tokenData)) + return } + self.storage.save(accessToken: accessToken, refreshToken: refreshToken) + + let tokenData = self.makeTokenData(accessToken: accessToken, refreshToken: refreshToken, expiration: expiration) + completion?(.success(tokenData)) } } @@ -210,6 +219,14 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog self[keyPath: keyPath] = value } } + + private func handleResponse(_ newValue: T, to keyPath: ReferenceWritableKeyPath, handler: @escaping (T) -> Void) { + processingQueue.async { + let completion = self[keyPath: keyPath] + self[keyPath: keyPath] = newValue + handler(completion) + } + } } // MARK: - Extensions diff --git a/Nynja/SDK/Session/SessionStorage.swift b/Nynja/SDK/Session/SessionStorage.swift index 1b241edfe..1a89bf33c 100644 --- a/Nynja/SDK/Session/SessionStorage.swift +++ b/Nynja/SDK/Session/SessionStorage.swift @@ -7,5 +7,13 @@ // protocol SessionStorage: class { - var token: String? { get set } + + var identityId: String? { get set } + var accountId: String? { get set } + + var token: String? { get } + var refreshToken: String? { get } + + func save(accessToken: String, refreshToken: String) + func clearToken() } diff --git a/Nynja/Services/HandleServices/ProfileHandler.swift b/Nynja/Services/HandleServices/ProfileHandler.swift index 9d3b73d7b..bb63ccfd6 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -41,7 +41,7 @@ final class ProfileHandler: BaseHandler, StaticDelegating { switch status { case "get", "init", "patch": - if !storageService.isUserLogined, let phoneId = profile.phoneId { + if !storageService.isUserLogined, case let phoneId = "\(profile.phone!)_\((profile.rosters!.first as! Roster).id!)" { LogService.log(topic: .db) { return "Setup DB: Prifile Handler" } storageService.setupDatabase(with: phoneId, application: UIApplication.shared) } diff --git a/Nynja/Services/MQTT/Extensions/MQTTService+Helper.swift b/Nynja/Services/MQTT/Extensions/MQTTService+Helper.swift index 052f20245..ac2abe8ee 100644 --- a/Nynja/Services/MQTT/Extensions/MQTTService+Helper.swift +++ b/Nynja/Services/MQTT/Extensions/MQTTService+Helper.swift @@ -13,7 +13,7 @@ extension MQTTService { func printMessage(msg: CocoaMQTTMessage, isSent: Bool) { queuePool.loggingQueue.async { [weak self] in - guard let `self` = self else { return } + guard let self = self else { return } let desc = self.prepareOutputMessage(msg: msg, isSent: isSent) guard let bert = desc.1 as? BertTuple else { diff --git a/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift index 55db5e650..b8e8472dc 100644 --- a/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift @@ -10,4 +10,5 @@ import NynjaSDK protocol MobileSDKFactoryProtocol: class { func makeLoginManager() -> LoginManager + func makeAccountManager() -> AccountManager } diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 99b7fcc17..7dbee2a05 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -58,7 +58,7 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { // Lazy wars is not thread safe, so it must be initialized from single thread private lazy var authService: AuthService = { let dependencies = AuthServiceImpl.Dependencies(loginManager: makeLoginManager(), - sessionStorage: makeStorageService(), + storage: makeStorageService(), appConfigurationProvider: makeAppConfigurationProvider()) return AuthServiceImpl(dependencies: dependencies) }() @@ -67,6 +67,16 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return authService } + // Lazy wars is not thread safe, so it must be initialized from single thread + private lazy var accountService: AccountService = { + let dependencies = AccountServiceImpl.Dependencies(accountManager: makeAccountManager(), storage: makeStorageService()) + return AccountServiceImpl(dependencies: dependencies) + }() + + func makeAccountService() -> AccountService { + return accountService + } + func makeGoogleAuthService() -> GoogleAuthService { return GoogleAuthServiceImpl() } @@ -125,6 +135,10 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return WalletFundingNetworkService(dependencies: dependencies) } + func makeTransferManager() -> TransferManager { + return TransferManager.shared + } + func makeSyncFileManager() -> SyncFileManager { return SyncFileManager.sharedInstance } @@ -229,4 +243,8 @@ extension ServiceFactory { func makeLoginManager() -> LoginManager { return makeCommunicator().getLoginManager() } + + func makeAccountManager() -> AccountManager { + return makeCommunicator().getAccountManager() + } } diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift index 91d0c858a..97801efc4 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -21,6 +21,7 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtoc func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol func makeAuthService() -> AuthService + func makeAccountService() -> AccountService func makeGoogleAuthService() -> GoogleAuthService func makeAppConfigurationProvider() -> AppConfigurationProvider @@ -37,6 +38,7 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtoc func makePermissionManager() -> PermissionManager func makeWalletService() -> WalletService + func makeTransferManager() -> TransferManager func makeSyncFileManager() -> SyncFileManager func makeSyncFileManager(with kind: FileDownloaderKind) -> SyncFileManager diff --git a/Nynja/Services/StorageService.swift b/Nynja/Services/StorageService.swift index fe9b0754b..3bf596a42 100644 --- a/Nynja/Services/StorageService.swift +++ b/Nynja/Services/StorageService.swift @@ -17,13 +17,8 @@ final class StorageService { private let passphraseKey = KeychainService.Keys.dataBasePassphrase - let userDefaults = UserDefaults(suiteName: Bundle.main.appGroupName) - let keychain = KeychainService.standard - - private(set) lazy var userInfo: UserInfo = { - return UserInfoImpl( - dependencies: .init(userDefaults: userDefaults)) - }() + let keychain = KeychainService.standard + let userInfo: UserInfo #if !SHARE_EXTENSION @@ -44,7 +39,10 @@ final class StorageService { static let sharedInstance = StorageService() - private init() {} + private init() { + let userDefaults = UserDefaults(suiteName: Bundle.main.appGroupName) + userInfo = UserInfoImpl(dependencies: .init(userDefaults: userDefaults)) + } // MARK: - Setup @@ -164,6 +162,16 @@ extension StorageService: DBManagerProtocol { extension StorageService: UserInfo, SessionStorage { + var identityId: String? { + get { return userInfo.identityId } + set { userInfo.identityId = newValue } + } + + var accountId: String? { + get { return userInfo.accountId } + set { userInfo.accountId = newValue } + } + var token: String? { get { return userInfo.token } set { userInfo.token = newValue } @@ -172,6 +180,15 @@ extension StorageService: UserInfo, SessionStorage { var tokenData: Data? { get { return userInfo.tokenData } } + + var refreshToken: String? { + get { return userInfo.refreshToken } + set { userInfo.refreshToken = newValue } + } + + var refreshTokenData: Data? { + get { return userInfo.refreshTokenData } + } var pushToken: String? { get { return userInfo.pushToken } @@ -208,8 +225,19 @@ extension StorageService: UserInfo, SessionStorage { self.token = token } + @available(*, deprecated, message: "Will be removed later") func setupUserInfo(from roster: Roster) { phone = roster.phone rosterId = roster.id } + + func save(accessToken: String, refreshToken: String) { + self.token = accessToken + self.refreshToken = refreshToken + } + + func clearToken() { + token = nil + refreshToken = nil + } } diff --git a/Nynja/TransferManager.swift b/Nynja/TransferManager.swift index 7f0f77c9e..a3d42f57a 100644 --- a/Nynja/TransferManager.swift +++ b/Nynja/TransferManager.swift @@ -20,8 +20,9 @@ private class ProcessingDescription { var url: URL? } -class TransferManager { - static var shared = TransferManager() +final class TransferManager { + + static let shared = TransferManager() private var isolationQueue = DispatchQueue(label: "com.nynja.dev.mobile.communicator.transfer-manager", qos: .userInitiated, diff --git a/Nynja/UserInfo.swift b/Nynja/UserInfo.swift index c04b6179c..8f5acc082 100644 --- a/Nynja/UserInfo.swift +++ b/Nynja/UserInfo.swift @@ -9,7 +9,11 @@ import Foundation enum UserIdentifiers: String { + case identityId + case accountId + case token + case refreshToken case pushToken case phone @@ -21,16 +25,21 @@ enum UserIdentifiers: String { } protocol UserInfo: class { + var identityId: String? { get set } + var accountId: String? { get set } + var token: String? { get set } var tokenData: Data? { get } + var refreshToken: String? { get set } + var refreshTokenData: Data? { get } + var pushToken: String? { get set } var phone: String? { get set } var rosterId: Int64? { get set } var clientId: String? { get set } - var wasLogined: Bool { get set } var wasRun: Bool { get set } } @@ -38,18 +47,20 @@ protocol UserInfo: class { extension UserInfo { var phoneId: String? { - get { - guard let phone = phone, let rosterId = rosterId else { - return nil - } - return "\(phone)_\(rosterId)" + guard let phone = phone, let rosterId = rosterId else { + return nil } + return "\(phone)_\(rosterId)" } var hasToken: Bool { return token != nil } + var hasRefreshToken: Bool { + return refreshToken != nil + } + var hasPhone: Bool { return phone != nil } @@ -59,8 +70,10 @@ extension UserInfo { } func dropUserInfo() { - LogService.log(topic: .userDefaults) { return "drop user info" } + identityId = nil + accountId = nil token = nil + refreshToken = nil phone = nil rosterId = nil clientId = nil diff --git a/Nynja/UserInfoImpl.swift b/Nynja/UserInfoImpl.swift index c253c77b8..2ea568e71 100644 --- a/Nynja/UserInfoImpl.swift +++ b/Nynja/UserInfoImpl.swift @@ -28,6 +28,16 @@ final class UserInfoImpl: UserInfo, InitializeInjectable { return .utf8 } + var identityId: String? { + get { return value(forId: .identityId) } + set { set(newValue, forId: .identityId) } + } + + var accountId: String? { + get { return value(forId: .accountId) } + set { set(newValue, forId: .accountId) } + } + var tokenData: Data? { return value(forId: .token) } @@ -44,10 +54,26 @@ final class UserInfoImpl: UserInfo, InitializeInjectable { userDefaults?.synchronize() return } - set(data as NSData, forId: .token) LogService.log(topic: .userDefaults) { return "Save token: \(token)" } - MQTTService.sharedInstance.reconnect() // FIXME: I don't think it is proper place to do such thing + } + } + + var refreshTokenData: Data? { + return value(forId: .refreshToken) + } + + var refreshToken: String? { + get { + return refreshTokenData.flatMap { String(data: $0, encoding: encoding) } + } + set { + guard let refreshToken = newValue, let data = refreshToken.data(using: encoding) else { + userDefaults?.removeObject(forKey: UserIdentifiers.refreshToken.rawValue) + userDefaults?.synchronize() + return + } + set(data as NSData, forId: .refreshToken) } } diff --git a/NynjaUnitTests/InputsCachePolicy/InputsCachePolicyTest.swift b/NynjaUnitTests/InputsCachePolicy/InputsCachePolicyTest.swift index ca4679f7b..1520288fa 100644 --- a/NynjaUnitTests/InputsCachePolicy/InputsCachePolicyTest.swift +++ b/NynjaUnitTests/InputsCachePolicy/InputsCachePolicyTest.swift @@ -7,6 +7,7 @@ // import XCTest +import NynjaUIKit class InputsCachePolicyTest: XCTestCase { diff --git a/NynjaUnitTests/InputsCachePolicy/TextFieldTest.swift b/NynjaUnitTests/InputsCachePolicy/TextFieldTest.swift index 6675a8b73..0f6933918 100644 --- a/NynjaUnitTests/InputsCachePolicy/TextFieldTest.swift +++ b/NynjaUnitTests/InputsCachePolicy/TextFieldTest.swift @@ -7,6 +7,7 @@ // import XCTest +import NynjaUIKit class TextFieldTest: XCTestCase { diff --git a/NynjaUnitTests/Services/UserInfo/UserInfoTest.swift b/NynjaUnitTests/Services/UserInfo/UserInfoTest.swift index 33f8f8054..77f2a8bc8 100644 --- a/NynjaUnitTests/Services/UserInfo/UserInfoTest.swift +++ b/NynjaUnitTests/Services/UserInfo/UserInfoTest.swift @@ -8,11 +8,14 @@ import XCTest -class UserInfoTest: XCTestCase { +final class UserInfoTest: XCTestCase { + private let identityId = "identity-id" + private let accountId = "123" private let phone = "380958219752" private let rosterId: Int64 = 17 private let token = "token" + private let refreshToken = "refreshToken" func testPhoneIdSuccess() { let userInfo = UserInfoMock() @@ -60,12 +63,25 @@ class UserInfoTest: XCTestCase { XCTAssertTrue(userInfo.hasToken) } + func testHasRefreshToken() { + let userInfo = UserInfoMock() + userInfo.refreshToken = refreshToken + + XCTAssertTrue(userInfo.hasRefreshToken) + } + func testNoToken() { let userInfo = UserInfoMock() XCTAssertFalse(userInfo.hasToken) } + func testNoRefreshToken() { + let userInfo = UserInfoMock() + + XCTAssertFalse(userInfo.hasRefreshToken) + } + func testIsUserLogined() { let userInfo = UserInfoMock() userInfo.phone = phone @@ -88,29 +104,46 @@ class UserInfoTest: XCTestCase { XCTAssertFalse(userInfo.isUserLogined) } + func testIsNotUserLoginedWithRefreshToken() { + let userInfo = UserInfoMock() + userInfo.phone = phone + userInfo.refreshToken = refreshToken + + XCTAssertFalse(userInfo.isUserLogined) + } + func testDropUserInfo() { let userInfo = UserInfoMock() + userInfo.identityId = identityId + userInfo.accountId = accountId userInfo.phone = phone userInfo.rosterId = rosterId userInfo.token = token + userInfo.refreshToken = refreshToken userInfo.clientId = "client_id" userInfo.dropUserInfo() + XCTAssertNil(userInfo.identityId) + XCTAssertNil(userInfo.accountId) XCTAssertNil(userInfo.phone) XCTAssertNil(userInfo.rosterId) XCTAssertNil(userInfo.token) + XCTAssertNil(userInfo.refreshToken) XCTAssertNil(userInfo.clientId) } - } private extension UserInfoTest { - class UserInfoMock: UserInfo { + final class UserInfoMock: UserInfo { + var identityId: String? + var accountId: String? var token: String? var tokenData: Data? + var refreshToken: String? + var refreshTokenData: Data? var pushToken: String? var phone: String? var rosterId: Int64? diff --git a/Podfile b/Podfile index b7ad47031..728ef86ed 100644 --- a/Podfile +++ b/Podfile @@ -40,7 +40,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.6' # pod 'NynjaSDK', '= 1.8' - pod 'NynjaSDK-MultiAcc', '= 0.5.6.1' + pod 'NynjaSDK-MultiAcc', '= 0.5.6.2' pod 'CryptoSwift', '= 0.13.0' @@ -68,6 +68,7 @@ def commonPodsForNynjaTests pod 'AutoScrollLabel', '= 0.4.3' pod 'JTAppleCalendar', '= 7.1.6' + pod 'MaterialComponents/ActivityIndicator', '= 55.3.0' pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' end diff --git a/Podfile.lock b/Podfile.lock index c49b8ebe3..5cf37df73 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -90,7 +90,7 @@ PODS: - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.6.1) + - NynjaSDK-MultiAcc (0.5.6.2) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -124,7 +124,7 @@ DEPENDENCIES: - MaterialComponents/ActivityIndicator (= 55.3.0) - MaterialComponents/FlexibleHeader (= 55.3.0) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.6.1) + - NynjaSDK-MultiAcc (= 0.5.6.2) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.2.0) @@ -214,7 +214,7 @@ SPEC CHECKSUMS: MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: 036c00a535be1b4e3f44bb9703a785a7bb63b18d + NynjaSDK-MultiAcc: 8f5abbb87a6094d330f6edd9e2ee1d08f0a9cab3 QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a @@ -222,6 +222,6 @@ SPEC CHECKSUMS: SwiftyJSON: c4bcba26dd9ec7a027fc8eade48e2c911f229e96 TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: 8efd1d6c055d050f06d3dd21496decb8758b29f7 +PODFILE CHECKSUM: c5861c545969574ff6552b225cf0e4aa55e62a7c COCOAPODS: 1.5.3 -- GitLab From a6c6ea0a89d793453eec23164bf98b9d3582d496 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 5 Dec 2018 13:29:06 +0200 Subject: [PATCH 125/138] [Multiple accounts] Edit profile - SDK integration (#1506) * [NY-3855] Add login options module skeleton. * [NY-3855] Implemented base UI for login options. * [NY-3855] Added alerts. * [NY-3855] Added LoginOption model. Minor changes. * Fixed compile issues. * [NY-3855] Fixed screen title * Fixed switch * [NY-5519] Add AuthProvider module from template * Minor renaming changes. * Remove unused references from project file * Added transition to 'add auth provider' screen. * Implemented base UI for add auth provider. * Fixed UI * Fixed UI hierarchy. * Fixed UI components reusability from auth screen. * Implemented code confirmation input as a separate view. * Add new files * Remove unused code * Minor refactor. * Fixed accessibility and fonts construction. * Present code confirmation * Present code confirmation * Add adjust on code confirmation screen. * [NY-5508] Added profile mock. * [NY-5508] Minot refactoring in AuthService * [NY-5508] Implemented profile mock provider. * [NY-5508] Save auth token in AuthService. Update test target. * [NY-5508] Make userInfo not lazy in StorageService. * [NY-5508] Added DBModels. * Added AccountTable * Fixed compile issues. * [NY-5508] Added ContactInfoTable * [NY-5508] Added db migration for account and contact info tables. Pass AccountService to CreateProfile module. * [NY-5508] Updated code confirmation flow * [NY-5508] Remove code confirmation views factory. * [NY-5508] Upload avatar logic * [NY-5508] Add loading indicator on Create profile screen. * [NY-5508] Added AppCoordinator and try to present MainWireframe but receive crash. * [NY-5508] Open profile screen with mocked data. * [NY-5508] Setup database * [NY-5508] Refactoring * [NY-5508] Merge DBProfile and DBAccount * [NY-5507] Fixed retain cycles in Account Settings module. Refactored protocol names. * [NY-5507] Pass dependencies to Account Settings * [NY-5507] Update account logic. * [NY-5507] Implemented avatar uploading. --- Nynja.xcodeproj/project.pbxproj | 58 ++++--- Nynja/DB/Models/DBAccount.swift | 12 +- Nynja/Generated/ColorsConstants.swift | 2 + Nynja/Generated/LocalizableConstants.swift | 8 +- Nynja/ImageUploader/ImageUploader.swift | 58 +++++++ .../AccountSettingsProtocols.swift | 23 ++- .../Entities/SettingsSetAvatarCellModel.swift | 6 +- .../Entities/StatusTimeout.swift | 1 - .../Entities/UserAccount.swift | 24 +++ .../Entities/UserContact.swift | 1 - .../Entities/UserProfile.swift | 5 +- .../AccountSettings/Entities/UserStatus.swift | 1 - .../AccountSettingsInteractor.swift | 154 +++++++++++++----- .../Presenter/AccountSettingsPresenter.swift | 55 ++++--- .../View/AccountSettingsViewController.swift | 128 ++++++++------- .../View/Cells/MaterialTextFieldTVCell.swift | 2 + .../View/Cells/SettingsSetAvatarTVCell.swift | 32 ++-- .../Header/SettingsSectionHeaderView.swift | 2 +- .../AccountSettingsViewsFactory.swift | 18 -- .../Wireframe/AccountSettingsWireframe.swift | 44 +++-- .../AuthProvider/AuthProviderProtocols.swift | 10 ++ .../AccountSettingsCoordinator.swift | 36 +++- .../LoginOptions/LoginOptionsProtocols.swift | 10 ++ Nynja/Modules/Auth Flow/AppCoordinator.swift | 9 +- Nynja/Modules/Auth Flow/AuthCoordinator.swift | 14 +- .../Auth Flow/AuthModule/AuthProtocols.swift | 16 +- .../Interactor/AuthInteractor.swift | 8 +- .../AuthModule/Presenter/AuthPresenter.swift | 10 +- .../AuthModule/View/AuthViewController.swift | 4 +- .../CodeConfirmationProtocols.swift | 6 +- .../CodeConfirmationInteractor.swift | 6 +- .../Presenter/CodeConfirmationPresenter.swift | 10 +- .../View/CodeConfirmationViewController.swift | 4 +- .../CreateProfileProtocols.swift | 16 +- .../Interactor/CreateProfileInteractor.swift | 42 +---- .../Presenter/CreateProfilePresenter.swift | 10 +- .../View/CreateProfileViewController.swift | 18 +- .../CreateProfileViewsFactory.swift | 2 +- .../Wireframe/CreateProfileWireframe.swift | 6 +- .../Facebook/FacebookAuthProtocols.swift | 16 +- .../Intreractor/FacebookAuthInteractor.swift | 8 +- .../Presenter/FacebookAuthPresenter.swift | 16 +- .../View/FacebookAuthViewController.swift | 4 +- .../Splash/Interactor/SplashInteractor.swift | 0 .../Splash/Presenter/SplashPresenter.swift | 0 .../Splash/SplashProtocols.swift | 0 .../Splash/View/SplashViewController.swift | 0 .../Splash/WireFrame/SplashWireframe.swift | 0 Nynja/Resources/Colors.json | 1 + Nynja/Resources/en.lproj/Localizable.strings | 7 +- Nynja/SDK/Auth/Service/AuthServiceImpl.swift | 4 +- .../ServiceFactory/DAOFactoryProtocol.swift | 11 ++ .../ServiceFactory/MQTTFactoryProtocol.swift | 2 - .../ServiceFactory/ServiceFactory.swift | 13 ++ .../ServiceFactoryProtocol.swift | 4 +- .../Storage/DAO/Account/AccountDAO.swift | 29 ++++ .../DAO/Account/AccountDAOProtocol.swift | 14 ++ 57 files changed, 658 insertions(+), 342 deletions(-) create mode 100644 Nynja/ImageUploader/ImageUploader.swift create mode 100644 Nynja/Modules/Account Flow/AccountSettings/Entities/UserAccount.swift delete mode 100644 Nynja/Modules/Account Flow/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift rename Nynja/Modules/{ => Auth Flow}/Splash/Interactor/SplashInteractor.swift (100%) rename Nynja/Modules/{ => Auth Flow}/Splash/Presenter/SplashPresenter.swift (100%) rename Nynja/Modules/{ => Auth Flow}/Splash/SplashProtocols.swift (100%) rename Nynja/Modules/{ => Auth Flow}/Splash/View/SplashViewController.swift (100%) rename Nynja/Modules/{ => Auth Flow}/Splash/WireFrame/SplashWireframe.swift (100%) create mode 100644 Nynja/Services/ServiceFactory/DAOFactoryProtocol.swift create mode 100644 Nynja/Services/Storage/DAO/Account/AccountDAO.swift create mode 100644 Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index a9eff9928..4d73bf144 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -481,6 +481,9 @@ 38182BD2C2E0C783796C8AA1 /* QRCodeReaderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D247CBC45C1C1267BBBB289 /* QRCodeReaderInteractor.swift */; }; 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */; }; 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0281F61F53794800206871 /* UIViewExtenstions.swift */; }; + 3A0A94D521B53478007421AA /* AccountDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A94D421B53478007421AA /* AccountDAOProtocol.swift */; }; + 3A0A94D721B53491007421AA /* AccountDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A94D621B53491007421AA /* AccountDAO.swift */; }; + 3A0A94D921B544B4007421AA /* DAOFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A94D821B544B4007421AA /* DAOFactoryProtocol.swift */; }; 3A0AEA6A21AFF0FE0066CBBA /* profile.bert in Resources */ = {isa = PBXBuildFile; fileRef = 3A0AEA6921AFF0FD0066CBBA /* profile.bert */; }; 3A0AEA6D21AFF3FE0066CBBA /* ProfileMockProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA6C21AFF3FE0066CBBA /* ProfileMockProvider.swift */; }; 3A0AEA7121B018380066CBBA /* DBAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA7021B018380066CBBA /* DBAccount.swift */; }; @@ -515,6 +518,7 @@ 3A27B0A71EF307A900B4B3CB /* DeleteUserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27B0A61EF307A900B4B3CB /* DeleteUserModel.swift */; }; 3A2843291EF9317100EFE21A /* Avenir.ttc in Resources */ = {isa = PBXBuildFile; fileRef = 3A2843261EF9314100EFE21A /* Avenir.ttc */; }; 3A2A99831EFAD2FB002749B3 /* PageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2A99821EFAD2FB002749B3 /* PageControl.swift */; }; + 3A37416121B58AAA00F212B9 /* ImageUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A37416021B58AAA00F212B9 /* ImageUploader.swift */; }; 3A3FD2831F39E0A000B6958F /* HistoryRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */; }; 3A62B7D81F4CB9D100F45B51 /* BaseMQTTModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */; }; 3A771CAA1F191B38008D968A /* ProfileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A771CA91F191B38008D968A /* ProfileHandler.swift */; }; @@ -804,7 +808,7 @@ 5E7D5D3F218C5A12009B5D8D /* StatusTimeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */; }; 5E7D5D41218C5A36009B5D8D /* UserContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D40218C5A36009B5D8D /* UserContact.swift */; }; 5E7D5D43218C5A4C009B5D8D /* UserContactAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */; }; - 5E7D5D45218C5A5D009B5D8D /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D44218C5A5D009B5D8D /* UserProfile.swift */; }; + 5E7D5D45218C5A5D009B5D8D /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D44218C5A5D009B5D8D /* UserAccount.swift */; }; 5E7D5D47218C5D0A009B5D8D /* SettingsSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */; }; 5E7D5D4A218C5D42009B5D8D /* SettingsSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */; }; 5E7D5D4C218C6239009B5D8D /* SettingsSetAvatarTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */; }; @@ -826,7 +830,6 @@ 5EDD455121885EE300C50BC8 /* AccountSettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */; }; 5EDD455321885F7800C50BC8 /* AccountSettingsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */; }; 5EDD45552188601400C50BC8 /* AccountSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */; }; - 5EDD455821899BCE00C50BC8 /* AccountSettingsViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */; }; 5EEB73AA215D406400D8ECE6 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */; }; 5EEB73B2216046FE00D8ECE6 /* CodeConfirmationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */; }; 5EEB73B4216047E000D8ECE6 /* CodeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEB73B3216047E000D8ECE6 /* CodeConfirmationViewController.swift */; }; @@ -2906,6 +2909,9 @@ 35F2DA601F73CAD400777920 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerViewController.swift; sourceTree = ""; }; 3A0281F61F53794800206871 /* UIViewExtenstions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtenstions.swift; sourceTree = ""; }; + 3A0A94D421B53478007421AA /* AccountDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AccountDAOProtocol.swift; path = Services/Storage/DAO/Account/AccountDAOProtocol.swift; sourceTree = ""; }; + 3A0A94D621B53491007421AA /* AccountDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AccountDAO.swift; path = Services/Storage/DAO/Account/AccountDAO.swift; sourceTree = ""; }; + 3A0A94D821B544B4007421AA /* DAOFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAOFactoryProtocol.swift; sourceTree = ""; }; 3A0AEA6921AFF0FD0066CBBA /* profile.bert */ = {isa = PBXFileReference; lastKnownFileType = file; path = profile.bert; sourceTree = ""; }; 3A0AEA6C21AFF3FE0066CBBA /* ProfileMockProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileMockProvider.swift; sourceTree = ""; }; 3A0AEA7021B018380066CBBA /* DBAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBAccount.swift; sourceTree = ""; }; @@ -2940,6 +2946,7 @@ 3A27B0A61EF307A900B4B3CB /* DeleteUserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeleteUserModel.swift; path = Services/Models/DeleteUserModel.swift; sourceTree = ""; }; 3A2843261EF9314100EFE21A /* Avenir.ttc */ = {isa = PBXFileReference; lastKnownFileType = file; path = Avenir.ttc; sourceTree = ""; }; 3A2A99821EFAD2FB002749B3 /* PageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PageControl.swift; path = PageControl/PageControl.swift; sourceTree = ""; }; + 3A37416021B58AAA00F212B9 /* ImageUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploader.swift; sourceTree = ""; }; 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HistoryRequestModel.swift; path = Services/Models/HistoryRequestModel.swift; sourceTree = ""; }; 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BaseMQTTModel.swift; path = Services/Models/BaseMQTTModel.swift; sourceTree = ""; }; 3A771CA91F191B38008D968A /* ProfileHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProfileHandler.swift; path = Services/HandleServices/ProfileHandler.swift; sourceTree = ""; }; @@ -3198,7 +3205,7 @@ 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTimeout.swift; sourceTree = ""; }; 5E7D5D40218C5A36009B5D8D /* UserContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContact.swift; sourceTree = ""; }; 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContactAction.swift; sourceTree = ""; }; - 5E7D5D44218C5A5D009B5D8D /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; + 5E7D5D44218C5A5D009B5D8D /* UserAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = ""; }; 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = ""; }; 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeaderView.swift; sourceTree = ""; }; 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSetAvatarTVCell.swift; sourceTree = ""; }; @@ -3218,7 +3225,6 @@ 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsProtocols.swift; sourceTree = ""; }; 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsWireframe.swift; sourceTree = ""; }; 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewController.swift; sourceTree = ""; }; - 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewsFactory.swift; sourceTree = ""; }; 5EEA3D18EFB98D7959F993E4 /* AddParticipantsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsProtocols.swift; sourceTree = ""; }; 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeConfirmationProtocols.swift; sourceTree = ""; }; @@ -6190,6 +6196,15 @@ path = Model; sourceTree = ""; }; + 3A0A94D321B533D0007421AA /* Account */ = { + isa = PBXGroup; + children = ( + 3A0A94D421B53478007421AA /* AccountDAOProtocol.swift */, + 3A0A94D621B53491007421AA /* AccountDAO.swift */, + ); + name = Account; + sourceTree = ""; + }; 3A14D83621ABEC25009CD23A /* Entities */ = { isa = PBXGroup; children = ( @@ -6236,6 +6251,14 @@ path = Fonts; sourceTree = ""; }; + 3A37415F21B58A8600F212B9 /* ImageUploader */ = { + isa = PBXGroup; + children = ( + 3A37416021B58AAA00F212B9 /* ImageUploader.swift */, + ); + path = ImageUploader; + sourceTree = ""; + }; 3A768DE41ECB3E7600108F7C /* Library */ = { isa = PBXGroup; children = ( @@ -6270,6 +6293,7 @@ 3A768E1C1ECD152300108F7C /* Services */ = { isa = PBXGroup; children = ( + 3A37415F21B58A8600F212B9 /* ImageUploader */, 855A4E99219B31F200B6E90B /* SDK */, 852BB8F721947A0800F2E8E4 /* Auth */, FEA656082167797E00B44029 /* WalletFundingNetworkService */, @@ -6743,7 +6767,6 @@ E61C394BD0E94E3DCF853D4F /* ScheduleMessage */, 859B86352048224B003272B2 /* Settings */, 267BE27D1FDE900900C47E18 /* SettingsGroup */, - 4FBB666690A18EEA5438EAB7 /* Splash */, 855AC52C208E435700DC2335 /* Stickers */, 975DB2471671357A9EEBF65B /* TimeZoneSelector */, 2528D43000589CBC2A877417 /* TopUpAccount */, @@ -6857,6 +6880,7 @@ 4B058F02204EA928004C7D9F /* DAOProtocol.swift */, 4B058EFF204EA762004C7D9F /* Profile */, A45F59A9205825EC00EAA780 /* Roster */, + 3A0A94D321B533D0007421AA /* Account */, 4B8996C6204ECE8500DCB183 /* Contact */, 4B058F09204EAEA7004C7D9F /* Room */, 2633EF6C205212DF00DB3868 /* Member */, @@ -7237,6 +7261,7 @@ children = ( 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */, 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, + 4FBB666690A18EEA5438EAB7 /* Splash */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, 5E07BC45216F64DB000E4558 /* CreateProfile */, @@ -7893,10 +7918,10 @@ isa = PBXGroup; children = ( 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */, - 5EDD454921885EC400C50BC8 /* Presenter */, - 5EDD454A21885EC400C50BC8 /* Wireframe */, 5EDD454B21885EC400C50BC8 /* View */, + 5EDD454921885EC400C50BC8 /* Presenter */, 5EDD454C21885EC400C50BC8 /* Interactor */, + 5EDD454A21885EC400C50BC8 /* Wireframe */, 5EDD454D21885EC400C50BC8 /* Entities */, ); path = AccountSettings; @@ -7923,7 +7948,6 @@ children = ( 5E7D5D48218C5D3A009B5D8D /* Header */, 5E7D5D3B218C44A7009B5D8D /* Cells */, - 5EDD45562188617A00C50BC8 /* ViewsFactory */, 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */, ); path = View; @@ -7940,11 +7964,11 @@ 5EDD454D21885EC400C50BC8 /* Entities */ = { isa = PBXGroup; children = ( + 5E7D5D44218C5A5D009B5D8D /* UserAccount.swift */, 5E7D5D3C218C59F1009B5D8D /* UserStatus.swift */, 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */, 5E7D5D40218C5A36009B5D8D /* UserContact.swift */, 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */, - 5E7D5D44218C5A5D009B5D8D /* UserProfile.swift */, 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */, 5E7D5D4D218C645A009B5D8D /* SettingsSetAvatarCellModel.swift */, 5E7D5D51218C68BA009B5D8D /* SettingsSelectorCellModel.swift */, @@ -7957,14 +7981,6 @@ path = Entities; sourceTree = ""; }; - 5EDD45562188617A00C50BC8 /* ViewsFactory */ = { - isa = PBXGroup; - children = ( - 5EDD455721899BCE00C50BC8 /* AccountSettingsViewsFactory.swift */, - ); - path = ViewsFactory; - sourceTree = ""; - }; 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */ = { isa = PBXGroup; children = ( @@ -14023,6 +14039,7 @@ 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */, 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */, 850B9DA2219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift */, + 3A0A94D821B544B4007421AA /* DAOFactoryProtocol.swift */, ); name = ServiceFactory; path = Services/ServiceFactory; @@ -16230,6 +16247,7 @@ 26DAE5D21FFAF7EE00EDF412 /* BackgroundModeService.swift in Sources */, 4B1D7DFA2029BF3400703228 /* HistoryItemsFactory.swift in Sources */, FEA65616216779F700B44029 /* WalletParams.swift in Sources */, + 3A37416121B58AAA00F212B9 /* ImageUploader.swift in Sources */, E79385871FC32ACC00744CB0 /* DBProfile.swift in Sources */, 26AB1419218775BB00F2BB83 /* ConversionState.swift in Sources */, 9BC9657620FF042E00052AE1 /* CallInProgressProtocols.swift in Sources */, @@ -16471,6 +16489,7 @@ 3A21EFFC1F3B154A00AE61EC /* SendModel.swift in Sources */, 2603139C20A0A4BA009AC66D /* LanguageSelectroInteractor.swift in Sources */, A49B81B320B4BB6400980D36 /* NynjaMTIConfig.swift in Sources */, + 3A0A94D521B53478007421AA /* AccountDAOProtocol.swift in Sources */, 8EC2AF6B20053FC300807B20 /* GroupCollectionCell.swift in Sources */, 4B8996D8204EDA7700DCB183 /* JobDAOProtocol.swift in Sources */, A4CE80C320C95E7F00400713 /* CollectionDisplayMode.swift in Sources */, @@ -16538,7 +16557,6 @@ FBD885782147F9640099B8C3 /* FontsConstants.swift in Sources */, 263D662D1FE8D03400A509F8 /* TypingModel.swift in Sources */, 853D0F9020C00806008C3684 /* StickerPreviewState.swift in Sources */, - 5EDD455821899BCE00C50BC8 /* AccountSettingsViewsFactory.swift in Sources */, A42D51BE206A361400EEB952 /* Vox.swift in Sources */, E77764C21FBDA9BD0042541D /* ImageWheelItemModel.swift in Sources */, 857A06612035E3360097C49B /* ForwardAvatarCollectionViewCell.swift in Sources */, @@ -16877,7 +16895,7 @@ 267BE90C2069405200153FB8 /* StarMessageDAOProtocol.swift in Sources */, 8E55172E200D095B00C12B5D /* UserGroupRulesVC.swift in Sources */, A4B544EA20EFB1A800EB7B0F /* errors_Spec.swift in Sources */, - 5E7D5D45218C5A5D009B5D8D /* UserProfile.swift in Sources */, + 5E7D5D45218C5A5D009B5D8D /* UserAccount.swift in Sources */, 4BDC7E63203494C000BCD381 /* ScheduleButton.swift in Sources */, FEA655E12167777E00B44029 /* SeedBackupWalletProtocols.swift in Sources */, 00E98250205C2668008BF03D /* SessionHeaderView.swift in Sources */, @@ -17173,6 +17191,7 @@ 2633EF6E205212F700DB3868 /* MemberDAOProtocol.swift in Sources */, B7EF8EDD210CB0A200E0E981 /* InterpretationModel.swift in Sources */, E79117921F97A48900462D68 /* ProfileDetailsViewLayout.swift in Sources */, + 3A0A94D721B53491007421AA /* AccountDAO.swift in Sources */, 8595E0DC204863DB00178171 /* CarouselPickerCellModel.swift in Sources */, 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */, 2648C41C2069B52100863614 /* ChangeNumberStep2Interactor.swift in Sources */, @@ -17458,6 +17477,7 @@ FEA6560D2167797E00B44029 /* WalletFundingNetworkService.swift in Sources */, 8ED0F3D21FBC5CF2004916AB /* GroupsListWireframe.swift in Sources */, 850C301C204DA87A00DB26C2 /* PrivacyListViewController.swift in Sources */, + 3A0A94D921B544B4007421AA /* DAOFactoryProtocol.swift in Sources */, E791178E1F97A31D00462D68 /* ProfileViewControllerLayout.swift in Sources */, 85482846204E918000DCBEC8 /* PrivacyCellModel.swift in Sources */, 5E7D5D3A218C42D0009B5D8D /* AccountSettingsInteractor.swift in Sources */, diff --git a/Nynja/DB/Models/DBAccount.swift b/Nynja/DB/Models/DBAccount.swift index 4f7be8009..7009b717f 100644 --- a/Nynja/DB/Models/DBAccount.swift +++ b/Nynja/DB/Models/DBAccount.swift @@ -32,7 +32,7 @@ final class DBAccount: Record, DBModel { let profileId: String let authenticationIdentifier: String let authenticationType: String - let avatar: String + let avatar: String? let accountMark: String let accountName: String let firstName: String @@ -134,6 +134,7 @@ final class DBAccount: Record, DBModel { // MARK: - DBModel func saveAggregate(_ db: Database) throws { + try save(db) try contactInfo?.forEach { try $0.saveAggregate(db) } } @@ -148,6 +149,15 @@ final class DBAccount: Record, DBModel { // MARK: - Query + static func account(from db: Database, accountId: String) throws -> DBAccount? { + let accountIdColumn = Column(AccountTable.Column.accountId.title) + + let account = try DBAccount.filter(accountIdColumn == accountId).fetchOne(db) + try account?.construct(db) + + return account + } + static func accounts(from db: Database, profileId: String) throws -> [DBAccount] { let profileIdColumn = Column(AccountTable.Column.profileId.title) diff --git a/Nynja/Generated/ColorsConstants.swift b/Nynja/Generated/ColorsConstants.swift index 9508aa8ea..29a3ab02a 100644 --- a/Nynja/Generated/ColorsConstants.swift +++ b/Nynja/Generated/ColorsConstants.swift @@ -59,6 +59,8 @@ internal extension SGColor { static let gray = #colorLiteral(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) /// 0x63666aff (r: 99, g: 102, b: 106, a: 255) static let lightGray = #colorLiteral(red: 0.3882353, green: 0.4, blue: 0.41568628, alpha: 1.0) + /// 0x0000001a (r: 0, g: 0, b: 0, a: 26) + static let lightTransparentBlack = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.101960786) /// 0xc90010ff (r: 201, g: 0, b: 16, a: 255) static let mainRed = #colorLiteral(red: 0.7882353, green: 0.0, blue: 0.0627451, alpha: 1.0) /// 0x959699ff (r: 149, g: 150, b: 153, a: 255) diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index ed73ea544..59d447079 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1094,8 +1094,6 @@ internal extension String { static var targetLocation: String { return localizable.tr("Localizable", "target_location") } /// Terms of Service static var termsOfService: String { return localizable.tr("Localizable", "terms_of_service") } - /// terms of use - static var termsOfUse: String { return localizable.tr("Localizable", "terms_of_use") } /// Th static var th: String { return localizable.tr("Localizable", "th") } /// MOBILE COMMUNICATOR @@ -1494,6 +1492,10 @@ internal extension String { static var youWasRemoved: String { return localizable.tr("Localizable", "you_was_removed") } /// Your device appears to be rooted. The security of your app can be compromised. static var yourDeviceIsRooted: String { return localizable.tr("Localizable", "your_device_is_rooted") } + /// SAVE + static var accountSettingsSaveButton: String { return localizable.tr("Localizable", "account_settings.save_button") } + /// ACCOUNT SETTINGS + static var accountSettingsScreenTitle: String { return localizable.tr("Localizable", "account_settings.screen_title") } /// OR static var authAlternativeLabel: String { return localizable.tr("Localizable", "auth.alternative_label") } /// Email @@ -1554,6 +1556,8 @@ internal extension String { } /// Welcome to static var codeConfirmationWelcome: String { return localizable.tr("Localizable", "code_confirmation.welcome") } + /// terms of use + static var createProfileTermsOfUse: String { return localizable.tr("Localizable", "create_profile.terms_of_use") } /// Add Login Provider static var loginOptionsAddAlertTitle: String { return localizable.tr("Localizable", "login_options.add_alert_title") } /// Add Login Option diff --git a/Nynja/ImageUploader/ImageUploader.swift b/Nynja/ImageUploader/ImageUploader.swift new file mode 100644 index 000000000..b0a19071a --- /dev/null +++ b/Nynja/ImageUploader/ImageUploader.swift @@ -0,0 +1,58 @@ +// +// ImageUploader.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/3/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol ImageUploader: class { + func uploadImageFile(_ image: UIImage, completion: @escaping (Result) -> Void) +} + +enum ImageUploaderError: Error { + case imageCouldNotBeSaved + case uploadFailed +} + +final class ImageUploaderImpl: ImageUploader { + + // MARK: - Services + + private let resourceManager: ResourceManagerProtocol + + private let transferManager: TransferManager + + + // MARK: - Init + + struct Dependencies { + let resourceManager: ResourceManagerProtocol + let transferManager: TransferManager + } + + init(dependencies: Dependencies) { + resourceManager = dependencies.resourceManager + transferManager = dependencies.transferManager + } + + + // MARK: - + + func uploadImageFile(_ image: UIImage, completion: @escaping (Result) -> Void) { + guard let localURL = resourceManager.savePhotoAsFile(image: image, setting: .highest) else { + completion(.failure(ImageUploaderError.imageCouldNotBeSaved)) + return + } + transferManager.upload(url: localURL, listener: nil) { result in + switch result { + case let .success(remoteURL, _): + completion(.success(remoteURL)) + case let .error(error): + completion(.failure(error ?? ImageUploaderError.uploadFailed)) + } + } + } +} diff --git a/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift index 0e5d9b8c1..7d241778c 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift @@ -8,6 +8,8 @@ import Foundation +// MARK: - Wireframe + protocol AccountSettingsWireframeProtocol: class { func back() @@ -19,11 +21,15 @@ protocol AccountSettingsWireframeProtocol: class { func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) } -protocol AccountSettingsViewProtocol: class where Self: UIViewController { +// MARK: - View + +protocol AccountSettingsViewInput: LoadingInteractive { func reloadData() } -protocol AccountSettingsPresenterProtocol: NavigationProtocol { +// MARK: - Presenter + +protocol AccountSettingsPresenterProtocol: BasePresenterProtocol, NavigationProtocol { func save() func countOfSections() -> Int @@ -47,10 +53,13 @@ protocol AccountSettingsPresenterProtocol: NavigationProtocol { func contacts() -> [UserContact] } -protocol AccountSettingsInputInteractorProtocol { - func save(completion: (Result) -> Void) +// MARK: - Interactor + +// MARK: Input +protocol AccountSettingsInteractorInput: BaseInteractorProtocol { + func save(completion: @escaping (Result) -> Void) - var avatar: UIImage? { get set } + var avatar: URL? { get } var status: UserStatus { get set } var statusTimeout: StatusTimeout { get set } var profileMessage: String? { get set } @@ -63,8 +72,10 @@ protocol AccountSettingsInputInteractorProtocol { var contacts: [UserContact] { get } + func saveAvatar(_ avatar: UIImage) func update(contact: UserContact, action: UserContactActions) } -protocol AccountSettingsOutputInteractorProtocol { +// MARK: Output +protocol AccountSettingsInteractorOutput: class { } diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift index 33dc2c081..4628eb43b 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift @@ -9,11 +9,11 @@ import Foundation struct SettingsSetAvatarCellModel: IdentityProtocol, VerticalSizeble { - let image: UIImage + let imageURL: URL? let height: CGFloat - init(image: UIImage, height: CGFloat = 145) { - self.image = image + init(imageURL: URL?, height: CGFloat = 145) { + self.imageURL = imageURL self.height = height } diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift index d836ddffc..0eb8545b9 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift @@ -8,7 +8,6 @@ import Foundation - enum StatusTimeout: String { case fiveMin = "5 min" case fifteenMin = "15 min" diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserAccount.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserAccount.swift new file mode 100644 index 000000000..385a05c70 --- /dev/null +++ b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserAccount.swift @@ -0,0 +1,24 @@ +// +// UserAccount.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct UserAccount { + let avatar: UIImage? + let status: UserStatus + let statusTimeout: StatusTimeout + let profileMessage: String + + let firstName: String + let lastName: String + let birthday: Date? + + let userName: String + + let contacts: [UserContact] +} diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift index 02f73d25e..70532c2ce 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift @@ -8,7 +8,6 @@ import Foundation - struct UserContact { enum ContactType { case email diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift index 62398bd60..385a05c70 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift @@ -1,5 +1,5 @@ // -// UserProfile.swift +// UserAccount.swift // Nynja // // Created by Ash on 11/2/18. @@ -8,8 +8,7 @@ import Foundation - -struct UserProfile { +struct UserAccount { let avatar: UIImage? let status: UserStatus let statusTimeout: StatusTimeout diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift index f0a706e16..10a3f499c 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift @@ -8,7 +8,6 @@ import Foundation - enum UserStatus: String { case active case inactive diff --git a/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift index 5b41e4cb1..386ec6ea6 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift @@ -8,10 +8,19 @@ import Foundation -final class AccountSettingsInteractor: AccountSettingsInputInteractorProtocol, SetInjectable { - private var presenter: AccountSettingsOutputInteractorProtocol? +final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractorInput, InitializeInjectable { - var avatar: UIImage? + private weak var presenter: AccountSettingsInteractorOutput? + + // MARK: - User Info + + private let accountId: String + + private var account: DBAccount? + + private var updatedAvatar: UIImage? + + var avatar: URL? var status: UserStatus = .active var statusTimeout: StatusTimeout = .fiveMin var profileMessage: String? @@ -23,51 +32,108 @@ final class AccountSettingsInteractor: AccountSettingsInputInteractorProtocol, S var userName: String? var contacts: [UserContact] = [] -} - -// MARK: - AccountSettingsInputInteractorProtocol - -extension AccountSettingsInteractor { - func save(completion: (Result) -> Void) { - let userProfile = UserProfile( - avatar: avatar, - status: status, - statusTimeout: statusTimeout, - profileMessage: profileMessage ?? "", - firstName: firstName ?? "", - lastName: lastName ?? "", - birthday: birthday, - userName: userName ?? "", - contacts: contacts) - - // should be saved in some service - - completion(.success(())) - } - func update(contact: UserContact, action: UserContactActions) { - - } -} - -// MARK: - SetInjectable - -extension AccountSettingsInteractor { + + // MARK: - Services + + private let accountDAO: AccountDAOProtocol + + private let accountService: AccountService + + private let imageUploader: ImageUploader + + + // MARK: - Init + struct Dependencies { - let presenter: AccountSettingsOutputInteractorProtocol - let userProfile: UserProfile + let presenter: AccountSettingsInteractorOutput + let accountId: String + let accountDAO: AccountDAOProtocol + let accountService: AccountService + let imageUploader: ImageUploader } - func inject(dependencies: AccountSettingsInteractor.Dependencies) { + init(dependencies: Dependencies) { presenter = dependencies.presenter - avatar = dependencies.userProfile.avatar - status = dependencies.userProfile.status - statusTimeout = dependencies.userProfile.statusTimeout - profileMessage = dependencies.userProfile.profileMessage - firstName = dependencies.userProfile.firstName - lastName = dependencies.userProfile.lastName - birthday = dependencies.userProfile.birthday - userName = dependencies.userProfile.userName - contacts.append(contentsOf: dependencies.userProfile.contacts) + accountId = dependencies.accountId + accountDAO = dependencies.accountDAO + accountService = dependencies.accountService + imageUploader = dependencies.imageUploader + } + + override func loadData() { + super.loadData() + if let account = accountDAO.fetchAccount(by: accountId) { + setup(account) + } + } + + private func setup(_ account: DBAccount) { + avatar = account.avatar.flatMap { URL(string: $0) } + profileMessage = account.accountMark + firstName = account.firstName + lastName = account.lastName + birthday = account.birthday.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } + userName = account.username +// contacts.append(contentsOf: dependencies.userProfile.contacts) + } + + + // MARK: - Interactor Input + + func saveAvatar(_ image: UIImage) { + updatedAvatar = image + } + + func save(completion: @escaping (Result) -> Void) { + if let avatar = updatedAvatar { + imageUploader.uploadImageFile(avatar) { [weak self] result in + guard let self = self else { return } + + switch result { + case let .success(avatarURL): + self.updateAccount(avatarURL: avatarURL, completion: completion) + case let .failure(error): + completion(.failure(error)) + } + } + } else { + updateAccount(avatarURL: nil, completion: completion) + } + } + + private func updateAccount(avatarURL: URL?, completion: @escaping (Result) -> Void) { + let accountInfo = AccountInfo( + accountId: accountId, + avatar: avatarURL?.absoluteString, + accountMark: profileMessage, + accountName: nil, + firstName: firstName, + lastName: lastName, + username: userName, + accountStatus: .enabled, + roles: nil, + qrCode: nil, + birthday: nil + ) + + accountService.updateAccount(accountInfo) { [weak self] result in + switch result { + case let .success(account): + do { + try self?.accountDAO.save(account) + self?.account = DBAccount(account: account) + completion(.success(())) + } catch { + completion(.failure(error)) + } + case let .failure(error): + completion(.failure(error)) + } + } + } + + func update(contact: UserContact, action: UserContactActions) { + } } diff --git a/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift index c8a83d544..e56a481bc 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -8,20 +8,27 @@ import Foundation - -final class AccountSettingsPresenter: AccountSettingsPresenterProtocol, SetInjectable, AccountSettingsOutputInteractorProtocol { - private var view: AccountSettingsViewProtocol? - private var wireframe: AccountSettingsWireframe? - private var interactor: AccountSettingsInputInteractorProtocol? -} - -// MARK: - AccountSettingsPresenterProtocol - -extension AccountSettingsPresenter { +final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterProtocol, SetInjectable, AccountSettingsInteractorOutput { + + private weak var view: AccountSettingsViewInput? + private var wireframe: AccountSettingsWireframeProtocol? + private var interactor: AccountSettingsInteractorInput? { + didSet { + _interactor = interactor + } + } + + override func loadData() { + super.loadData() + view?.reloadData() + } + func save() { - interactor?.save { + view?.showLoading() + interactor?.save { [weak self] in + self?.view?.hideLoading() $0.onSuccess { - wireframe?.back() + self?.wireframe?.back() } } } @@ -29,11 +36,7 @@ extension AccountSettingsPresenter { func back() { wireframe?.back() } -} - -// MARK: - AccountSettingsOutputInteractorProtocol - -extension AccountSettingsPresenter { + func countOfSections() -> Int { return 4 } @@ -61,7 +64,7 @@ extension AccountSettingsPresenter { switch section { case 0: switch row { - case 0: return SettingsSetAvatarCellModel(image: interactor?.avatar ?? UIImage.nynja.icPhotoPlaceholder.image) as AnyObject + case 0: return SettingsSetAvatarCellModel(imageURL: interactor?.avatar) as AnyObject case 1: return SettingsSelectorCellModel(title: "Status".localized, details: (interactor?.status.rawValue.localized ?? "")) as AnyObject case 2: return SettingsSelectorCellModel(title: "Idle Timeout".localized, details: (interactor?.statusTimeout.rawValue.localized ?? "")) as AnyObject case 3: return MaterialTextFieldCellModel(profileField: .profileMessage, value: interactor?.profileMessage ?? "", action: setProfileMessage) @@ -91,9 +94,11 @@ extension AccountSettingsPresenter { } func chooseAvatar(completion: @escaping (UIImage?) -> Void) { - wireframe?.chooseAvatar { - self.interactor?.avatar = $0 - completion($0) + wireframe?.chooseAvatar { image in + if let image = image { + self.interactor?.saveAvatar(image) + } + completion(image) } } @@ -162,16 +167,14 @@ extension AccountSettingsPresenter { extension AccountSettingsPresenter { struct Dependencies { - let view: AccountSettingsViewProtocol + let view: AccountSettingsViewInput let wireframe: AccountSettingsWireframe - let interactor: AccountSettingsInputInteractorProtocol + let interactor: AccountSettingsInteractorInput } - func inject(dependencies: AccountSettingsPresenter.Dependencies) { + func inject(dependencies: Dependencies) { view = dependencies.view wireframe = dependencies.wireframe interactor = dependencies.interactor } } - - diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift index 45c26eb36..b4987ba7a 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift @@ -9,35 +9,14 @@ import Foundation -final class AccountSettingsViewController: UIViewController, AccountSettingsViewProtocol, InitializeInjectable, UITableViewDelegate, UITableViewDataSource, KeyboardInteractive { - private let viewsFactory: AccountSettingsViewsFactoryProtocol +final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, LoadingDisplayable, InitializeInjectable, UITableViewDelegate, UITableViewDataSource, KeyboardInteractive { + private let presenter: AccountSettingsPresenterProtocol - private lazy var topHeaderLayoutGuide: UILayoutGuide = { - let layoutGuide = UILayoutGuide() - view.addLayoutGuide(layoutGuide) - - layoutGuide.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.height.equalTo(UIWindow.safeAreaTopPadding()) - } - - return layoutGuide - }() - private lazy var headerView: NavigationView = { - let header = UIView.makeHeaderView( - on: view, - top: topHeaderLayoutGuide, - config: NavigationView.Config( - isVisibleSeparator: true, - isVisibleBackButton: true, - title: "Account settings".localized.uppercased(), - navigationHandler: presenter, - backButtonImage: UIImage.nynja.icBackNavigation.image)) - header.backgroundColor = UIColor.nynja.darkLight - return header - }() + // MARK: - Views + + private(set) lazy var progressHUD = makeProgressHUD(on: view) private lazy var tableView: UITableView = { let table = UITableView(frame: CGRect.zero, style: .grouped) @@ -52,20 +31,40 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView table.register(DescriptionTVCell.self, forCellReuseIdentifier: DescriptionCellModel.identifier) table.register(AddContactTVCell.self, forCellReuseIdentifier: AddContactCellModel.identifier) table.register(ContactTVCell.self, forCellReuseIdentifier: ContactTVCellModel.identifier) - - table.backgroundColor = UIColor.nynja.darkLight - + table.separatorStyle = .none + table.backgroundColor = .clear + + table.contentInset.bottom = 28 table.snp.makeConstraints{ (make) in - make.top.equalTo(headerView.snp.bottom) + make.top.equalTo(navigationView.snp.bottom) make.left.right.equalToSuperview() - make.bottom.equalTo(saveButton.snp.top).offset(-28) + make.bottom.equalTo(saveButton.snp.top) } return table }() + private lazy var gradientView: GradientView = { + let gradientHeight = 29.0.adjustedByWidth + + let backgroundColor = UIColor.nynja.backgroundColor + let colors = [backgroundColor.withAlphaComponent(0), backgroundColor] + + let gradientView = GradientView(colors: colors) + gradientView.isUserInteractionEnabled = false + + view.addSubview(gradientView) + gradientView.snp.makeConstraints { maker in + maker.bottom.equalTo(tableView.snp.bottom) + maker.left.right.equalToSuperview() + maker.height.equalTo(gradientHeight) + } + + return gradientView + }() + private lazy var saveButton: UIButton = { let button = UIButton() view.addSubview(button) @@ -73,7 +72,7 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.mainRed), for: .normal) button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.darkRed), for: .disabled) - button.setTitle("Save".localized.uppercased(), for: .normal) + button.setTitle(String.localizable.accountSettingsSaveButton, for: .normal) button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) button.addTarget(self, action: #selector(saveAction(sender:)), for: .touchUpInside) @@ -84,34 +83,40 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView button.snp.makeConstraints { (make) in make.height.equalTo(44) make.left.right.equalToSuperview().inset(16) - make.bottom.equalToSuperview().offset(-28) + adjustVerticalInset(.bottom, make: make, offset: -28) } return button }() struct Dependencies { - let viewsFactory: AccountSettingsViewsFactoryProtocol let presenter: AccountSettingsPresenterProtocol } - init(dependencies: AccountSettingsViewController.Dependencies) { - viewsFactory = dependencies.viewsFactory + init(dependencies: Dependencies) { presenter = dependencies.presenter - super.init(nibName: nil, bundle: nil) + _presenter = dependencies.presenter } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { - super.viewDidLoad() + override func initialize() { + super.initialize() + + screenTitle = String.localizable.accountSettingsScreenTitle - _ = [topHeaderLayoutGuide, headerView, tableView, saveButton] + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) - view.backgroundColor = UIColor.nynja.darkLight + _ = [tableView, gradientView, saveButton] } override func viewWillAppear(_ animated: Bool) { @@ -128,15 +133,17 @@ final class AccountSettingsViewController: UIViewController, AccountSettingsView return .lightContent } + + // MARK: - View Input + func reloadData() { tableView.reloadData() } -} - -// MARK: - Actions + + + // MARK: - Actions -extension AccountSettingsViewController { - @objc func saveAction(sender: UIButton) { + @objc private func saveAction(sender: UIButton) { presenter.save() } } @@ -224,10 +231,12 @@ extension AccountSettingsViewController { return } - cell.configure(config: SettingsSetAvatarTVCell.Config(image: item.image, chooseAvatarAction: { [weak self] in - self?.presenter.chooseAvatar(completion: { (image) in - cell.avatarButton.setImage(image, for: .normal) - }) + cell.configure(config: SettingsSetAvatarTVCell.Config(imageURL: item.imageURL, chooseAvatarAction: { [weak self] in + self?.presenter.chooseAvatar { image in + if let image = image { + cell.avatarImageView.image = image + } + } })) } @@ -245,6 +254,7 @@ extension AccountSettingsViewController { } cell.configure(config: MaterialTextFieldTVCell.Config( + value: item.value, fieldType: item.profileField, textChangedHandler: { [weak self] (field, value) in switch field { @@ -254,23 +264,21 @@ extension AccountSettingsViewController { case .userName: self?.presenter.setUserName(value: value) case .profileMessage: self?.presenter.setProfileMessage(message: value) } - }, - shouldChangeTextHandler: nil)) + }, + shouldChangeTextHandler: nil) + ) } } extension AccountSettingsViewController { + func keyboardNotified(endFrame: CGRect) { - var bottomInset: CGFloat = 28 + let bottomInset: CGFloat = 28 - if endFrame.origin.y < UIScreen.main.bounds.size.height { - bottomInset += endFrame.height + if endFrame.origin.y >= UIScreen.main.bounds.size.height { + updateToHide(view: saveButton, offset: -bottomInset) } else { - bottomInset += UIWindow.safeAreaBottomPadding() - } - - saveButton.snp.updateConstraints { (make) in - make.bottom.equalToSuperview().inset(bottomInset) + updateToShow(view: saveButton, offset: -bottomInset - endFrame.height) } } } diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift index 4682992cb..6a231dff0 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift @@ -24,12 +24,14 @@ final class MaterialTextFieldTVCell: UITableViewCell, Configurable { }() struct Config { + let value: String let fieldType: ProfileField let textChangedHandler: ((ProfileField, String) -> Void)? let shouldChangeTextHandler: ((ProfileField, String) -> Bool)? } func configure(config: MaterialTextFieldTVCell.Config) { + mainTextField.text = config.value contentView.backgroundColor = UIColor.nynja.clear backgroundColor = UIColor.nynja.clear diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift index 03ac9dec0..aab5fd586 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift @@ -12,36 +12,48 @@ import Foundation final class SettingsSetAvatarTVCell: UITableViewCell, Configurable { private var chooseAvatarAction: (() -> Void)? - lazy var avatarButton: UIButton = { + private lazy var avatarButton: UIButton = { let button = UIButton() + button.addTarget(self, action: #selector(chooseAvatarAction(sender:)), for: .touchUpInside) + contentView.addSubview(button) + button.snp.makeConstraints { maker in + maker.edges.equalTo(avatarImageView) + } - button.layer.cornerRadius = 47 - button.clipsToBounds = true + return button + }() + + private(set) lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + contentView.addSubview(imageView) - button.addTarget(self, action: #selector(chooseAvatarAction(sender:)), for: .touchUpInside) + imageView.layer.cornerRadius = 47 + imageView.clipsToBounds = true - button.snp.makeConstraints { (make) in + imageView.snp.makeConstraints { (make) in make.center.equalToSuperview() make.height.width.equalTo(95) } - return button + return imageView }() struct Config { - let image: UIImage + let imageURL: URL? let chooseAvatarAction: () -> Void } - func configure(config: SettingsSetAvatarTVCell.Config) { - avatarButton.setImage(config.image, for: .normal) + func configure(config: Config) { + avatarImageView.setImage(url: config.imageURL, placeHolder: UIImage.nynja.icPhotoPlaceholder.image) backgroundColor = UIColor.nynja.clear contentView.backgroundColor = UIColor.nynja.clear chooseAvatarAction = config.chooseAvatarAction + avatarImageView.isHidden = false + avatarButton.isHidden = false } - @objc func chooseAvatarAction(sender: UIButton) { + @objc func chooseAvatarAction(sender: UIImageView) { chooseAvatarAction?() } } diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift index 38af207bd..5249d716b 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift @@ -32,7 +32,7 @@ final class SettingsSectionHeaderView: UIView, Configurable { } func configure(config: SettingsSectionHeaderView.Config) { - backgroundColor = UIColor.nynja.backgroundColor + backgroundColor = UIColor.nynja.lightTransparentBlack label.text = config.title } } diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift b/Nynja/Modules/Account Flow/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift deleted file mode 100644 index 5ea3d648e..000000000 --- a/Nynja/Modules/Account Flow/AccountSettings/View/ViewsFactory/AccountSettingsViewsFactory.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AccountSettingsViewsFactory.swift -// Nynja -// -// Created by Ash on 10/31/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -protocol AccountSettingsViewsFactoryProtocol { - -} - -final class AccountSettingsViewsFactory: AccountSettingsViewsFactoryProtocol { - -} diff --git a/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift index 111a3c393..21b4a6cb1 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -14,6 +14,22 @@ protocol AccountSettingsCoordinatorProtocol: class { final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtocol { + private let coordinator: AccountSettingsCoordinatorProtocol + + init(coordinator: AccountSettingsCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let accountId: String + } + + struct Dependencies { + let accountDAO: AccountDAOProtocol + let accountService: AccountService + let imageUploader: ImageUploader + } + enum State { case back case chooseAvatar(completion: (UIImage?) -> Void) @@ -23,28 +39,20 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco case contactDetails(contact: UserContact, completion: (Result) -> Void) } - private let coordinator: AccountSettingsCoordinatorProtocol - - init(coordinator: AccountSettingsCoordinatorProtocol) { - self.coordinator = coordinator - } - func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = AccountSettingsPresenter() - let viewDep = AccountSettingsViewController.Dependencies(viewsFactory: AccountSettingsViewsFactory(), presenter: presenter) - let view = AccountSettingsViewController(dependencies: viewDep) - let interactor = AccountSettingsInteractor() - - let presenterDep = AccountSettingsPresenter.Dependencies(view: view, wireframe: self, interactor: interactor) - - let userContact = UserContact(type: .phone, value: "380678888888", detailType: .work) - let userProfile = UserProfile(avatar: nil, status: .active, statusTimeout: .fiveMin, profileMessage: "Some", firstName: "Alan", lastName: "Po", birthday: nil, userName: "AlanPo", contacts: [userContact]) - - let interactorDep = AccountSettingsInteractor.Dependencies(presenter: presenter, userProfile: userProfile) + let view = AccountSettingsViewController(dependencies: .init(presenter: presenter)) + + let interactor = AccountSettingsInteractor(dependencies: .init( + presenter: presenter, + accountId: parameters.accountId, + accountDAO: dependencies.accountDAO, + accountService: dependencies.accountService, + imageUploader: dependencies.imageUploader) + ) - presenter.inject(dependencies: presenterDep) - interactor.inject(dependencies: interactorDep) + presenter.inject(dependencies: .init(view: view, wireframe: self, interactor: interactor)) return view } diff --git a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift index 9fb0771f5..04d0422f6 100644 --- a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift +++ b/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift @@ -8,12 +8,16 @@ import UIKit +// MARK: - Wireframe + protocol AuthProviderWireframeProtocol: class { func dismiss() func selectCountry(completion: @escaping (Result) -> Void) func confirm(data: ConfirmationData) } +// MARK: - View + protocol AuthProviderViewInput: LoadingInteractive where Self: UIViewController { var screenTitle: String? { get set } @@ -25,6 +29,8 @@ protocol AuthProviderViewInput: LoadingInteractive where Self: UIViewController func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) } +// MARK: - Presenter + protocol AuthProviderPresenterProtocol: BasePresenterProtocol, NavigationProtocol { var authProvider: AuthProvider { get } @@ -36,6 +42,9 @@ protocol AuthProviderPresenterProtocol: BasePresenterProtocol, NavigationProtoco func next(inputText: String) } +// MARK: - Interactor + +// MARK: Input protocol AuthProviderInteractorInput: class { func fetchDefaultCountry() -> Country @@ -45,6 +54,7 @@ protocol AuthProviderInteractorInput: class { func addPhoneNumberProvider(_ phoneNumber: PhoneNumberInfo) } +// MARK: Output protocol AuthProviderInteractorOutput: class { func didAddAuthProvider(with confirmationData: ConfirmationData) func didReceiveFailure(_ error: Error?) diff --git a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift index 7114ac476..b120bc3b6 100644 --- a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift @@ -9,6 +9,7 @@ import Foundation final class AccountSettingsCoordinator: Coordinator, AccountSettingsCoordinatorProtocol { + private let navigation: UINavigationController private let serviceFactory: ServiceFactoryProtocol @@ -18,8 +19,22 @@ final class AccountSettingsCoordinator: Coordinator, AccountSettingsCoordinatorP } func start() { - let wireframe = AccountSettingsWireframe.init(coordinator: self) - let view = wireframe.prepareModule(dependencies: AccountSettingsWireframe.Dependencies()) + let storageService = serviceFactory.makeStorageService() + + guard let accountId = storageService.accountId else { + return + } + + let wireframe = AccountSettingsWireframe(coordinator: self) + + let view = wireframe.prepareModule( + parameters: .init(accountId: accountId), + dependencies: .init( + accountDAO: serviceFactory.makeAccountDAO(), + accountService: serviceFactory.makeAccountService(), + imageUploader: serviceFactory.makeImageUploader() + ) + ) navigation.pushViewController(view, animated: true) } @@ -29,17 +44,22 @@ final class AccountSettingsCoordinator: Coordinator, AccountSettingsCoordinatorP } } -// MARK: - AccountSettingsCoordinatorProtocol +// MARK: - Account Settings extension AccountSettingsCoordinator { func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) { switch state { case .back: navigation.popViewController(animated: true) - case .addContact(let completion): showAddContactPopup(completion: completion) - case .chooseAvatar(let completion): selectAvatar(with: completion) - case .chooseStatus(let completion): chooseStatus(completion: completion) - case .chooseTimeout(let completion): chooseTimeout(completion: completion) - case .contactDetails(let contact, let completion): break + case .addContact(let completion): + showAddContactPopup(completion: completion) + case .chooseAvatar(let completion): + selectAvatar(with: completion) + case .chooseStatus(let completion): + chooseStatus(completion: completion) + case .chooseTimeout(let completion): + chooseTimeout(completion: completion) + case .contactDetails(let contact, let completion): + break } } diff --git a/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift b/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift index cba5aac94..68eef8aa2 100644 --- a/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift +++ b/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift @@ -8,20 +8,29 @@ import UIKit +// MARK: - Wireframe + protocol LoginOptionsWireframeProtocol: class { func dismiss() func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]) func addAuthProvider(ofType provider: AuthProvider) } +// MARK: - View + protocol LoginOptionsViewInput: class where Self: UIViewController { func setup(form: Form) func removeItem(at index: Int) } +// MARK: - Presenter + protocol LoginOptionsPresenterProtocol: BasePresenterProtocol, NavigationProtocol { } +// MARK: - Interactor + +// MARK: Input protocol LoginOptionsInteractorInput: class { var maxAvailableLoginOptionsCount: Int { get } @@ -32,6 +41,7 @@ protocol LoginOptionsInteractorInput: class { func update(_ loginOption: LoginOption, isAvailableForSearch: Bool) } +// MARK: Output protocol LoginOptionsInteractorOutput: class { func didDelete(_ loginOption: LoginOption) func didUpdate(_ loginOption: LoginOption) diff --git a/Nynja/Modules/Auth Flow/AppCoordinator.swift b/Nynja/Modules/Auth Flow/AppCoordinator.swift index 583bf8dba..53835e210 100644 --- a/Nynja/Modules/Auth Flow/AppCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AppCoordinator.swift @@ -10,7 +10,7 @@ import Foundation final class AppCoordinator: Coordinator { - private unowned let navigation: UINavigationController + private let navigation: UINavigationController private let serviceFactory: ServiceFactoryProtocol @@ -23,9 +23,14 @@ final class AppCoordinator: Coordinator { } func start() { + let storage = StorageService.sharedInstance + if let passcode = storage.identityId { + storage.setupDatabase(with: passcode, application: UIApplication.shared) + } + let authCoordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) authCoordinator.delegate = self - authCoordinator.start() + authCoordinator.end() } func end() { diff --git a/Nynja/Modules/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Auth Flow/AuthCoordinator.swift index ea8a5f33d..1a1a870da 100644 --- a/Nynja/Modules/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AuthCoordinator.swift @@ -33,21 +33,12 @@ final class AuthCoordinator: Coordinator, NavigationContainer { } func start() { -// SplashWireFrame().presentSplash(navigation: navigation!) - let wireframe = AuthWireframe(coordinator: self) let view = wireframe.prepareModule( dependencies: .init(authService: serviceFactory.makeAuthService(), googleAuthService: serviceFactory.makeGoogleAuthService(), countriesProvider: serviceFactory.makeCountriesProvider()) ) - -// let wireframe = CodeConfirmationWireframe(coordinator: self) -// let view = wireframe.prepareModule( -// parameters: .init(confirmationData: .email("anton.poltoratskyi@gmail.com")), -// dependencies: .init(authService: serviceFactory.makeAuthService()) -// ) - navigation?.pushViewController(view, animated: true) } @@ -213,8 +204,7 @@ extension AuthCoordinator: CodeConfirmationCoordinatorProtocol { parameters: .init(accountId: accountId), dependencies: .init( storageService: serviceFactory.makeStorageService(), - resourceManager: serviceFactory.makeResourceManager(), - transferManager: serviceFactory.makeTransferManager(), + imageUploader: serviceFactory.makeImageUploader(), authService: serviceFactory.makeAuthService(), accountService: serviceFactory.makeAccountService() ) @@ -254,7 +244,7 @@ extension AuthCoordinator: CreateProfileCoordinatorProtocol { guard let navigation = navigation else { return } WebFullScreenWireFrame().presentWebFullScreen(navigation: navigation, - title: String.localizable.termsOfUse.uppercased(), + title: String.localizable.createProfileTermsOfUse.uppercased(), inputURL: url) } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift index dc8517348..0de01ec3b 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift @@ -8,6 +8,8 @@ import Foundation +// MARK: - Wireframe + protocol AuthWireframeProtocol: class { func selectCountry(completion: @escaping (Result) -> Void) func confirmInputData(loginOption: PlainLoginOption, confirmationHandler: @escaping (Bool) -> Void) @@ -19,11 +21,15 @@ protocol AuthWireframeProtocol: class { func presentAlert(title: String, message: String, actions: [UIAlertAction]) } -protocol AuthViewProtocol: LoadingInteractive where Self: UIViewController { +// MARK: - View + +protocol AuthViewInput: LoadingInteractive where Self: UIViewController { func select(country: Country) func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) } +// MARK: - Presenter + protocol AuthPresenterProtocol: class { var loginOption: PlainLoginOption { get } @@ -40,7 +46,10 @@ protocol AuthPresenterProtocol: class { func processPhoneAutoFillInfo(_ autofillInfo: PhoneAutoFillInfo) } -protocol AuthInputInteractorProtocol: class { +// MARK: - Interactor + +// MARK: Input +protocol AuthInteractorInput: class { func fetchDefaultCountry() -> Country func fetchCountry(by code: String) -> Country? @@ -50,7 +59,8 @@ protocol AuthInputInteractorProtocol: class { func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) } -protocol AuthOutputInteractorProtocol: class { +// MARK: Output +protocol AuthInteractorOutput: class { func didAuthenticated(with loginFlow: LoginFlow) func didReceiveAuthenticationFailure(_ error: Error?) } diff --git a/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift index d37a2c6e1..10f5527f7 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift @@ -8,9 +8,9 @@ import Foundation -final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { +final class AuthInteractor: AuthInteractorInput, InitializeInjectable { - private weak var presenter: AuthOutputInteractorProtocol? + private weak var presenter: AuthInteractorOutput? // MARK: - Services @@ -22,7 +22,7 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { // MARK: - Init struct Dependencies { - let presenter: AuthOutputInteractorProtocol + let presenter: AuthInteractorOutput let authService: AuthService let googleAuthService: GoogleAuthService let countriesProvider: CountriesProviding @@ -36,7 +36,7 @@ final class AuthInteractor: AuthInputInteractorProtocol, InitializeInjectable { } - // MARK: - AuthInputInteractorProtocol + // MARK: - Interactor Input func fetchDefaultCountry() -> Country { return countriesProvider.fetchDefaultCountry() diff --git a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift index 6f69f9031..e7e7fe229 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift @@ -8,9 +8,9 @@ import Foundation -final class AuthPresenter: AuthPresenterProtocol, AuthOutputInteractorProtocol, GoogleAuthServiceUIDelegate { - private weak var view: AuthViewProtocol? - private var interactor: AuthInputInteractorProtocol! +final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAuthServiceUIDelegate { + private weak var view: AuthViewInput? + private var interactor: AuthInteractorInput! private var wireframe: AuthWireframeProtocol! private(set) var loginOption: PlainLoginOption = .phoneNumber("") @@ -122,8 +122,8 @@ extension AuthPresenter { extension AuthPresenter: SetInjectable { struct Dependencies { - let view: AuthViewProtocol - let interactor: AuthInputInteractorProtocol + let view: AuthViewInput + let interactor: AuthInteractorInput let wireframe: AuthWireframeProtocol } diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift index a408e4ce3..9f5a92bc3 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift @@ -9,7 +9,7 @@ import UIKit import NynjaUIKit -final class AuthViewController: UIViewController, AuthViewProtocol, InitializeInjectable, KeyboardInteractive, LoadingDisplayable { +final class AuthViewController: UIViewController, AuthViewInput, InitializeInjectable, KeyboardInteractive, LoadingDisplayable { private let presenter: AuthPresenterProtocol @@ -249,7 +249,7 @@ final class AuthViewController: UIViewController, AuthViewProtocol, InitializeIn } } -// MARK: - AuthViewProtocol +// MARK: - View Input extension AuthViewController { diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift index ad6c4a8c4..c7c83ccd1 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift @@ -18,7 +18,7 @@ protocol CodeConfirmationWireframeProtocol: class { // MARK: - View -protocol CodeConfirmationViewProtocol: LoadingInteractive where Self: UIViewController { +protocol CodeConfirmationViewInput: LoadingInteractive where Self: UIViewController { func updateTimerLabel(text: String) func showButtons() } @@ -40,7 +40,7 @@ protocol CodeConfirmationPresenterProtocol: NavigationProtocol { // MARK: - Interactor // MARK: Input -protocol CodeConfirmationInputInteractorProtocol: class { +protocol CodeConfirmationInteractorInput: class { var address: String { get } var confirmationData: ConfirmationData { get } @@ -52,7 +52,7 @@ protocol CodeConfirmationInputInteractorProtocol: class { } // MARK: Output -protocol CodeConfirmationOutputInteractorProtocol: class { +protocol CodeConfirmationInteractorOutput: class { func didResendCode() func didReceiveResendCodeFailure(_ error: Error) diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index 93f6a59f0..93aec4d6f 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -8,9 +8,9 @@ import Foundation -final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, InitializeInjectable { +final class CodeConfirmationInteractor: CodeConfirmationInteractorInput, InitializeInjectable { - private weak var presenter: CodeConfirmationOutputInteractorProtocol? + private weak var presenter: CodeConfirmationInteractorOutput? let confirmationData: ConfirmationData @@ -36,7 +36,7 @@ final class CodeConfirmationInteractor: CodeConfirmationInputInteractorProtocol, // MARK: - Init struct Dependencies { - let presenter: CodeConfirmationOutputInteractorProtocol + let presenter: CodeConfirmationInteractorOutput let confirmationData: ConfirmationData let storageService: StorageService let authService: AuthService diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index f47b78fed..3a02e1e9e 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -8,10 +8,10 @@ import Foundation -final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeConfirmationOutputInteractorProtocol, SetInjectable { +final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeConfirmationInteractorOutput, SetInjectable { - private weak var view: CodeConfirmationViewProtocol? - private var interactor: CodeConfirmationInputInteractorProtocol! + private weak var view: CodeConfirmationViewInput? + private var interactor: CodeConfirmationInteractorInput! private var wireframe: CodeConfirmationWireframe! private var timer: Timer? @@ -146,8 +146,8 @@ extension CodeConfirmationPresenter { extension CodeConfirmationPresenter { struct Dependencies { - let view: CodeConfirmationViewProtocol - let interactor: CodeConfirmationInputInteractorProtocol + let view: CodeConfirmationViewInput + let interactor: CodeConfirmationInteractorInput let wireframe: CodeConfirmationWireframe } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift index d3e114b66..19b49b242 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -9,7 +9,7 @@ import UIKit import NynjaUIKit -final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewProtocol, LoadingDisplayable, InitializeInjectable { +final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewInput, LoadingDisplayable, InitializeInjectable { private let presenter: CodeConfirmationPresenterProtocol @@ -114,7 +114,7 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi } } -// MARK: - CodeConfirmationViewProtocol +// MARK: - View Input extension CodeConfirmationViewController { diff --git a/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift index 3d842f384..3af5676f1 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift @@ -8,6 +8,8 @@ import Foundation +// MARK: - Wireframe + protocol CreateProfileWireframeProtocol: class { func back() func end() @@ -15,11 +17,15 @@ protocol CreateProfileWireframeProtocol: class { func open(url: URL) } -protocol CreateProfileViewProtocol: LoadingInteractive where Self: UIViewController { +// MARK: - View + +protocol CreateProfileViewInput: LoadingInteractive where Self: UIViewController { func updateProfileField(_ field: ProfileField, value: String) func setCreateEnabled(_ enabled: Bool) } +// MARK: - Presenter + protocol CreateProfilePresenterProtocol: NavigationProtocol { func createAccount() func isValidValue(_ value: String, for field: ProfileField) -> Result @@ -29,7 +35,10 @@ protocol CreateProfilePresenterProtocol: NavigationProtocol { func open(url: URL) } -protocol CreateProfileInputInteractorProtocol: class { +// MARK: - Interactor + +// MARK: Input +protocol CreateProfileInteractorInput: class { func setProfileField(_ field: ProfileField, value: String) func checkTermsOfUse() -> Bool func setAvatar(image: UIImage?) @@ -37,7 +46,8 @@ protocol CreateProfileInputInteractorProtocol: class { func createAccount() } -protocol CreateProfileOutputInteractorProtocol: class { +// MARK: Output +protocol CreateProfileInteractorOutput: class { func minimalRequirementsAreSatisfied(_ satisfied: Bool) func profileFieldUpdated(_ profileField: ProfileField, value: String) diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift index b2c5667f8..5ca4a4411 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -8,16 +8,11 @@ import Foundation -final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, InitializeInjectable { +final class CreateProfileInteractor: CreateProfileInteractorInput, InitializeInjectable { - private enum UploadingError: Error { - case imageCouldNotBeSaved - case uploadFailed - } - - private weak var presenter: CreateProfileOutputInteractorProtocol? + private weak var presenter: CreateProfileInteractorOutput? - // MARK: - Fields + // MARK: - User Info private let accountId: String @@ -64,9 +59,7 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, Initi private let storageService: StorageService - private let resourceManager: ResourceManagerProtocol - - private let transferManager: TransferManager + private let imageUploader: ImageUploader private let authService: AuthService @@ -76,11 +69,10 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, Initi // MARK: - Init struct Dependencies { - let presenter: CreateProfileOutputInteractorProtocol + let presenter: CreateProfileInteractorOutput let accountId: String let storageService: StorageService - let resourceManager: ResourceManagerProtocol - let transferManager: TransferManager + let imageUploader: ImageUploader let authService: AuthService let accountService: AccountService } @@ -89,18 +81,17 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, Initi presenter = dependencies.presenter accountId = dependencies.accountId storageService = dependencies.storageService - resourceManager = dependencies.resourceManager - transferManager = dependencies.transferManager + imageUploader = dependencies.imageUploader authService = dependencies.authService accountService = dependencies.accountService } - // MARK: - CreateProfileInputInteractorProtocol + // MARK: - Interactor Input func createAccount() { if let avatar = avatar { - uploadAvatar(avatar) { [weak self] result in + imageUploader.uploadImageFile(avatar) { [weak self] result in switch result { case let .success(avatarURL): self?.createAccount(withAvatar: avatarURL) @@ -143,21 +134,6 @@ final class CreateProfileInteractor: CreateProfileInputInteractorProtocol, Initi } } - private func uploadAvatar(_ image: UIImage, completion: @escaping (Result) -> Void) { - guard let localURL = resourceManager.savePhotoAsFile(image: image, setting: .highest) else { - completion(.failure(UploadingError.imageCouldNotBeSaved)) - return - } - transferManager.upload(url: localURL, listener: nil) { result in - switch result { - case let .success(remoteURL, _): - completion(.success(remoteURL)) - case let .error(error): - completion(.failure(error ?? UploadingError.uploadFailed)) - } - } - } - func checkTermsOfUse() -> Bool { checkTermsOfUsage = !checkTermsOfUsage diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift index d2861eece..ad8dd2e48 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -8,10 +8,10 @@ import Foundation -final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, CreateProfilePresenterProtocol, SetInjectable { +final class CreateProfilePresenter: CreateProfileInteractorOutput, CreateProfilePresenterProtocol, SetInjectable { - private weak var view: CreateProfileViewProtocol? - private var interactor: CreateProfileInputInteractorProtocol! + private weak var view: CreateProfileViewInput? + private var interactor: CreateProfileInteractorInput! private var wireframe: CreateProfileWireframeProtocol! @@ -75,8 +75,8 @@ final class CreateProfilePresenter: CreateProfileOutputInteractorProtocol, Creat extension CreateProfilePresenter { struct Dependencies { let wireframe: CreateProfileWireframe - let interactor: CreateProfileInputInteractorProtocol - let view: CreateProfileViewProtocol + let interactor: CreateProfileInteractorInput + let view: CreateProfileViewInput } func inject(dependencies: CreateProfilePresenter.Dependencies) { diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift index 265e39018..ce4bd1704 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift @@ -8,7 +8,7 @@ import Foundation -final class CreateProfileViewController: UIViewController, CreateProfileViewProtocol, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { +final class CreateProfileViewController: UIViewController, CreateProfileViewInput, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { private let presenter: CreateProfilePresenterProtocol private let viewsFactory: CreateProfileViewsFactoryProtocol @@ -20,6 +20,11 @@ final class CreateProfileViewController: UIViewController, CreateProfileViewProt private lazy var container: UIView = viewsFactory.makeContainer(on: view, headerView: headerView, footerView: createButton) private lazy var scrollView: UIScrollView = viewsFactory.makeScrollView(on: container) private lazy var contentContainer: CreateProfileContentView = viewsFactory.makeContentContainer(on: scrollView, widthView: view, presenter: presenter) + + struct Dependencies { + let presenter: CreateProfilePresenterProtocol + let viewsFactory: CreateProfileViewsFactoryProtocol + } init(dependencies: Dependencies) { presenter = dependencies.presenter @@ -59,7 +64,7 @@ final class CreateProfileViewController: UIViewController, CreateProfileViewProt } } -// MARK: - CreateProfileViewProtocol +// MARK: - View Input extension CreateProfileViewController { func updateProfileField(_ field: ProfileField, value: String) { @@ -71,15 +76,6 @@ extension CreateProfileViewController { } } -// MARK: - InitializeInjectable - -extension CreateProfileViewController { - struct Dependencies { - let presenter: CreateProfilePresenterProtocol - let viewsFactory: CreateProfileViewsFactoryProtocol - } -} - // MARK: - KeyboardInteractive extension CreateProfileViewController { diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift index 73af9b4e2..40f2879ac 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift @@ -260,7 +260,7 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { .font: FontFamily.NotoSans.regular.font(size: 14) ] - let termsOfUseStr = NSMutableAttributedString(string: String.localizable.termsOfUse) + let termsOfUseStr = NSMutableAttributedString(string: String.localizable.createProfileTermsOfUse) termsOfUseStr.addAttributes(attributes, range: NSMakeRange(0, termsOfUseStr.length)) beginOfStr.append(NSAttributedString(string: " ")) diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift index 752d2b452..fccba6de6 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -27,8 +27,7 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { struct Dependencies { let storageService: StorageService - let resourceManager: ResourceManagerProtocol - let transferManager: TransferManager + let imageUploader: ImageUploader let authService: AuthService let accountService: AccountService } @@ -51,8 +50,7 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { presenter: presenter, accountId: parameters.accountId, storageService: dependencies.storageService, - resourceManager: dependencies.resourceManager, - transferManager: dependencies.transferManager, + imageUploader: dependencies.imageUploader, authService: dependencies.authService, accountService: dependencies.accountService) ) diff --git a/Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift b/Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift index a33c753fa..08d58a8c1 100644 --- a/Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift +++ b/Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift @@ -8,27 +8,37 @@ import UIKit +// MARK: - Wireframe + protocol FacebookAuthWireframeProtocol: class { func finishAuthentication(code: String) func dismiss() } -protocol FacebookAuthViewProtocol: class { +// MARK: - View + +protocol FacebookAuthViewInput: class { func load(_ request: URLRequest) } +// MARK: - Presenter + protocol FacebookAuthPresenterProtocol: class { func viewDidLoad() func dismiss() func handleRedirect(to url: URL) -> Bool } -protocol FacebookAuthInteractorInputProtocol: class { +// MARK: - Interactor + +// MARK: Input +protocol FacebookAuthInteractorInput: class { func authenticate() func handleRedirect(to url: URL) -> Bool } -protocol FacebookAuthInteractorOutputProtocol: class { +// MARK: Output +protocol FacebookAuthInteractorOutput: class { func load(_ request: URLRequest) func didAuthenticated(code: String) } diff --git a/Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift b/Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift index a6ea8c3a2..6f08181b2 100644 --- a/Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift +++ b/Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift @@ -8,9 +8,9 @@ import Foundation -final class FacebookAuthInteractor: FacebookAuthInteractorInputProtocol, InitializeInjectable { +final class FacebookAuthInteractor: FacebookAuthInteractorInput, InitializeInjectable { - private(set) weak var presenter: FacebookAuthInteractorOutputProtocol! + private(set) weak var presenter: FacebookAuthInteractorOutput! // MARK: - API @@ -35,7 +35,7 @@ final class FacebookAuthInteractor: FacebookAuthInteractorInputProtocol, Initial // MARK: - Init struct Dependencies { - let presenter: FacebookAuthInteractorOutputProtocol + let presenter: FacebookAuthInteractorOutput } init(dependencies: Dependencies) { @@ -43,7 +43,7 @@ final class FacebookAuthInteractor: FacebookAuthInteractorInputProtocol, Initial } - // MARK: - FacebookAuthInteractorInputProtocol + // MARK: - FacebookAuthInteractorInput func authenticate() { let request = makeAuthenticationRequest() diff --git a/Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift b/Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift index a654aee84..93808d6cb 100644 --- a/Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift +++ b/Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift @@ -8,14 +8,14 @@ import Foundation -final class FacebookAuthPresenter: FacebookAuthPresenterProtocol, FacebookAuthInteractorOutputProtocol { +final class FacebookAuthPresenter: FacebookAuthPresenterProtocol, FacebookAuthInteractorOutput { - private(set) weak var view: FacebookAuthViewProtocol? - private(set) var interactor: FacebookAuthInteractorInputProtocol! + private(set) weak var view: FacebookAuthViewInput? + private(set) var interactor: FacebookAuthInteractorInput! private(set) var wireframe: FacebookAuthWireframeProtocol! - // MARK: - FacebookAuthPresenterProtocol + // MARK: - Presenter func viewDidLoad() { interactor.authenticate() @@ -30,7 +30,7 @@ final class FacebookAuthPresenter: FacebookAuthPresenterProtocol, FacebookAuthIn } - // MARK: - FacebookAuthInteractorOutputProtocol + // MARK: - Interactor Output func load(_ request: URLRequest) { view?.load(request) @@ -41,11 +41,13 @@ final class FacebookAuthPresenter: FacebookAuthPresenterProtocol, FacebookAuthIn } } +// MARK: - Injection + extension FacebookAuthPresenter: SetInjectable { struct Dependencies { - let view: FacebookAuthViewProtocol - let interactor: FacebookAuthInteractorInputProtocol + let view: FacebookAuthViewInput + let interactor: FacebookAuthInteractorInput let wireframe: FacebookAuthWireframeProtocol } diff --git a/Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift b/Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift index 1f6f8eff0..9a9f2180b 100644 --- a/Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift +++ b/Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit import WebKit -final class FacebookAuthViewController: UIViewController, InitializeInjectable, FacebookAuthViewProtocol { +final class FacebookAuthViewController: UIViewController, InitializeInjectable, FacebookAuthViewInput { private let presenter: FacebookAuthPresenterProtocol @@ -101,7 +101,7 @@ final class FacebookAuthViewController: UIViewController, InitializeInjectable, } - // MARK: - FacebookAuthViewProtocol + // MARK: - View Input func load(_ request: URLRequest) { webView.load(request) diff --git a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Auth Flow/Splash/Interactor/SplashInteractor.swift similarity index 100% rename from Nynja/Modules/Splash/Interactor/SplashInteractor.swift rename to Nynja/Modules/Auth Flow/Splash/Interactor/SplashInteractor.swift diff --git a/Nynja/Modules/Splash/Presenter/SplashPresenter.swift b/Nynja/Modules/Auth Flow/Splash/Presenter/SplashPresenter.swift similarity index 100% rename from Nynja/Modules/Splash/Presenter/SplashPresenter.swift rename to Nynja/Modules/Auth Flow/Splash/Presenter/SplashPresenter.swift diff --git a/Nynja/Modules/Splash/SplashProtocols.swift b/Nynja/Modules/Auth Flow/Splash/SplashProtocols.swift similarity index 100% rename from Nynja/Modules/Splash/SplashProtocols.swift rename to Nynja/Modules/Auth Flow/Splash/SplashProtocols.swift diff --git a/Nynja/Modules/Splash/View/SplashViewController.swift b/Nynja/Modules/Auth Flow/Splash/View/SplashViewController.swift similarity index 100% rename from Nynja/Modules/Splash/View/SplashViewController.swift rename to Nynja/Modules/Auth Flow/Splash/View/SplashViewController.swift diff --git a/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift b/Nynja/Modules/Auth Flow/Splash/WireFrame/SplashWireframe.swift similarity index 100% rename from Nynja/Modules/Splash/WireFrame/SplashWireframe.swift rename to Nynja/Modules/Auth Flow/Splash/WireFrame/SplashWireframe.swift diff --git a/Nynja/Resources/Colors.json b/Nynja/Resources/Colors.json index 0162b355b..454ea75fd 100644 --- a/Nynja/Resources/Colors.json +++ b/Nynja/Resources/Colors.json @@ -45,6 +45,7 @@ "white": "#ffffff", "clear": "#00000000", "black": "#000000", + "lightTransparentBlack": "#0000001A", "callBackground": "#2c2e33", "separatorGrayColor": "#3f3f3f", "callGradientStart": "#2c2e33ff", diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index a513239c3..7e3631c1b 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -1029,11 +1029,16 @@ // MARK: Create Profile -"terms_of_use" = "terms of use"; +"create_profile.terms_of_use" = "terms of use"; // MARK: - Account Edit Flow +// MARK: Account Settings + +"account_settings.screen_title" = "ACCOUNT SETTINGS"; +"account_settings.save_button" = "SAVE"; + // MARK: Login Options "login_options.screen_title" = "LOGIN OPTIONS"; diff --git a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift index 52d4e4752..ef825bc72 100644 --- a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -106,9 +106,9 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog func processAuthenticatedAccount(_ account: Account) throws { let account = DBAccount(account: account) - if case let identityId = account.profileId, !identityId.isEmpty { + if case let passcode = account.profileId, !passcode.isEmpty { LogService.log(topic: .db) { return "Setup DB: Prifile Handler" } - storage.setupDatabase(with: identityId, application: UIApplication.shared) + storage.setupDatabase(with: passcode, application: UIApplication.shared) } else { assertionFailure("Unable to setup database") } diff --git a/Nynja/Services/ServiceFactory/DAOFactoryProtocol.swift b/Nynja/Services/ServiceFactory/DAOFactoryProtocol.swift new file mode 100644 index 000000000..5fcb0945d --- /dev/null +++ b/Nynja/Services/ServiceFactory/DAOFactoryProtocol.swift @@ -0,0 +1,11 @@ +// +// DAOFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/3/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol DAOFactoryProtocol: class { + func makeAccountDAO() -> AccountDAOProtocol +} diff --git a/Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift index a60e24ba3..fd5a892a4 100644 --- a/Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift @@ -6,8 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - protocol MQTTFactoryProtocol: class { func makeTypingHandler() -> TypingHandler } diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 7dbee2a05..ede61df81 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -135,6 +135,11 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return WalletFundingNetworkService(dependencies: dependencies) } + func makeImageUploader() -> ImageUploader { + return ImageUploaderImpl(dependencies: .init(resourceManager: makeResourceManager(), + transferManager: makeTransferManager())) + } + func makeTransferManager() -> TransferManager { return TransferManager.shared } @@ -248,3 +253,11 @@ extension ServiceFactory { return makeCommunicator().getAccountManager() } } + +// MARK: - DAO +extension ServiceFactory { + + func makeAccountDAO() -> AccountDAOProtocol { + return AccountDAO(dbManager: makeStorageService()) + } +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift index 97801efc4..f4f0f65c2 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtocol, MobileSDKFactoryProtocol { +protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtocol, MobileSDKFactoryProtocol, DAOFactoryProtocol { func makeMessageSendingService() -> MessageSendingServiceProtocol func makeResourceManager() -> ResourceManagerProtocol func makeMessageFactory() -> MessageFactoryProtocol @@ -38,6 +38,8 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtoc func makePermissionManager() -> PermissionManager func makeWalletService() -> WalletService + + func makeImageUploader() -> ImageUploader func makeTransferManager() -> TransferManager func makeSyncFileManager() -> SyncFileManager func makeSyncFileManager(with kind: FileDownloaderKind) -> SyncFileManager diff --git a/Nynja/Services/Storage/DAO/Account/AccountDAO.swift b/Nynja/Services/Storage/DAO/Account/AccountDAO.swift new file mode 100644 index 000000000..8390a7141 --- /dev/null +++ b/Nynja/Services/Storage/DAO/Account/AccountDAO.swift @@ -0,0 +1,29 @@ +// +// AccountDAO.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/3/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import GRDBCipher + +final class AccountDAO: AccountDAOProtocol { + + private let dbManager: DBManagerProtocol + + init(dbManager: DBManagerProtocol) { + self.dbManager = dbManager + } + + func fetchAccount(by accountId: String) -> DBAccount? { + return dbManager.fetch { db in + return try DBAccount.account(from: db, accountId: accountId) + } + } + + func save(_ account: Account) throws { + try dbManager.perform(action: .save, with: account) + } +} diff --git a/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift b/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift new file mode 100644 index 000000000..d38643db0 --- /dev/null +++ b/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift @@ -0,0 +1,14 @@ +// +// AccountDAOProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/3/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AccountDAOProtocol: class { + func fetchAccount(by accountId: String) -> DBAccount? + func save(_ account: Account) throws +} -- GitLab From 64e69f6a3b0707bac95b2f2e7a871d643d2af445 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 5 Dec 2018 13:45:17 +0200 Subject: [PATCH 126/138] [Multiple accounts] Delete Account UI + minor fixes (#1511) * [NY-3855] Add login options module skeleton. * [NY-3855] Implemented base UI for login options. * [NY-3855] Added alerts. * [NY-3855] Added LoginOption model. Minor changes. * Fixed compile issues. * [NY-3855] Fixed screen title * Fixed switch * [NY-5519] Add AuthProvider module from template * Minor renaming changes. * Remove unused references from project file * Added transition to 'add auth provider' screen. * Implemented base UI for add auth provider. * Fixed UI * Fixed UI hierarchy. * Fixed UI components reusability from auth screen. * Implemented code confirmation input as a separate view. * Add new files * Remove unused code * Minor refactor. * Fixed accessibility and fonts construction. * Present code confirmation * Present code confirmation * Add adjust on code confirmation screen. * [NY-5508] Added profile mock. * [NY-5508] Minot refactoring in AuthService * [NY-5508] Implemented profile mock provider. * [NY-5508] Save auth token in AuthService. Update test target. * [NY-5508] Make userInfo not lazy in StorageService. * [NY-5508] Added DBModels. * Added AccountTable * Fixed compile issues. * [NY-5508] Added ContactInfoTable * [NY-5508] Added db migration for account and contact info tables. Pass AccountService to CreateProfile module. * [NY-5508] Updated code confirmation flow * [NY-5508] Remove code confirmation views factory. * [NY-5508] Upload avatar logic * [NY-5508] Add loading indicator on Create profile screen. * [NY-5508] Added AppCoordinator and try to present MainWireframe but receive crash. * [NY-5508] Open profile screen with mocked data. * [NY-5508] Setup database * [NY-5508] Refactoring * [NY-5508] Merge DBProfile and DBAccount * [NY-5507] Fixed retain cycles in Account Settings module. Refactored protocol names. * [NY-5507] Pass dependencies to Account Settings * [NY-5507] Update account logic. * [NY-5507] Implemented avatar uploading. * [NY-5418] Fixed typos and localization. * [NY-5623] Add custom camera option for choosing avatar on create profile screen. * [NY-5621] Dismissed account name text field from Create Profile screen. * [NY-5418] Fixed UI on Create profile screen. * [NY-5502] Implemented Delete Account screen UI. * [NY-5502] Implemented adjust on Delete Account screen UI. * Remove adjust in some places on code confirmation screen. * Fixed font heights * Updated alert presentation logic. * MInor fix. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 12 ++ .../NynjaUIKit/Core/Alerts/Alert.swift | 80 ++++++++ Nynja.xcodeproj/project.pbxproj | 68 +++++++ Nynja/Generated/AssetsConstants.swift | 2 + Nynja/Generated/LocalizableConstants.swift | 28 +++ Nynja/Library/UI/Alert/AlertDisplayable.swift | 34 ++-- .../AccountSettingsProtocols.swift | 4 + .../Entities/DestructiveActionCellModel.swift | 28 +++ .../AccountSettingsInteractor.swift | 2 +- .../Presenter/AccountSettingsPresenter.swift | 15 +- .../View/AccountSettingsViewController.swift | 29 ++- .../DestructiveActionTableViewCell.swift | 63 +++++++ .../Wireframe/AccountSettingsWireframe.swift | 5 + .../AccountSettingsCoordinator.swift | 92 +++++++-- .../Coordinator/LoginOptionsCoordinator.swift | 18 +- .../DeleteAccountProtocols.swift | 39 ++++ .../Interactor/DeleteAccountInteractor.swift | 45 +++++ .../Presenter/DeleteAccountPresenter.swift | 56 ++++++ .../View/DeleteAccountViewController.swift | 176 ++++++++++++++++++ .../WIreframe/DeleteAccountWireframe.swift | 60 ++++++ .../LoginOptions/LoginOptionsProtocols.swift | 6 +- .../Presenter/LoginOptionsPresenter.swift | 36 ++-- .../Wireframe/LoginOptionsWireframe.swift | 11 +- Nynja/Modules/Auth Flow/AppCoordinator.swift | 10 +- Nynja/Modules/Auth Flow/AuthCoordinator.swift | 77 +++++--- .../Auth Flow/AuthModule/AuthProtocols.swift | 4 +- .../AuthModule/Presenter/AuthPresenter.swift | 10 +- .../AuthModule/Wireframe/AuthWireframe.swift | 11 +- .../View/CodeConfirmationViewController.swift | 37 +++- .../CreateProfile/Entities/ProfileField.swift | 15 +- .../View/CreateProfileViewController.swift | 73 ++++++-- .../Subviews/CreateProfileContentView.swift | 31 ++- .../CreateProfileViewsFactory.swift | 49 +---- .../Contents.json | 12 ++ ..._States_Images_img_empty_states_delete.pdf | Bin 0 -> 9737 bytes .../ic_contacts_empty.imageset/Contents.json | 11 +- .../ic_search_empty.imageset/Contents.json | 11 +- .../Contents.json | 11 +- .../Contents.json | 11 +- .../Contents.json | 11 +- .../Contents.json | 11 +- Nynja/Resources/en.lproj/Localizable.strings | 21 +++ 42 files changed, 1046 insertions(+), 279 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Core/Alerts/Alert.swift create mode 100644 Nynja/Modules/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift create mode 100644 Nynja/Modules/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift create mode 100644 Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift create mode 100644 Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift create mode 100644 Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift create mode 100644 Nynja/Modules/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift create mode 100644 Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift create mode 100644 Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Empty_States_Images_img_empty_states_delete.pdf diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 8779c566e..27034684c 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 3A07C5B421AED3FC00AE3429 /* SecureCodeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B221AED3FC00AE3429 /* SecureCodeContainerView.swift */; }; 3A07C5B521AED3FC00AE3429 /* SecureCodeInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B321AED3FC00AE3429 /* SecureCodeInputView.swift */; }; 3A07C5B921AEDF7800AE3429 /* UITextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B821AEDF7800AE3429 /* UITextInput.swift */; }; + 3AE2F99D21B6E31C0068C3BC /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F99C21B6E31C0068C3BC /* Alert.swift */; }; 850A2E9E219F37AD00C784D9 /* TextInputUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */; }; 8514D4C620EE27080002378A /* NynjaUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 8514D4C420EE27080002378A /* NynjaUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8514D4DF20EE2D970002378A /* InteractiveCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D4CF20EE2D970002378A /* InteractiveCellViewModel.swift */; }; @@ -58,6 +59,7 @@ 3A07C5B221AED3FC00AE3429 /* SecureCodeContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureCodeContainerView.swift; sourceTree = ""; }; 3A07C5B321AED3FC00AE3429 /* SecureCodeInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureCodeInputView.swift; sourceTree = ""; }; 3A07C5B821AEDF7800AE3429 /* UITextInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextInput.swift; sourceTree = ""; }; + 3AE2F99C21B6E31C0068C3BC /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; 600B9308D041B8E4DE0DBEF1 /* Pods-NynjaUIKit.loaddb.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.loaddb.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.loaddb.xcconfig"; sourceTree = ""; }; 7336042AC840197E622730FD /* Pods-NynjaUIKit.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.prerelease.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.prerelease.xcconfig"; sourceTree = ""; }; 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputUtils.swift; sourceTree = ""; }; @@ -155,6 +157,14 @@ path = TextInput; sourceTree = ""; }; + 3AE2F99721B6D9B00068C3BC /* Alerts */ = { + isa = PBXGroup; + children = ( + 3AE2F99C21B6E31C0068C3BC /* Alert.swift */, + ); + path = Alerts; + sourceTree = ""; + }; 703C0F257F98BCEAD4D25D95 /* Pods */ = { isa = PBXGroup; children = ( @@ -440,6 +450,7 @@ 8514D4DD20EE2D970002378A /* Layout */, 8514D4CC20EE2D970002378A /* Collection */, 855A4E8B219AFEE000B6E90B /* TextInput */, + 3AE2F99721B6D9B00068C3BC /* Alerts */, ); path = Core; sourceTree = ""; @@ -588,6 +599,7 @@ 8514D51820EE40540002378A /* NynjaContextMenuItemsFactory.swift in Sources */, 8540A00A2181EB87003A010F /* TypingView.swift in Sources */, 85EB37F621832D41003A2D6F /* RecordingIndicatorView.swift in Sources */, + 3AE2F99D21B6E31C0068C3BC /* Alert.swift in Sources */, 8514D51520EE40540002378A /* NynjaContextMenuItemCollectionViewCell.swift in Sources */, 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */, 3A07C5B421AED3FC00AE3429 /* SecureCodeContainerView.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Alerts/Alert.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Alerts/Alert.swift new file mode 100644 index 000000000..3dfd5f5e4 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Alerts/Alert.swift @@ -0,0 +1,80 @@ +// +// Alert.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public struct Alert { + public let title: String? + public let message: String? + public let style: Style + public let actions: [Action] + + public init(title: String? = nil, message: String? = nil, style: Style = .alert, actions: [Action]) { + self.title = title + self.message = message + self.style = style + self.actions = actions + } + + public func makeAlertController() -> UIAlertController { + let alert = UIAlertController(title: title, message: message, preferredStyle: style.nativeStyle) + actions.forEach { + alert.addAction($0.nativeAction) + } + return alert + } + + public enum Style { + case alert + case actionSheet + + public var nativeStyle: UIAlertController.Style { + switch self { + case .alert: + return .alert + case .actionSheet: + return .actionSheet + } + } + } + + public final class Action { + public enum Style { + case `default` + case cancel + case destructive + + var nativeStyle: UIAlertAction.Style { + switch self { + case .default: + return .default + case .cancel: + return .cancel + case .destructive: + return .destructive + } + } + } + + public let title: String? + public let style: Style + public let handler: ((Action) -> Void)? + + public init(title: String?, style: Style, handler: ((Action) -> Void)? = nil) { + self.title = title + self.style = style + self.handler = handler + } + + public var nativeAction: UIAlertAction { + return UIAlertAction(title: title, style: style.nativeStyle) { _ in + self.handler?(self) + } + } + } +} diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 4d73bf144..e7db57924 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -546,6 +546,13 @@ 3AE0A84B1F20321A008A04F3 /* Wheel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A83F1F20321A008A04F3 /* Wheel.swift */; }; 3AE0A84C1F20321A008A04F3 /* WheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A8401F20321A008A04F3 /* WheelItemModel.swift */; }; 3AE0A84D1F20321A008A04F3 /* WheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A8411F20321A008A04F3 /* WheelItemView.swift */; }; + 3AE2F98621B6A8D30068C3BC /* DestructiveActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98421B6A8D30068C3BC /* DestructiveActionTableViewCell.swift */; }; + 3AE2F98721B6A8D30068C3BC /* DestructiveActionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98521B6A8D30068C3BC /* DestructiveActionCellModel.swift */; }; + 3AE2F98E21B6B5B30068C3BC /* DeleteAccountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98921B6B5B30068C3BC /* DeleteAccountPresenter.swift */; }; + 3AE2F98F21B6B5B30068C3BC /* DeleteAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98A21B6B5B30068C3BC /* DeleteAccountViewController.swift */; }; + 3AE2F99021B6B5B30068C3BC /* DeleteAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98B21B6B5B30068C3BC /* DeleteAccountProtocols.swift */; }; + 3AE2F99121B6B5B30068C3BC /* DeleteAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98C21B6B5B30068C3BC /* DeleteAccountInteractor.swift */; }; + 3AE2F99221B6B5B30068C3BC /* DeleteAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98D21B6B5B30068C3BC /* DeleteAccountWireframe.swift */; }; 3AF4A3DA1EFD7DFA0059B405 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF4A3D91EFD7DFA0059B405 /* StringExtensions.swift */; }; 3AF8E26F1F42E33300D81390 /* ReturnToCallContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF8E26E1F42E33300D81390 /* ReturnToCallContentView.swift */; }; 3CDA490701EC3FEAAC2E9AFE /* TopUpAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498AA2E3A69072FEC336C1ED /* TopUpAccountInteractor.swift */; }; @@ -2978,6 +2985,13 @@ 3AE0A83F1F20321A008A04F3 /* Wheel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wheel.swift; sourceTree = ""; }; 3AE0A8401F20321A008A04F3 /* WheelItemModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WheelItemModel.swift; sourceTree = ""; }; 3AE0A8411F20321A008A04F3 /* WheelItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WheelItemView.swift; sourceTree = ""; }; + 3AE2F98421B6A8D30068C3BC /* DestructiveActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveActionTableViewCell.swift; sourceTree = ""; }; + 3AE2F98521B6A8D30068C3BC /* DestructiveActionCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveActionCellModel.swift; sourceTree = ""; }; + 3AE2F98921B6B5B30068C3BC /* DeleteAccountPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountPresenter.swift; sourceTree = ""; }; + 3AE2F98A21B6B5B30068C3BC /* DeleteAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountViewController.swift; sourceTree = ""; }; + 3AE2F98B21B6B5B30068C3BC /* DeleteAccountProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountProtocols.swift; sourceTree = ""; }; + 3AE2F98C21B6B5B30068C3BC /* DeleteAccountInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountInteractor.swift; sourceTree = ""; }; + 3AE2F98D21B6B5B30068C3BC /* DeleteAccountWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountWireframe.swift; sourceTree = ""; }; 3AF4A3D91EFD7DFA0059B405 /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 3AF8E26E1F42E33300D81390 /* ReturnToCallContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReturnToCallContentView.swift; sourceTree = ""; }; 3CDE788B1BF51A83EA2F0056 /* QRCodeReaderPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeReaderPresenter.swift; sourceTree = ""; }; @@ -6604,6 +6618,50 @@ path = WheelContainer; sourceTree = ""; }; + 3AE2F98821B6B4A00068C3BC /* DeleteAccount */ = { + isa = PBXGroup; + children = ( + 3AE2F98B21B6B5B30068C3BC /* DeleteAccountProtocols.swift */, + 3AE2F99321B6B5BA0068C3BC /* View */, + 3AE2F99421B6B5C60068C3BC /* Presenter */, + 3AE2F99521B6B5D10068C3BC /* Interactor */, + 3AE2F99621B6B5EE0068C3BC /* WIreframe */, + ); + path = DeleteAccount; + sourceTree = ""; + }; + 3AE2F99321B6B5BA0068C3BC /* View */ = { + isa = PBXGroup; + children = ( + 3AE2F98A21B6B5B30068C3BC /* DeleteAccountViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 3AE2F99421B6B5C60068C3BC /* Presenter */ = { + isa = PBXGroup; + children = ( + 3AE2F98921B6B5B30068C3BC /* DeleteAccountPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 3AE2F99521B6B5D10068C3BC /* Interactor */ = { + isa = PBXGroup; + children = ( + 3AE2F98C21B6B5B30068C3BC /* DeleteAccountInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 3AE2F99621B6B5EE0068C3BC /* WIreframe */ = { + isa = PBXGroup; + children = ( + 3AE2F98D21B6B5B30068C3BC /* DeleteAccountWireframe.swift */, + ); + path = WIreframe; + sourceTree = ""; + }; 3AED33923C943D0FCE611C55 /* AddParticipants */ = { isa = PBXGroup; children = ( @@ -7882,6 +7940,7 @@ 5E7D5D5921901BC6009B5D8D /* DescriptionTVCell.swift */, 5E7D5D5D2190415F009B5D8D /* AddContactCell.swift */, 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */, + 3AE2F98421B6A8D30068C3BC /* DestructiveActionTableViewCell.swift */, ); path = Cells; sourceTree = ""; @@ -7899,6 +7958,7 @@ children = ( 5EDD454721885EC400C50BC8 /* Coordinator */, 5EDD454821885EC400C50BC8 /* AccountSettings */, + 3AE2F98821B6B4A00068C3BC /* DeleteAccount */, 851452A421A5865C00DF10A6 /* LoginOptions */, 3A80BF9121A8637F0016285E /* AuthProvider */, ); @@ -7977,6 +8037,7 @@ 5E7D5D5B21901D18009B5D8D /* DescriptionCellModel.swift */, 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */, 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */, + 3AE2F98521B6A8D30068C3BC /* DestructiveActionCellModel.swift */, ); path = Entities; sourceTree = ""; @@ -16158,6 +16219,7 @@ FB0B721320907DB5003B9757 /* MessageEditService.swift in Sources */, 859B862C204820DC003272B2 /* ThemePickerPresenter.swift in Sources */, 8509FC872158F7FC00734D93 /* DirectoryWatcher.swift in Sources */, + 3AE2F99221B6B5B30068C3BC /* DeleteAccountWireframe.swift in Sources */, 2603139B20A0A4BA009AC66D /* LanguageSelectorViewController.swift in Sources */, 2686D3201FC3E39C0079CB75 /* ContentNavigationVC.swift in Sources */, B7EF8EDB210C759400E0E981 /* InterpretationTypeCellModel.swift in Sources */, @@ -16459,6 +16521,7 @@ 35F2DA611F73CAD400777920 /* NotificationManager.swift in Sources */, 4B86C562219C12840006A192 /* DAO.swift in Sources */, 26C1A3ED2031D3030009F7F0 /* OtherUserContainerViewController.swift in Sources */, + 3AE2F98621B6A8D30068C3BC /* DestructiveActionTableViewCell.swift in Sources */, FB16E79920EFAFA8009FA203 /* Money.swift in Sources */, BA982E458F95A7A5AB4A8A73 /* TutorialViewController.swift in Sources */, B723C625204D86AF00884FFD /* SettingsDataAndStoragePresenter.swift in Sources */, @@ -16540,6 +16603,7 @@ 4B06D31E2028A6D6003B275B /* MySelfItemsFactory.swift in Sources */, 26588E6720A20E49000D3E1A /* Customizable.swift in Sources */, 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */, + 3AE2F99021B6B5B30068C3BC /* DeleteAccountProtocols.swift in Sources */, F11786DE20A9ED65007A9A1B /* DownloadOperation.swift in Sources */, 851452AA21A586E900DF10A6 /* LoginOptionsPresenter.swift in Sources */, 852E8473213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift in Sources */, @@ -16572,6 +16636,7 @@ 5BC1D38220D3B54B002A44B3 /* CallInfoViewLayout.swift in Sources */, A42D51AF206A361400EEB952 /* receiveTask.swift in Sources */, 857C070620DB8A3D00626EEB /* StickerInputState.swift in Sources */, + 3AE2F98E21B6B5B30068C3BC /* DeleteAccountPresenter.swift in Sources */, A4B544EF20EFB4DF00EB7B0F /* BertTupleExtension.swift in Sources */, 5E7D5D43218C5A4C009B5D8D /* UserContactAction.swift in Sources */, 8502DB512061030100613C8C /* WheelPositionPickerProtocols.swift in Sources */, @@ -16877,6 +16942,7 @@ 8580BAC720BD983400239D9D /* MentionFetchProtocols.swift in Sources */, 4B749F09214FEE4F002F3A33 /* VerifyNumberWireFrame.swift in Sources */, 4B052CB0203614D400BC2A9B /* StringAtomExtension.swift in Sources */, + 3AE2F98F21B6B5B30068C3BC /* DeleteAccountViewController.swift in Sources */, A43E67EA206E855600048916 /* BadgeNumberService.swift in Sources */, 4D53FE7454959323B1CCFD96 /* ProfileViewController.swift in Sources */, E79061BC1FBF2D43009FD83A /* ContactTable.swift in Sources */, @@ -16979,6 +17045,7 @@ 4B6D20E92164D4AB003ADB29 /* ChatCellModelType.swift in Sources */, E7302A8F1FC821FA002892F8 /* DBRoom.swift in Sources */, 8562853420D16242000C9739 /* StickerPreviewing.swift in Sources */, + 3AE2F99121B6B5B30068C3BC /* DeleteAccountInteractor.swift in Sources */, 8586CAC5203335C7009F2A75 /* ForwardAvatarViewModel.swift in Sources */, 2600DAD420346AD300A2D4F7 /* ButtonHeaderView.swift in Sources */, A42D52AE206A53AA00EEB952 /* Contact_Spec.swift in Sources */, @@ -17209,6 +17276,7 @@ 268C341121067F1D00F1472A /* AudioLongTranscribeOperation.swift in Sources */, A4ED79B020C8041500A41F67 /* TableViewDataSourceProxy.swift in Sources */, 26E0C44721469E9800A58ECD /* ConnectionService.swift in Sources */, + 3AE2F98721B6A8D30068C3BC /* DestructiveActionCellModel.swift in Sources */, A45F113E20B4218D00F45004 /* MessageInteractor+Utils.swift in Sources */, A4CB153521038A7A00C3B68B /* UIDevice+Jailbreak.swift in Sources */, 3AE0A84C1F20321A008A04F3 /* WheelItemModel.swift in Sources */, diff --git a/Nynja/Generated/AssetsConstants.swift b/Nynja/Generated/AssetsConstants.swift index 81028dd21..ae213dc02 100644 --- a/Nynja/Generated/AssetsConstants.swift +++ b/Nynja/Generated/AssetsConstants.swift @@ -106,6 +106,8 @@ internal extension Image { } /// "Disclosure_Indicator" static var disclosureIndicator: ImageAsset { return ImageAsset(name: "Disclosure_Indicator") } + /// "Empty_States_Images_img_empty_states_delete" + static var emptyStatesImagesImgEmptyStatesDelete: ImageAsset { return ImageAsset(name: "Empty_States_Images_img_empty_states_delete") } enum Forward { /// "ic_forward_contacts" static var icForwardContacts: ImageAsset { return ImageAsset(name: "ic_forward_contacts") } diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 59d447079..9d1535b52 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -48,6 +48,10 @@ internal extension String { static var admin: String { return localizable.tr("Localizable", "admin") } /// Group Admins static var admins: String { return localizable.tr("Localizable", "admins") } + /// Take from Camera + static var alertActionTakeFromCamera: String { return localizable.tr("Localizable", "alert_action_take_from_camera") } + /// Take from Gallery + static var alertActionTakeFromGallery: String { return localizable.tr("Localizable", "alert_action_take_from_gallery") } /// Which way would you like to get a code? static var alertGetCode: String { return localizable.tr("Localizable", "alert_get_code") } /// SMS @@ -1556,8 +1560,32 @@ internal extension String { } /// Welcome to static var codeConfirmationWelcome: String { return localizable.tr("Localizable", "code_confirmation.welcome") } + /// Account Name + static var createProfileAccountNameFieldPlaceholder: String { return localizable.tr("Localizable", "create_profile.account_name_field_placeholder") } + /// I agree at + static var createProfileAgreeAtTerms: String { return localizable.tr("Localizable", "create_profile.agree_at_terms") } + /// CREATE + static var createProfileCreateButton: String { return localizable.tr("Localizable", "create_profile.create_button") } + /// First Name + static var createProfileFirstNameFieldPlaceholder: String { return localizable.tr("Localizable", "create_profile.first_name_field_placeholder") } + /// Last Name + static var createProfileLastNameFieldPlaceholder: String { return localizable.tr("Localizable", "create_profile.last_name_field_placeholder") } + /// Profile Message + static var createProfileProfileMessageFieldPlaceholder: String { return localizable.tr("Localizable", "create_profile.profile_message_field_placeholder") } + /// CREATE PROFILE + static var createProfileScreenTitle: String { return localizable.tr("Localizable", "create_profile.screen_title") } + /// You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.\n\nYou can use a-z, 0-9 and underscores. Minimum length is 2 characters. + static var createProfileTermsHint: String { return localizable.tr("Localizable", "create_profile.terms_hint") } /// terms of use static var createProfileTermsOfUse: String { return localizable.tr("Localizable", "create_profile.terms_of_use") } + /// Username + static var createProfileUsernameFieldPlaceholder: String { return localizable.tr("Localizable", "create_profile.username_field_placeholder") } + /// DELETE ACCOUNT + static var deleteAccountDeleteAccountButton: String { return localizable.tr("Localizable", "delete_account.delete_account_button") } + /// If you delete your account, you will permanently lose access to your wallet, account data, messages and media. This cannot be undone.\n\nBy deleting your account you confirm that you withdraw your consent with NYNJA terms of use.\n\nAlso, make sure you have saved off the 12 word seed to regenerate your keys later in other platforms. + static var deleteAccountDescriptionText: String { return localizable.tr("Localizable", "delete_account.description_text") } + /// DELETE ACCOUNT + static var deleteAccountScreenTitle: String { return localizable.tr("Localizable", "delete_account.screen_title") } /// Add Login Provider static var loginOptionsAddAlertTitle: String { return localizable.tr("Localizable", "login_options.add_alert_title") } /// Add Login Option diff --git a/Nynja/Library/UI/Alert/AlertDisplayable.swift b/Nynja/Library/UI/Alert/AlertDisplayable.swift index cc8d4901a..8c2d336b9 100644 --- a/Nynja/Library/UI/Alert/AlertDisplayable.swift +++ b/Nynja/Library/UI/Alert/AlertDisplayable.swift @@ -7,40 +7,28 @@ // import Foundation +import NynjaUIKit protocol AlertDisplayable: class { - func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?, completion: (() -> Void)?) - func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?, completion: (() -> Void)?) - - func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?) - func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?) + func present(_ alert: Alert, completion: (() -> Void)?) + func present(_ alert: Alert) } extension AlertDisplayable { - - func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?, completion: (() -> Void)?) { - presentAlert(title: title, message: message, style: .alert, actions: actions, completion: completion) - } - - func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?) { - presentAlert(title: title, message: message, style: style, actions: actions, completion: nil) - } - - func presentAlert(title: String?, message: String?, actions: [UIAlertAction]?) { - presentAlert(title: title, message: message, style: .alert, actions: actions, completion: nil) + func present(_ alert: Alert) { + present(alert, completion: nil) } } +// MARK: - Wireframe + Coordinator + protocol NavigationContainer: AlertDisplayable { - var navigation: UINavigationController? { get } + var navigation: UINavigationController! { get } } extension NavigationContainer { - func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?, completion: (() -> Void)?) { - let alert = UIAlertController(title: title, message: message, preferredStyle: style) - - actions?.forEach { alert.addAction($0) } - - navigation?.present(alert, animated: true, completion: completion) + func present(_ alert: Alert, completion: (() -> Void)?) { + let alertController = alert.makeAlertController() + navigation?.present(alertController, animated: true, completion: completion) } } diff --git a/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift index 7d241778c..b4e22c196 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift @@ -19,6 +19,8 @@ protocol AccountSettingsWireframeProtocol: class { func addContact(completion: @escaping (Result) -> Void) func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) + + func deleteAccount(accountId: String) } // MARK: - View @@ -59,6 +61,8 @@ protocol AccountSettingsPresenterProtocol: BasePresenterProtocol, NavigationProt protocol AccountSettingsInteractorInput: BaseInteractorProtocol { func save(completion: @escaping (Result) -> Void) + var accountId: String { get } + var avatar: URL? { get } var status: UserStatus { get set } var statusTimeout: StatusTimeout { get set } diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift b/Nynja/Modules/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift new file mode 100644 index 000000000..a592f2ab7 --- /dev/null +++ b/Nynja/Modules/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift @@ -0,0 +1,28 @@ +// +// DestructiveActionCellModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class DestructiveActionCellModel: CellViewModel, InteractiveCellViewModel { + + let title: String + let accessibilityIdentifier: String + var select: (() -> Void)? + + init(title: String, accessibilityIdentifier: String, selectionHandler: (() -> Void)?) { + self.title = title + self.accessibilityIdentifier = accessibilityIdentifier + self.select = selectionHandler + } + + func setup(cell: DestructiveActionTableViewCell) { + cell.titleLabel.text = title + } +} + diff --git a/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift index 386ec6ea6..6a2fa9bb2 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift @@ -14,7 +14,7 @@ final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractor // MARK: - User Info - private let accountId: String + let accountId: String private var account: DBAccount? diff --git a/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift index e56a481bc..0021e5a86 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -38,7 +38,7 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } func countOfSections() -> Int { - return 4 + return 5 } func rowsInSection(section: Int) -> Int { @@ -47,6 +47,7 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro case 1: return 2//3 case 2: return 2 case 3: return 0// 1 + (interactor?.contacts.count ?? 0) + case 4: return 1 // delete profile default: return 0 } } @@ -56,6 +57,7 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro case 1: return SettingsSectionHeader(text: "Personal Information".localized) case 2: return SettingsSectionHeader(text: "Username".localized) case 3: return SettingsSectionHeader(text: "Contact Information".localized) + case 4: return nil // delete profile default: return nil } } @@ -89,7 +91,16 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro let contact = interactor?.contacts[row - 1] return ContactTVCellModel(typeImage: UIImage(named: "arrow_up")!, title: contact?.value ?? "", details: "") } - default: return nil + case 4: + // FIXME: localization (in whole module) + return DestructiveActionCellModel(title: "Delete Account", accessibilityIdentifier: "delete_account_item") { [weak self] in + guard let self = self, let accountId = self.interactor?.accountId else { + return + } + self.wireframe?.deleteAccount(accountId: accountId) + } + default: + return nil } } diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift index b4987ba7a..c4da46fc9 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift @@ -7,7 +7,7 @@ // import Foundation - +import NynjaUIKit final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, LoadingDisplayable, InitializeInjectable, UITableViewDelegate, UITableViewDataSource, KeyboardInteractive { @@ -31,7 +31,8 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa table.register(DescriptionTVCell.self, forCellReuseIdentifier: DescriptionCellModel.identifier) table.register(AddContactTVCell.self, forCellReuseIdentifier: AddContactCellModel.identifier) table.register(ContactTVCell.self, forCellReuseIdentifier: ContactTVCellModel.identifier) - + table.register(viewModel: DestructiveActionCellModel.self) + table.separatorStyle = .none table.backgroundColor = .clear @@ -89,6 +90,9 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa return button }() + + // MARK: - Init + struct Dependencies { let presenter: AccountSettingsPresenterProtocol } @@ -103,6 +107,9 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa fatalError("init(coder:) has not been implemented") } + + // MARK: - Life Cycle + override func initialize() { super.initialize() @@ -129,10 +136,6 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa unregisterForKeyboardNotifications() } - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - // MARK: - View Input @@ -185,6 +188,11 @@ extension AccountSettingsViewController { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + // FIXME: temp workaround, will be removed soon + if let viewModel = presenter.item(for: indexPath.section, row: indexPath.row) as? InteractiveCellViewModel { + viewModel.select?() + return + } switch (indexPath.section, indexPath.row) { case (0, 1): presenter.chooseStatus { _ in tableView.reloadRows(at: [indexPath], with: .automatic) @@ -199,6 +207,13 @@ extension AccountSettingsViewController { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + // FIXME: temp workaround, will be removed soon + if let viewModel = presenter.item(for: indexPath.section, row: indexPath.row) as? AnyCellViewModel { + let cell = tableView.dequeueReusableCell(withModel: viewModel, for: indexPath) + cell.selectionStyle = .none + return cell + } + guard let item = presenter.item(for: indexPath.section, row: indexPath.row) as? IdentityProtocol else { return UITableViewCell() } @@ -270,6 +285,8 @@ extension AccountSettingsViewController { } } +// MARK: - KeyboardInteractive + extension AccountSettingsViewController { func keyboardNotified(endFrame: CGRect) { diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift new file mode 100644 index 000000000..d62c12fbf --- /dev/null +++ b/Nynja/Modules/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift @@ -0,0 +1,63 @@ +// +// DestructiveActionTableViewCell.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class DestructiveActionTableViewCell: UITableViewCell { + + // MARK: - Views + + private(set) lazy var titleLabel: UILabel = { + let textLabel = UILabel(height: Constraints.titleLabel.fontHeight, + color: UIColor.nynja.mainRed, + font: FontFamily.NotoSans.medium) + + contentView.addSubview(textLabel) + textLabel.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(Constraints.titleLabel.left) + maker.right.equalToSuperview().inset(Constraints.titleLabel.right) + maker.centerY.equalToSuperview() + } + + return textLabel + }() + + + // MARK: - Init + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + backgroundColor = .clear + contentView.backgroundColor = .clear + titleLabel.isHidden = false + } + + + // MARK: - Layout + + private enum Constraints { + + enum titleLabel { + static let fontHeight: CGFloat = 20 + static let left: CGFloat = 16 + static let right: CGFloat = 16 + } + } +} diff --git a/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift index 21b4a6cb1..e69b4816f 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -37,6 +37,7 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco case chooseTimeout(completion: (StatusTimeout) -> Void) case addContact(completion: (Result) -> Void) case contactDetails(contact: UserContact, completion: (Result) -> Void) + case deleteAccount(accountId: String) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -80,4 +81,8 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) { coordinator.wireframe(self, didEndWithState: .contactDetails(contact: contact, completion: completion) ) } + + func deleteAccount(accountId: String) { + coordinator.wireframe(self, didEndWithState: .deleteAccount(accountId: accountId)) + } } diff --git a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift index b120bc3b6..a342dd778 100644 --- a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift @@ -8,9 +8,10 @@ import Foundation -final class AccountSettingsCoordinator: Coordinator, AccountSettingsCoordinatorProtocol { +final class AccountSettingsCoordinator: Coordinator, NavigationContainer { + + let navigation: UINavigationController! - private let navigation: UINavigationController private let serviceFactory: ServiceFactoryProtocol init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { @@ -46,24 +47,39 @@ final class AccountSettingsCoordinator: Coordinator, AccountSettingsCoordinatorP // MARK: - Account Settings -extension AccountSettingsCoordinator { +extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { + func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) { switch state { - case .back: navigation.popViewController(animated: true) + case .back: + navigation.popViewController(animated: true) + case .addContact(let completion): showAddContactPopup(completion: completion) + case .chooseAvatar(let completion): - selectAvatar(with: completion) + showAvatarSourceOptionPopup { [weak self] result in + guard case let .success(imageSource) = result else { + return + } + self?.getAvatar(source: imageSource, completion: completion) + } + case .chooseStatus(let completion): chooseStatus(completion: completion) + case .chooseTimeout(let completion): chooseTimeout(completion: completion) + case .contactDetails(let contact, let completion): break + + case let .deleteAccount(accountId): + showDeleteAccount(accountId: accountId) } } - func showAddContactPopup(completion: (Result) -> Void) { + private func showAddContactPopup(completion: (Result) -> Void) { let view = UIAlertController.init(title: "Add Contact Info".localized, message: nil, preferredStyle: .actionSheet) let phoneNumberAction = UIAlertAction(title: "Phone Number".localized, style: .default) { _ in @@ -89,7 +105,7 @@ extension AccountSettingsCoordinator { navigation.present(view, animated: true, completion: nil) } - func getAvatar(source: SelectAvatarFlowCoordinatorSource, completion: @escaping (UIImage?) -> Void) { + private func getAvatar(source: SelectAvatarFlowCoordinatorSource, completion: @escaping (UIImage?) -> Void) { let coordinatorDependencies = SelectAvatarFlowCoordinator.Dependencies( source: source, rootViewController: navigation.viewControllers.last!, @@ -103,20 +119,32 @@ extension AccountSettingsCoordinator { coordinator.start() } - func selectAvatar(with completion: @escaping (UIImage?) -> Void) { - let view = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + private func showAvatarSourceOptionPopup(completion: @escaping (Result) -> Void) { + enum AvatarSourceError: Error { + case cancelled + } + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let cameraAction = UIAlertAction(title: "Take from Camera".localized, style: .default) { [weak self] _ in self?.getAvatar(source: .camera, completion: completion) } - let galleryAction = UIAlertAction(title: "Take from Gallery".localized, style: .default) { [weak self] _ in self?.getAvatar(source: .gallery, completion: completion) } - let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) + let camera = UIAlertAction(title: String.localizable.alertActionTakeFromCamera, style: .default) { _ in + completion(.success(.camera)) + } - [cameraAction, galleryAction, cancelAction].forEach { view.addAction($0) } + let gallery = UIAlertAction(title: String.localizable.alertActionTakeFromGallery, style: .default) { _ in + completion(.success(.gallery)) + } - navigation.present(view, animated: true, completion: nil) + let cancel = UIAlertAction(title: String.localizable.cancel, style: .cancel) { _ in + completion(.failure(AvatarSourceError.cancelled)) + } + + [camera, gallery, cancel].forEach { alert.addAction($0) } + + navigation.present(alert, animated: true, completion: nil) } - func chooseStatus(completion: @escaping (UserStatus) -> Void) { - let view = UIAlertController.init(title: "Status".localized, message: nil, preferredStyle: .actionSheet) + private func chooseStatus(completion: @escaping (UserStatus) -> Void) { + let view = UIAlertController(title: "Status".localized, message: nil, preferredStyle: .actionSheet) let activeAction = UIAlertAction(title: "Active".localized, style: .default) { _ in completion(.active) @@ -130,14 +158,14 @@ extension AccountSettingsCoordinator { completion(.busy) } - let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil) [activeAction, inactiveAction, busyAction, cancelAction].forEach { view.addAction($0) } navigation.present(view, animated: true, completion: nil) } - func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { + private func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { let view = UIAlertController.init(title: "Idle Timeout".localized, message: nil, preferredStyle: .actionSheet) let timeout5MinAction = UIAlertAction(title: "5 min".localized, style: .default) { _ in @@ -160,10 +188,36 @@ extension AccountSettingsCoordinator { completion(.never) } - let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil) [timeout5MinAction, timeout15MinAction, timeout30MinAction, timeout60MinAction, timeoutNeverAction, cancelAction].forEach { view.addAction($0) } navigation.present(view, animated: true, completion: nil) } + + private func showDeleteAccount(accountId: String) { + let wireframe = DeleteAccountWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init( + accountId: accountId + ), + dependencies: .init( + accountService: serviceFactory.makeAccountService() + ) + ) + + navigation.pushViewController(view, animated: true) + } +} + +// MARK: - Delete Account + +extension AccountSettingsCoordinator: DeleteAccountCoordinatorProtocol { + + func wireframe(_ wireframe: DeleteAccountWireframe, didEndWithState state: DeleteAccountWireframe.State) { + switch state { + case .dismiss: + navigation.popViewController(animated: true) + } + } } diff --git a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift index c6324b452..3f84cd773 100644 --- a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift +++ b/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift @@ -10,7 +10,7 @@ import UIKit final class LoginOptionsCoordinator: Coordinator, NavigationContainer { - private(set) weak var navigation: UINavigationController? + let navigation: UINavigationController! private let serviceFactory: ServiceFactoryProtocol @@ -24,11 +24,11 @@ final class LoginOptionsCoordinator: Coordinator, NavigationContainer { func start() { let wireframe = LoginOptionsWireframe(coordinator: self) let view = wireframe.prepareModule() - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) } func end() { - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) } } @@ -46,7 +46,7 @@ extension LoginOptionsCoordinator: LoginOptionsCoordinatorProtocol { parameters: .init(authProvider: provider), dependencies: .init(countriesProvider: serviceFactory.makeCountriesProvider()) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) } } } @@ -58,7 +58,7 @@ extension LoginOptionsCoordinator: AuthProviderCoordinatorProtocol { func wireframe(_ wireframe: AuthProviderWireframe, didEndWithState state: AuthProviderWireframe.State) { switch state { case .dismiss: - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) case let .selectCountry(callback): selectCountryCallback = callback @@ -70,7 +70,7 @@ extension LoginOptionsCoordinator: AuthProviderCoordinatorProtocol { ) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) case let .confirmProvider(confirmationData): let wireframe = CodeConfirmationWireframe(coordinator: self) @@ -81,7 +81,7 @@ extension LoginOptionsCoordinator: AuthProviderCoordinatorProtocol { accountService: serviceFactory.makeAccountService()) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) } } } @@ -98,7 +98,7 @@ extension LoginOptionsCoordinator: CountrySelectorCoordinatorProtocol { selectCountryCallback?(.failure(NavigationError.dismissed)) } selectCountryCallback = nil - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) } } @@ -109,7 +109,7 @@ extension LoginOptionsCoordinator: CodeConfirmationCoordinatorProtocol { func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { switch state { case .back: - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) case .loggedIn: break case .registered: diff --git a/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift b/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift new file mode 100644 index 000000000..6dd5d9d07 --- /dev/null +++ b/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift @@ -0,0 +1,39 @@ +// +// DeleteAccountProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +// MARK: - Wireframe + +protocol DeleteAccountWireframeProtocol: AlertDisplayable { + func dismiss() +} + +// MARK: - View + +protocol DeleteAccountViewInput: LoadingInteractive { +} + +// MARK: - Presenter + +protocol DeleteAccountPresenterProtocol: BasePresenterProtocol, NavigationProtocol { + func deleteAccount() +} + +// MARK: - Interactor + +// MARK: Input +protocol DeleteAccountInteractorInput: class { + func deleteAccount() +} + +// MARK: Output +protocol DeleteAccountInteractorOutput: class { + func accountDidDelete() + func didReceiveFailure(_ error: Error?) +} diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift b/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift new file mode 100644 index 000000000..8df21bd4d --- /dev/null +++ b/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift @@ -0,0 +1,45 @@ +// +// DeleteAccountInteractor.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class DeleteAccountInteractor: BaseInteractor, DeleteAccountInteractorInput { + + private weak var presenter: DeleteAccountInteractorOutput? + + private let accountId: String + + + // MARK: - Services + + private let accountService: AccountService + + + // MARK: - Init + + struct Dependencies { + let presenter: DeleteAccountInteractorOutput + let accountId: String + let accountService: AccountService + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + accountId = dependencies.accountId + accountService = dependencies.accountService + } + + + // MARK: - Interactor Input + + func deleteAccount() { + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + self.presenter?.accountDidDelete() + } + } +} diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift b/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift new file mode 100644 index 000000000..cf4f7e94c --- /dev/null +++ b/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift @@ -0,0 +1,56 @@ +// +// DeleteAccountPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class DeleteAccountPresenter: BasePresenter, DeleteAccountPresenterProtocol, DeleteAccountInteractorOutput { + + private weak var view: DeleteAccountViewInput? + private var interactor: DeleteAccountInteractorInput! + private var wireframe: DeleteAccountWireframeProtocol! + + + // MARK: - Presenter + + func deleteAccount() { + view?.showLoading() + interactor.deleteAccount() + } + + func back() { + wireframe.dismiss() + } + + + // MARK: - Interactor Output + + func accountDidDelete() { + view?.hideLoading() + } + + func didReceiveFailure(_ error: Error?) { + view?.hideLoading() + } +} + +// MARK: - Injection + +extension DeleteAccountPresenter: SetInjectable { + + struct Dependencies { + let view: DeleteAccountViewInput + let interactor: DeleteAccountInteractorInput + let wireframe: DeleteAccountWireframeProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift b/Nynja/Modules/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift new file mode 100644 index 000000000..b197fc822 --- /dev/null +++ b/Nynja/Modules/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift @@ -0,0 +1,176 @@ +// +// DeleteAccountViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class DeleteAccountViewController: BaseVC, DeleteAccountViewInput, LoadingDisplayable { + + private let presenter: DeleteAccountPresenterProtocol + + + // MARK: - Views + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + private lazy var imageView: UIImageView = { + let top = Constraints.imageView.top + + let imageView = UIImageView() + imageView.image = UIImage.nynja.emptyStatesImagesImgEmptyStatesDelete.image + + view.addSubview(imageView) + imageView.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom).offset(top) + maker.left.right.equalToSuperview() + maker.height.equalTo(imageView.snp.width).multipliedBy(0.45) + } + + return imageView + }() + + private lazy var descriptionLabel: UILabel = { + let fontHeight = Constraints.descriptionLabel.fontHeight + let horizontal = Constraints.descriptionLabel.horizontal + let vertical = Constraints.descriptionLabel.vertical + + let label = UILabel(height: fontHeight, color: UIColor.nynja.white, font: FontFamily.NotoSans.regular) + + label.numberOfLines = 0 + label.text = String.localizable.deleteAccountDescriptionText + + view.addSubview(label) + label.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(horizontal) + maker.right.equalToSuperview().inset(horizontal) + maker.top.equalTo(imageView.snp.bottom).offset(vertical) + maker.bottom.equalTo(deleteButton.snp.top).offset(-vertical) + } + + return label + }() + + private lazy var gradientView: GradientView = { + let gradientHeight = Constraints.gradientView.height.adjustedByWidth + + let backgroundColor = UIColor.nynja.backgroundColor + let colors = [backgroundColor.withAlphaComponent(0), backgroundColor] + + let gradientView = GradientView(colors: colors) + gradientView.isUserInteractionEnabled = false + + view.addSubview(gradientView) + gradientView.snp.makeConstraints { maker in + maker.bottom.equalTo(deleteButton.snp.top) + maker.left.right.equalToSuperview() + maker.height.equalTo(gradientHeight) + } + + return gradientView + }() + + private lazy var deleteButton: BaseNynjaButton = { + let height = Constraints.deleteButton.height + let fontHeight = Constraints.deleteButton.fontHeight + let horizontal = Constraints.deleteButton.horizontal + let bottom = Constraints.deleteButton.bottom + + let button = BaseNynjaButton(frame: .zero, fontName: FontFamily.NotoSans.medium.name, labelHeight: fontHeight) + + button.setTitle(String.localizable.deleteAccountDeleteAccountButton, for: .normal) + button.addTarget(self, action: #selector(actionDeleteAccount(sender:)), for: .touchUpInside) + + button.layer.cornerRadius = height / 2 + + view.addSubview(button) + button.snp.makeConstraints { maker in + maker.left.equalToSuperview().offset(horizontal) + maker.right.equalToSuperview().inset(horizontal) + maker.bottom.equalTo(view.safeArea.bottom).offset(-bottom) + maker.height.equalTo(height) + } + + return button + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: DeleteAccountPresenterProtocol + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + super.init(nibName: nil, bundle: nil) + _presenter = presenter + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Life Cycle + + override func initialize() { + super.initialize() + setupUI() + } + + + // MARK: - UI Setup + + private func setupUI() { + screenTitle = String.localizable.deleteAccountScreenTitle + + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: false, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) + + _ = [imageView, descriptionLabel, deleteButton, gradientView] + } + + + // MARK: - Actions + + @objc private func actionDeleteAccount(sender: Any) { + presenter.deleteAccount() + } + + + // MARK: - Layout + + private enum Constraints { + + enum imageView { + static let top: CGFloat = CGFloat(32).adjustedByWidth + } + + enum descriptionLabel { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + static let vertical: CGFloat = CGFloat(16).adjustedByWidth + } + + enum gradientView { + static let height: CGFloat = CGFloat(29).adjustedByWidth + } + + enum deleteButton { + static let height: CGFloat = CGFloat(44).adjustedByWidth + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + static let bottom: CGFloat = CGFloat(28).adjustedByWidth + } + } +} diff --git a/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift b/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift new file mode 100644 index 000000000..27f381eca --- /dev/null +++ b/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift @@ -0,0 +1,60 @@ +// +// DeleteAccountWireframe.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import UIKit.UIViewController +import NynjaUIKit + +protocol DeleteAccountCoordinatorProtocol: AlertDisplayable { + func wireframe(_ wireframe: DeleteAccountWireframe, didEndWithState state: DeleteAccountWireframe.State) +} + +final class DeleteAccountWireframe: Wireframe, DeleteAccountWireframeProtocol { + + private let coordinator: DeleteAccountCoordinatorProtocol + + init(coordinator: DeleteAccountCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let accountId: String + } + + struct Dependencies { + let accountService: AccountService + } + + enum State { + case dismiss + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = DeleteAccountPresenter() + + let view = DeleteAccountViewController(dependencies: .init(presenter: presenter)) + + let interactor = DeleteAccountInteractor(dependencies: .init( + presenter: presenter, + accountId: parameters.accountId, + accountService: dependencies.accountService) + ) + + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + + return view + } + + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } +} diff --git a/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift b/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift index 68eef8aa2..215c59dd4 100644 --- a/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift +++ b/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift @@ -7,18 +7,18 @@ // import UIKit +import NynjaUIKit // MARK: - Wireframe -protocol LoginOptionsWireframeProtocol: class { +protocol LoginOptionsWireframeProtocol: AlertDisplayable { func dismiss() - func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]) func addAuthProvider(ofType provider: AuthProvider) } // MARK: - View -protocol LoginOptionsViewInput: class where Self: UIViewController { +protocol LoginOptionsViewInput: class { func setup(form: Form) func removeItem(at index: Int) } diff --git a/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift b/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift index fcdfa46be..9974373c4 100644 --- a/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift +++ b/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit final class LoginOptionsPresenter: BasePresenter, LoginOptionsPresenterProtocol, LoginOptionsInteractorOutput { @@ -104,32 +105,31 @@ final class LoginOptionsPresenter: BasePresenter, LoginOptionsPresenterProtocol, } private func handleLongPress(on loginOption: LoginOption) { - let actions: [UIAlertAction] = [ - UIAlertAction(title: String.localizable.delete, style: .destructive) { [weak self] _ in + let actions: [Alert.Action] = [ + Alert.Action(title: String.localizable.delete, style: .destructive) { [weak self] _ in self?.delete(loginOption) }, - UIAlertAction(title: String.localizable.cancel, style: .cancel, handler: nil) + Alert.Action(title: String.localizable.cancel, style: .cancel, handler: nil) ] - wireframe.presentAlert(title: nil, message: nil, style: .actionSheet, actions: actions) + let alert = Alert(style: .actionSheet, actions: actions) + wireframe.present(alert) } // MARK: - Actions private func addLoginOption() { - let actions: [UIAlertAction] = [ - UIAlertAction(title: String.localizable.loginOptionsPhoneNumberOptionActionText, style: .default) { [weak self] _ in + let actions: [Alert.Action] = [ + Alert.Action(title: String.localizable.loginOptionsPhoneNumberOptionActionText, style: .default) { [weak self] _ in self?.wireframe.addAuthProvider(ofType: .phoneNumber) }, - UIAlertAction(title: String.localizable.loginOptionsEmailOptionActionText, style: .default) { [weak self] _ in + Alert.Action(title: String.localizable.loginOptionsEmailOptionActionText, style: .default) { [weak self] _ in self?.wireframe.addAuthProvider(ofType: .email) }, - UIAlertAction(title: String.localizable.cancel, style: .cancel, handler: nil) + Alert.Action(title: String.localizable.cancel, style: .cancel, handler: nil) ] - wireframe.presentAlert(title: String.localizable.loginOptionsAddAlertTitle, - message: nil, - style: .actionSheet, - actions: actions) + let alert = Alert(title: String.localizable.loginOptionsAddAlertTitle, style: .actionSheet, actions: actions) + wireframe.present(alert) } private func toggle(_ loginOption: LoginOption, isOn: Bool) { @@ -137,16 +137,14 @@ final class LoginOptionsPresenter: BasePresenter, LoginOptionsPresenterProtocol, } private func delete(_ loginOption: LoginOption) { - let actions: [UIAlertAction] = [ - UIAlertAction(title: String.localizable.no, style: .default, handler: nil), - UIAlertAction(title: String.localizable.yes, style: .default) { [weak self] _ in + let actions: [Alert.Action] = [ + Alert.Action(title: String.localizable.no, style: .default, handler: nil), + Alert.Action(title: String.localizable.yes, style: .default) { [weak self] _ in self?.interactor.delete(loginOption) } ] - wireframe.presentAlert(title: nil, - message: String.localizable.loginOptionsDeleteOptionAlertMessage, - style: .alert, - actions: actions) + let alert = Alert(message: String.localizable.loginOptionsDeleteOptionAlertMessage, style: .alert, actions: actions) + wireframe.present(alert) } diff --git a/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift b/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift index 64299f920..04a7d0054 100644 --- a/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift +++ b/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift @@ -8,13 +8,14 @@ import Foundation import UIKit.UIViewController +import NynjaUIKit protocol LoginOptionsCoordinatorProtocol: AlertDisplayable { func wireframe(_ wireframe: LoginOptionsWireframe, didEndWithState state: LoginOptionsWireframe.State) } final class LoginOptionsWireframe: Wireframe, LoginOptionsWireframeProtocol { - + private let coordinator: LoginOptionsCoordinatorProtocol init(coordinator: LoginOptionsCoordinatorProtocol) { @@ -40,12 +41,12 @@ final class LoginOptionsWireframe: Wireframe, LoginOptionsWireframeProtocol { return view } - func dismiss() { - coordinator.wireframe(self, didEndWithState: .dismiss) + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) } - func presentAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]) { - coordinator.presentAlert(title: title, message: message, style: style, actions: actions) + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) } func addAuthProvider(ofType provider: AuthProvider) { diff --git a/Nynja/Modules/Auth Flow/AppCoordinator.swift b/Nynja/Modules/Auth Flow/AppCoordinator.swift index 53835e210..6d772ccc4 100644 --- a/Nynja/Modules/Auth Flow/AppCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AppCoordinator.swift @@ -23,14 +23,14 @@ final class AppCoordinator: Coordinator { } func start() { - let storage = StorageService.sharedInstance - if let passcode = storage.identityId { - storage.setupDatabase(with: passcode, application: UIApplication.shared) - } +// let storage = StorageService.sharedInstance +// if let passcode = storage.identityId { +// storage.setupDatabase(with: passcode, application: UIApplication.shared) +// } let authCoordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) authCoordinator.delegate = self - authCoordinator.end() + authCoordinator.start() } func end() { diff --git a/Nynja/Modules/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Auth Flow/AuthCoordinator.swift index 1a1a870da..3885fd468 100644 --- a/Nynja/Modules/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AuthCoordinator.swift @@ -17,7 +17,7 @@ final class AuthCoordinator: Coordinator, NavigationContainer { weak var delegate: AuthCoordinatorDelegate? - private(set) weak var navigation: UINavigationController? + let navigation: UINavigationController! private let serviceFactory: ServiceFactoryProtocol @@ -39,7 +39,7 @@ final class AuthCoordinator: Coordinator, NavigationContainer { googleAuthService: serviceFactory.makeGoogleAuthService(), countriesProvider: serviceFactory.makeCountriesProvider()) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) } func end() { @@ -69,17 +69,17 @@ extension AuthCoordinator: AuthCoordinatorProtocol { ) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) case let .showFacebookAuth(callback): let wireframe = FacebookAuthWireframe(coordinator: self) let view = wireframe.prepareModule() facebookAuthCodeCallback = callback - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) case let .present(viewController): - navigation?.present(viewController, animated: true) + navigation.present(viewController, animated: true) case let .dismiss(viewController): viewController.dismiss(animated: true) @@ -98,7 +98,7 @@ extension AuthCoordinator: AuthCoordinatorProtocol { accountService: serviceFactory.makeAccountService() ) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) case let .phoneNumber(numberInfo): let wireframe = CodeConfirmationWireframe(coordinator: self) @@ -110,7 +110,7 @@ extension AuthCoordinator: AuthCoordinatorProtocol { accountService: serviceFactory.makeAccountService() ) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) case .google: break @@ -136,7 +136,7 @@ extension AuthCoordinator: AuthCoordinatorProtocol { [modify, confirm].forEach { popup.addAction($0) } - navigation?.present(popup, animated: true, completion: nil) + navigation.present(popup, animated: true, completion: nil) } private func titleForPopup(loginOption: PlainLoginOption) -> String { @@ -170,7 +170,7 @@ extension AuthCoordinator: FacebookAuthCoordinatorProtocol { facebookAuthCodeCallback?(.failure(NavigationError.dismissed)) } facebookAuthCodeCallback = nil - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) } } @@ -185,7 +185,7 @@ extension AuthCoordinator: CountrySelectorCoordinatorProtocol { selectCountryCallback?(.failure(NavigationError.dismissed)) } selectCountryCallback = nil - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) } } @@ -195,7 +195,7 @@ extension AuthCoordinator: CodeConfirmationCoordinatorProtocol { func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { switch state { case .back: - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) case .loggedIn: end() case let .registered(accountId): @@ -209,7 +209,7 @@ extension AuthCoordinator: CodeConfirmationCoordinatorProtocol { accountService: serviceFactory.makeAccountService() ) ) - navigation?.pushViewController(view, animated: true) + navigation.pushViewController(view, animated: true) } } } @@ -220,32 +220,59 @@ extension AuthCoordinator: CreateProfileCoordinatorProtocol { func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) { switch state { case .back: - if let authViewController = navigation?.viewControllers.first(where: { $0 is AuthViewController }) { - navigation?.popToViewController(authViewController, animated: true) + if let authViewController = navigation.viewControllers.first(where: { $0 is AuthViewController }) { + navigation.popToViewController(authViewController, animated: true) } else { - navigation?.popViewController(animated: true) + navigation.popViewController(animated: true) } case .next: end() case let .chooseAvatar(completion): - guard let navigation = navigation else { return } + guard let rootViewController = navigation.viewControllers.last else { return } - let dependencies = SelectAvatarFlowCoordinator.Dependencies( - source: .gallery, - rootViewController: navigation.viewControllers.last!, - serviceFactory: serviceFactory) { url in - completion(UIImage.sd_image(with: try? Data(contentsOf: url))) + showAvatarSourceOptionPopup { result in + guard case let .success(imageSource) = result else { + return + } + let chooseAvatarCoordinator = SelectAvatarFlowCoordinator(dependencies: + .init( + source: imageSource, + rootViewController: rootViewController, + serviceFactory: self.serviceFactory, + completion: { url in completion(UIImage.sd_image(with: try? Data(contentsOf: url))) } + ) + ) + chooseAvatarCoordinator.start() } - let chooseAvatarCoordinator = SelectAvatarFlowCoordinator(dependencies: dependencies) - chooseAvatarCoordinator.start() case let .openTerms(url): - guard let navigation = navigation else { return } - WebFullScreenWireFrame().presentWebFullScreen(navigation: navigation, title: String.localizable.createProfileTermsOfUse.uppercased(), inputURL: url) } } + + private func showAvatarSourceOptionPopup(completion: @escaping (Result) -> Void) { + enum AvatarSourceError: Error { + case cancelled + } + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + let camera = UIAlertAction(title: String.localizable.alertActionTakeFromCamera, style: .default) { _ in + completion(.success(.camera)) + } + + let gallery = UIAlertAction(title: String.localizable.alertActionTakeFromGallery, style: .default) { _ in + completion(.success(.gallery)) + } + + let cancel = UIAlertAction(title: String.localizable.cancel, style: .cancel) { _ in + completion(.failure(AvatarSourceError.cancelled)) + } + + [camera, gallery, cancel].forEach { alert.addAction($0) } + + navigation.present(alert, animated: true, completion: nil) + } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift index 0de01ec3b..27bc057f4 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift @@ -7,10 +7,11 @@ // import Foundation +import NynjaUIKit // MARK: - Wireframe -protocol AuthWireframeProtocol: class { +protocol AuthWireframeProtocol: AlertDisplayable { func selectCountry(completion: @escaping (Result) -> Void) func confirmInputData(loginOption: PlainLoginOption, confirmationHandler: @escaping (Bool) -> Void) func continueLogin(loginFlow: LoginFlow) @@ -18,7 +19,6 @@ protocol AuthWireframeProtocol: class { func present(_ viewController: UIViewController) func dismiss(_ viewController: UIViewController) - func presentAlert(title: String, message: String, actions: [UIAlertAction]) } // MARK: - View diff --git a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift index e7e7fe229..62226f450 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAuthServiceUIDelegate { private weak var view: AuthViewInput? @@ -111,10 +112,11 @@ extension AuthPresenter { func didReceiveAuthenticationFailure(_ error: Error?) { view?.hideLoading() - let actions = [UIAlertAction(title: "OK", style: .default, handler: nil)] - wireframe.presentAlert(title: "Failure", - message: error?.localizedDescription ?? "Something went wrong", - actions: actions) + + let actions = [Alert.Action(title: "OK", style: .default)] + let alert = Alert(title: "Failure", message: error?.localizedDescription ?? "Something went wrong", actions: actions) + + wireframe.present(alert) } } diff --git a/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift index 9ad671d96..92ed6c922 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift @@ -8,13 +8,14 @@ import Foundation import UIKit.UIViewController +import NynjaUIKit protocol AuthCoordinatorProtocol: AlertDisplayable { func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) } final class AuthWireframe: Wireframe, AuthWireframeProtocol { - + private let coordinator: AuthCoordinatorProtocol init(coordinator: AuthCoordinatorProtocol) { @@ -53,6 +54,10 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { return view } + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) + } + func selectCountry(completion: @escaping (Result) -> Void) { coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) } @@ -77,8 +82,4 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { func dismiss(_ viewController: UIViewController) { coordinator.wireframe(self, didEndWithState: .dismiss(viewController)) } - - func presentAlert(title: String, message: String, actions: [UIAlertAction]) { - coordinator.presentAlert(title: title, message: message, actions: actions) - } } diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift index 19b49b242..bf905fb65 100644 --- a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift +++ b/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -33,9 +33,9 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi }() private lazy var codeInputView: SecureCodeContainerView = { - let titleFontHeight = Constraints.titleLabel.fontHeight.adjustedByWidth - let textFontHeight = Constraints.codeInputView.fontHeight.adjustedByWidth - let descriptionFontHeight = Constraints.descriptionLabel.fontHeight.adjustedByWidth + let titleFontHeight = Constraints.titleLabel.fontHeight + let textFontHeight = Constraints.codeInputView.fontHeight + let descriptionFontHeight = Constraints.descriptionLabel.fontHeight let codeInputView = SecureCodeContainerView() codeInputView.appearance = SecureCodeContainerView.Appearance( @@ -50,7 +50,7 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi view.addSubview(codeInputView) codeInputView.snp.makeConstraints { maker in - maker.top.equalTo(headerView.snp.bottom).offset(Constraints.codeInputView.top.adjustedByWidth) + maker.top.equalTo(headerView.snp.bottom).offset(Constraints.codeInputView.top) maker.centerX.equalToSuperview() maker.left.greaterThanOrEqualToSuperview() maker.right.lessThanOrEqualToSuperview() @@ -95,6 +95,8 @@ final class CodeConfirmationViewController: UIViewController, CodeConfirmationVi _ = [headerView, codeInputView, timerLabel, resendCodeButton, callMeButton, backButton] view.backgroundColor = UIColor.nynja.backgroundColor + headerView.isHidden = !isLogoVisible + codeInputView.titleLabel.text = presenter.address codeInputView.descriptionLabel.text = presenter.descriptionText @@ -179,12 +181,14 @@ private extension CodeConfirmationViewController { } func makeTimerLabel(on view: UIView) -> UILabel { + let fontHeight = Constraints.timerLabel.fontHeight + let label = UILabel() view.addSubview(label) label.textAlignment = .center label.textColor = UIColor.nynja.white - label.font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 16.0.adjustedByWidth) + label.font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: fontHeight) label.snp.makeConstraints { (make) in make.centerX.equalToSuperview() @@ -196,13 +200,15 @@ private extension CodeConfirmationViewController { } func makeResendCodeButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { + let fontHeight = Constraints.resendButton.fontHeight + let button = UIButton() view.addSubview(button) button.setTitle(String.localizable.codeConfirmationResendCode, for: .normal) button.setTitleColor(UIColor.nynja.mainRed, for: .normal) button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: fontHeight) button.addTarget(target, action: selector, for: .touchUpInside) button.snp.makeConstraints { (make) in @@ -214,13 +220,15 @@ private extension CodeConfirmationViewController { } func makeCallMeButton(on view: UIView, top: UIView, target: AnyObject, selector: Selector) -> UIButton { + let fontHeight = Constraints.callButton.fontHeight + let button = UIButton() view.addSubview(button) button.setTitle(String.localizable.codeConfirmationCall, for: .normal) button.setTitleColor(UIColor.nynja.mainRed, for: .normal) button.setTitleColor(UIColor.nynja.darkRed, for: .highlighted) - button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 16.0.adjustedByWidth) + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: fontHeight) button.addTarget(target, action: selector, for: .touchUpInside) button.snp.makeConstraints { (make) in @@ -231,6 +239,7 @@ private extension CodeConfirmationViewController { return button } + // FIXME: add adjust if needed enum Constraints { enum titleLabel { @@ -242,8 +251,20 @@ private extension CodeConfirmationViewController { } enum codeInputView { - static let top: CGFloat = 10.0 + static let top: CGFloat = CGFloat(10.0).adjustedByWidth static let fontHeight: CGFloat = 22.0 } + + enum timerLabel { + static let fontHeight: CGFloat = 22 + } + + enum resendButton { + static let fontHeight: CGFloat = 22 + } + + enum callButton { + static let fontHeight: CGFloat = 22 + } } } diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift b/Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift index 01a5e19b5..ef3cce061 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift @@ -23,11 +23,16 @@ enum ProfileField { var placeholder: String { switch self { - case .firstName: return "First Name" - case .lastName: return "Last Name" - case .accountName: return "Account Name" - case .userName: return "Username" - case .profileMessage: return "Profile Message" + case .firstName: + return String.localizable.createProfileFirstNameFieldPlaceholder + case .lastName: + return String.localizable.createProfileLastNameFieldPlaceholder + case .accountName: + return String.localizable.createProfileAccountNameFieldPlaceholder + case .userName: + return String.localizable.createProfileUsernameFieldPlaceholder + case .profileMessage: + return String.localizable.createProfileProfileMessageFieldPlaceholder } } } diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift index ce4bd1704..2fa1fdc6e 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift @@ -8,18 +8,41 @@ import Foundation -final class CreateProfileViewController: UIViewController, CreateProfileViewInput, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { +final class CreateProfileViewController: BaseVC, CreateProfileViewInput, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { private let presenter: CreateProfilePresenterProtocol private let viewsFactory: CreateProfileViewsFactoryProtocol + // MARK: - Views + private(set) lazy var progressHUD = makeProgressHUD(on: view) - private lazy var topHeaderLayoutGuide: UILayoutGuide = viewsFactory.makeTopLayoutGuide(on: view) - private lazy var headerView: NavigationView = viewsFactory.makeHeaderView(on: view, topLayoutGuide: topHeaderLayoutGuide, navigationHandler: presenter) - private lazy var createButton: UIButton = viewsFactory.makeCreateButton(on: view, target: self, selector: #selector(createAccount(sender:))) - private lazy var container: UIView = viewsFactory.makeContainer(on: view, headerView: headerView, footerView: createButton) - private lazy var scrollView: UIScrollView = viewsFactory.makeScrollView(on: container) - private lazy var contentContainer: CreateProfileContentView = viewsFactory.makeContentContainer(on: scrollView, widthView: view, presenter: presenter) + private lazy var container = viewsFactory.makeContainer(on: view, headerView: navigationView, footerView: createButton) + private lazy var scrollView = viewsFactory.makeScrollView(on: container) + private lazy var contentContainer = viewsFactory.makeContentContainer(on: scrollView, widthView: view, presenter: presenter) + + private lazy var createButton = viewsFactory.makeCreateButton(on: view, target: self, selector: #selector(createAccount(sender:))) + + private lazy var gradientView: GradientView = { + let gradientHeight = 29.0.adjustedByWidth + + let backgroundColor = UIColor.nynja.backgroundColor + let colors = [backgroundColor.withAlphaComponent(0), backgroundColor] + + let gradientView = GradientView(colors: colors) + gradientView.isUserInteractionEnabled = false + + view.addSubview(gradientView) + gradientView.snp.makeConstraints { maker in + maker.bottom.equalTo(createButton.snp.top) + maker.left.right.equalToSuperview() + maker.height.equalTo(gradientHeight) + } + + return gradientView + }() + + + // MARK: - Init struct Dependencies { let presenter: CreateProfilePresenterProtocol @@ -29,7 +52,6 @@ final class CreateProfileViewController: UIViewController, CreateProfileViewInpu init(dependencies: Dependencies) { presenter = dependencies.presenter viewsFactory = dependencies.viewsFactory - super.init(nibName: nil, bundle: nil) } @@ -37,15 +59,12 @@ final class CreateProfileViewController: UIViewController, CreateProfileViewInpu fatalError("init(coder:) has not been implemented") } + + // MARK: - Life Cycle + override func viewDidLoad() { super.viewDidLoad() - - view.backgroundColor = UIColor.nynja.darkLight - - _ = [topHeaderLayoutGuide, headerView, createButton, container, scrollView, contentContainer] - - contentContainer.termsOfUseTextView.delegate = self - + setupUI() enableKeyboardHidingWhenTappedAround() } @@ -59,8 +78,23 @@ final class CreateProfileViewController: UIViewController, CreateProfileViewInpu unregisterForKeyboardNotifications() } - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent + + // MARK: - UI Setup + + private func setupUI() { + screenTitle = String.localizable.createProfileScreenTitle + + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) + + _ = [container, scrollView, contentContainer, createButton, gradientView] + + contentContainer.termsOfUseTextView.delegate = self } } @@ -104,9 +138,10 @@ extension CreateProfileViewController: UITextViewDelegate { } } -//MARK: - Actions +// MARK: - Actions -extension CreateProfileViewController { +private extension CreateProfileViewController { + @objc func createAccount(sender: UIButton) { presenter.createAccount() } diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift index e4fa858e3..f303d4cd2 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift @@ -41,15 +41,9 @@ final class CreateProfileContentView: UIView, Configurable { textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - private(set) lazy var accountNameTextField = viewsFactory.makeAccountNameTextField( - on: self, - top: lastNameTextField, - textChangedHandler: textChangedHandler, - shouldChangeTextHandler: shouldChangeTextHandler) - private(set) lazy var usernameTextField = viewsFactory.makeUsernameTextField( on: self, - top: accountNameTextField, + top: lastNameTextField, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) @@ -81,7 +75,7 @@ extension CreateProfileContentView { isValidAction = config.isValidAction textChangedAction = config.textChangedAction - _ = [avatarbutton, firstNameTextField, lastNameTextField, accountNameTextField, usernameTextField, descriptionLabel, checkBoxContainerView, checkButton, termsOfUseTextView] + _ = [avatarbutton, firstNameTextField, lastNameTextField, usernameTextField, descriptionLabel, checkBoxContainerView, checkButton, termsOfUseTextView] } } @@ -90,17 +84,22 @@ extension CreateProfileContentView { extension CreateProfileContentView { func textFieldValueDidChange(value: String, field: ProfileField) { switch field { - case .firstName: firstNameTextField.text = value - case .lastName: lastNameTextField.text = value - case .accountName: accountNameTextField.text = value - case .userName: usernameTextField.text = value - case .profileMessage: break + case .firstName: + firstNameTextField.text = value + case .lastName: + lastNameTextField.text = value + case .accountName: +// accountNameTextField.text = value + break + case .userName: + usernameTextField.text = value + case .profileMessage: + break } } func updateAvatar(with image: UIImage?) { - let avatar = image ?? UIImage(named: "ic_empty_avatar") - + let avatar = image ?? UIImage.nynja.icEmptyAvatar.image avatarbutton.setImage(avatar, for: .normal) } } @@ -118,7 +117,7 @@ extension CreateProfileContentView { } let imageForSender = result - ? UIImage(named: "table_overrides_right_overrides_checkbox_ic_unchecked") + ? UIImage.nynja.tableOverridesRightOverridesCheckboxIcUnchecked.image : nil sender.setImage(imageForSender, for: .normal) diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift index 40f2879ac..6c4070bf1 100644 --- a/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift +++ b/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift @@ -14,8 +14,6 @@ protocol CreateProfileViewsFactoryProtocol { // MARK: - CreateProfileViewController - func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide - func makeHeaderView(on view: UIView, topLayoutGuide: UILayoutGuide, navigationHandler: NavigationProtocol) -> NavigationView func makeCreateButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton func makeContainer(on view: UIView, headerView: UIView, footerView: UIView) -> UIView func makeScrollView(on view: UIView) -> UIScrollView @@ -26,7 +24,6 @@ protocol CreateProfileViewsFactoryProtocol { func makeAvatarButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton func makeFirstNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField func makeLastNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField - func makeAccountNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField func makeUsernameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel func makeCheckBoxContainerView(on view: UIView, top: UIView) -> UIView @@ -38,32 +35,6 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { // MARK: - CreateProfileViewController - func makeTopLayoutGuide(on view: UIView) -> UILayoutGuide { - let layoutGuide = UILayoutGuide() - view.addLayoutGuide(layoutGuide) - - layoutGuide.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.height.equalTo(UIWindow.safeAreaTopPadding()) - } - - return layoutGuide - } - - func makeHeaderView(on view: UIView, topLayoutGuide: UILayoutGuide, navigationHandler: NavigationProtocol) -> NavigationView { - let navigationView = UIView.makeHeaderView( - on: view, - top: topLayoutGuide, - config: NavigationView.Config( - isVisibleSeparator: true, - isVisibleBackButton: true, - title: "Create profile".localized.uppercased(), - navigationHandler: navigationHandler, - backButtonImage: UIImage.nynja.icBackNavigation.image)) - - return navigationView - } - func makeCreateButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { let button = UIButton() view.addSubview(button) @@ -71,7 +42,7 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.mainRed), for: .normal) button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.darkRed), for: .disabled) - button.setTitle("create".localized.uppercased(), for: .normal) + button.setTitle(String.localizable.createProfileCreateButton, for: .normal) button.setTitleColor(UIColor.nynja.white, for: .normal) button.setTitleColor(UIColor.nynja.gray, for: .disabled) @@ -96,10 +67,9 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { let container = UIView() view.addSubview(container) + container.backgroundColor = UIColor.nynja.clear container.clipsToBounds = true - container.backgroundColor = UIColor.nynja.darkLight - container.snp.makeConstraints { (make) in make.top.equalTo(headerView.snp.bottom) make.left.right.equalToSuperview() @@ -151,7 +121,7 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { let button = UIButton() view.addSubview(button) - button.setImage(UIImage(named: "ic_empty_avatar"), for: .normal) + button.setImage(UIImage.nynja.icEmptyAvatar.image, for: .normal) button.addTarget(target, action: selector, for: .touchUpInside) button.layer.cornerRadius = 47 @@ -174,10 +144,6 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { return makeTextField(fieldType: .lastName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) } - func makeAccountNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { - return makeTextField(fieldType: .accountName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - } - func makeUsernameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { return makeTextField(fieldType: .userName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) } @@ -186,10 +152,7 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { let label = UILabel() view.addSubview(label) - label.text = - "You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.".localized - + "\n" - + "You can use a-z, 0-9 and underscores. Minimum lenght is 2 characters.".localized + label.text = String.localizable.createProfileTermsHint label.font = FontFamily.NotoSans.regular.font(size: 14) label.textColor = UIColor.nynja.dustyGray @@ -248,7 +211,7 @@ final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { textView.dataDetectorTypes = .link textView.isEditable = false - let beginOfStr = NSMutableAttributedString(string: "I agree at".localized) + let beginOfStr = NSMutableAttributedString(string: String.localizable.createProfileAgreeAtTerms) beginOfStr.addAttributes([.foregroundColor : UIColor.nynja.dustyGray, .font: FontFamily.NotoSans.regular.font(size: 14)], range: NSMakeRange(0, beginOfStr.length)) @@ -285,7 +248,7 @@ private extension CreateProfileViewsFactory { let textField = MaterialTextField() view.addSubview(textField) - textField.placeholder = fieldType.placeholder.localized + (fieldType.isRequired ? "*" : "") + textField.placeholder = fieldType.placeholder + (fieldType.isRequired ? "*" : "") textField.snp.makeConstraints { (make) in make.left.right.equalToSuperview().inset(16) diff --git a/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Contents.json new file mode 100644 index 000000000..5fff51125 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Empty_States_Images_img_empty_states_delete.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Empty_States_Images_img_empty_states_delete.pdf b/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Empty_States_Images_img_empty_states_delete.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b3d045f2dd1e5a9788fda95a2d9edd8d8e260ed0 GIT binary patch literal 9737 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 Date: Wed, 5 Dec 2018 17:18:01 +0200 Subject: [PATCH 127/138] Update SDK version --- .../Account/Service/AccountServiceImpl.swift | 22 ++++++++++++++++--- Podfile | 2 +- Podfile.lock | 8 +++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Nynja/SDK/Account/Service/AccountServiceImpl.swift b/Nynja/SDK/Account/Service/AccountServiceImpl.swift index ddb8cc3be..16b6e6cd8 100644 --- a/Nynja/SDK/Account/Service/AccountServiceImpl.swift +++ b/Nynja/SDK/Account/Service/AccountServiceImpl.swift @@ -257,7 +257,7 @@ extension AccountServiceImpl { } } - public func addAuthenticationProviderToProfileDidFinish(withStatus status: String, withError error: Error?) { + func addAuthenticationProviderToProfileDidFinish(withStatus status: String, withError error: Error?, withProfileId profileId: String) { handleResponse(nil, to: \AccountServiceImpl.addAuthProviderToProfileCompletion) { completion in if let error = error { completion?(.failure(error)) @@ -313,7 +313,7 @@ extension AccountServiceImpl { } } - public func deleteAccountDidFinish(withStatus status: String, withError error: Error?) { + func deleteAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { handleResponse(nil, to: \AccountServiceImpl.deleteAccountCompletion) { completion in if let error = error { completion?(.failure(error)) @@ -360,7 +360,7 @@ extension AccountServiceImpl { // MARK: Account's Contact Info - public func addContactInfoToAccountDidFinish(withStatus status: String, withError error: Error?) { + func addContactInfoToAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { handleResponse(nil, to: \AccountServiceImpl.addContactInfoCompletion) { completion in if let error = error { completion?(.failure(error)) @@ -388,4 +388,20 @@ extension AccountServiceImpl { func searchByQrCodeDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { } + + func searchByEmailDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + + } + + func searchByUsernameDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + + } + + func getAccountByUsernameDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + + } + + func getAccountByQrCodeDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + + } } diff --git a/Podfile b/Podfile index caa3c344c..7bd55a6f3 100644 --- a/Podfile +++ b/Podfile @@ -40,7 +40,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.6' # pod 'NynjaSDK', '= 1.8' - pod 'NynjaSDK-MultiAcc', '= 0.5.6.2' + pod 'NynjaSDK-MultiAcc', '= 0.5.6.3' pod 'CryptoSwift', '= 0.13.0' diff --git a/Podfile.lock b/Podfile.lock index 31cfc2776..ceedf7794 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -91,7 +91,7 @@ PODS: - MQTTClient/Min - SocketRocket - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.6.2) + - NynjaSDK-MultiAcc (0.5.6.3) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -126,7 +126,7 @@ DEPENDENCIES: - MaterialComponents/FlexibleHeader (= 55.3.0) - MQTTClient/Websocket (= 0.15.2) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.6.2) + - NynjaSDK-MultiAcc (= 0.5.6.3) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.2.0) @@ -215,7 +215,7 @@ SPEC CHECKSUMS: MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MQTTClient: 902c7bcac1501595f3d0b15178c7205b40331fb0 MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: 8f5abbb87a6094d330f6edd9e2ee1d08f0a9cab3 + NynjaSDK-MultiAcc: 825314ad71cf1b39ea79ec9015d4768f51fa8b90 QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a @@ -224,6 +224,6 @@ SPEC CHECKSUMS: SwiftyJSON: c4bcba26dd9ec7a027fc8eade48e2c911f229e96 TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: 057b2c6d4c015b6059fc2cfa7c801d95630a6aa1 +PODFILE CHECKSUM: d79872a5a9599e7527c52e75f89eafbbff8ef0c1 COCOAPODS: 1.5.3 -- GitLab From 925f946fb09e64188b4de660411f95a4445489bb Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 6 Dec 2018 12:12:18 +0200 Subject: [PATCH 128/138] [NY-5180] SDK integration for delete account screen --- Nynja.xcodeproj/project.pbxproj | 12 ++++++++ Nynja/Generated/LocalizableConstants.swift | 6 ++++ .../AccountSettingsProtocols.swift | 3 +- .../AccountSettingsInteractor.swift | 4 +++ .../Presenter/AccountSettingsPresenter.swift | 4 +-- .../Wireframe/AccountSettingsWireframe.swift | 8 +++-- .../AccountSettingsCoordinator.swift | 30 +++++++++++++++---- .../DeleteAccountProtocols.swift | 1 + .../Entities/DeleteAccountErrors.swift | 13 ++++++++ .../Interactor/DeleteAccountInteractor.swift | 17 +++++++++-- .../Presenter/DeleteAccountPresenter.swift | 29 ++++++++++++++++-- .../WIreframe/DeleteAccountWireframe.swift | 7 +++++ Nynja/Resources/en.lproj/Localizable.strings | 3 ++ 13 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 Nynja/Modules/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index e0786dc1b..4d2c7844c 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -473,6 +473,7 @@ 38182BD2C2E0C783796C8AA1 /* QRCodeReaderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D247CBC45C1C1267BBBB289 /* QRCodeReaderInteractor.swift */; }; 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */; }; 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0281F61F53794800206871 /* UIViewExtenstions.swift */; }; + 3A0A4C5B21B91D9000BA0D09 /* DeleteAccountErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A4C5A21B91D9000BA0D09 /* DeleteAccountErrors.swift */; }; 3A0A50CB21B7FEFE0052D334 /* MyGroupAliasPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50A821B7FEFE0052D334 /* MyGroupAliasPresenter.swift */; }; 3A0A50CC21B7FEFE0052D334 /* MyGroupAliasWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50AA21B7FEFE0052D334 /* MyGroupAliasWireframe.swift */; }; 3A0A50CD21B7FEFE0052D334 /* MyGroupAliasProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50AB21B7FEFE0052D334 /* MyGroupAliasProtocols.swift */; }; @@ -2916,6 +2917,7 @@ 35F2DA601F73CAD400777920 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerViewController.swift; sourceTree = ""; }; 3A0281F61F53794800206871 /* UIViewExtenstions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtenstions.swift; sourceTree = ""; }; + 3A0A4C5A21B91D9000BA0D09 /* DeleteAccountErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountErrors.swift; sourceTree = ""; }; 3A0A50A821B7FEFE0052D334 /* MyGroupAliasPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyGroupAliasPresenter.swift; sourceTree = ""; }; 3A0A50AA21B7FEFE0052D334 /* MyGroupAliasWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyGroupAliasWireframe.swift; sourceTree = ""; }; 3A0A50AB21B7FEFE0052D334 /* MyGroupAliasProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyGroupAliasProtocols.swift; sourceTree = ""; }; @@ -6206,6 +6208,14 @@ path = Model; sourceTree = ""; }; + 3A0A4C5921B91D7600BA0D09 /* Entities */ = { + isa = PBXGroup; + children = ( + 3A0A4C5A21B91D9000BA0D09 /* DeleteAccountErrors.swift */, + ); + path = Entities; + sourceTree = ""; + }; 3A0A50A521B7FEFE0052D334 /* CreateGroupFlow */ = { isa = PBXGroup; children = ( @@ -6802,6 +6812,7 @@ 3AE2F99421B6B5C60068C3BC /* Presenter */, 3AE2F99521B6B5D10068C3BC /* Interactor */, 3AE2F99621B6B5EE0068C3BC /* WIreframe */, + 3A0A4C5921B91D7600BA0D09 /* Entities */, ); path = DeleteAccount; sourceTree = ""; @@ -17111,6 +17122,7 @@ E7ABD3011FC2EF3800E233F7 /* StarTable.swift in Sources */, A43B25A220AB1DFA00FF8107 /* EditField.swift in Sources */, A45F114020B4218D00F45004 /* PresenceStatusProvider.swift in Sources */, + 3A0A4C5B21B91D9000BA0D09 /* DeleteAccountErrors.swift in Sources */, A49E6C4420D9A812007D85F5 /* MainViewController+Recents.swift in Sources */, 4B8996E6204EEC6300DCB183 /* MessageDAO.swift in Sources */, 32868DDF1F31CB6D0028B260 /* ChatsListInteractor.swift in Sources */, diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 86c1e209b..6c36e0ee5 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1592,6 +1592,12 @@ internal extension String { static var createProfileTermsOfUse: String { return localizable.tr("Localizable", "create_profile.terms_of_use") } /// Username static var createProfileUsernameFieldPlaceholder: String { return localizable.tr("Localizable", "create_profile.username_field_placeholder") } + /// You're the only admin of a group(s). You can delete your account after you assign another admin. + static var deleteAccountAlertAdminCanNotBeDeleted: String { return localizable.tr("Localizable", "delete_account.alert_admin_can_not_be_deleted") } + /// This can not be undone + static var deleteAccountConfirmationAlertMessage: String { return localizable.tr("Localizable", "delete_account.confirmation_alert_message") } + /// Are you sure you want to delete your account? + static var deleteAccountConfirmationAlertTitle: String { return localizable.tr("Localizable", "delete_account.confirmation_alert_title") } /// DELETE ACCOUNT static var deleteAccountDeleteAccountButton: String { return localizable.tr("Localizable", "delete_account.delete_account_button") } /// If you delete your account, you will permanently lose access to your wallet, account data, messages and media. This cannot be undone.\n\nBy deleting your account you confirm that you withdraw your consent with NYNJA terms of use.\n\nAlso, make sure you have saved off the 12 word seed to regenerate your keys later in other platforms. diff --git a/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift index b4e22c196..8e0325a40 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift @@ -20,7 +20,7 @@ protocol AccountSettingsWireframeProtocol: class { func addContact(completion: @escaping (Result) -> Void) func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) - func deleteAccount(accountId: String) + func deleteAccount(identityId: String, accountId: String) } // MARK: - View @@ -61,6 +61,7 @@ protocol AccountSettingsPresenterProtocol: BasePresenterProtocol, NavigationProt protocol AccountSettingsInteractorInput: BaseInteractorProtocol { func save(completion: @escaping (Result) -> Void) + var identityId: String { get } var accountId: String { get } var avatar: URL? { get } diff --git a/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift index 6a2fa9bb2..d40579bf1 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift @@ -14,6 +14,8 @@ final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractor // MARK: - User Info + let identityId: String + let accountId: String private var account: DBAccount? @@ -47,6 +49,7 @@ final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractor struct Dependencies { let presenter: AccountSettingsInteractorOutput + let identityId: String let accountId: String let accountDAO: AccountDAOProtocol let accountService: AccountService @@ -55,6 +58,7 @@ final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractor init(dependencies: Dependencies) { presenter = dependencies.presenter + identityId = dependencies.identityId accountId = dependencies.accountId accountDAO = dependencies.accountDAO accountService = dependencies.accountService diff --git a/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift index 0021e5a86..616423e68 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -94,10 +94,10 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro case 4: // FIXME: localization (in whole module) return DestructiveActionCellModel(title: "Delete Account", accessibilityIdentifier: "delete_account_item") { [weak self] in - guard let self = self, let accountId = self.interactor?.accountId else { + guard let self = self, let identityId = self.interactor?.identityId, let accountId = self.interactor?.accountId else { return } - self.wireframe?.deleteAccount(accountId: accountId) + self.wireframe?.deleteAccount(identityId: identityId, accountId: accountId) } default: return nil diff --git a/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift index e69b4816f..983cc5aef 100644 --- a/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift +++ b/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -21,6 +21,7 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco } struct Parameters { + let identityId: String let accountId: String } @@ -37,7 +38,7 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco case chooseTimeout(completion: (StatusTimeout) -> Void) case addContact(completion: (Result) -> Void) case contactDetails(contact: UserContact, completion: (Result) -> Void) - case deleteAccount(accountId: String) + case deleteAccount(identityId: String, accountId: String) } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -47,6 +48,7 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco let interactor = AccountSettingsInteractor(dependencies: .init( presenter: presenter, + identityId: parameters.identityId, accountId: parameters.accountId, accountDAO: dependencies.accountDAO, accountService: dependencies.accountService, @@ -82,7 +84,7 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco coordinator.wireframe(self, didEndWithState: .contactDetails(contact: contact, completion: completion) ) } - func deleteAccount(accountId: String) { - coordinator.wireframe(self, didEndWithState: .deleteAccount(accountId: accountId)) + func deleteAccount(identityId: String, accountId: String) { + coordinator.wireframe(self, didEndWithState: .deleteAccount(identityId: identityId, accountId: accountId)) } } diff --git a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift index a342dd778..fbd88b563 100644 --- a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift @@ -8,8 +8,19 @@ import Foundation +protocol AccountSettingsCoordinatorDelegate: class { + func accountSettingsCoordinatorDidFinish(with state: AccountSettingsCoordinator.State) +} + final class AccountSettingsCoordinator: Coordinator, NavigationContainer { + enum State { + case dismissed + case accountDeleted + } + + weak var delegate: AccountSettingsCoordinatorDelegate? + let navigation: UINavigationController! private let serviceFactory: ServiceFactoryProtocol @@ -22,14 +33,14 @@ final class AccountSettingsCoordinator: Coordinator, NavigationContainer { func start() { let storageService = serviceFactory.makeStorageService() - guard let accountId = storageService.accountId else { + guard let identityId = storageService.identityId, let accountId = storageService.accountId else { return } let wireframe = AccountSettingsWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: .init(accountId: accountId), + parameters: .init(identityId: identityId, accountId: accountId), dependencies: .init( accountDAO: serviceFactory.makeAccountDAO(), accountService: serviceFactory.makeAccountService(), @@ -41,7 +52,11 @@ final class AccountSettingsCoordinator: Coordinator, NavigationContainer { } func end() { - + end(with: .dismissed) + } + + private func end(with state: State) { + delegate?.accountSettingsCoordinatorDidFinish(with: state) } } @@ -74,8 +89,8 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { case .contactDetails(let contact, let completion): break - case let .deleteAccount(accountId): - showDeleteAccount(accountId: accountId) + case let .deleteAccount(identityId, accountId): + showDeleteAccount(identityId: identityId, accountId: accountId) } } @@ -195,10 +210,11 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { navigation.present(view, animated: true, completion: nil) } - private func showDeleteAccount(accountId: String) { + private func showDeleteAccount(identityId: String, accountId: String) { let wireframe = DeleteAccountWireframe(coordinator: self) let view = wireframe.prepareModule( parameters: .init( + identityId: identityId, accountId: accountId ), dependencies: .init( @@ -218,6 +234,8 @@ extension AccountSettingsCoordinator: DeleteAccountCoordinatorProtocol { switch state { case .dismiss: navigation.popViewController(animated: true) + case .accountDeleted: + end(with: .accountDeleted) } } } diff --git a/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift b/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift index 6dd5d9d07..860c86224 100644 --- a/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift +++ b/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift @@ -12,6 +12,7 @@ import UIKit protocol DeleteAccountWireframeProtocol: AlertDisplayable { func dismiss() + func handleAccountDeletion() } // MARK: - View diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift b/Nynja/Modules/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift new file mode 100644 index 000000000..d0bb82227 --- /dev/null +++ b/Nynja/Modules/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift @@ -0,0 +1,13 @@ +// +// DeleteAccountErrors.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum DeleteAccountError: Error { + case singleGroupAdminCanNotBeDeleted +} diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift b/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift index 8df21bd4d..7d500ba5b 100644 --- a/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift +++ b/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift @@ -12,6 +12,8 @@ final class DeleteAccountInteractor: BaseInteractor, DeleteAccountInteractorInpu private weak var presenter: DeleteAccountInteractorOutput? + private let identityId: String + private let accountId: String @@ -24,12 +26,14 @@ final class DeleteAccountInteractor: BaseInteractor, DeleteAccountInteractorInpu struct Dependencies { let presenter: DeleteAccountInteractorOutput + let identityId: String let accountId: String let accountService: AccountService } init(dependencies: Dependencies) { presenter = dependencies.presenter + identityId = dependencies.identityId accountId = dependencies.accountId accountService = dependencies.accountService } @@ -38,8 +42,17 @@ final class DeleteAccountInteractor: BaseInteractor, DeleteAccountInteractorInpu // MARK: - Interactor Input func deleteAccount() { - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - self.presenter?.accountDidDelete() + // TODO: Use deleteProfile for now, because of request from BE team. In the future must be replaced with: +// accountService.deleteAccount(accountId) { [weak self] result in + accountService.deleteProfile(identityId) { [weak self] result in + switch result { + case let .success(status): + print(status) + // TODO: check status + self?.presenter?.accountDidDelete() + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } } } } diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift b/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift index cf4f7e94c..4932df23e 100644 --- a/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift +++ b/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit final class DeleteAccountPresenter: BasePresenter, DeleteAccountPresenterProtocol, DeleteAccountInteractorOutput { @@ -18,8 +19,19 @@ final class DeleteAccountPresenter: BasePresenter, DeleteAccountPresenterProtoco // MARK: - Presenter func deleteAccount() { - view?.showLoading() - interactor.deleteAccount() + let title = String.localizable.deleteAccountConfirmationAlertTitle + let message = String.localizable.deleteAccountConfirmationAlertMessage + + let actions: [Alert.Action] = [ + .init(title: String.localizable.cancel, style: .default), + .init(title: String.localizable.delete, style: .default) { _ in + self.view?.showLoading() + self.interactor.deleteAccount() + } + ] + + let alert = Alert(title: title, message: message, actions: actions) + wireframe.present(alert) } func back() { @@ -35,6 +47,19 @@ final class DeleteAccountPresenter: BasePresenter, DeleteAccountPresenterProtoco func didReceiveFailure(_ error: Error?) { view?.hideLoading() + + if let error = error as? DeleteAccountError { + switch error { + case .singleGroupAdminCanNotBeDeleted: + let actions: [Alert.Action] = [ + .init(title: String.localizable.ok, style: .default) + ] + let title = String.localizable.deleteAccountAlertAdminCanNotBeDeleted + + let alert = Alert(title: title, actions: actions) + wireframe.present(alert) + } + } } } diff --git a/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift b/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift index 27f381eca..a9260c434 100644 --- a/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift +++ b/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift @@ -23,6 +23,7 @@ final class DeleteAccountWireframe: Wireframe, DeleteAccountWireframeProtocol { } struct Parameters { + let identityId: String let accountId: String } @@ -32,6 +33,7 @@ final class DeleteAccountWireframe: Wireframe, DeleteAccountWireframeProtocol { enum State { case dismiss + case accountDeleted } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { @@ -41,6 +43,7 @@ final class DeleteAccountWireframe: Wireframe, DeleteAccountWireframeProtocol { let interactor = DeleteAccountInteractor(dependencies: .init( presenter: presenter, + identityId: parameters.identityId, accountId: parameters.accountId, accountService: dependencies.accountService) ) @@ -57,4 +60,8 @@ final class DeleteAccountWireframe: Wireframe, DeleteAccountWireframeProtocol { func dismiss() { coordinator.wireframe(self, didEndWithState: .dismiss) } + + func handleAccountDeletion() { + coordinator.wireframe(self, didEndWithState: .accountDeleted) + } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 9825f6c57..eb7fea0fe 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -1078,6 +1078,9 @@ "delete_account.screen_title" = "DELETE ACCOUNT"; "delete_account.description_text" = "If you delete your account, you will permanently lose access to your wallet, account data, messages and media. This cannot be undone.\n\nBy deleting your account you confirm that you withdraw your consent with NYNJA terms of use.\n\nAlso, make sure you have saved off the 12 word seed to regenerate your keys later in other platforms."; +"delete_account.alert_admin_can_not_be_deleted" = "You're the only admin of a group(s). You can delete your account after you assign another admin."; +"delete_account.confirmation_alert_title" = "Are you sure you want to delete your account?"; +"delete_account.confirmation_alert_message" = "This can not be undone"; "delete_account.delete_account_button" = "DELETE ACCOUNT"; -- GitLab From 96fd38066093265d48361a60bbe03ce2ea5bb92d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 6 Dec 2018 13:15:07 +0200 Subject: [PATCH 129/138] Update navigation logic. Handle logout. --- Nynja/AppDelegate.swift | 2 +- .../AccountSettingsCoordinator.swift | 31 +++++++++++-------- Nynja/Modules/Auth Flow/AppCoordinator.swift | 20 +++++++++++- Nynja/Modules/Auth Flow/AuthCoordinator.swift | 2 +- .../AuthModule/Presenter/AuthPresenter.swift | 3 +- .../Main/WireFrame/MainWireframe.swift | 20 ++++++++++-- 6 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 945520a31..606547acf 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -34,7 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private let storageService = StorageService.sharedInstance private let antiDebuggingService = AntiDebuggingService() - private var appCoordinator: AppCoordinator! + private(set) var appCoordinator: AppCoordinatorInput! private let serviceFactory = ServiceFactory() diff --git a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift index fbd88b563..6f214274e 100644 --- a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift @@ -9,7 +9,7 @@ import Foundation protocol AccountSettingsCoordinatorDelegate: class { - func accountSettingsCoordinatorDidFinish(with state: AccountSettingsCoordinator.State) + func accountSettingsCoordinator(_ coordinator: AccountSettingsCoordinator, didFinishWithState state: AccountSettingsCoordinator.State) } final class AccountSettingsCoordinator: Coordinator, NavigationContainer { @@ -56,7 +56,15 @@ final class AccountSettingsCoordinator: Coordinator, NavigationContainer { } private func end(with state: State) { - delegate?.accountSettingsCoordinatorDidFinish(with: state) + if case .dismissed = state, let index = navigation.viewControllers.firstIndex(where: { $0 is AccountSettingsViewController }), index > 0 { + + let previousFlowViewControllerIndex = navigation.viewControllers.index(before: index) + let viewController = navigation.viewControllers[previousFlowViewControllerIndex] + + navigation.popToViewController(viewController, animated: true) + } + + delegate?.accountSettingsCoordinator(self, didFinishWithState: state) } } @@ -67,12 +75,12 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) { switch state { case .back: - navigation.popViewController(animated: true) + end(with: .dismissed) - case .addContact(let completion): + case let .addContact(completion): showAddContactPopup(completion: completion) - case .chooseAvatar(let completion): + case let .chooseAvatar(completion): showAvatarSourceOptionPopup { [weak self] result in guard case let .success(imageSource) = result else { return @@ -80,13 +88,13 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { self?.getAvatar(source: imageSource, completion: completion) } - case .chooseStatus(let completion): + case let .chooseStatus(completion): chooseStatus(completion: completion) - case .chooseTimeout(let completion): + case let .chooseTimeout(completion): chooseTimeout(completion: completion) - case .contactDetails(let contact, let completion): + case let .contactDetails(contact, completion): break case let .deleteAccount(identityId, accountId): @@ -125,16 +133,13 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { source: source, rootViewController: navigation.viewControllers.last!, serviceFactory: serviceFactory, - completion: { (url) in - completion(UIImage.sd_image(with: try? Data(contentsOf: url))) - }) + completion: { url in completion(UIImage.sd_image(with: try? Data(contentsOf: url))) } + ) let coordinator = SelectAvatarFlowCoordinator(dependencies: coordinatorDependencies) - coordinator.start() } - private func showAvatarSourceOptionPopup(completion: @escaping (Result) -> Void) { enum AvatarSourceError: Error { case cancelled diff --git a/Nynja/Modules/Auth Flow/AppCoordinator.swift b/Nynja/Modules/Auth Flow/AppCoordinator.swift index 6d772ccc4..71045cf12 100644 --- a/Nynja/Modules/Auth Flow/AppCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AppCoordinator.swift @@ -8,7 +8,11 @@ import Foundation -final class AppCoordinator: Coordinator { +protocol AppCoordinatorInput: Coordinator { + func logout() +} + +final class AppCoordinator: AppCoordinatorInput { private let navigation: UINavigationController @@ -33,6 +37,10 @@ final class AppCoordinator: Coordinator { authCoordinator.start() } + func logout() { + start() + } + func end() { } } @@ -45,3 +53,13 @@ extension AppCoordinator: AuthCoordinatorDelegate { MainWireFrame().presentMain(navigation: navigation, isRegistered: true) } } + +// MARK: - Account Settings Delegate + +// Should be handled here when new navigation will be implemented +extension AppCoordinator: AccountSettingsCoordinatorDelegate { + + func accountSettingsCoordinator(_ coordinator: AccountSettingsCoordinator, didFinishWithState state: AccountSettingsCoordinator.State) { + + } +} diff --git a/Nynja/Modules/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Auth Flow/AuthCoordinator.swift index 3885fd468..bf2e25d48 100644 --- a/Nynja/Modules/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Auth Flow/AuthCoordinator.swift @@ -39,7 +39,7 @@ final class AuthCoordinator: Coordinator, NavigationContainer { googleAuthService: serviceFactory.makeGoogleAuthService(), countriesProvider: serviceFactory.makeCountriesProvider()) ) - navigation.pushViewController(view, animated: true) + navigation.setViewControllers([view], animated: true) } func end() { diff --git a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift index 62226f450..941a4561e 100644 --- a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift @@ -114,7 +114,8 @@ extension AuthPresenter { view?.hideLoading() let actions = [Alert.Action(title: "OK", style: .default)] - let alert = Alert(title: "Failure", message: error?.localizedDescription ?? "Something went wrong", actions: actions) + let message: String = error.flatMap { "\(($0 as NSError).code): \($0.localizedDescription)" } ?? "Something went wrong" + let alert = Alert(title: "Failure", message: message, actions: actions) wireframe.present(alert) } diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 89026f3ac..65648c7d7 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -390,9 +390,8 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega } func logout() { - if let nav = self.navigation { - LoginWireFrame().presentLogin(navigation: nav) - } + // FIXME: replace when new navigation logic will be implemented + (UIApplication.shared.delegate as? AppDelegate)?.appCoordinator.logout() } func showNotificationsSettings() { @@ -464,6 +463,7 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega guard let navigation = navigation else { return } let coordinator = AccountSettingsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.delegate = self coordinator.start() } @@ -656,3 +656,17 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega view?.notifyAvailability(snackBar: snackBar, isAvailable: isAvailable) } } + + +// MARK: - AccountSettingsCoordinatorDelegate +extension MainWireFrame: AccountSettingsCoordinatorDelegate { + + func accountSettingsCoordinator(_ coordinator: AccountSettingsCoordinator, didFinishWithState state: AccountSettingsCoordinator.State) { + switch state { + case .dismissed: + break + case .accountDeleted: + logout() + } + } +} -- GitLab From 54d3b883387cf2af5c67e585b54bd85722cdbb13 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 6 Dec 2018 15:56:44 +0200 Subject: [PATCH 130/138] Updates for auto login flow. (#1516) * Update project structure * Added autologin logic for new auth flow. --- Nynja.xcodeproj/project.pbxproj | 6 +- Nynja/BadgeNumberService.swift | 2 +- Nynja/BadgeNumberServiceProtocol.swift | 21 ++- .../Splash/Interactor/SplashInteractor.swift | 95 ------------- .../Splash/Presenter/SplashPresenter.swift | 46 ------- .../Auth Flow/Splash/SplashProtocols.swift | 66 --------- .../Splash/WireFrame/SplashWireframe.swift | 47 ------- .../AccountSettingsProtocols.swift | 0 .../Entities/AddContactCellModel.swift | 0 .../Entities/ContactTVCellModel.swift | 0 .../Entities/DescriptionCellModel.swift | 0 .../Entities/DestructiveActionCellModel.swift | 0 .../Entities/MaterialTextFieldCellModel.swift | 0 .../Entities/SettingsSectionHeader.swift | 0 .../Entities/SettingsSelectorCellModel.swift | 0 .../Entities/SettingsSetAvatarCellModel.swift | 0 .../AccountSettings/Entities/Sizeble.swift | 0 .../Entities/StatusTimeout.swift | 0 .../Entities/UserAccount.swift | 0 .../Entities/UserContact.swift | 0 .../Entities/UserContactAction.swift | 0 .../Entities/UserProfile.swift | 0 .../AccountSettings/Entities/UserStatus.swift | 0 .../AccountSettingsInteractor.swift | 0 .../Presenter/AccountSettingsPresenter.swift | 0 .../View/AccountSettingsViewController.swift | 0 .../View/Cells/AddContactCell.swift | 0 .../View/Cells/ContactTVCell.swift | 0 .../View/Cells/DescriptionTVCell.swift | 0 .../DestructiveActionTableViewCell.swift | 0 .../View/Cells/MaterialTextFieldTVCell.swift | 0 .../View/Cells/SettingsSelectorTVCell.swift | 0 .../View/Cells/SettingsSetAvatarTVCell.swift | 0 .../Header/SettingsSectionHeaderView.swift | 0 .../Wireframe/AccountSettingsWireframe.swift | 0 .../AuthProvider/AuthProviderProtocols.swift | 0 .../AuthProvider/Entities/AuthProvider.swift | 0 .../AuthProviderUIConfiguration.swift | 0 .../Interactor/AuthProviderInteractor.swift | 0 .../Presenter/AuthProviderPresenter.swift | 0 .../View/AuthProviderViewController.swift | 0 .../Subviews/SearchAvailabilityView.swift | 0 .../Wireframe/AuthProviderWireframe.swift | 0 .../AccountSettingsCoordinator.swift | 0 .../Coordinator/LoginOptionsCoordinator.swift | 0 .../DeleteAccountProtocols.swift | 0 .../Entities/DeleteAccountErrors.swift | 0 .../Interactor/DeleteAccountInteractor.swift | 0 .../Presenter/DeleteAccountPresenter.swift | 0 .../View/DeleteAccountViewController.swift | 0 .../WIreframe/DeleteAccountWireframe.swift | 0 .../LoginOptions/Entities/LoginOption.swift | 0 .../Interactor/LoginOptionsInteractor.swift | 0 .../LoginOptions/LoginOptionsProtocols.swift | 0 .../Presenter/LoginOptionsPresenter.swift | 0 .../Forms/FieldRowItem/AnyFieldRowItem.swift | 0 .../Forms/FieldRowItem/FieldRowItem.swift | 0 .../LoginOptions/View/Forms/Form.swift | 0 .../View/Forms/Items/ActionRowItemView.swift | 0 .../View/Forms/Items/SwitchRowItemView.swift | 0 .../Forms/Items/TextFieldRowItemView.swift | 0 .../View/Forms/Items/TextRowItemView.swift | 0 .../View/LoginOptionsViewController.swift | 0 .../LoginOptionSwitchRowItemView.swift | 0 .../Wireframe/LoginOptionsWireframe.swift | 0 .../{Auth Flow => Flows}/AppCoordinator.swift | 7 +- .../Auth Flow/AuthCoordinator.swift | 75 ++++++++++- .../Auth Flow/AuthModule/AuthProtocols.swift | 0 .../Entities/EmailTextController.swift | 0 .../AuthModule/Entities/LoginFlow.swift | 0 .../Entities/PhoneNumberFormatter.swift | 0 .../Entities/PhoneNumberTextController.swift | 0 .../Entities/PlainLoginOption.swift | 0 .../AuthModule/Entities/Validator.swift | 0 .../Interactor/AuthInteractor.swift | 0 .../AuthModule/Presenter/AuthPresenter.swift | 0 .../AuthModule/View/AuthViewController.swift | 0 .../View/Subviews/AuthHeaderView.swift | 0 .../View/Subviews/EmailLoginView.swift | 0 .../View/Subviews/LoginContainerView.swift | 0 .../View/Subviews/LoginOptionsView.swift | 0 .../View/Subviews/PhoneNumberLoginView.swift | 0 .../AuthModule/Wireframe/AuthWireframe.swift | 0 .../CodeConfirmationProtocols.swift | 0 .../Entities/ConfirmationData.swift | 0 .../CodeConfirmationInteractor.swift | 0 .../Presenter/CodeConfirmationPresenter.swift | 0 .../View/CodeConfirmationViewController.swift | 0 .../Wireframe/CodeConfirmationWireframe.swift | 0 .../CreateProfileProtocols.swift | 0 .../CreateProfile/Entities/ProfileField.swift | 0 .../Interactor/CreateProfileInteractor.swift | 0 .../Presenter/CreateProfilePresenter.swift | 0 .../View/CreateProfileViewController.swift | 0 .../Subviews/CreateProfileContentView.swift | 0 .../CreateProfileViewsFactory.swift | 0 .../Wireframe/CreateProfileWireframe.swift | 0 .../Facebook/FacebookAuthProtocols.swift | 0 .../Intreractor/FacebookAuthInteractor.swift | 0 .../Presenter/FacebookAuthPresenter.swift | 0 .../View/FacebookAuthViewController.swift | 0 .../Wireframe/FacebookAuthWireframe.swift | 0 .../Login/Interactor/LoginInteractor.swift | 0 .../Auth Flow/Login/Interactor/Modelka.swift | 0 .../Auth Flow/Login/LoginProtocols.swift | 0 .../Login/Presenter/LoginPresenter.swift | 0 .../Login/View/LoginViewController.swift | 0 .../View/LoginWheelContainerDataSource.swift | 0 .../View/LoginWheelContainerDelegate.swift | 0 .../Login/WireFrame/LoginWireframe.swift | 0 .../Entities/CountriesSection.swift | 0 .../SelectCountry/Entities/Country.swift | 0 .../Interactor/SelectCountryInteractor.swift | 0 .../Presenter/SelectCountryPresenter.swift | 0 .../SelectCountryProtocols.swift | 0 .../TableView/Cell/CountryCellModel.swift | 0 .../TableView/Cell/CountryTableViewCell.swift | 0 .../Header/SelectCountryHeaderView.swift | 0 .../SelectCountryViewController.swift | 0 .../WireFrame/SelectCountryWireframe.swift | 0 .../Splash/Interactor/SplashInteractor.swift | 125 ++++++++++++++++++ .../Splash/Presenter/SplashPresenter.swift | 63 +++++++++ .../Auth Flow/Splash/SplashProtocols.swift | 45 +++++++ .../Splash/View/SplashViewController.swift | 2 +- .../Splash/WireFrame/SplashWireframe.swift | 71 ++++++++++ .../Interactor/VerifyNumberInteractor.swift | 0 .../Presenter/VerifyNumberPresenter.swift | 0 .../VerifyNumber/VerifyNumberProtocols.swift | 0 .../View/VerifyNumberViewController.swift | 0 .../Wireframe/VerifyNumberWireFrame.swift | 0 .../Main/WireFrame/MainWireframe.swift | 8 +- .../Modules/tutorial/TutorialProtocols.swift | 4 +- .../WireFrame/TutorialWireframe.swift | 31 +++-- Nynja/UserInfo.swift | 4 + 134 files changed, 423 insertions(+), 295 deletions(-) delete mode 100644 Nynja/Modules/Auth Flow/Splash/Interactor/SplashInteractor.swift delete mode 100644 Nynja/Modules/Auth Flow/Splash/Presenter/SplashPresenter.swift delete mode 100644 Nynja/Modules/Auth Flow/Splash/SplashProtocols.swift delete mode 100644 Nynja/Modules/Auth Flow/Splash/WireFrame/SplashWireframe.swift rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/AccountSettingsProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/AddContactCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/Sizeble.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/StatusTimeout.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/UserAccount.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/UserContact.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/UserContactAction.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/UserProfile.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Entities/UserStatus.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/AccountSettingsViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Cells/AddContactCell.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/AuthProviderProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/Entities/AuthProvider.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/View/AuthProviderViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/Coordinator/AccountSettingsCoordinator.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/Coordinator/LoginOptionsCoordinator.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/DeleteAccount/DeleteAccountProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/Entities/LoginOption.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/LoginOptionsProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Forms/Form.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/LoginOptionsViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift (100%) rename Nynja/Modules/{ => Flows}/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift (100%) rename Nynja/Modules/{Auth Flow => Flows}/AppCoordinator.swift (86%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthCoordinator.swift (82%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/AuthProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Entities/EmailTextController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Entities/LoginFlow.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Entities/PlainLoginOption.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Entities/Validator.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Interactor/AuthInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Presenter/AuthPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/View/AuthViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/CreateProfileProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/Entities/ProfileField.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/View/CreateProfileViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Facebook/FacebookAuthProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Facebook/View/FacebookAuthViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/Interactor/LoginInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/Interactor/Modelka.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/LoginProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/Presenter/LoginPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/View/LoginViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/View/LoginWheelContainerDataSource.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/View/LoginWheelContainerDelegate.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/Login/WireFrame/LoginWireframe.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/Entities/CountriesSection.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/Entities/Country.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/SelectCountryProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift (100%) create mode 100644 Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift create mode 100644 Nynja/Modules/Flows/Auth Flow/Splash/Presenter/SplashPresenter.swift create mode 100644 Nynja/Modules/Flows/Auth Flow/Splash/SplashProtocols.swift rename Nynja/Modules/{ => Flows}/Auth Flow/Splash/View/SplashViewController.swift (97%) create mode 100644 Nynja/Modules/Flows/Auth Flow/Splash/WireFrame/SplashWireframe.swift rename Nynja/Modules/{ => Flows}/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift (100%) rename Nynja/Modules/{ => Flows}/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift (100%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 4d2c7844c..e62b9e86f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -6941,8 +6941,6 @@ isa = PBXGroup; children = ( FEA6555D2167777E00B44029 /* Wallet Flows */, - 4B749F0E214FEFC8002F3A33 /* Auth Flow */, - 5EDD454621885EC400C50BC8 /* Account Flow */, ED33B21C6D660153B22D18BF /* AddContact */, 80CA53AB5B009455E0ECDC30 /* AddContactByUsername */, C4AE70AB74CA331DD03D830B /* AddContactViaPhone */, @@ -7509,7 +7507,6 @@ 4B749F0E214FEFC8002F3A33 /* Auth Flow */ = { isa = PBXGroup; children = ( - 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */, 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, 4FBB666690A18EEA5438EAB7 /* Splash */, 5EEB73BE216199DE00D8ECE6 /* AuthModule */, @@ -13824,6 +13821,9 @@ F105C690209F71BE0091786A /* Flows */ = { isa = PBXGroup; children = ( + 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */, + 4B749F0E214FEFC8002F3A33 /* Auth Flow */, + 5EDD454621885EC400C50BC8 /* Account Flow */, 3A0A50A521B7FEFE0052D334 /* CreateGroupFlow */, F105C691209F71BE0091786A /* CameraFlow */, F11786F420ACF017007A9A1B /* CameraSettingsFlow */, diff --git a/Nynja/BadgeNumberService.swift b/Nynja/BadgeNumberService.swift index f86d18afc..ef88a8fec 100644 --- a/Nynja/BadgeNumberService.swift +++ b/Nynja/BadgeNumberService.swift @@ -56,7 +56,7 @@ final class BadgeNumberService: BadgeNumberServiceProtocol, StorageSubscriber { // MARK: - Register subscribers // TODO: sync - func observeBadgeNumber(_ object: AnyObject, notifyImmediately: Bool = true, handler: @escaping BadgeHandler) { + func observeBadgeNumber(_ object: AnyObject, notifyImmediately: Bool, handler: @escaping BadgeHandler) { let subscriber = AnyWeakSubscriber(object: object, handler: handler) subscribers.append(subscriber) diff --git a/Nynja/BadgeNumberServiceProtocol.swift b/Nynja/BadgeNumberServiceProtocol.swift index f7021e3e7..37a9b5dc8 100644 --- a/Nynja/BadgeNumberServiceProtocol.swift +++ b/Nynja/BadgeNumberServiceProtocol.swift @@ -6,18 +6,25 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -/// Subscribing closure which used to notify about changes of badge number -typealias BadgeHandler = (_ badgeNumber: Int64) -> Void - /// Service which provide access to badge number and capability to observe changes -protocol BadgeNumberServiceProtocol { +protocol BadgeNumberServiceProtocol: class { + + typealias BadgeHandler = (_ badgeNumber: Int64) -> Void + + func initCounters() + + func observeBadgeNumber(_ object: AnyObject, handler: @escaping BadgeHandler) - // MARK: - Register subscribers - /// Add subscriber which observe changes of 'badgeNumber' property func observeBadgeNumber(_ object: AnyObject, notifyImmediately: Bool, handler: @escaping BadgeHandler) - /// Remove subscriber which observe changes of 'badgeNumber' property func removeObserver(_ object: AnyObject) func clean() } + +extension BadgeNumberServiceProtocol { + + func observeBadgeNumber(_ object: AnyObject, handler: @escaping BadgeHandler) { + observeBadgeNumber(object, notifyImmediately: true, handler: handler) + } +} diff --git a/Nynja/Modules/Auth Flow/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Auth Flow/Splash/Interactor/SplashInteractor.swift deleted file mode 100644 index f683f5feb..000000000 --- a/Nynja/Modules/Auth Flow/Splash/Interactor/SplashInteractor.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// SplashSplashInteractor.swift -// Nynja -// -// Created by Anton Makarov on 23/08/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -class SplashInteractor: SplashInteractorInputProtocol { - - weak var presenter: SplashInteractorOutputProtocol! - - private let storageService = StorageService.sharedInstance - private let mqttService = MQTTService.sharedInstance - private let badgeService = BadgeNumberService.shared - private let callService = NynjaCommunicatorService.sharedInstance - - func showed() { - checkJailbreak { [weak self] in - self?.setupFlow() - } - } - - private func checkJailbreak(with completion: @escaping () -> Void) { - if UIDevice.current.isJailbroken { - presenter.showJailbreakAlert(with: completion) - } else { - setupFlow() - } - } - - private func setupFlow() { - decideRoute() - configureDependencies() - } - - private func configureDependencies() { - let application = UIApplication.shared - - guard let appDelegate = application.delegate else { - return - } - badgeService.initCounters() - badgeService.observeBadgeNumber(appDelegate) { (badgeNumber) in - application.applicationIconBadgeNumber = Int(badgeNumber) - } - - mqttService.connect() - callService.initialize() - - MediaDownloadManager.setupAppDataUsageSettingsIfNeeded() - } - - private func decideRoute() { - guard storageService.wasLogined else { - AppLanguage.current = .english - presenter.showTutorial() - return - } - - guard storageService.isUserLogined, let phoneId = storageService.phoneId else { - LogService.log(topic: .db) { [weak self] in return """ - Clear storage: hasPhone = \(self?.storageService.hasPhone), hasToken = \(self?.storageService.hasToken), - phoneId = \(self?.storageService.phoneId ?? "none") - """ } - prepareToShowAuth() - return - } - - LogService.log(topic: .db) { return "Setup DB: Splash" } - storageService.setupDatabase(with: phoneId, application: UIApplication.shared) - - guard let contact = ContactDAO.currentContact else { - LogService.log(topic: .db) { return "Clear storage: can't find current contact" } - prepareToShowAuth() - return - } - - if contact.hasName { - presenter.showMain() - } else { - presenter.showEditProfile() - } - } - - private func prepareToShowAuth() { - LogService.log(topic: .db) { return "Clear storage: Splash" } - - storageService.clearStorage() - mqttService.reconnect() - - presenter.showAuth() - } - -} diff --git a/Nynja/Modules/Auth Flow/Splash/Presenter/SplashPresenter.swift b/Nynja/Modules/Auth Flow/Splash/Presenter/SplashPresenter.swift deleted file mode 100644 index f001fed03..000000000 --- a/Nynja/Modules/Auth Flow/Splash/Presenter/SplashPresenter.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// SplashSplashPresenter.swift -// Nynja -// -// Created by Anton Makarov on 23/08/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -class SplashPresenter: BasePresenter, SplashPresenterProtocol, SplashInteractorOutputProtocol { - - override var itemsFactory: WCItemsFactory? { - return AboutItemsFactory() - } - - weak var view: SplashViewProtocol! - var interactor: SplashInteractorInputProtocol! - var wireFrame: SplashWireFrameProtocol! - - func showed() { - self.interactor.showed() - } - - // MARK: - SplashInteractorOutputProtocol - func showAuth() { - self.wireFrame.showAuth() - } - - func showTutorial() { - self.wireFrame.showTutorial() - } - - func showMain() { - self.wireFrame.showMain() - } - - func showEditProfile() { - self.wireFrame.showEditProfile() - } - - func showJailbreakAlert(with completion: @escaping () -> Void) { - AlertManager.sharedInstance.showAlertOk(message: String.localizable.yourDeviceIsRooted, completion: completion) - } - -} - - diff --git a/Nynja/Modules/Auth Flow/Splash/SplashProtocols.swift b/Nynja/Modules/Auth Flow/Splash/SplashProtocols.swift deleted file mode 100644 index 93800181e..000000000 --- a/Nynja/Modules/Auth Flow/Splash/SplashProtocols.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// SplashSplashProtocols.swift -// Nynja -// -// Created by Anton Makarov on 23/08/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -protocol SplashWireFrameProtocol: class { - - func presentSplash(navigation: UINavigationController) - - /** - * Add here your methods for communication PRESENTER -> WIREFRAME - */ - - func showAuth() - func showTutorial() - func showMain() - func showEditProfile() -} - -protocol SplashViewProtocol: class { - - var presenter: SplashPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ -} - -protocol SplashPresenterProtocol: AnyObject, BasePresenterProtocol { - - var view: SplashViewProtocol! { get set } - var interactor: SplashInteractorInputProtocol! { get set } - var wireFrame: SplashWireFrameProtocol! { get set } - - /** - * Add here your methods for communication VIEW -> PRESENTER - */ - func showed() -} - -protocol SplashInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ - func showAuth() - func showTutorial() - func showMain() - func showEditProfile() - func showJailbreakAlert(with completion: @escaping () -> Void) -} - -protocol SplashInteractorInputProtocol: class { - - var presenter: SplashInteractorOutputProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ - func showed() -} diff --git a/Nynja/Modules/Auth Flow/Splash/WireFrame/SplashWireframe.swift b/Nynja/Modules/Auth Flow/Splash/WireFrame/SplashWireframe.swift deleted file mode 100644 index 949955c74..000000000 --- a/Nynja/Modules/Auth Flow/Splash/WireFrame/SplashWireframe.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// SplashSplashWireframe.swift -// Nynja -// -// Created by Anton Makarov on 23/08/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class SplashWireFrame: SplashWireFrameProtocol { - - weak var navigation : UINavigationController? - - func presentSplash(navigation: UINavigationController) { - let view = SplashViewController() - let presenter = SplashPresenter() - let interactor = SplashInteractor() - - self.navigation = navigation - - // Connecting - view.presenter = presenter - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - navigation.pushViewController(view as UIViewController, animated: false) - } - - func showAuth() { - LoginWireFrame().presentLogin(navigation: navigation!) - } - - func showTutorial() { - let tutorial = TutorialWireFrame() - tutorial.presentTutorial(navigation: navigation!) - } - - func showMain() { - MainWireFrame().presentMain(navigation: navigation!, isRegistered: false, checkSession: true) - } - - func showEditProfile() { - EditProfileWireFrame().presentEditProfile(navigation: navigation!, isRegistered: true, main: nil) - } -} diff --git a/Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/AccountSettingsProtocols.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/AddContactCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AddContactCellModel.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/AddContactCellModel.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AddContactCellModel.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/Sizeble.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/Sizeble.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/Sizeble.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/Sizeble.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/StatusTimeout.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/StatusTimeout.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/StatusTimeout.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserAccount.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserAccount.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/UserAccount.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserAccount.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContact.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/UserContact.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContact.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserContactAction.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContactAction.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/UserContactAction.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContactAction.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserProfile.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/UserProfile.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserProfile.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserStatus.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Entities/UserStatus.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserStatus.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/AccountSettingsViewController.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/AddContactCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/AddContactCell.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Cells/AddContactCell.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/AddContactCell.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift diff --git a/Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift similarity index 100% rename from Nynja/Modules/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift rename to Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/AuthProviderProtocols.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProvider.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProvider.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProvider.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProvider.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/View/AuthProviderViewController.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift diff --git a/Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift similarity index 100% rename from Nynja/Modules/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift rename to Nynja/Modules/Flows/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift diff --git a/Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift similarity index 100% rename from Nynja/Modules/Account Flow/Coordinator/AccountSettingsCoordinator.swift rename to Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift diff --git a/Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift b/Nynja/Modules/Flows/Account Flow/Coordinator/LoginOptionsCoordinator.swift similarity index 100% rename from Nynja/Modules/Account Flow/Coordinator/LoginOptionsCoordinator.swift rename to Nynja/Modules/Flows/Account Flow/Coordinator/LoginOptionsCoordinator.swift diff --git a/Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/DeleteAccountProtocols.swift similarity index 100% rename from Nynja/Modules/Account Flow/DeleteAccount/DeleteAccountProtocols.swift rename to Nynja/Modules/Flows/Account Flow/DeleteAccount/DeleteAccountProtocols.swift diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift similarity index 100% rename from Nynja/Modules/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift rename to Nynja/Modules/Flows/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift similarity index 100% rename from Nynja/Modules/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift rename to Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift diff --git a/Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift similarity index 100% rename from Nynja/Modules/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift rename to Nynja/Modules/Flows/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift diff --git a/Nynja/Modules/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift similarity index 100% rename from Nynja/Modules/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift rename to Nynja/Modules/Flows/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift diff --git a/Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift similarity index 100% rename from Nynja/Modules/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift rename to Nynja/Modules/Flows/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/Entities/LoginOption.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Entities/LoginOption.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/Entities/LoginOption.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/Entities/LoginOption.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/LoginOptionsProtocols.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/LoginOptionsProtocols.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/LoginOptionsProtocols.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Form.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Form.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Forms/Form.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Form.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/LoginOptionsViewController.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/LoginOptionsViewController.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/LoginOptionsViewController.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/LoginOptionsViewController.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift diff --git a/Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift similarity index 100% rename from Nynja/Modules/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift rename to Nynja/Modules/Flows/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift diff --git a/Nynja/Modules/Auth Flow/AppCoordinator.swift b/Nynja/Modules/Flows/AppCoordinator.swift similarity index 86% rename from Nynja/Modules/Auth Flow/AppCoordinator.swift rename to Nynja/Modules/Flows/AppCoordinator.swift index 71045cf12..decb2a3c2 100644 --- a/Nynja/Modules/Auth Flow/AppCoordinator.swift +++ b/Nynja/Modules/Flows/AppCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +import UIKit protocol AppCoordinatorInput: Coordinator { func logout() @@ -27,11 +27,6 @@ final class AppCoordinator: AppCoordinatorInput { } func start() { -// let storage = StorageService.sharedInstance -// if let passcode = storage.identityId { -// storage.setupDatabase(with: passcode, application: UIApplication.shared) -// } - let authCoordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) authCoordinator.delegate = self authCoordinator.start() diff --git a/Nynja/Modules/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift similarity index 82% rename from Nynja/Modules/Auth Flow/AuthCoordinator.swift rename to Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift index bf2e25d48..3724b668c 100644 --- a/Nynja/Modules/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift @@ -13,6 +13,10 @@ protocol AuthCoordinatorDelegate: class { func authCoordinatorDidFinish(_ coordinator: AuthCoordinator) } +protocol AuthCoordinatorInput: class { + func restart() +} + final class AuthCoordinator: Coordinator, NavigationContainer { weak var delegate: AuthCoordinatorDelegate? @@ -33,18 +37,81 @@ final class AuthCoordinator: Coordinator, NavigationContainer { } func start() { - let wireframe = AuthWireframe(coordinator: self) + let wireframe = SplashWireframe(coordinator: self) let view = wireframe.prepareModule( - dependencies: .init(authService: serviceFactory.makeAuthService(), - googleAuthService: serviceFactory.makeGoogleAuthService(), - countriesProvider: serviceFactory.makeCountriesProvider()) + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + mqttService: serviceFactory.makeMQTTService(), + badgeService: serviceFactory.makeBadgeNumberService(), + callService: serviceFactory.makeNynjaCommunicatorService() + ) ) + navigation.pushViewController(view, animated: false) + } + + func restart() { + let view = makeAuthView() navigation.setViewControllers([view], animated: true) } func end() { delegate?.authCoordinatorDidFinish(self) } + + private func makeAuthView() -> UIViewController { + let wireframe = AuthWireframe(coordinator: self) + let view = wireframe.prepareModule( + dependencies: .init( + authService: serviceFactory.makeAuthService(), + googleAuthService: serviceFactory.makeGoogleAuthService(), + countriesProvider: serviceFactory.makeCountriesProvider() + ) + ) + return view + } +} + +// MARK: - Splash + +extension AuthCoordinator: SplashCoordinatorProtocol { + + func wireframe(_ wireframe: SplashWireframe, didEndWithState state: SplashWireframe.State) { + switch state { + case .showTutorial: + showTutorial() + + case .showAuth, .showCreateProfile: + // TODO: show create profile if needed. + showAuth() + + case let .showMain(isRegistered, checkSession): + let wireframe = MainWireFrame() + wireframe.presentMain(navigation: navigation, isRegistered: isRegistered, checkSession: checkSession) + } + } + + private func showTutorial() { + let wireframe = TutorialWireFrame(coordinator: self, navigation: navigation) + let view = wireframe.prepareModule() + navigation.pushViewController(view, animated: true) + } + + private func showAuth() { + let view = makeAuthView() + navigation.pushViewController(view, animated: true) + } +} + +// MARK: - Tutorial + +extension AuthCoordinator: TutorialCoordinatorProtocol { + + func wireframe(_ wireframe: TutorialWireFrame, didEndWithState state: TutorialWireFrame.State) { + switch state { + case .getStarted: + showAuth() + } + } } // MARK: - Auth diff --git a/Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/AuthProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Entities/EmailTextController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Entities/EmailTextController.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Entities/LoginFlow.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Entities/LoginFlow.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Entities/PlainLoginOption.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PlainLoginOption.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Entities/PlainLoginOption.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PlainLoginOption.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Entities/Validator.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/Validator.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Entities/Validator.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/Validator.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Interactor/AuthInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Presenter/AuthPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/View/AuthViewController.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift diff --git a/Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift similarity index 100% rename from Nynja/Modules/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift rename to Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift rename to Nynja/Modules/Flows/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift diff --git a/Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift rename to Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/CreateProfileProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/ProfileField.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/Entities/ProfileField.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/ProfileField.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/View/CreateProfileViewController.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift diff --git a/Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift similarity index 100% rename from Nynja/Modules/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift rename to Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift diff --git a/Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/FacebookAuthProtocols.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Facebook/FacebookAuthProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/Facebook/FacebookAuthProtocols.swift diff --git a/Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift diff --git a/Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift diff --git a/Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/View/FacebookAuthViewController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Facebook/View/FacebookAuthViewController.swift rename to Nynja/Modules/Flows/Auth Flow/Facebook/View/FacebookAuthViewController.swift diff --git a/Nynja/Modules/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift rename to Nynja/Modules/Flows/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift diff --git a/Nynja/Modules/Auth Flow/Login/Interactor/LoginInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Login/Interactor/LoginInteractor.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/Interactor/LoginInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/Login/Interactor/LoginInteractor.swift diff --git a/Nynja/Modules/Auth Flow/Login/Interactor/Modelka.swift b/Nynja/Modules/Flows/Auth Flow/Login/Interactor/Modelka.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/Interactor/Modelka.swift rename to Nynja/Modules/Flows/Auth Flow/Login/Interactor/Modelka.swift diff --git a/Nynja/Modules/Auth Flow/Login/LoginProtocols.swift b/Nynja/Modules/Flows/Auth Flow/Login/LoginProtocols.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/LoginProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/Login/LoginProtocols.swift diff --git a/Nynja/Modules/Auth Flow/Login/Presenter/LoginPresenter.swift b/Nynja/Modules/Flows/Auth Flow/Login/Presenter/LoginPresenter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/Presenter/LoginPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/Login/Presenter/LoginPresenter.swift diff --git a/Nynja/Modules/Auth Flow/Login/View/LoginViewController.swift b/Nynja/Modules/Flows/Auth Flow/Login/View/LoginViewController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/View/LoginViewController.swift rename to Nynja/Modules/Flows/Auth Flow/Login/View/LoginViewController.swift diff --git a/Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDataSource.swift b/Nynja/Modules/Flows/Auth Flow/Login/View/LoginWheelContainerDataSource.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDataSource.swift rename to Nynja/Modules/Flows/Auth Flow/Login/View/LoginWheelContainerDataSource.swift diff --git a/Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDelegate.swift b/Nynja/Modules/Flows/Auth Flow/Login/View/LoginWheelContainerDelegate.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/View/LoginWheelContainerDelegate.swift rename to Nynja/Modules/Flows/Auth Flow/Login/View/LoginWheelContainerDelegate.swift diff --git a/Nynja/Modules/Auth Flow/Login/WireFrame/LoginWireframe.swift b/Nynja/Modules/Flows/Auth Flow/Login/WireFrame/LoginWireframe.swift similarity index 100% rename from Nynja/Modules/Auth Flow/Login/WireFrame/LoginWireframe.swift rename to Nynja/Modules/Flows/Auth Flow/Login/WireFrame/LoginWireframe.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/Entities/CountriesSection.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/CountriesSection.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/Entities/CountriesSection.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/CountriesSection.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/Entities/Country.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/Country.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/Entities/Country.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/Country.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/SelectCountryProtocols.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/SelectCountryProtocols.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/SelectCountryProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/SelectCountryProtocols.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift diff --git a/Nynja/Modules/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift similarity index 100% rename from Nynja/Modules/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift diff --git a/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift new file mode 100644 index 000000000..6c692cd67 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift @@ -0,0 +1,125 @@ +// +// SplashSplashInteractor.swift +// Nynja +// +// Created by Anton Makarov on 23/08/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +final class SplashInteractor: SplashInteractorInput, InitializeInjectable { + + private weak var presenter: SplashInteractorOutput? + + // MARK: - Services + + private let storageService: StorageService + private let mqttService: MQTTService + private let badgeService: BadgeNumberServiceProtocol + private let callService: NynjaCommunicatorService + + + // MARK: - Init + + struct Dependencies { + let presenter: SplashInteractorOutput + let storageService: StorageService + let mqttService: MQTTService + let badgeService: BadgeNumberServiceProtocol + let callService: NynjaCommunicatorService + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + storageService = dependencies.storageService + mqttService = dependencies.mqttService + badgeService = dependencies.badgeService + callService = dependencies.callService + } + + + // MARK: - Interactor Input + + func showed() { + checkJailbreak { [weak self] in + self?.setupFlow() + } + } + + private func checkJailbreak(with completion: @escaping () -> Void) { + if UIDevice.current.isJailbroken { + presenter?.showJailbreakAlert(with: completion) + } else { + setupFlow() + } + } + + private func setupFlow() { + decideRoute() + configureDependencies() + } + + private func configureDependencies() { + let application = UIApplication.shared + + guard let appDelegate = application.delegate else { + return + } + badgeService.initCounters() + badgeService.observeBadgeNumber(appDelegate) { badgeNumber in + application.applicationIconBadgeNumber = Int(badgeNumber) + } + mqttService.connect() + callService.initialize() + + MediaDownloadManager.setupAppDataUsageSettingsIfNeeded() + } + + private func decideRoute() { + guard storageService.wasLogined else { + AppLanguage.current = .english + presenter?.showTutorial() + return + } + + guard storageService.isUserLogined, let identityId = storageService.identityId else { + let hasIdentity = storageService.hasIdentity + let hasToken = storageService.hasToken + + LogService.log(topic: .db) { [weak self] in + """ + Clear storage: { + hasIdentity = \(hasIdentity), + hasToken = \(hasToken), + identityId = nil + } + """ + } + showAuth() + return + } + + LogService.log(topic: .db) { "Setup DB: Splash" } + storageService.setupDatabase(with: identityId, application: UIApplication.shared) + + guard let contact = ContactDAO.currentContact else { + LogService.log(topic: .db) { "Clear storage: can't find current contact" } + showAuth() + return + } + + if contact.hasName { + presenter?.showMain() + } else { + presenter?.showCreateProfile() + } + } + + private func showAuth() { + LogService.log(topic: .db) { "Clear storage: Splash" } + + storageService.clearStorage() + mqttService.reconnect() + + presenter?.showAuth() + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/Splash/Presenter/SplashPresenter.swift b/Nynja/Modules/Flows/Auth Flow/Splash/Presenter/SplashPresenter.swift new file mode 100644 index 000000000..8945b6509 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Splash/Presenter/SplashPresenter.swift @@ -0,0 +1,63 @@ +// +// SplashSplashPresenter.swift +// Nynja +// +// Created by Anton Makarov on 23/08/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +final class SplashPresenter: BasePresenter, SplashPresenterProtocol, SplashInteractorOutput { + + override var itemsFactory: WCItemsFactory? { + return AboutItemsFactory() + } + + private weak var view: SplashViewInput? + private var interactor: SplashInteractorInput! + private var wireframe: SplashWireFrameProtocol! + + + // MARK: - Presenter + + func showed() { + interactor.showed() + } + + + // MARK: - Interactor Output + + func showAuth() { + wireframe.showAuth() + } + + func showTutorial() { + wireframe.showTutorial() + } + + func showMain() { + wireframe.showMain() + } + + func showCreateProfile() { + wireframe.showCreateProfile() + } + + func showJailbreakAlert(with completion: @escaping () -> Void) { + AlertManager.sharedInstance.showAlertOk(message: String.localizable.yourDeviceIsRooted, completion: completion) + } +} + +extension SplashPresenter: SetInjectable { + + struct Dependencies { + let view: SplashViewInput + let interactor: SplashInteractorInput + let wireframe: SplashWireFrameProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/Splash/SplashProtocols.swift b/Nynja/Modules/Flows/Auth Flow/Splash/SplashProtocols.swift new file mode 100644 index 000000000..b1a1a0bed --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Splash/SplashProtocols.swift @@ -0,0 +1,45 @@ +// +// SplashSplashProtocols.swift +// Nynja +// +// Created by Anton Makarov on 23/08/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import UIKit + +// MARK: - Wireframe + +protocol SplashWireFrameProtocol: class { + func showAuth() + func showTutorial() + func showCreateProfile() + func showMain() +} + +// MARK: - View + +protocol SplashViewInput: class { +} + +// MARK: - Presenter + +protocol SplashPresenterProtocol: BasePresenterProtocol { + func showed() +} + +// MARK: - Interactor + +// MARK: Input +protocol SplashInteractorOutput: class { + func showAuth() + func showTutorial() + func showCreateProfile() + func showMain() + func showJailbreakAlert(with completion: @escaping () -> Void) +} + +// MARK: Output +protocol SplashInteractorInput: class { + func showed() +} diff --git a/Nynja/Modules/Auth Flow/Splash/View/SplashViewController.swift b/Nynja/Modules/Flows/Auth Flow/Splash/View/SplashViewController.swift similarity index 97% rename from Nynja/Modules/Auth Flow/Splash/View/SplashViewController.swift rename to Nynja/Modules/Flows/Auth Flow/Splash/View/SplashViewController.swift index 43a74e7c4..87106ccc5 100644 --- a/Nynja/Modules/Auth Flow/Splash/View/SplashViewController.swift +++ b/Nynja/Modules/Flows/Auth Flow/Splash/View/SplashViewController.swift @@ -8,7 +8,7 @@ import UIKit -class SplashViewController: BaseVC, SplashViewProtocol { +final class SplashViewController: BaseVC, SplashViewInput { var presenter: SplashPresenterProtocol! { didSet { diff --git a/Nynja/Modules/Flows/Auth Flow/Splash/WireFrame/SplashWireframe.swift b/Nynja/Modules/Flows/Auth Flow/Splash/WireFrame/SplashWireframe.swift new file mode 100644 index 000000000..ed71b205e --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Splash/WireFrame/SplashWireframe.swift @@ -0,0 +1,71 @@ +// +// SplashSplashWireframe.swift +// Nynja +// +// Created by Anton Makarov on 23/08/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol SplashCoordinatorProtocol: class { + func wireframe(_ wireframe: SplashWireframe, didEndWithState state: SplashWireframe.State) +} + +final class SplashWireframe: Wireframe, SplashWireFrameProtocol { + + private let coordinator: SplashCoordinatorProtocol + + init(coordinator: SplashCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Dependencies { + let storageService: StorageService + let mqttService: MQTTService + let badgeService: BadgeNumberServiceProtocol + let callService: NynjaCommunicatorService + } + + enum State { + case showTutorial + case showAuth + case showCreateProfile + case showMain(isRegistered: Bool, checkSession: Bool) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = SplashPresenter() + + let view = SplashViewController() + view.presenter = presenter + + let interactor = SplashInteractor(dependencies: .init( + presenter: presenter, + storageService: dependencies.storageService, + mqttService: dependencies.mqttService, + badgeService: dependencies.badgeService, + callService: dependencies.callService) + ) + + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + + return view + } + + func showAuth() { + coordinator.wireframe(self, didEndWithState: .showAuth) + } + + func showTutorial() { + coordinator.wireframe(self, didEndWithState: .showTutorial) + } + + func showMain() { + coordinator.wireframe(self, didEndWithState: .showMain(isRegistered: false, checkSession: true)) + } + + func showCreateProfile() { + coordinator.wireframe(self, didEndWithState: .showCreateProfile) + } +} diff --git a/Nynja/Modules/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift similarity index 100% rename from Nynja/Modules/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift diff --git a/Nynja/Modules/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift similarity index 100% rename from Nynja/Modules/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift diff --git a/Nynja/Modules/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift similarity index 100% rename from Nynja/Modules/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift diff --git a/Nynja/Modules/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift similarity index 100% rename from Nynja/Modules/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift diff --git a/Nynja/Modules/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift similarity index 100% rename from Nynja/Modules/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 65648c7d7..9cdfe6fed 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -145,13 +145,7 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega MapSearchWireFrame().presentMapSearch(navigation: navigation, delegate: delegate!) } } - - func showEditProfile(isRegistered: Bool = false) { - if let navigation = self.navigation { - EditProfileWireFrame().presentEditProfile(navigation: navigation, isRegistered: isRegistered, main: self) - } - } - + func showHistory() { navigation?.popToRootViewController(animated: false) HistoryWireFrame().presentHistory(navigation: contentNavigation, mainWireFrame: self) diff --git a/Nynja/Modules/tutorial/TutorialProtocols.swift b/Nynja/Modules/tutorial/TutorialProtocols.swift index d385b843f..98903084e 100644 --- a/Nynja/Modules/tutorial/TutorialProtocols.swift +++ b/Nynja/Modules/tutorial/TutorialProtocols.swift @@ -9,9 +9,7 @@ import UIKit protocol TutorialWireFrameProtocol: class { - - func presentTutorial(navigation: UINavigationController) - + /** * Add here your methods for communication PRESENTER -> WIREFRAME */ diff --git a/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift b/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift index 5ca5deb81..73c778f89 100644 --- a/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift +++ b/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift @@ -8,29 +8,42 @@ import UIKit -class TutorialWireFrame: TutorialWireFrameProtocol { +protocol TutorialCoordinatorProtocol: class { + func wireframe(_ wireframe: TutorialWireFrame, didEndWithState state: TutorialWireFrame.State) +} + +final class TutorialWireFrame: Wireframe, TutorialWireFrameProtocol { + + weak var navigation: UINavigationController? + + private let coordinator: TutorialCoordinatorProtocol + + init(coordinator: TutorialCoordinatorProtocol, navigation: UINavigationController) { + self.coordinator = coordinator + self.navigation = navigation + } - weak var navigation : UINavigationController? + enum State { + case getStarted + } - func presentTutorial(navigation: UINavigationController) { + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let view = TutorialViewController() let presenter = TutorialPresenter() let interactor = TutorialInteractor() - self.navigation = navigation - - // Connecting view.presenter = presenter + presenter.view = view presenter.wireFrame = self presenter.interactor = interactor + interactor.presenter = presenter - navigation.pushViewController(view as UIViewController, animated: true) + return view } func getStarted() { - guard let navigation = navigation else { return } - LoginWireFrame().presentLogin(navigation: navigation) + coordinator.wireframe(self, didEndWithState: .getStarted) } } diff --git a/Nynja/UserInfo.swift b/Nynja/UserInfo.swift index 8f5acc082..c45ae191a 100644 --- a/Nynja/UserInfo.swift +++ b/Nynja/UserInfo.swift @@ -61,6 +61,10 @@ extension UserInfo { return refreshToken != nil } + var hasIdentity: Bool { + return identityId != nil + } + var hasPhone: Bool { return phone != nil } -- GitLab From 354dfa8937152fbd914e03bc8ce74eb0efa52cf9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 6 Dec 2018 16:07:29 +0200 Subject: [PATCH 131/138] Added migration for Account and ContactInfo tables. --- Nynja.xcodeproj/project.pbxproj | 4 ++++ .../AddAccountAndContactInfoTables.swift | 17 +++++++++++++++++ .../MigrationsProvider/MigrationsProvider.swift | 1 - .../MigrationsProviderImpl.swift | 3 ++- 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 Nynja/MigrationManager/Migrations/AddAccountAndContactInfoTables.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index e62b9e86f..0286e35ad 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -554,6 +554,7 @@ 3AA13C761F2252F900BE5D8F /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA13C751F2252F900BE5D8F /* SearchModel.swift */; }; 3AA4E6ACDBCB060172A7A279 /* FavoritesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 462440AD41D807CE8957FDD9 /* FavoritesProtocols.swift */; }; 3AAA92AE21B1A6C800EF5F1E /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */; }; + 3AB73FFA21B962F200D1E967 /* AddAccountAndContactInfoTables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB73FF921B962F200D1E967 /* AddAccountAndContactInfoTables.swift */; }; 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */; }; 3ABCE9061EC9357900A80B15 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */; }; 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC07E3B1F055B3F00ADBE26 /* DoubleExtensions.swift */; }; @@ -2998,6 +2999,7 @@ 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContainerView.swift; sourceTree = ""; }; 3AA13C751F2252F900BE5D8F /* SearchModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 3AB73FF921B962F200D1E967 /* AddAccountAndContactInfoTables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountAndContactInfoTables.swift; sourceTree = ""; }; 3ABCE8ED1EC9330D00A80B15 /* Nynja.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nynja.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; @@ -7277,6 +7279,7 @@ 4B2C502F21B56AE900FBA9B1 /* CorrectMessageIdTypeInStarTable.swift */, 4B2C503121B56B2300FBA9B1 /* RemoveRoomMemberTable.swift */, 4B2C503821B573A100FBA9B1 /* RemoveP2pAndMucTables.swift */, + 3AB73FF921B962F200D1E967 /* AddAccountAndContactInfoTables.swift */, ); path = Migrations; sourceTree = ""; @@ -17120,6 +17123,7 @@ A42CE5A920692EDB000889CC /* Decoder.swift in Sources */, 8503B53320504B93006F0593 /* TextSwitchCellModel.swift in Sources */, E7ABD3011FC2EF3800E233F7 /* StarTable.swift in Sources */, + 3AB73FFA21B962F200D1E967 /* AddAccountAndContactInfoTables.swift in Sources */, A43B25A220AB1DFA00FF8107 /* EditField.swift in Sources */, A45F114020B4218D00F45004 /* PresenceStatusProvider.swift in Sources */, 3A0A4C5B21B91D9000BA0D09 /* DeleteAccountErrors.swift in Sources */, diff --git a/Nynja/MigrationManager/Migrations/AddAccountAndContactInfoTables.swift b/Nynja/MigrationManager/Migrations/AddAccountAndContactInfoTables.swift new file mode 100644 index 000000000..0a3a8611a --- /dev/null +++ b/Nynja/MigrationManager/Migrations/AddAccountAndContactInfoTables.swift @@ -0,0 +1,17 @@ +// +// AddAccountAndContactInfoTables.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +final class AddAccountAndContactInfoTables: Migration { + + func migrate(_ db: Database) throws { + try AccountTable.createIfNotExists(in: db) + try ContactInfoTable.createIfNotExists(in: db) + } +} diff --git a/Nynja/MigrationManager/MigrationsProvider/MigrationsProvider.swift b/Nynja/MigrationManager/MigrationsProvider/MigrationsProvider.swift index 8f43f03ed..bd205802e 100644 --- a/Nynja/MigrationManager/MigrationsProvider/MigrationsProvider.swift +++ b/Nynja/MigrationManager/MigrationsProvider/MigrationsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // - protocol MigrationsProvider { var migrations: [Migration] { get } var migrationTitles: [String] { get } diff --git a/Nynja/MigrationManager/MigrationsProvider/MigrationsProviderImpl.swift b/Nynja/MigrationManager/MigrationsProvider/MigrationsProviderImpl.swift index cdc25bc2c..4e7d90262 100644 --- a/Nynja/MigrationManager/MigrationsProvider/MigrationsProviderImpl.swift +++ b/Nynja/MigrationManager/MigrationsProvider/MigrationsProviderImpl.swift @@ -14,6 +14,7 @@ final class MigrationsProviderImpl: MigrationsProvider { AddAutoColumnToConvertMessage(), CorrectMessageIdTypeInStarTable(), RemoveRoomMemberTable(), - RemoveP2pAndMucTables() + RemoveP2pAndMucTables(), + AddAccountAndContactInfoTables() ] } -- GitLab From 72015d8e2256796e324bce3ed6ab8b22eb701ab8 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 6 Dec 2018 18:50:15 +0200 Subject: [PATCH 132/138] [Multiple accounts] Social login flow updates (#1517) [NY-5503] Social login flow updates + Facebook SDK integration --- Nynja/CountriesProvider.swift | 4 + Nynja/CountriesProviding.swift | 1 + .../Flows/Auth Flow/AuthCoordinator.swift | 164 ++++++++---------- .../Auth Flow/AuthModule/AuthProtocols.swift | 7 +- .../AuthModule/Entities/LoginFlow.swift | 4 +- .../Interactor/AuthInteractor.swift | 110 ++++++++---- .../AuthModule/Presenter/AuthPresenter.swift | 45 ++++- .../AuthModule/Wireframe/AuthWireframe.swift | 14 +- .../CodeConfirmationInteractor.swift | 2 +- .../Intreractor/FacebookAuthInteractor.swift | 4 +- .../Auth/Entities/AuthenticationType.swift | 3 + Nynja/SDK/Auth/Service/AuthService.swift | 8 +- Nynja/SDK/Auth/Service/AuthServiceImpl.swift | 72 +++++--- 13 files changed, 267 insertions(+), 171 deletions(-) diff --git a/Nynja/CountriesProvider.swift b/Nynja/CountriesProvider.swift index 883af2c6f..25a529748 100644 --- a/Nynja/CountriesProvider.swift +++ b/Nynja/CountriesProvider.swift @@ -23,6 +23,10 @@ final class CountriesProvider: CountriesProviding { .sorted { $0.name > $1.name } } + func fetchCountry(by countryCode: String) -> Country? { + return fetchCountries().first { $0.code == countryCode } + } + func fetchDefaultCountry() -> Country { let countries = fetchCountries() let regionCode = NSLocale.current.regionCode ?? countries.last?.code diff --git a/Nynja/CountriesProviding.swift b/Nynja/CountriesProviding.swift index b0e5837bb..5e76894c0 100644 --- a/Nynja/CountriesProviding.swift +++ b/Nynja/CountriesProviding.swift @@ -8,5 +8,6 @@ protocol CountriesProviding { func fetchCountries() -> [Country] + func fetchCountry(by countryCode: String) -> Country? func fetchDefaultCountry() -> Country } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift index 3724b668c..004a070b2 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift @@ -25,8 +25,6 @@ final class AuthCoordinator: Coordinator, NavigationContainer { private let serviceFactory: ServiceFactoryProtocol - private var inputConfirmationCallback: ((Bool) -> Void)? - private var selectCountryCallback: ((Result) -> Void)? private var facebookAuthCodeCallback: ((Result) -> Void)? @@ -37,15 +35,7 @@ final class AuthCoordinator: Coordinator, NavigationContainer { } func start() { - let wireframe = SplashWireframe(coordinator: self) - let view = wireframe.prepareModule( - dependencies: .init( - storageService: serviceFactory.makeStorageService(), - mqttService: serviceFactory.makeMQTTService(), - badgeService: serviceFactory.makeBadgeNumberService(), - callService: serviceFactory.makeNynjaCommunicatorService() - ) - ) + let view = makeSplashView() navigation.pushViewController(view, animated: false) } @@ -58,11 +48,25 @@ final class AuthCoordinator: Coordinator, NavigationContainer { delegate?.authCoordinatorDidFinish(self) } + private func makeSplashView() -> UIViewController { + let wireframe = SplashWireframe(coordinator: self) + let view = wireframe.prepareModule( + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + mqttService: serviceFactory.makeMQTTService(), + badgeService: serviceFactory.makeBadgeNumberService(), + callService: serviceFactory.makeNynjaCommunicatorService() + ) + ) + return view + } + private func makeAuthView() -> UIViewController { let wireframe = AuthWireframe(coordinator: self) let view = wireframe.prepareModule( dependencies: .init( authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService(), googleAuthService: serviceFactory.makeGoogleAuthService(), countriesProvider: serviceFactory.makeCountriesProvider() ) @@ -81,7 +85,7 @@ extension AuthCoordinator: SplashCoordinatorProtocol { showTutorial() case .showAuth, .showCreateProfile: - // TODO: show create profile if needed. + // TODO: show create profile if needed in new UI flow. showAuth() case let .showMain(isRegistered, checkSession): @@ -117,33 +121,19 @@ extension AuthCoordinator: TutorialCoordinatorProtocol { // MARK: - Auth extension AuthCoordinator: AuthCoordinatorProtocol { + func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) { switch state { - case let .confirmInputData(loginOption, confirmationHandler): - inputConfirmationCallback = confirmationHandler - showConfirmationPopup(loginOption: loginOption) - case let .continueLogin(loginFlow): continueLoginProcess(with: loginFlow) case let .selectCountry(callback): selectCountryCallback = callback - - let wireframe = SelectCountryWireFrame(coordinator: self) - let view = wireframe.prepareModule( - dependencies: .init( - countriesProvider: serviceFactory.makeCountriesProvider() - ) - ) - - navigation.pushViewController(view, animated: true) + showCountrySelector() case let .showFacebookAuth(callback): - let wireframe = FacebookAuthWireframe(coordinator: self) - let view = wireframe.prepareModule() - facebookAuthCodeCallback = callback - navigation.pushViewController(view, animated: true) + showFacebookAuth() case let .present(viewController): navigation.present(viewController, animated: true) @@ -156,72 +146,48 @@ extension AuthCoordinator: AuthCoordinatorProtocol { private func continueLoginProcess(with loginFlow: LoginFlow) { switch loginFlow { case let .email(email): - let wireframe = CodeConfirmationWireframe(coordinator: self) - let view = wireframe.prepareModule( - parameters: .init(confirmationData: .email(email), isLogoVisible: true), - dependencies: .init( - storageService: serviceFactory.makeStorageService(), - authService: serviceFactory.makeAuthService(), - accountService: serviceFactory.makeAccountService() - ) - ) - navigation.pushViewController(view, animated: true) + showCodeConfirmation(with: .email(email)) case let .phoneNumber(numberInfo): - let wireframe = CodeConfirmationWireframe(coordinator: self) - let view = wireframe.prepareModule( - parameters: .init(confirmationData: .phoneNumber(numberInfo), isLogoVisible: true), - dependencies: .init( - storageService: serviceFactory.makeStorageService(), - authService: serviceFactory.makeAuthService(), - accountService: serviceFactory.makeAccountService() - ) - ) - navigation.pushViewController(view, animated: true) + showCodeConfirmation(with: .phoneNumber(numberInfo)) - case .google: - break - - case .facebook: - break + case let .google(accountId, authenticationType), let .facebook(accountId, authenticationType): + switch authenticationType { + case .register: + showCreateProfile(with: accountId) + case .login: + end() + } } } - private func showConfirmationPopup(loginOption: PlainLoginOption) { - let popup = UIAlertController(title: titleForPopup(loginOption: loginOption), - message: messageForPopup(loginOption: loginOption), - preferredStyle: .alert) - - let modify = UIAlertAction(title: String.localizable.authPopupModifyAction, style: .cancel) { [weak self] _ in - self?.inputConfirmationCallback?(false) - self?.inputConfirmationCallback = nil - } - let confirm = UIAlertAction.init(title: String.localizable.authPopupConfirmAction, style: .default) { [weak self] _ in - self?.inputConfirmationCallback?(true) - self?.inputConfirmationCallback = nil - } - - [modify, confirm].forEach { popup.addAction($0) } - - navigation.present(popup, animated: true, completion: nil) + private func showCodeConfirmation(with confirmationData: ConfirmationData) { + let wireframe = CodeConfirmationWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(confirmationData: confirmationData, isLogoVisible: true), + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService() + ) + ) + navigation.pushViewController(view, animated: true) } - private func titleForPopup(loginOption: PlainLoginOption) -> String { - switch loginOption { - case .email: - return String.localizable.authPopupConfirmEmailTitle - case .phoneNumber: - return String.localizable.authPopupConfirmPhoneTitle - } + private func showCountrySelector() { + let wireframe = SelectCountryWireFrame(coordinator: self) + let view = wireframe.prepareModule( + dependencies: .init( + countriesProvider: serviceFactory.makeCountriesProvider() + ) + ) + navigation.pushViewController(view, animated: true) } - private func messageForPopup(loginOption: PlainLoginOption) -> String { - switch loginOption { - case let .email(email): - return email - case let .phoneNumber(number): - return number - } + private func showFacebookAuth() { + let wireframe = FacebookAuthWireframe(coordinator: self) + let view = wireframe.prepareModule() + navigation.pushViewController(view, animated: true) } } @@ -259,6 +225,7 @@ extension AuthCoordinator: CountrySelectorCoordinatorProtocol { // MARK: - Code Confirmation extension AuthCoordinator: CodeConfirmationCoordinatorProtocol { + func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { switch state { case .back: @@ -266,19 +233,23 @@ extension AuthCoordinator: CodeConfirmationCoordinatorProtocol { case .loggedIn: end() case let .registered(accountId): - let wireframe = CreateProfileWireframe(coordinator: self) - let view = wireframe.prepareModule( - parameters: .init(accountId: accountId), - dependencies: .init( - storageService: serviceFactory.makeStorageService(), - imageUploader: serviceFactory.makeImageUploader(), - authService: serviceFactory.makeAuthService(), - accountService: serviceFactory.makeAccountService() - ) - ) - navigation.pushViewController(view, animated: true) + showCreateProfile(with: accountId) } } + + private func showCreateProfile(with accountId: String) { + let wireframe = CreateProfileWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(accountId: accountId), + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + imageUploader: serviceFactory.makeImageUploader(), + authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService() + ) + ) + navigation.pushViewController(view, animated: true) + } } // MARK: - Create Profile @@ -320,6 +291,7 @@ extension AuthCoordinator: CreateProfileCoordinatorProtocol { } } + // FIXME: should be in presenter private func showAvatarSourceOptionPopup(completion: @escaping (Result) -> Void) { enum AvatarSourceError: Error { case cancelled diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift index 27bc057f4..b5cca653e 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift @@ -13,9 +13,8 @@ import NynjaUIKit protocol AuthWireframeProtocol: AlertDisplayable { func selectCountry(completion: @escaping (Result) -> Void) - func confirmInputData(loginOption: PlainLoginOption, confirmationHandler: @escaping (Bool) -> Void) - func continueLogin(loginFlow: LoginFlow) func showFacebookAuth(completion: @escaping (Result) -> Void) + func continueLogin(loginFlow: LoginFlow) func present(_ viewController: UIViewController) func dismiss(_ viewController: UIViewController) @@ -53,10 +52,10 @@ protocol AuthInteractorInput: class { func fetchDefaultCountry() -> Country func fetchCountry(by code: String) -> Country? + func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) + func loginViaEmail(_ email: String) func loginViaFacebook(code: String) func loginViaGoogle() - func loginViaEmail(_ email: String) - func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) } // MARK: Output diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift index e027bebac..b1ae231fb 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift @@ -9,6 +9,6 @@ enum LoginFlow { case phoneNumber(PhoneNumberInfo) case email(String) - case facebook - case google + case facebook(accountId: String, authenticationType: AuthenticationType) + case google(accountId: String, authenticationType: AuthenticationType) } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift index 10f5527f7..eefae8801 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift @@ -15,7 +15,11 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { // MARK: - Services private let authService: AuthService + + private let accountService: AccountService + private let googleAuthService: GoogleAuthService + private let countriesProvider: CountriesProviding @@ -24,6 +28,7 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { struct Dependencies { let presenter: AuthInteractorOutput let authService: AuthService + let accountService: AccountService let googleAuthService: GoogleAuthService let countriesProvider: CountriesProviding } @@ -31,6 +36,7 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { init(dependencies: Dependencies) { presenter = dependencies.presenter authService = dependencies.authService + accountService = dependencies.accountService googleAuthService = dependencies.googleAuthService countriesProvider = dependencies.countriesProvider } @@ -43,42 +49,20 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { } func fetchCountry(by code: String) -> Country? { - return countriesProvider.fetchCountries().first { $0.code == code } + return countriesProvider.fetchCountry(by: code) } - func loginViaFacebook(code: String) { - authService.loginByFacebook(serverCode: code) { [weak self] result in + func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) { + authService.login(by: phoneNumberInfo, confirmVia: .sms) { [weak self] result in switch result { case .success: - self?.presenter?.didAuthenticated(with: .facebook) + self?.presenter?.didAuthenticated(with: .phoneNumber(phoneNumberInfo)) case let .failure(error): self?.presenter?.didReceiveAuthenticationFailure(error) } } } - func loginViaGoogle() { - googleAuthService.signIn { [weak self] result in - guard let self = self else { return } - - switch result { - case let .success(code): - self.authService.loginByGoogle(serverCode: code) { [weak self] googleResult in - guard let self = self else { return } - - switch googleResult { - case .success: - self.presenter?.didAuthenticated(with: .google) - case let .failure(error): - self.presenter?.didReceiveAuthenticationFailure(error) - } - } - case let .failure(error): - self.presenter?.didReceiveAuthenticationFailure(error) - } - } - } - func loginViaEmail(_ email: String) { authService.login(by: email) { [weak self] result in switch result { @@ -90,14 +74,80 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { } } - func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) { - authService.login(by: phoneNumberInfo, confirmVia: .sms) { [weak self] result in + func loginViaFacebook(code: String) { + authService.loginByFacebook(serverCode: code) { [weak self] result in + guard let self = self else { return } + + self.handleSocialAuthResponse(result: result) { authResult in + switch authResult { + case let .success(accountId, authenticationType): + self.presenter?.didAuthenticated(with: .facebook(accountId: accountId, authenticationType: authenticationType)) + case let .failure(error): + self.presenter?.didReceiveAuthenticationFailure(error) + } + } + } + } + + func loginViaGoogle() { + googleAuthService.signIn { [weak self] result in switch result { - case .success: - self?.presenter?.didAuthenticated(with: .phoneNumber(phoneNumberInfo)) + case let .success(code): + self?.confirmGoogleCode(code) case let .failure(error): self?.presenter?.didReceiveAuthenticationFailure(error) } } } + + private func confirmGoogleCode(_ serverCode: String) { + authService.loginByGoogle(serverCode: serverCode) { [weak self] result in + guard let self = self else { return } + + self.handleSocialAuthResponse(result: result) { authResult in + switch authResult { + case let .success(accountId, authenticationType): + self.presenter?.didAuthenticated(with: .google(accountId: accountId, authenticationType: authenticationType)) + case let .failure(error): + self.presenter?.didReceiveAuthenticationFailure(error) + } + } + } + } + + /// Generic method for social auth processing + /// + /// - Parameters: + /// - completion: accountId + authenticationType for success case, otherwise - error. + private func handleSocialAuthResponse(result: Result, completion: @escaping (Result<(String, AuthenticationType)>) -> Void) { + switch result { + case let .success(authResponse): + let accountId = authResponse.accountId + let authType = authResponse.authenticationType + + + switch authType { + case .login: + accountService.getAccount(by: accountId) { [weak self] accountLoadingResult in + guard let self = self else { return } + + switch accountLoadingResult { + case let .success(account): + do { + try self.authService.processAuthenticatedAccount(account) + completion(.success((accountId, authType))) + } catch { + completion(.failure(error)) + } + case let .failure(error): + completion(.failure(error)) + } + } + case .register: + completion(.success((accountId, authType))) + } + case let .failure(error): + completion(.failure(error)) + } + } } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift index 941a4561e..862f799f5 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift @@ -10,10 +10,13 @@ import Foundation import NynjaUIKit final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAuthServiceUIDelegate { + private weak var view: AuthViewInput? private var interactor: AuthInteractorInput! private var wireframe: AuthWireframeProtocol! + // MARK: - Presenter + private(set) var loginOption: PlainLoginOption = .phoneNumber("") private(set) lazy var selectedCountry: Country = { @@ -46,7 +49,8 @@ final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAu func loginViaEmail(_ email: String) { let email = email.trimmed() - wireframe.confirmInputData(loginOption: .email(email)) { isConfirmed in + + confirmInputData(for: .email(email)) { isConfirmed in if isConfirmed { self.view?.showLoading() self.interactor.loginViaEmail(email) @@ -57,7 +61,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAu func loginViaPhoneNumber(_ phoneNumber: String, country: Country) { let numberInfo = PhoneNumberInfo(country: country, number: phoneNumber.replacingOccurrences(of: " ", with: "")) - wireframe.confirmInputData(loginOption: .phoneNumber(numberInfo.displayString)) { isConfirmed in + confirmInputData(for: .phoneNumber(numberInfo.displayString)) { isConfirmed in if isConfirmed { self.view?.showLoading() self.interactor.loginViaPhoneNumber(numberInfo) @@ -82,6 +86,41 @@ final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAu let phoneNumberInfo = PhoneNumberInfo(country: country, number: autofillInfo.phoneNumber) view?.update(phone: phoneNumberInfo) } + + private func confirmInputData(for loginOption: PlainLoginOption, completion: @escaping (_ isConfirmed: Bool) -> Void) { + let title = titleForPopup(loginOption: loginOption) + let message = messageForPopup(loginOption: loginOption) + + let modifyAction = Alert.Action(title: String.localizable.authPopupModifyAction, style: .default) { _ in + completion(false) + } + let confirmAction = Alert.Action(title: String.localizable.authPopupModifyAction, style: .default) { _ in + completion(true) + } + + let actions: [Alert.Action] = [modifyAction, confirmAction] + + let alert = Alert(title: title, message: message, actions: actions) + wireframe.present(alert) + } + + private func titleForPopup(loginOption: PlainLoginOption) -> String { + switch loginOption { + case .email: + return String.localizable.authPopupConfirmEmailTitle + case .phoneNumber: + return String.localizable.authPopupConfirmPhoneTitle + } + } + + private func messageForPopup(loginOption: PlainLoginOption) -> String { + switch loginOption { + case let .email(email): + return email + case let .phoneNumber(number): + return number + } + } } // MARK: - GoogleAuthServiceUIDelegate @@ -121,7 +160,7 @@ extension AuthPresenter { } } -// MARK: - SetInjectable +// MARK: - Injection extension AuthPresenter: SetInjectable { struct Dependencies { diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift index 92ed6c922..47dd06564 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift @@ -24,15 +24,15 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { struct Dependencies { let authService: AuthService + let accountService: AccountService let googleAuthService: GoogleAuthService let countriesProvider: CountriesProviding } enum State { case selectCountry(callback: (Result) -> Void) - case confirmInputData(loginOption: PlainLoginOption, confirmationHandler: (Bool) -> Void) - case continueLogin(loginFlow: LoginFlow) case showFacebookAuth(callback: (Result) -> Void) + case continueLogin(loginFlow: LoginFlow) case present(UIViewController) case dismiss(UIViewController) } @@ -45,6 +45,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { let interactor = AuthInteractor(dependencies: .init( presenter: presenter, authService: dependencies.authService, + accountService: dependencies.accountService, googleAuthService: dependencies.googleAuthService, countriesProvider: dependencies.countriesProvider) ) @@ -62,19 +63,14 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) } - func confirmInputData(loginOption: PlainLoginOption, confirmationHandler: @escaping (Bool) -> Void) { - coordinator.wireframe(self, didEndWithState: .confirmInputData(loginOption: loginOption, - confirmationHandler: confirmationHandler)) + func showFacebookAuth(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .showFacebookAuth(callback: completion)) } func continueLogin(loginFlow: LoginFlow) { coordinator.wireframe(self, didEndWithState: .continueLogin(loginFlow: loginFlow)) } - func showFacebookAuth(completion: @escaping (Result) -> Void) { - coordinator.wireframe(self, didEndWithState: .showFacebookAuth(callback: completion)) - } - func present(_ viewController: UIViewController) { coordinator.wireframe(self, didEndWithState: .present(viewController)) } diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index 93aec4d6f..3e16b7958 100644 --- a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -55,7 +55,7 @@ final class CodeConfirmationInteractor: CodeConfirmationInteractorInput, Initial // MARK: - Interactor Input func sendConfirmationCode(_ code: String) { - authService.confirm(code: code, with: "") { [weak self] result in + authService.confirmNynjaCode(code) { [weak self] result in switch result { case let .success(response): self?.presenter?.didConfirmCode(response: response) diff --git a/Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift index 6f08181b2..01c711009 100644 --- a/Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift @@ -17,8 +17,8 @@ final class FacebookAuthInteractor: FacebookAuthInteractorInput, InitializeInjec private let loginURL = "https://www.facebook.com/v3.1/dialog/oauth" private enum Request { - static let fbRedirectURL = "https://beta.nynja.net/oauth" - static let fbClientId = "915846355282708" + static let fbRedirectURL = "https://web.dev-eu.nynja.net/oauth/facebook" + static let fbClientId = "306118319994975" } private enum Parameters { diff --git a/Nynja/SDK/Auth/Entities/AuthenticationType.swift b/Nynja/SDK/Auth/Entities/AuthenticationType.swift index 095c711d8..c254b8892 100644 --- a/Nynja/SDK/Auth/Entities/AuthenticationType.swift +++ b/Nynja/SDK/Auth/Entities/AuthenticationType.swift @@ -7,6 +7,9 @@ // enum AuthenticationType { + /// User hasn't been registered yet case register + + /// User has already registered case login } diff --git a/Nynja/SDK/Auth/Service/AuthService.swift b/Nynja/SDK/Auth/Service/AuthService.swift index 48aa62561..b5fae6c84 100644 --- a/Nynja/SDK/Auth/Service/AuthService.swift +++ b/Nynja/SDK/Auth/Service/AuthService.swift @@ -18,11 +18,13 @@ protocol AuthService: class { func login(by phoneNumber: PhoneNumberInfo, confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) - func loginByFacebook(serverCode: String, completion: @escaping LoginCompletion) + func loginByFacebook(serverCode: String, completion: @escaping CodeConfirmationCompletion) - func loginByGoogle(serverCode: String, completion: @escaping LoginCompletion) + func loginByGoogle(serverCode: String, completion: @escaping CodeConfirmationCompletion) - func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) + func confirmNynjaCode(_ code: String, completion: @escaping CodeConfirmationCompletion) + + func confirmSocialServerAuthCode(_ code: String, completion: @escaping CodeConfirmationCompletion) func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) diff --git a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift index ef825bc72..b6999b493 100644 --- a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -31,6 +31,12 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog private var confirmCodeCompletion: CodeConfirmationCompletion? private var refreshTokenCompletion: RefreshTokenCompletion? + // MARK: - Properties + + private var appToken: String { + return appConfigurationProvider.sdkCredentials.appToken + } + // MARK: - Init @@ -53,9 +59,6 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog initialize() } - - // MARK: - API - private func initialize() { let credentials = appConfigurationProvider.sdkCredentials @@ -65,35 +68,63 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog withOrgId: credentials.bundleId) } + + // MARK: - API + func login(by email: String, completion: @escaping LoginCompletion) { bind(completion, to: \AuthServiceImpl.loginByEmailCompletion) - loginManager.sendLogin(byEmail: email, withAppToken: appConfigurationProvider.sdkCredentials.appToken) + loginManager.sendLogin(byEmail: email, withAppToken: appToken) } - func login(by numberInfo: PhoneNumberInfo, - confirmVia authConfirmationType: AuthConfirmationType, - completion: @escaping LoginCompletion) { - - bind(completion, to: \AuthServiceImpl.loginByPhoneCompletion) - + func login(by numberInfo: PhoneNumberInfo, confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) { let country = numberInfo.country let numberFormat = "\(country.ISO):\(country.code)\(numberInfo.number)" - let appToken = appConfigurationProvider.sdkCredentials.appToken + bind(completion, to: \AuthServiceImpl.loginByPhoneCompletion) loginManager.sendLogin(byPhone: numberFormat, withAppToken: appToken, withSendTokenVia: authConfirmationType.sdkValue) } - func loginByFacebook(serverCode: String, completion: @escaping LoginCompletion) { + func loginByFacebook(serverCode: String, completion: @escaping CodeConfirmationCompletion) { + _loginByFacebook { result in + switch result { + case .success: + self.confirmSocialServerAuthCode(serverCode, completion: completion) + case let .failure(error): + completion(.failure(error)) + } + } + } + + private func _loginByFacebook(completion: @escaping LoginCompletion) { bind(completion, to: \AuthServiceImpl.loginByFacebookCompletion) - loginManager.sendLoginByFacebook(withAppToken: serverCode) + loginManager.sendLoginByFacebook(withAppToken: appToken) + } + + func loginByGoogle(serverCode: String, completion: @escaping CodeConfirmationCompletion) { + _loginByGoogle { result in + switch result { + case .success: + self.confirmSocialServerAuthCode(serverCode, completion: completion) + case let .failure(error): + completion(.failure(error)) + } + } } - func loginByGoogle(serverCode: String, completion: @escaping LoginCompletion) { + private func _loginByGoogle(completion: @escaping LoginCompletion) { bind(completion, to: \AuthServiceImpl.loginByGoogleCompletion) - loginManager.sendLoginByGooglePlus(withAppToken: serverCode) + loginManager.sendLoginByGooglePlus(withAppToken: appToken) + } + + func confirmNynjaCode(_ code: String, completion: @escaping CodeConfirmationCompletion) { + confirm(code: code, with: nil, completion: completion) + } + + func confirmSocialServerAuthCode(_ code: String, completion: @escaping CodeConfirmationCompletion) { + confirm(code: "", with: code, completion: completion) } - func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) { + private func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) { bind(completion, to: \AuthServiceImpl.confirmCodeCompletion) loginManager.confirmCode(code, withCredential: socialToken) } @@ -106,13 +137,13 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog func processAuthenticatedAccount(_ account: Account) throws { let account = DBAccount(account: account) - if case let passcode = account.profileId, !passcode.isEmpty { - LogService.log(topic: .db) { return "Setup DB: Prifile Handler" } - storage.setupDatabase(with: passcode, application: UIApplication.shared) - } else { + guard case let passcode = account.profileId, !passcode.isEmpty else { assertionFailure("Unable to setup database") + return } + storage.setupDatabase(with: passcode, application: UIApplication.shared) + storage.clientId = appConfigurationProvider.sdkCredentials.deviceId storage.identityId = account.profileId storage.accountId = account.accountId @@ -123,7 +154,6 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog let roster = profile.rosters.first! storage.phone = roster.phone storage.rosterId = roster.id - storage.clientId = UIDevice.current.persistentIdentifier try storage.perform(action: .save, with: profile) } -- GitLab From 4ea4d330830069633aa7b2c985a359371b90eeaf Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 6 Dec 2018 19:06:41 +0200 Subject: [PATCH 133/138] [NY-5503] Update GoogleSignIn's GoogleServerClientId --- Nynja/Resources/DevAutoTests.xcconfig | 3 +-- Nynja/Resources/DevConfig.xcconfig | 3 +-- Nynja/Resources/LoadDBConfig.xcconfig | 13 +++++++++++++ Nynja/Resources/PrereleaseConfig.xcconfig | 3 +-- Nynja/Resources/PrereleaseDebugConfig.xcconfig | 11 +++++++++++ Nynja/Resources/ReleaseConfig.xcconfig | 3 +-- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Nynja/Resources/DevAutoTests.xcconfig b/Nynja/Resources/DevAutoTests.xcconfig index 2393b22d9..e9919324a 100644 --- a/Nynja/Resources/DevAutoTests.xcconfig +++ b/Nynja/Resources/DevAutoTests.xcconfig @@ -6,7 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // - BundleIdentifier = com.nynja.dev.mobile.communicator ExtensionBundleIdentifier = com.nynja.dev.mobile.communicator.NynjaShare ServerURL = dev.ci.nynja.net @@ -23,7 +22,7 @@ AssociatedDomain = applinks:join.dev-eu.nynja.net GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a -GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com +GoogleServerClientId = 139525496349-9ndqp3t69kudvseiovhsbqa4rn5o6gt8.apps.googleusercontent.com AUTH_SERVER_HOST = auth.dev-eu.nynja.net AUTH_SERVER_PORT = 443 diff --git a/Nynja/Resources/DevConfig.xcconfig b/Nynja/Resources/DevConfig.xcconfig index 2393b22d9..e9919324a 100644 --- a/Nynja/Resources/DevConfig.xcconfig +++ b/Nynja/Resources/DevConfig.xcconfig @@ -6,7 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // - BundleIdentifier = com.nynja.dev.mobile.communicator ExtensionBundleIdentifier = com.nynja.dev.mobile.communicator.NynjaShare ServerURL = dev.ci.nynja.net @@ -23,7 +22,7 @@ AssociatedDomain = applinks:join.dev-eu.nynja.net GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a -GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com +GoogleServerClientId = 139525496349-9ndqp3t69kudvseiovhsbqa4rn5o6gt8.apps.googleusercontent.com AUTH_SERVER_HOST = auth.dev-eu.nynja.net AUTH_SERVER_PORT = 443 diff --git a/Nynja/Resources/LoadDBConfig.xcconfig b/Nynja/Resources/LoadDBConfig.xcconfig index 7b7891380..1a33bb98c 100644 --- a/Nynja/Resources/LoadDBConfig.xcconfig +++ b/Nynja/Resources/LoadDBConfig.xcconfig @@ -18,3 +18,16 @@ isServerConnectionSecure = false ConfServerAddress = 35.198.118.190 ConfServerPort = 80 ConfServerSecure = false +AssociatedDomain = applinks:join.dev-eu.nynja.net + +GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com +ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a +GoogleServerClientId = 139525496349-9ndqp3t69kudvseiovhsbqa4rn5o6gt8.apps.googleusercontent.com + +AUTH_SERVER_HOST = auth.dev-eu.nynja.net +AUTH_SERVER_PORT = 443 +AUTH_SERVER_SECURE = true + +ACCOUNT_SERVER_HOST = account.dev-eu.nynja.net +ACCOUNT_SERVER_PORT = 443 +ACCOUNT_SERVER_SECURE = true diff --git a/Nynja/Resources/PrereleaseConfig.xcconfig b/Nynja/Resources/PrereleaseConfig.xcconfig index da3d479a0..984659371 100644 --- a/Nynja/Resources/PrereleaseConfig.xcconfig +++ b/Nynja/Resources/PrereleaseConfig.xcconfig @@ -6,7 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // - BundleIdentifier = com.nynja.rc.mobile.communicator ExtensionBundleIdentifier = com.nynja.rc.mobile.communicator.NynjaShare ServerURL = im.staging.nynja.net @@ -23,7 +22,7 @@ AssociatedDomain = applinks:join.staging.nynja.net GoogleClientId = 13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-hr88cvf22h5okn4233vnrdgjiktlkcng -GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com +GoogleServerClientId = 139525496349-9ndqp3t69kudvseiovhsbqa4rn5o6gt8.apps.googleusercontent.com AUTH_SERVER_HOST = auth.dev-eu.nynja.net AUTH_SERVER_PORT = 443 diff --git a/Nynja/Resources/PrereleaseDebugConfig.xcconfig b/Nynja/Resources/PrereleaseDebugConfig.xcconfig index af5e0d2c4..b15700dcf 100644 --- a/Nynja/Resources/PrereleaseDebugConfig.xcconfig +++ b/Nynja/Resources/PrereleaseDebugConfig.xcconfig @@ -23,3 +23,14 @@ ConfServerPort = 443 ConfServerSecure = true AssociatedDomain = applinks:join.staging.nynja.net +GoogleClientId = 13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a.apps.googleusercontent.com +ReversedGoogleClientId = com.googleusercontent.apps.13807320472-88dsdiehegt5a2cb7cps6sj14h2trl6a +GoogleServerClientId = 139525496349-9ndqp3t69kudvseiovhsbqa4rn5o6gt8.apps.googleusercontent.com + +AUTH_SERVER_HOST = auth.dev-eu.nynja.net +AUTH_SERVER_PORT = 443 +AUTH_SERVER_SECURE = true + +ACCOUNT_SERVER_HOST = account.dev-eu.nynja.net +ACCOUNT_SERVER_PORT = 443 +ACCOUNT_SERVER_SECURE = true diff --git a/Nynja/Resources/ReleaseConfig.xcconfig b/Nynja/Resources/ReleaseConfig.xcconfig index d54106771..de3a63baf 100644 --- a/Nynja/Resources/ReleaseConfig.xcconfig +++ b/Nynja/Resources/ReleaseConfig.xcconfig @@ -6,7 +6,6 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // - BundleIdentifier = com.nynja.mobile.communicator ExtensionBundleIdentifier = com.nynja.mobile.communicator.Nynja-Share ServerURL = im-fallback.nynja.net @@ -23,7 +22,7 @@ AssociatedDomain = applinks:join.nynja.net GoogleClientId = 13807320472-002cda3sovo7hef61dm4niekkm8jdaf1.apps.googleusercontent.com ReversedGoogleClientId = com.googleusercontent.apps.13807320472-002cda3sovo7hef61dm4niekkm8jdaf1 -GoogleServerClientId = 13807320472-b7cmhqb5kntvt1oqp00g2pgaatjealrg.apps.googleusercontent.com +GoogleServerClientId = 139525496349-9ndqp3t69kudvseiovhsbqa4rn5o6gt8.apps.googleusercontent.com AUTH_SERVER_HOST = auth.dev-eu.nynja.net AUTH_SERVER_PORT = 443 -- GitLab From ec5d0919ffc467db3dc00f41f74af7edbdc89890 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 20 Dec 2018 19:12:07 +0200 Subject: [PATCH 134/138] [Multiple Accounts] Search by phone, email, username (#1532) * [NY-3852] Added base module skeleton for contact info from template. * Fixed auth and account services. * Update app version for build. * [NY-5507] Updated SDK for the latest version. * Refactored phone number text controller * Add error handling popups to code confirmation screen. * [NY-3852] Implemented UI for base container. * Update input model * [NY-5178] Added search module from xcode template * [NY-5187] Implemented base layout for search screen. * [NY-5187] Handle empty state. * [NY-5187] Implemented search UI. * Fixed auth * [NY-5187] Minor updates. * [NY-5187] Implemented search results table UI. Minor UI fixes. * [NY-5187] Fixed search result UI. * [NY-5187] Fixed validators * [Multiple accounts] Search by QR Code (#1536) * [NY-5178] Minor refactoring in QR Code reader / generator modules. * [NY-5178] Implemented new logic for search by QR code. * [NY-5178] Refactored search result processing logic. * [NY-5178] Update SDK version * [NY-5178] Implemented full search logic * [NY-5178] Handle empty phone book response * [NY-5178] Minor UI fix. * [NY-5178] Setup proper wheel factory --- .../NynjaUIKit.xcodeproj/project.pbxproj | 28 +- .../DataSource/TableViewDataSource.swift | 51 +++ .../UICollectionView+ViewModel.swift | 0 .../UITableView+ViewModels.swift | 0 .../Views/Utils/RoundImageView.swift | 18 + Nynja-Share/Resources/Info.plist | 2 +- Nynja.xcodeproj/project.pbxproj | 224 +++++++++++- Nynja/ContactsItemsFactory.swift | 42 ++- Nynja/CountriesProviding.swift | 10 +- Nynja/Generated/ColorsConstants.swift | 12 +- Nynja/Generated/LocalizableConstants.swift | 24 +- Nynja/Library/UI/Alert/Alert+Defaults.swift | 19 + Nynja/Library/UI/BaseVC/BaseVC.swift | 4 + .../Buttons/NynjaButton/BaseNynjaButton.swift | 2 + .../Buttons/NynjaButton/NynjaCellButton.swift | 19 +- .../NynjaControlContainerView.swift | 4 - .../Material/Validator/LengthValidator.swift | 8 +- .../Material/Validator/MTIValidator.swift | 9 + .../UI/View/GradientContainerView.swift | 121 ++++++ Nynja/Library/UI/View/GradientView.swift | 26 +- .../Call/View/CallInProgressView.swift | 4 +- .../AuthProvider/AuthProviderProtocols.swift | 1 - .../Presenter/AuthProviderPresenter.swift | 23 +- .../View/AuthProviderViewController.swift | 8 +- .../ContactInfoManagementProtocols.swift | 41 +++ .../Entities/ContactInfoInputModel.swift | 53 +++ .../ContactInfoManagementInteractor.swift | 45 +++ .../ContactInfoManagementPresenter.swift | 63 ++++ .../ContactInfoManagementViewController.swift | 156 ++++++++ .../ContactInfoManagementWireframe.swift | 61 ++++ .../AccountSettingsCoordinator.swift | 12 + .../Interactor/DeleteAccountInteractor.swift | 2 +- Nynja/Modules/Flows/AppCoordinator.swift | 23 +- .../Flows/Auth Flow/AuthCoordinator.swift | 4 +- .../Auth Flow/AuthModule/AuthProtocols.swift | 5 - .../Entities/EmailTextController.swift | 8 +- .../Entities/PhoneNumberTextController.swift | 14 +- .../Interactor/AuthInteractor.swift | 7 +- .../AuthModule/Presenter/AuthPresenter.swift | 15 +- .../AuthModule/View/AuthViewController.swift | 91 ++--- ...erView.swift => DetailContainerView.swift} | 8 +- .../View/Subviews/EmailLoginView.swift | 4 +- .../View/Subviews/LoginOptionsView.swift | 12 +- .../View/Subviews/PhoneNumberLoginView.swift | 29 +- .../AuthModule/Wireframe/AuthWireframe.swift | 8 +- .../CodeConfirmationProtocols.swift | 4 +- .../CodeConfirmationInteractor.swift | 2 +- .../Presenter/CodeConfirmationPresenter.swift | 11 +- .../Wireframe/CodeConfirmationWireframe.swift | 7 +- .../Interactor/CreateProfileInteractor.swift | 2 +- .../Splash/Interactor/SplashInteractor.swift | 4 +- .../Entities/SearchContactResponse.swift | 94 +++++ .../Entities/SearchContactResult.swift | 14 + .../Entities/SearchInputMode.swift | 13 + .../Interactor/SearchContactInteractor.swift | 100 +++++ .../Presenter/SearchContactPresenter.swift | 243 +++++++++++++ .../SearchContactCoordinator.swift | 131 +++++++ .../Search Flow/SearchContactProtocols.swift | 48 +++ .../View/InputView/ContentViewModel.swift | 13 + .../InputView/EmailContentViewModel.swift | 39 ++ .../PhoneNumberContentViewModel.swift | 61 ++++ .../InputView/TextFieldContentViewModel.swift | 58 +++ .../InputView/UsernameContentViewModel.swift | 39 ++ .../View/SearchContactViewController.swift | 240 ++++++++++++ .../View/SearchContactViewModel.swift | 13 + .../TableView/SearchResultCellModel.swift | 44 +++ .../TableView/SearchResultTableViewCell.swift | 136 +++++++ .../Wireframe/SearchContactWireframe.swift | 86 +++++ .../InviteFriends/Entity/PhoneContact.swift | 41 ++- .../Interactor/InviteFriendsInteractor.swift | 42 +-- .../View/Cell/InviteFriendsCell.swift | 2 +- .../WireFrame/InviteFriendsWireframe.swift | 4 +- .../LeaveVoiceMessageViewController.swift | 4 +- Nynja/Modules/Main/MainProtocols.swift | 2 + .../Main/Presenter/MainPresenter.swift | 4 + .../Main/View/MainNavigationItem.swift | 3 +- .../MainViewController+NavigateProtocol.swift | 5 + .../Modules/Main/View/NavigateProtocol.swift | 1 + .../Main/WireFrame/MainWireframe.swift | 28 +- .../QRCodeGeneratorInteractor.swift | 20 +- .../Presenter/QRCodeGeneratorPresenter.swift | 19 +- .../QRCodeGeneratorProtocols.swift | 39 +- .../View/QRCodeGeneratorViewController.swift | 32 +- .../WireFrame/QRCodeGeneratorWireframe.swift | 35 +- .../Interactor/QRCodeReaderInteractor.swift | 76 ++-- .../Presenter/QRCodeReaderPresenter.swift | 30 +- .../QRCodeReader/QRCodeReaderProtocols.swift | 52 +-- .../View/QRCodeReaderViewController.swift | 168 ++++----- .../WireFrame/QRCodeReaderWireframe.swift | 56 ++- Nynja/Resources/Colors.json | 6 +- Nynja/Resources/Info.plist | 50 +-- Nynja/Resources/LaunchScreen.storyboard | 18 +- Nynja/Resources/en.lproj/Localizable.strings | 21 +- Nynja/SDK/Account/Entities/AccountError.swift | 13 - Nynja/SDK/Account/Entities/AccountInfo.swift | 27 +- .../SDK/Account/Service/AccountService.swift | 48 ++- .../Account/Service/AccountServiceImpl.swift | 344 ++++++++++-------- Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift | 8 + Nynja/SDK/Auth/Service/AuthServiceImpl.swift | 51 +-- Nynja/SearchModel.swift | 1 - Nynja/Services/ContactManager.swift | 109 ++++-- .../ServiceFactory/ServiceFactory.swift | 8 + .../ServiceFactoryProtocol.swift | 4 + Podfile | 2 +- Podfile.lock | 8 +- .../Handlers/IoHandler/IoHandler.swift | 8 - .../IoHandler/IoHandlerDelegate.swift | 4 - 107 files changed, 3334 insertions(+), 781 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/DataSource/TableViewDataSource.swift rename Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/{Extensions => ViewModels}/UICollectionView+ViewModel.swift (100%) rename Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/{Extensions => ViewModels}/UITableView+ViewModels.swift (100%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundImageView.swift create mode 100644 Nynja/Library/UI/Alert/Alert+Defaults.swift create mode 100644 Nynja/Library/UI/View/GradientContainerView.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift rename Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/{LoginContainerView.swift => DetailContainerView.swift} (88%) create mode 100644 Nynja/Modules/Flows/Search Flow/Entities/SearchContactResponse.swift create mode 100644 Nynja/Modules/Flows/Search Flow/Entities/SearchContactResult.swift create mode 100644 Nynja/Modules/Flows/Search Flow/Entities/SearchInputMode.swift create mode 100644 Nynja/Modules/Flows/Search Flow/Interactor/SearchContactInteractor.swift create mode 100644 Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift create mode 100644 Nynja/Modules/Flows/Search Flow/SearchContactCoordinator.swift create mode 100644 Nynja/Modules/Flows/Search Flow/SearchContactProtocols.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/InputView/ContentViewModel.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/InputView/EmailContentViewModel.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/InputView/PhoneNumberContentViewModel.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/InputView/UsernameContentViewModel.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/SearchContactViewController.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/SearchContactViewModel.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultCellModel.swift create mode 100644 Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultTableViewCell.swift create mode 100644 Nynja/Modules/Flows/Search Flow/Wireframe/SearchContactWireframe.swift delete mode 100644 Nynja/SDK/Account/Entities/AccountError.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 27034684c..c40d9a8ca 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 3A07C5B421AED3FC00AE3429 /* SecureCodeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B221AED3FC00AE3429 /* SecureCodeContainerView.swift */; }; 3A07C5B521AED3FC00AE3429 /* SecureCodeInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B321AED3FC00AE3429 /* SecureCodeInputView.swift */; }; 3A07C5B921AEDF7800AE3429 /* UITextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07C5B821AEDF7800AE3429 /* UITextInput.swift */; }; + 3A2C2E0021C26B95006A53BB /* RoundImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2C2DFF21C26B95006A53BB /* RoundImageView.swift */; }; + 3ABA189621C001210026B96B /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABA189521C001210026B96B /* TableViewDataSource.swift */; }; 3AE2F99D21B6E31C0068C3BC /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F99C21B6E31C0068C3BC /* Alert.swift */; }; 850A2E9E219F37AD00C784D9 /* TextInputUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2E9D219F37AD00C784D9 /* TextInputUtils.swift */; }; 8514D4C620EE27080002378A /* NynjaUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 8514D4C420EE27080002378A /* NynjaUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -59,6 +61,8 @@ 3A07C5B221AED3FC00AE3429 /* SecureCodeContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureCodeContainerView.swift; sourceTree = ""; }; 3A07C5B321AED3FC00AE3429 /* SecureCodeInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureCodeInputView.swift; sourceTree = ""; }; 3A07C5B821AEDF7800AE3429 /* UITextInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextInput.swift; sourceTree = ""; }; + 3A2C2DFF21C26B95006A53BB /* RoundImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundImageView.swift; sourceTree = ""; }; + 3ABA189521C001210026B96B /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = ""; }; 3AE2F99C21B6E31C0068C3BC /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; 600B9308D041B8E4DE0DBEF1 /* Pods-NynjaUIKit.loaddb.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.loaddb.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.loaddb.xcconfig"; sourceTree = ""; }; 7336042AC840197E622730FD /* Pods-NynjaUIKit.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.prerelease.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.prerelease.xcconfig"; sourceTree = ""; }; @@ -157,6 +161,14 @@ path = TextInput; sourceTree = ""; }; + 3ABA189421C000EF0026B96B /* DataSource */ = { + isa = PBXGroup; + children = ( + 3ABA189521C001210026B96B /* TableViewDataSource.swift */, + ); + path = DataSource; + sourceTree = ""; + }; 3AE2F99721B6D9B00068C3BC /* Alerts */ = { isa = PBXGroup; children = ( @@ -217,7 +229,7 @@ 8514D4D820EE2D970002378A /* Reusable.swift */, 8514D4D920EE2D970002378A /* XibInitializable.swift */, 8514D4CD20EE2D970002378A /* ViewModels */, - 8514D4DA20EE2D970002378A /* Extensions */, + 3ABA189421C000EF0026B96B /* DataSource */, ); path = Collection; sourceTree = ""; @@ -228,6 +240,8 @@ 8514D4CE20EE2D970002378A /* Cell */, 8514D4D320EE2D970002378A /* SupplementaryView */, 8514D4D520EE2D970002378A /* Accessibility */, + 8514D4DB20EE2D970002378A /* UITableView+ViewModels.swift */, + 8514D4DC20EE2D970002378A /* UICollectionView+ViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -260,15 +274,6 @@ path = Accessibility; sourceTree = ""; }; - 8514D4DA20EE2D970002378A /* Extensions */ = { - isa = PBXGroup; - children = ( - 8514D4DB20EE2D970002378A /* UITableView+ViewModels.swift */, - 8514D4DC20EE2D970002378A /* UICollectionView+ViewModel.swift */, - ); - path = Extensions; - sourceTree = ""; - }; 8514D4DD20EE2D970002378A /* Layout */ = { isa = PBXGroup; children = ( @@ -412,6 +417,7 @@ isa = PBXGroup; children = ( 8540A018218213E2003A010F /* RoundView.swift */, + 3A2C2DFF21C26B95006A53BB /* RoundImageView.swift */, ); path = Utils; sourceTree = ""; @@ -574,7 +580,9 @@ 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, 3A07C5B521AED3FC00AE3429 /* SecureCodeInputView.swift in Sources */, 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, + 3A2C2E0021C26B95006A53BB /* RoundImageView.swift in Sources */, 855A4E86219AFA8200B6E90B /* UnderlinedTextField.swift in Sources */, + 3ABA189621C001210026B96B /* TableViewDataSource.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, 855A4E92219B0C5600B6E90B /* UnderlinedButton.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/DataSource/TableViewDataSource.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/DataSource/TableViewDataSource.swift new file mode 100644 index 000000000..943784489 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/DataSource/TableViewDataSource.swift @@ -0,0 +1,51 @@ +// +// TableViewDataSource.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +open class TableViewDataSource: NSObject, UITableViewDataSource { + + public typealias DataProvider = (TableViewDataSource, IndexPath) -> AnyCellViewModel + + open var data: [T] = [] { + didSet { + tableView?.reloadData() + } + } + + private let dataProvider: DataProvider + + private weak var tableView: UITableView? + + public init(tableView: UITableView, dataProvider: @escaping DataProvider) { + self.tableView = tableView + self.dataProvider = dataProvider + super.init() + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return data.count + } + + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return tableView.dequeueReusableCell(withModel: itemModel(at: indexPath), for: indexPath) + } + + open func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { + return dataProvider(self, indexPath) + } +} + +extension TableViewDataSource where T: CellViewModel { + + public convenience init(tableView: UITableView) { + self.init(tableView: tableView) { dataSource, indexPath in + dataSource.data[indexPath.row] + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UICollectionView+ViewModel.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/UICollectionView+ViewModel.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UICollectionView+ViewModel.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/UICollectionView+ViewModel.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UITableView+ViewModels.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/UITableView+ViewModels.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UITableView+ViewModels.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/UITableView+ViewModels.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundImageView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundImageView.swift new file mode 100644 index 000000000..c4bae62a7 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundImageView.swift @@ -0,0 +1,18 @@ +// +// RoundImageView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 12/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +open class RoundImageView: UIImageView { + + open override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = min(bounds.width, bounds.height) / 2 + layer.masksToBounds = true + } +} diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index 30f12f746..9481f98c4 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.5.5 + 0.5.5.multi-acc Config $(Config) ModelsVersion diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 0286e35ad..c09c9904b 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -505,6 +505,11 @@ 3A0AEA7121B018380066CBBA /* DBAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA7021B018380066CBBA /* DBAccount.swift */; }; 3A0AEA7321B01EC50066CBBA /* DBContactInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA7221B01EC50066CBBA /* DBContactInfo.swift */; }; 3A0AEA7521B028120066CBBA /* AccountTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0AEA7421B028120066CBBA /* AccountTable.swift */; }; + 3A0E426021BFBE99001A3F3C /* SearchContactPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E425B21BFBE99001A3F3C /* SearchContactPresenter.swift */; }; + 3A0E426121BFBE99001A3F3C /* SearchContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E425C21BFBE99001A3F3C /* SearchContactViewController.swift */; }; + 3A0E426221BFBE99001A3F3C /* SearchContactProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E425D21BFBE99001A3F3C /* SearchContactProtocols.swift */; }; + 3A0E426321BFBE99001A3F3C /* SearchContactInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E425E21BFBE99001A3F3C /* SearchContactInteractor.swift */; }; + 3A0E426421BFBE99001A3F3C /* SearchContactWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E425F21BFBE99001A3F3C /* SearchContactWireframe.swift */; }; 3A0E865921B130DC00BAF80B /* ContactInfoTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E865821B130DC00BAF80B /* ContactInfoTable.swift */; }; 3A0E865C21B14ECB00BAF80B /* AccountExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0E865B21B14ECB00BAF80B /* AccountExtension.swift */; }; 3A1146681ED6F047006BA132 /* ring.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 3A1146671ED6F047006BA132 /* ring.mp3 */; }; @@ -512,7 +517,16 @@ 3A14D83B21AC03A3009CD23A /* SearchAvailabilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */; }; 3A14D83D21AC136F009CD23A /* AuthProviderUIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */; }; 3A14D83F21AC1F07009CD23A /* NavigationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83E21AC1F07009CD23A /* NavigationError.swift */; }; + 3A184D1B21C0FD800083D367 /* UsernameContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1A21C0FD800083D367 /* UsernameContentViewModel.swift */; }; + 3A184D1D21C0FD8C0083D367 /* EmailContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1C21C0FD8C0083D367 /* EmailContentViewModel.swift */; }; + 3A184D1F21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */; }; + 3A184D2121C0FEBC0083D367 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */; }; + 3A184D2421C100630083D367 /* SearchInputMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2321C100630083D367 /* SearchInputMode.swift */; }; + 3A184D2621C103740083D367 /* SearchContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2521C103740083D367 /* SearchContactViewModel.swift */; }; + 3A184D2821C128380083D367 /* TextFieldContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2721C128380083D367 /* TextFieldContentViewModel.swift */; }; + 3A184D2C21C12C080083D367 /* SearchContactCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2B21C12C080083D367 /* SearchContactCoordinator.swift */; }; 3A19FEAD1F3B7F1D00ACE750 /* MessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A19FEAC1F3B7F1D00ACE750 /* MessageHandler.swift */; }; + 3A1A513021BABE7A00369206 /* ContactInfoInputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1A512F21BABE7A00369206 /* ContactInfoInputModel.swift */; }; 3A1AAFCE1F3DF0470098780A /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1AAFCD1F3DF0470098780A /* DateExtensions.swift */; }; 3A1C87421F6101A50029B0BC /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1C87411F6101A50029B0BC /* Reachability.swift */; }; 3A1C87441F6103820029B0BC /* ReachabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1C87431F6103820029B0BC /* ReachabilityService.swift */; }; @@ -534,6 +548,7 @@ 3A27B0A71EF307A900B4B3CB /* DeleteUserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27B0A61EF307A900B4B3CB /* DeleteUserModel.swift */; }; 3A2843291EF9317100EFE21A /* Avenir.ttc in Resources */ = {isa = PBXBuildFile; fileRef = 3A2843261EF9314100EFE21A /* Avenir.ttc */; }; 3A2A99831EFAD2FB002749B3 /* PageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2A99821EFAD2FB002749B3 /* PageControl.swift */; }; + 3A2C2DFC21C26708006A53BB /* SearchContactResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2C2DFB21C26708006A53BB /* SearchContactResponse.swift */; }; 3A37416121B58AAA00F212B9 /* ImageUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A37416021B58AAA00F212B9 /* ImageUploader.swift */; }; 3A3FD2831F39E0A000B6958F /* HistoryRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */; }; 3A62B7D81F4CB9D100F45B51 /* BaseMQTTModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */; }; @@ -550,11 +565,19 @@ 3A80BF9921A864220016285E /* AuthProviderProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9421A864220016285E /* AuthProviderProtocols.swift */; }; 3A80BF9A21A864220016285E /* AuthProviderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9521A864220016285E /* AuthProviderInteractor.swift */; }; 3A80BF9B21A864220016285E /* AuthProviderWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A80BF9621A864220016285E /* AuthProviderWireframe.swift */; }; - 3A9635EB21AC4EE300ABC2C5 /* LoginContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */; }; + 3A9635EB21AC4EE300ABC2C5 /* DetailContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9635EA21AC4EE300ABC2C5 /* DetailContainerView.swift */; }; 3AA13C761F2252F900BE5D8F /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA13C751F2252F900BE5D8F /* SearchModel.swift */; }; 3AA4E6ACDBCB060172A7A279 /* FavoritesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 462440AD41D807CE8957FDD9 /* FavoritesProtocols.swift */; }; 3AAA92AE21B1A6C800EF5F1E /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */; }; 3AB73FFA21B962F200D1E967 /* AddAccountAndContactInfoTables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB73FF921B962F200D1E967 /* AddAccountAndContactInfoTables.swift */; }; + 3AB7400321B9954100D1E967 /* ContactInfoManagementPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB73FFE21B9954100D1E967 /* ContactInfoManagementPresenter.swift */; }; + 3AB7400421B9954100D1E967 /* ContactInfoManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB73FFF21B9954100D1E967 /* ContactInfoManagementViewController.swift */; }; + 3AB7400521B9954100D1E967 /* ContactInfoManagementProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7400021B9954100D1E967 /* ContactInfoManagementProtocols.swift */; }; + 3AB7400621B9954100D1E967 /* ContactInfoManagementInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7400121B9954100D1E967 /* ContactInfoManagementInteractor.swift */; }; + 3AB7400721B9954100D1E967 /* ContactInfoManagementWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7400221B9954100D1E967 /* ContactInfoManagementWireframe.swift */; }; + 3ABA188F21BFF3D40026B96B /* GradientContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABA188E21BFF3D40026B96B /* GradientContainerView.swift */; }; + 3ABA189D21C005880026B96B /* SearchResultTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABA189B21C005880026B96B /* SearchResultTableViewCell.swift */; }; + 3ABA189E21C005880026B96B /* SearchResultCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABA189C21C005880026B96B /* SearchResultCellModel.swift */; }; 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */; }; 3ABCE9061EC9357900A80B15 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */; }; 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC07E3B1F055B3F00ADBE26 /* DoubleExtensions.swift */; }; @@ -953,10 +976,11 @@ 85057962206D0C8400565C60 /* MediaPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85057961206D0C8400565C60 /* MediaPlaceholderWheelItemModel.swift */; }; 85057964206D0CE500565C60 /* LocationPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85057963206D0CE500565C60 /* LocationPlaceholderWheelItemModel.swift */; }; 8506F001206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8506F000206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift */; }; - 8507622421A4735E00E4CEFE /* AccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8507622321A4735E00E4CEFE /* AccountError.swift */; }; 85082DDD2045A873000AE4B2 /* UserSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85082DDC2045A873000AE4B2 /* UserSettingsService.swift */; }; 85082DDF2045A8C2000AE4B2 /* WheelPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85082DDE2045A8C2000AE4B2 /* WheelPosition.swift */; }; 850833DB2037171600587EEF /* FileExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850833DA2037171600587EEF /* FileExtensionView.swift */; }; + 85086F4921C64D6D00194361 /* SearchContactResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F4821C64D6D00194361 /* SearchContactResult.swift */; }; + 85086F4B21C672FD00194361 /* Alert+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F4A21C672FD00194361 /* Alert+Defaults.swift */; }; 8509452B206E684300B43C1C /* AddParticipantsContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509452A206E684300B43C1C /* AddParticipantsContactCell.swift */; }; 8509AC62206A54420089089B /* ResponseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509AC61206A54420089089B /* ResponseResult.swift */; }; 8509FC7B2158CCA800734D93 /* MessageInteractor+Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509FC7A2158CCA800734D93 /* MessageInteractor+Reply.swift */; }; @@ -2949,6 +2973,11 @@ 3A0AEA7021B018380066CBBA /* DBAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBAccount.swift; sourceTree = ""; }; 3A0AEA7221B01EC50066CBBA /* DBContactInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBContactInfo.swift; sourceTree = ""; }; 3A0AEA7421B028120066CBBA /* AccountTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTable.swift; sourceTree = ""; }; + 3A0E425B21BFBE99001A3F3C /* SearchContactPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactPresenter.swift; sourceTree = ""; }; + 3A0E425C21BFBE99001A3F3C /* SearchContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactViewController.swift; sourceTree = ""; }; + 3A0E425D21BFBE99001A3F3C /* SearchContactProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactProtocols.swift; sourceTree = ""; }; + 3A0E425E21BFBE99001A3F3C /* SearchContactInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactInteractor.swift; sourceTree = ""; }; + 3A0E425F21BFBE99001A3F3C /* SearchContactWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactWireframe.swift; sourceTree = ""; }; 3A0E865821B130DC00BAF80B /* ContactInfoTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoTable.swift; sourceTree = ""; }; 3A0E865B21B14ECB00BAF80B /* AccountExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExtension.swift; sourceTree = ""; }; 3A1146671ED6F047006BA132 /* ring.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ring.mp3; sourceTree = ""; }; @@ -2956,7 +2985,16 @@ 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAvailabilityView.swift; sourceTree = ""; }; 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderUIConfiguration.swift; sourceTree = ""; }; 3A14D83E21AC1F07009CD23A /* NavigationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationError.swift; sourceTree = ""; }; + 3A184D1A21C0FD800083D367 /* UsernameContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameContentViewModel.swift; sourceTree = ""; }; + 3A184D1C21C0FD8C0083D367 /* EmailContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailContentViewModel.swift; sourceTree = ""; }; + 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberContentViewModel.swift; sourceTree = ""; }; + 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; + 3A184D2321C100630083D367 /* SearchInputMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchInputMode.swift; sourceTree = ""; }; + 3A184D2521C103740083D367 /* SearchContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactViewModel.swift; sourceTree = ""; }; + 3A184D2721C128380083D367 /* TextFieldContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldContentViewModel.swift; sourceTree = ""; }; + 3A184D2B21C12C080083D367 /* SearchContactCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactCoordinator.swift; sourceTree = ""; }; 3A19FEAC1F3B7F1D00ACE750 /* MessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageHandler.swift; path = Services/HandleServices/MessageHandler.swift; sourceTree = ""; }; + 3A1A512F21BABE7A00369206 /* ContactInfoInputModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoInputModel.swift; sourceTree = ""; }; 3A1AAFCD1F3DF0470098780A /* DateExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; 3A1C87411F6101A50029B0BC /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Reachability.swift; path = Library/Reachability.swift; sourceTree = ""; }; 3A1C87431F6103820029B0BC /* ReachabilityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReachabilityService.swift; path = Services/ReachabilityService.swift; sourceTree = ""; }; @@ -2978,6 +3016,7 @@ 3A27B0A61EF307A900B4B3CB /* DeleteUserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeleteUserModel.swift; path = Services/Models/DeleteUserModel.swift; sourceTree = ""; }; 3A2843261EF9314100EFE21A /* Avenir.ttc */ = {isa = PBXFileReference; lastKnownFileType = file; path = Avenir.ttc; sourceTree = ""; }; 3A2A99821EFAD2FB002749B3 /* PageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PageControl.swift; path = PageControl/PageControl.swift; sourceTree = ""; }; + 3A2C2DFB21C26708006A53BB /* SearchContactResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactResponse.swift; sourceTree = ""; }; 3A37416021B58AAA00F212B9 /* ImageUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploader.swift; sourceTree = ""; }; 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HistoryRequestModel.swift; path = Services/Models/HistoryRequestModel.swift; sourceTree = ""; }; 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BaseMQTTModel.swift; path = Services/Models/BaseMQTTModel.swift; sourceTree = ""; }; @@ -2996,10 +3035,18 @@ 3A80BF9521A864220016285E /* AuthProviderInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderInteractor.swift; sourceTree = ""; }; 3A80BF9621A864220016285E /* AuthProviderWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderWireframe.swift; sourceTree = ""; }; 3A8C12DDFD0D831F21959665 /* Pods-Nynja-Share.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.devautotests.xcconfig"; sourceTree = ""; }; - 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContainerView.swift; sourceTree = ""; }; + 3A9635EA21AC4EE300ABC2C5 /* DetailContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailContainerView.swift; sourceTree = ""; }; 3AA13C751F2252F900BE5D8F /* SearchModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 3AB73FF921B962F200D1E967 /* AddAccountAndContactInfoTables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountAndContactInfoTables.swift; sourceTree = ""; }; + 3AB73FFE21B9954100D1E967 /* ContactInfoManagementPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoManagementPresenter.swift; sourceTree = ""; }; + 3AB73FFF21B9954100D1E967 /* ContactInfoManagementViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoManagementViewController.swift; sourceTree = ""; }; + 3AB7400021B9954100D1E967 /* ContactInfoManagementProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoManagementProtocols.swift; sourceTree = ""; }; + 3AB7400121B9954100D1E967 /* ContactInfoManagementInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoManagementInteractor.swift; sourceTree = ""; }; + 3AB7400221B9954100D1E967 /* ContactInfoManagementWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoManagementWireframe.swift; sourceTree = ""; }; + 3ABA188E21BFF3D40026B96B /* GradientContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientContainerView.swift; sourceTree = ""; }; + 3ABA189B21C005880026B96B /* SearchResultTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultTableViewCell.swift; sourceTree = ""; }; + 3ABA189C21C005880026B96B /* SearchResultCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultCellModel.swift; sourceTree = ""; }; 3ABCE8ED1EC9330D00A80B15 /* Nynja.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nynja.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; @@ -3374,10 +3421,11 @@ 85057961206D0C8400565C60 /* MediaPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaPlaceholderWheelItemModel.swift; path = Nynja/Library/UI/WheelContainer/Wheel/Factory/MediaPlaceholderWheelItemModel.swift; sourceTree = SOURCE_ROOT; }; 85057963206D0CE500565C60 /* LocationPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPlaceholderWheelItemModel.swift; sourceTree = ""; }; 8506F000206BF5DA008B2D7F /* ChatPlaceholderWheelItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemView.swift; sourceTree = ""; }; - 8507622321A4735E00E4CEFE /* AccountError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountError.swift; sourceTree = ""; }; 85082DDC2045A873000AE4B2 /* UserSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsService.swift; sourceTree = ""; }; 85082DDE2045A8C2000AE4B2 /* WheelPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelPosition.swift; sourceTree = ""; }; 850833DA2037171600587EEF /* FileExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtensionView.swift; sourceTree = ""; }; + 85086F4821C64D6D00194361 /* SearchContactResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactResult.swift; sourceTree = ""; }; + 85086F4A21C672FD00194361 /* Alert+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Alert+Defaults.swift"; sourceTree = ""; }; 8509452A206E684300B43C1C /* AddParticipantsContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsContactCell.swift; sourceTree = ""; }; 8509AC61206A54420089089B /* ResponseResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseResult.swift; sourceTree = ""; }; 8509FC7A2158CCA800734D93 /* MessageInteractor+Reply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+Reply.swift"; sourceTree = ""; }; @@ -6406,6 +6454,55 @@ name = Account; sourceTree = ""; }; + 3A0E425A21BFB69C001A3F3C /* Search Flow */ = { + isa = PBXGroup; + children = ( + 3A184D2B21C12C080083D367 /* SearchContactCoordinator.swift */, + 3A0E425D21BFBE99001A3F3C /* SearchContactProtocols.swift */, + 3A0E426521BFBF1E001A3F3C /* View */, + 3A0E426621BFBF2A001A3F3C /* Presenter */, + 3A0E426721BFBF30001A3F3C /* Interactor */, + 3A0E426821BFBF37001A3F3C /* Wireframe */, + 3A184D2221C100330083D367 /* Entities */, + ); + path = "Search Flow"; + sourceTree = ""; + }; + 3A0E426521BFBF1E001A3F3C /* View */ = { + isa = PBXGroup; + children = ( + 3A184D2521C103740083D367 /* SearchContactViewModel.swift */, + 3A0E425C21BFBE99001A3F3C /* SearchContactViewController.swift */, + 3A184D1921C0FD1D0083D367 /* InputView */, + 3ABA189121BFFF6B0026B96B /* TableView */, + ); + path = View; + sourceTree = ""; + }; + 3A0E426621BFBF2A001A3F3C /* Presenter */ = { + isa = PBXGroup; + children = ( + 3A0E425B21BFBE99001A3F3C /* SearchContactPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 3A0E426721BFBF30001A3F3C /* Interactor */ = { + isa = PBXGroup; + children = ( + 3A0E425E21BFBE99001A3F3C /* SearchContactInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 3A0E426821BFBF37001A3F3C /* Wireframe */ = { + isa = PBXGroup; + children = ( + 3A0E425F21BFBE99001A3F3C /* SearchContactWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; 3A14D83621ABEC25009CD23A /* Entities */ = { isa = PBXGroup; children = ( @@ -6423,6 +6520,36 @@ path = Subviews; sourceTree = ""; }; + 3A184D1921C0FD1D0083D367 /* InputView */ = { + isa = PBXGroup; + children = ( + 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */, + 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */, + 3A184D2721C128380083D367 /* TextFieldContentViewModel.swift */, + 3A184D1C21C0FD8C0083D367 /* EmailContentViewModel.swift */, + 3A184D1A21C0FD800083D367 /* UsernameContentViewModel.swift */, + ); + path = InputView; + sourceTree = ""; + }; + 3A184D2221C100330083D367 /* Entities */ = { + isa = PBXGroup; + children = ( + 3A184D2321C100630083D367 /* SearchInputMode.swift */, + 3A2C2DFB21C26708006A53BB /* SearchContactResponse.swift */, + 85086F4821C64D6D00194361 /* SearchContactResult.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 3A1A512C21BAB1CA00369206 /* Entities */ = { + isa = PBXGroup; + children = ( + 3A1A512F21BABE7A00369206 /* ContactInfoInputModel.swift */, + ); + path = Entities; + sourceTree = ""; + }; 3A1DC7371EF151B6006A8E9F /* Handlers */ = { isa = PBXGroup; children = ( @@ -6697,6 +6824,60 @@ path = Login; sourceTree = ""; }; + 3AB73FFD21B9948300D1E967 /* ContactInfoManagement */ = { + isa = PBXGroup; + children = ( + 3AB7400021B9954100D1E967 /* ContactInfoManagementProtocols.swift */, + 3AB7400921B9955900D1E967 /* View */, + 3AB7400821B9955000D1E967 /* Presenter */, + 3AB7400A21B9956200D1E967 /* Interactor */, + 3AB7400B21B9956D00D1E967 /* Wireframe */, + 3A1A512C21BAB1CA00369206 /* Entities */, + ); + path = ContactInfoManagement; + sourceTree = ""; + }; + 3AB7400821B9955000D1E967 /* Presenter */ = { + isa = PBXGroup; + children = ( + 3AB73FFE21B9954100D1E967 /* ContactInfoManagementPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 3AB7400921B9955900D1E967 /* View */ = { + isa = PBXGroup; + children = ( + 3AB73FFF21B9954100D1E967 /* ContactInfoManagementViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 3AB7400A21B9956200D1E967 /* Interactor */ = { + isa = PBXGroup; + children = ( + 3AB7400121B9954100D1E967 /* ContactInfoManagementInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 3AB7400B21B9956D00D1E967 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 3AB7400221B9954100D1E967 /* ContactInfoManagementWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 3ABA189121BFFF6B0026B96B /* TableView */ = { + isa = PBXGroup; + children = ( + 3ABA189C21C005880026B96B /* SearchResultCellModel.swift */, + 3ABA189B21C005880026B96B /* SearchResultTableViewCell.swift */, + ); + path = TableView; + sourceTree = ""; + }; 3ABCE8E41EC9330D00A80B15 = { isa = PBXGroup; children = ( @@ -8178,6 +8359,7 @@ children = ( 5EDD454721885EC400C50BC8 /* Coordinator */, 5EDD454821885EC400C50BC8 /* AccountSettings */, + 3AB73FFD21B9948300D1E967 /* ContactInfoManagement */, 3AE2F98821B6B4A00068C3BC /* DeleteAccount */, 851452A421A5865C00DF10A6 /* LoginOptions */, 3A80BF9121A8637F0016285E /* AuthProvider */, @@ -8388,7 +8570,7 @@ children = ( 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */, 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */, - 3A9635EA21AC4EE300ABC2C5 /* LoginContainerView.swift */, + 3A9635EA21AC4EE300ABC2C5 /* DetailContainerView.swift */, 5EEB73D52161DBF100D8ECE6 /* EmailLoginView.swift */, 5EEB73D72162227B00D8ECE6 /* PhoneNumberLoginView.swift */, ); @@ -8960,6 +9142,7 @@ children = ( 4BB0EFBA2151347900704136 /* AlertManager.swift */, 850A2E93219EF9B800C784D9 /* AlertDisplayable.swift */, + 85086F4A21C672FD00194361 /* Alert+Defaults.swift */, ); path = Alert; sourceTree = ""; @@ -10393,7 +10576,6 @@ isa = PBXGroup; children = ( 859ECA6721A43DC1003630A0 /* AccountInfo.swift */, - 8507622321A4735E00E4CEFE /* AccountError.swift */, ); path = Entities; sourceTree = ""; @@ -13615,6 +13797,7 @@ E74597761FA2226900D3C88C /* NavigationView */, E79117891F97874D00462D68 /* GradientView.swift */, F11DF05E20BD93FB00F3E005 /* UIViewExtensions.swift */, + 3ABA188E21BFF3D40026B96B /* GradientContainerView.swift */, ); path = View; sourceTree = ""; @@ -13827,6 +14010,7 @@ 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */, 4B749F0E214FEFC8002F3A33 /* Auth Flow */, 5EDD454621885EC400C50BC8 /* Account Flow */, + 3A0E425A21BFB69C001A3F3C /* Search Flow */, 3A0A50A521B7FEFE0052D334 /* CreateGroupFlow */, F105C691209F71BE0091786A /* CameraFlow */, F11786F420ACF017007A9A1B /* CameraSettingsFlow */, @@ -16281,7 +16465,7 @@ F1AC0DE3207252E1001C68F7 /* Testable.swift in Sources */, A408A0BD20C174040029F54B /* ChannelsListInteractor.swift in Sources */, A458FABD20EB8B320075D55E /* MessageChannelActionsProtocol.swift in Sources */, - 3A9635EB21AC4EE300ABC2C5 /* LoginContainerView.swift in Sources */, + 3A9635EB21AC4EE300ABC2C5 /* DetailContainerView.swift in Sources */, 4BE2C5D92142EAC500A73DD9 /* AudioPlayer.swift in Sources */, F11786EE20AC39E9007A9A1B /* Job_Spec.swift in Sources */, 0008E92420347A8E003E316E /* DBJobMessage.swift in Sources */, @@ -16394,7 +16578,6 @@ 26AB1419218775BB00F2BB83 /* ConversionState.swift in Sources */, 9BC9657620FF042E00052AE1 /* CallInProgressProtocols.swift in Sources */, 4B1D7E0D2029DACF00703228 /* ByNumberItemsFactory.swift in Sources */, - 8507622421A4735E00E4CEFE /* AccountError.swift in Sources */, F11DF06C20BEF43A00F3E005 /* ResourceManager.swift in Sources */, F117871920ACF018007A9A1B /* CameraSettingsInteractor.swift in Sources */, 26FA420A2017ADF000E6F6EC /* StarMessageCell.swift in Sources */, @@ -16403,6 +16586,7 @@ A4CB15252103751900C3B68B /* JDFilePermissionMechanism.swift in Sources */, 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */, A42D51B5206A361400EEB952 /* chain.swift in Sources */, + 3A1A513021BABE7A00369206 /* ContactInfoInputModel.swift in Sources */, 3A0A50CB21B7FEFE0052D334 /* MyGroupAliasPresenter.swift in Sources */, 8509452B206E684300B43C1C /* AddParticipantsContactCell.swift in Sources */, 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */, @@ -16457,6 +16641,7 @@ 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */, 267BE2831FDE905D00C47E18 /* SettingsProtocols.swift in Sources */, 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */, + 3A184D2621C103740083D367 /* SearchContactViewModel.swift in Sources */, 264638231FFFE269002590E6 /* RepliesHeaderView.swift in Sources */, 263D66331FE8D95100A509F8 /* TypingHandler.swift in Sources */, 26AC7B641F9F79D400D448AE /* NavigateProtocol.swift in Sources */, @@ -16512,6 +16697,7 @@ FEA655F02167777E00B44029 /* TransferHistoryTableModel.swift in Sources */, A42D51BA206A361400EEB952 /* io.swift in Sources */, 4B6D20E82164D4AB003ADB29 /* ProgressDisplayable.swift in Sources */, + 3A184D1F21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift in Sources */, 3A1EB9A51F3A848A00658E93 /* HistoryHandler.swift in Sources */, 850FC5F42032F4CE00832D87 /* ForwardTargets.swift in Sources */, 5E07BC4D216F64EC000E4558 /* CreateProfileProtocols.swift in Sources */, @@ -16524,6 +16710,7 @@ FEA656072167777F00B44029 /* WalletBalancesOutput.swift in Sources */, 853FB0702049B396000996C5 /* SupportItemsFactory.swift in Sources */, 005B0B202029ABC2000D6416 /* TimeZoneItemView.swift in Sources */, + 3ABA188F21BFF3D40026B96B /* GradientContainerView.swift in Sources */, E7598F591FA1CDFD0082FBE7 /* ProfileAction.swift in Sources */, 005B0B1E2029AB9F000D6416 /* MessageToView.swift in Sources */, 3A0A50DD21B7FEFE0052D334 /* EditGroupNameProtocols.swift in Sources */, @@ -16619,6 +16806,7 @@ 8EBFF1692004033F00CC4C25 /* GroupAudiosCell.swift in Sources */, 855AC540208E45AA00DC2335 /* StickerCellModel.swift in Sources */, 4B877131219312570014AD09 /* ColumnExtension.swift in Sources */, + 85086F4B21C672FD00194361 /* Alert+Defaults.swift in Sources */, 3A2A99831EFAD2FB002749B3 /* PageControl.swift in Sources */, E784388156A5228026955A54 /* TutorialWireframe.swift in Sources */, E73315F21FB0BB0300C273FF /* Array+Contact.swift in Sources */, @@ -16632,6 +16820,7 @@ 853E595720D70F9A007799B9 /* DBStickerPack.swift in Sources */, 3A21EFFC1F3B154A00AE61EC /* SendModel.swift in Sources */, 2603139C20A0A4BA009AC66D /* LanguageSelectroInteractor.swift in Sources */, + 3A0E426221BFBE99001A3F3C /* SearchContactProtocols.swift in Sources */, A49B81B320B4BB6400980D36 /* NynjaMTIConfig.swift in Sources */, 3A0A50DA21B7FEFE0052D334 /* GroupInputViewController.swift in Sources */, 3A0A94D521B53478007421AA /* AccountDAOProtocol.swift in Sources */, @@ -16656,6 +16845,7 @@ A45F112E20B4218D00F45004 /* MessageContentProtocol.swift in Sources */, 0008E9132032D5AC003E316E /* MQTTServiceSchedule.swift in Sources */, 4B8771782195AC5B0014AD09 /* HistoryHandlerSubscriber.swift in Sources */, + 3A0E426421BFBE99001A3F3C /* SearchContactWireframe.swift in Sources */, A432CF1A20B4347D00993AFB /* MaterialTextInput.swift in Sources */, B77C11E62109254800CCB42E /* InterpretationTypePresenter.swift in Sources */, B750EF062046D7C700A99F9C /* SpeedStringRepresentable.swift in Sources */, @@ -16671,6 +16861,7 @@ 85EB37F321831094003A2D6F /* ChatListMessageDetailsView.swift in Sources */, 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, + 3AB7400521B9954100D1E967 /* ContactInfoManagementProtocols.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, 268C34152107479600F1472A /* TranscribeLongResponseData.swift in Sources */, @@ -16744,6 +16935,7 @@ A477CE8420613A5A00081D34 /* StarMessageDAO.swift in Sources */, A42CE5AB20692EDB000889CC /* Bert.swift in Sources */, 8562853620D164B5000C9739 /* ScaleAnimatableGrid.swift in Sources */, + 3ABA189E21C005880026B96B /* SearchResultCellModel.swift in Sources */, 0062D9402062EC4100B915AC /* InviteFriendsCell.swift in Sources */, 8E6C4BE61FF83D93009C8374 /* IntExtensions.swift in Sources */, E785EF2A1FB9D99400F0C689 /* PinView.swift in Sources */, @@ -16793,6 +16985,7 @@ 85788C4A20442887003600C9 /* Bundle+Keys.swift in Sources */, 4B6D20E72164D4AB003ADB29 /* InfoType.swift in Sources */, 260313A820A0A4BA009AC66D /* LabeledHeaderView.swift in Sources */, + 3AB7400721B9954100D1E967 /* ContactInfoManagementWireframe.swift in Sources */, 2652D6161FA82EFE005E62C7 /* EditProfileVCLayout.swift in Sources */, 26773F2B215BE15800C09248 /* Array+Operation.swift in Sources */, A432CF1920B4347D00993AFB /* UIView+Animate.swift in Sources */, @@ -17090,6 +17283,7 @@ 4B2C502E21B56AB700FBA9B1 /* AddAutoColumnToConvertMessage.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, + 3A0E426321BFBE99001A3F3C /* SearchContactInteractor.swift in Sources */, 8548341D218744AC002064E1 /* Observable.swift in Sources */, 4B1D7DFE2029C41C00703228 /* AboutItemsFactory.swift in Sources */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, @@ -17128,6 +17322,7 @@ A45F114020B4218D00F45004 /* PresenceStatusProvider.swift in Sources */, 3A0A4C5B21B91D9000BA0D09 /* DeleteAccountErrors.swift in Sources */, A49E6C4420D9A812007D85F5 /* MainViewController+Recents.swift in Sources */, + 3ABA189D21C005880026B96B /* SearchResultTableViewCell.swift in Sources */, 4B8996E6204EEC6300DCB183 /* MessageDAO.swift in Sources */, 32868DDF1F31CB6D0028B260 /* ChatsListInteractor.swift in Sources */, 8EC767DE200CBE9200655F80 /* GroupLinksListVC.swift in Sources */, @@ -17203,6 +17398,7 @@ 8EE9BC1A1FFE79FF00ECBBC7 /* GroupStorageCell.swift in Sources */, FEA655E02167777E00B44029 /* SeedBackupWalletPresenter.swift in Sources */, A42D51A2206A361400EEB952 /* Desc.swift in Sources */, + 3A184D2421C100630083D367 /* SearchInputMode.swift in Sources */, C940514B204C7FAF00D72B04 /* DataAndStorageProtocols.swift in Sources */, 5E7D5D5C21901D18009B5D8D /* DescriptionCellModel.swift in Sources */, F105C69C209F71BF0091786A /* CameraPresenter.swift in Sources */, @@ -17216,6 +17412,7 @@ B77C11DC2109242200CCB42E /* AssigningInterpreterViewController.swift in Sources */, A45F111420B4218D00F45004 /* SystemCell.swift in Sources */, 26C1A3E92031AAA30009F7F0 /* OtherUserViewController.swift in Sources */, + 85086F4921C64D6D00194361 /* SearchContactResult.swift in Sources */, 2606F3BC20BFE20500CF7F15 /* MessageInteractor+Translation.swift in Sources */, ACD15567460FFE46A0AAF51E /* EditProfileInteractor.swift in Sources */, FE9E70D021175DDC0034067A /* ChatScreenAlertFactory.swift in Sources */, @@ -17292,6 +17489,7 @@ 263A60AC1FB4F8F7006F9D52 /* ParticipantsDataSource.swift in Sources */, 852E8475213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift in Sources */, 2AC52C9C5598DB3C4D3D9364 /* AddContactViaPhoneWireframe.swift in Sources */, + 3AB7400321B9954100D1E967 /* ContactInfoManagementPresenter.swift in Sources */, 3A0A50CD21B7FEFE0052D334 /* MyGroupAliasProtocols.swift in Sources */, 4B8FC31A2163CC6700602D6B /* BaseChatCellModel+ModelType.swift in Sources */, 8504DEAD2069438D006722AC /* Media.swift in Sources */, @@ -17330,6 +17528,7 @@ 855AC534208E441500DC2335 /* StickersInputProtocols.swift in Sources */, 266F04CB2015050400B97A83 /* DBStarMessage.swift in Sources */, 1D31D13E6E53E71F8279C55C /* HistoryProtocols.swift in Sources */, + 3A0E426021BFBE99001A3F3C /* SearchContactPresenter.swift in Sources */, A4CE80C020C9318700400713 /* EmptyStateTableViewDS.swift in Sources */, A458FAC920ECDB480075D55E /* Cloneable.swift in Sources */, E7598F571FA1CDB20082FBE7 /* ProfileActionModel.swift in Sources */, @@ -17400,6 +17599,7 @@ B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */, A42D51CD206A361400EEB952 /* operation.swift in Sources */, 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, + 3A184D1D21C0FD8C0083D367 /* EmailContentViewModel.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */, F10AFEBC20F7B1D200C7CE83 /* WheelPreviewFactory.swift in Sources */, @@ -17433,6 +17633,7 @@ A42D52C1206A53AA00EEB952 /* p2p_Spec.swift in Sources */, 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */, A45F113A20B4218D00F45004 /* AvatarView.swift in Sources */, + 3AB7400421B9954100D1E967 /* ContactInfoManagementViewController.swift in Sources */, 26B5F7421FB0FF7B00CEC6AE /* FileManager.swift in Sources */, F10AFEB520F7B1B000C7CE83 /* WheelDefaultItemPreview.swift in Sources */, 858A72E5A4AE48CE24AFF649 /* MainInteractor.swift in Sources */, @@ -17571,6 +17772,7 @@ 2600CCBF216D447200EDC9C3 /* OptionallyActionCell.swift in Sources */, 853E595120D6AF59007799B9 /* Desc+Room.swift in Sources */, 850B9DA3219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift in Sources */, + 3A184D2121C0FEBC0083D367 /* ContentViewModel.swift in Sources */, A4679B8820B2DA550021FE9C /* ArrayExtension.swift in Sources */, 4B058F03204EA928004C7D9F /* DAOProtocol.swift in Sources */, A4B544E620EFAECE00EB7B0F /* LinkModel.swift in Sources */, @@ -17587,6 +17789,7 @@ 26D6D229212EDADC00EA2419 /* ConvertMessageDAO.swift in Sources */, 85991DB52113437D0056F3E0 /* UITextView+Extensions.swift in Sources */, 69309CB4317F99B9C299F7D6 /* SplashPresenter.swift in Sources */, + 3A184D2C21C12C080083D367 /* SearchContactCoordinator.swift in Sources */, 85482844204E915400DCBEC8 /* PrivacyTableViewCell.swift in Sources */, 9B9670A02152356D0058E98F /* LeaveVoiceMessageProtocols.swift in Sources */, A481BD1F20EE73BD008FFED8 /* InfoInjectableConstants.swift in Sources */, @@ -17694,6 +17897,7 @@ 85D66A0020BD963C00FBD803 /* InputTextMessage.swift in Sources */, A4868F312121D360001F624E /* DebuggingPreventing.swift in Sources */, A45F114920B421AB00F45004 /* ContactExtension.swift in Sources */, + 3AB7400621B9954100D1E967 /* ContactInfoManagementInteractor.swift in Sources */, 26B32B931FE20B8B00888A0A /* mucExtension+BERT.swift in Sources */, FEA655E52167777E00B44029 /* SeedBackupWalletInteractor.swift in Sources */, A43B25D720AB1EE400FF8107 /* NewChannelWireFrame.swift in Sources */, @@ -17718,6 +17922,7 @@ A45F113220B4218D00F45004 /* BaseChatCellLayout.swift in Sources */, A411D95C20AC3A5A009D107C /* ConversationsProvider.swift in Sources */, 859F9B4C2035CB1E009D017A /* ForwardContent.swift in Sources */, + 3A184D2821C128380083D367 /* TextFieldContentViewModel.swift in Sources */, 2B924FFB43474DF387A06D67 /* VideoPreviewPresenter.swift in Sources */, 2686D3251FC63EB30079CB75 /* SyncFileTable.swift in Sources */, A42D52DE206A53AB00EEB952 /* io_Spec.swift in Sources */, @@ -17786,6 +17991,7 @@ 85D66A1020BD965300FBD803 /* MentionPanelView.swift in Sources */, 4B7C73F1215A5509007924DB /* SMSCodeProviding.swift in Sources */, 45F60C4B14438C65076457AB /* EditUsernameProtocols.swift in Sources */, + 3A184D1B21C0FD800083D367 /* UsernameContentViewModel.swift in Sources */, 00F7B33E2029DD4B00E443E1 /* AudioItemView.swift in Sources */, 4B7C73F9215A5522007924DB /* DebugLogs.swift in Sources */, 0062D9412062EC4100B915AC /* InviteFriendsSelectionCell.swift in Sources */, @@ -17971,6 +18177,7 @@ B7EF8ED2210C502D00E0E981 /* InterpretationTypeTableDelegate.swift in Sources */, 1325429A6216D23E2E67B6B7 /* EditGroupPhotoWireframe.swift in Sources */, 2603139720A0A4B9009AC66D /* LangCell.swift in Sources */, + 3A0E426121BFBE99001A3F3C /* SearchContactViewController.swift in Sources */, A4213AF320D9240100B6BE7D /* PHFetchOptions+Utils.swift in Sources */, 268C341921074D6C00F1472A /* TranscribeLongOperationResponseData.swift in Sources */, 8511D3712034427F00B2A620 /* UIView+SafeArea.swift in Sources */, @@ -18058,6 +18265,7 @@ 852C3DCD216E34FC00447878 /* TypingSenderService.swift in Sources */, 859ECA6C21A441A9003630A0 /* AuthService.swift in Sources */, 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */, + 3A2C2DFC21C26708006A53BB /* SearchContactResponse.swift in Sources */, 26DCB24C2064B9CC001EF0AB /* ContactCellModel.swift in Sources */, 850EE2B421A75E270051F873 /* Country.swift in Sources */, 856A8EFC219C8D7A0004E11E /* AuthenticationType.swift in Sources */, diff --git a/Nynja/ContactsItemsFactory.swift b/Nynja/ContactsItemsFactory.swift index ae42d24a0..893f476ca 100644 --- a/Nynja/ContactsItemsFactory.swift +++ b/Nynja/ContactsItemsFactory.swift @@ -30,57 +30,63 @@ class ContactsItemsFactory: WCBaseItemsFactory { } - //MARK: - Items + // MARK: - Items var all: ImageActionItemModel { - let item = ImageActionItemModel(nameImage: "ic_list", navItem: .all, action: { [weak navigateDelegate] (item, indexPath) in + let item = ImageActionItemModel(nameImage: "ic_list", navItem: .all) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showListContacts(indexPath: indexPath) - }) + } item.state = .highlighted return item } var history: ImageActionItemModel { - return ImageActionItemModel(nameImage: "ic_history", navItem: .history, action: { [weak navigateDelegate] (item, indexPath) in + return ImageActionItemModel(nameImage: "ic_history", navItem: .history) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showHistory(indexPath: indexPath) - }) + } } var newContact: ImageActionItemModel { - let item = ImageActionItemModel(nameImage: "ic_new_contact", navItem: .newContact, action: { [weak navigateDelegate] (item, indexPath) in + let item = ImageActionItemModel(nameImage: "ic_new_contact", navItem: .newContact) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showNewContact(indexPath: indexPath) - }) - item.subitems = [inviteFriends, byContacts, byQr, byNumber, byUsername] + } + item.subitems = [inviteFriends, byEmail, byContacts, byQr, byNumber, byUsername] return item } + var byEmail: ImageFilledItemModel { + return ImageFilledItemModel(nameImage: "ic_email_storage", navItem: .byEmail) { [weak navigateDelegate] item, indexPath in + navigateDelegate?.showSearchByEmail(indexPath: indexPath) + } + } + var byUsername: ImageFilledItemModel { - return ImageFilledItemModel(nameImage: "ic_by_username", navItem: .byUsername, action: { [weak navigateDelegate] (item, indexPath) in + return ImageFilledItemModel(nameImage: "ic_by_username", navItem: .byUsername) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showByUserName(indexPath: indexPath) - }) + } } var byNumber: ImageFilledItemModel { - return ImageFilledItemModel(nameImage: "ic_by_number", navItem: .byNumber, action: { [weak navigateDelegate] (item, indexPath) in + return ImageFilledItemModel(nameImage: "ic_by_number", navItem: .byNumber) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showByNumber(indexPath: indexPath) - }) + } } var byQr: ImageFilledItemModel { - return ImageFilledItemModel(nameImage: "ic_by_qr_code", navItem: .byQRCode, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in + return ImageFilledItemModel(nameImage: "ic_by_qr_code", navItem: .byQRCode, isSelectable: false) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showByQR(indexPath: indexPath) - }) + } } var byContacts: ImageFilledItemModel { - return ImageFilledItemModel(nameImage: "ic_by_contacts", navItem: .byContacts, action: { [weak navigateDelegate] (item, indexPath) in + return ImageFilledItemModel(nameImage: "ic_by_contacts", navItem: .byContacts) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showByContacts(indexPath: indexPath) - }) + } } var inviteFriends: ImageFilledItemModel { - return ImageFilledItemModel(nameImage: "icon_invitation", navItem: .inviteFriends, action: { [weak navigateDelegate] (item, indexPath) in + return ImageFilledItemModel(nameImage: "icon_invitation", navItem: .inviteFriends) { [weak navigateDelegate] item, indexPath in navigateDelegate?.showInviteFriends(indexPath: indexPath) - }) + } } } diff --git a/Nynja/CountriesProviding.swift b/Nynja/CountriesProviding.swift index 5e76894c0..b2fff1479 100644 --- a/Nynja/CountriesProviding.swift +++ b/Nynja/CountriesProviding.swift @@ -6,8 +6,14 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol CountriesProviding { - func fetchCountries() -> [Country] +protocol CountrySearchProviding { func fetchCountry(by countryCode: String) -> Country? +} + +protocol LocalCountryProviding { func fetchDefaultCountry() -> Country } + +protocol CountriesProviding: CountrySearchProviding, LocalCountryProviding { + func fetchCountries() -> [Country] +} diff --git a/Nynja/Generated/ColorsConstants.swift b/Nynja/Generated/ColorsConstants.swift index 29a3ab02a..b83408908 100644 --- a/Nynja/Generated/ColorsConstants.swift +++ b/Nynja/Generated/ColorsConstants.swift @@ -15,8 +15,8 @@ internal extension SGColor { static let almostWhite = #colorLiteral(red: 0.8980392, green: 0.8980392, blue: 0.8980392, alpha: 1.0) /// 0x6dbee1ff (r: 109, g: 190, b: 225, a: 255) static let aquaBlue = #colorLiteral(red: 0.42745098, green: 0.74509805, blue: 0.88235295, alpha: 1.0) - /// 0x272a30ff (r: 39, g: 42, b: 48, a: 255) - static let backgroundColor = #colorLiteral(red: 0.15294118, green: 0.16470589, blue: 0.1882353, alpha: 1.0) + /// 0x2c2d32ff (r: 44, g: 45, b: 50, a: 255) + static let backgroundColor = #colorLiteral(red: 0.17254902, green: 0.1764706, blue: 0.19607843, alpha: 1.0) /// 0xddddddff (r: 221, g: 221, b: 221, a: 255) static let backgroundColorLight = #colorLiteral(red: 0.8666667, green: 0.8666667, blue: 0.8666667, alpha: 1.0) /// 0x3f3f3fff (r: 63, g: 63, b: 63, a: 255) @@ -29,10 +29,6 @@ internal extension SGColor { static let brown = #colorLiteral(red: 0.58431375, green: 0.4509804, blue: 0.28235295, alpha: 1.0) /// 0x2c2e33ff (r: 44, g: 46, b: 51, a: 255) static let callBackground = #colorLiteral(red: 0.17254902, green: 0.18039216, blue: 0.2, alpha: 1.0) - /// 0x2c2e3300 (r: 44, g: 46, b: 51, a: 0) - static let callGradientEnd = #colorLiteral(red: 0.17254902, green: 0.18039216, blue: 0.2, alpha: 0.0) - /// 0x2c2e33ff (r: 44, g: 46, b: 51, a: 255) - static let callGradientStart = #colorLiteral(red: 0.17254902, green: 0.18039216, blue: 0.2, alpha: 1.0) /// 0x01ae45ff (r: 1, g: 174, b: 69, a: 255) static let callGreen = #colorLiteral(red: 0.003921569, green: 0.68235296, blue: 0.27058825, alpha: 1.0) /// 0x00000000 (r: 0, g: 0, b: 0, a: 0) @@ -55,6 +51,10 @@ internal extension SGColor { static let facebookBackground = #colorLiteral(red: 0.23137255, green: 0.34901962, blue: 0.59607846, alpha: 1.0) /// 0x213980ff (r: 33, g: 57, b: 128, a: 255) static let facebookHighlighted = #colorLiteral(red: 0.12941177, green: 0.22352941, blue: 0.5019608, alpha: 1.0) + /// 0x2c2e3300 (r: 44, g: 46, b: 51, a: 0) + static let gradientEnd = #colorLiteral(red: 0.17254902, green: 0.18039216, blue: 0.2, alpha: 0.0) + /// 0x2c2e33ff (r: 44, g: 46, b: 51, a: 255) + static let gradientStart = #colorLiteral(red: 0.17254902, green: 0.18039216, blue: 0.2, alpha: 1.0) /// 0x666666ff (r: 102, g: 102, b: 102, a: 255) static let gray = #colorLiteral(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) /// 0x63666aff (r: 99, g: 102, b: 106, a: 255) diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 6c36e0ee5..33c411b00 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -1348,8 +1348,10 @@ internal extension String { static var wheelItemActions: String { return localizable.tr("Localizable", "wheel_item_actions") } /// All static var wheelItemAll: String { return localizable.tr("Localizable", "wheel_item_all") } - /// By Contacts + /// In Contacts static var wheelItemByContacts: String { return localizable.tr("Localizable", "wheel_item_byContacts") } + /// By Email + static var wheelItemByEmail: String { return localizable.tr("Localizable", "wheel_item_byEmail") } /// By Number static var wheelItemByNumber: String { return localizable.tr("Localizable", "wheel_item_byNumber") } /// By Password @@ -1618,6 +1620,26 @@ internal extension String { static var loginOptionsPhoneNumberOptionActionText: String { return localizable.tr("Localizable", "login_options.phone_number_option_action_text") } /// LOGIN OPTIONS static var loginOptionsScreenTitle: String { return localizable.tr("Localizable", "login_options.screen_title") } + /// Invite to NYNJA + static var searchContactInviteButtonTitle: String { return localizable.tr("Localizable", "search_contact.invite_button_title") } + /// Email + static var searchContactEmailPlaceholder: String { return localizable.tr("Localizable", "search_contact.email.placeholder") } + /// BY EMAIL + static var searchContactEmailScreenTitle: String { return localizable.tr("Localizable", "search_contact.email.screen_title") } + /// Enter Email + static var searchContactEmailSubtitle: String { return localizable.tr("Localizable", "search_contact.email.subtitle") } + /// Please choose a country code and enter a phone number. + static var searchContactPhoneDescriptionText: String { return localizable.tr("Localizable", "search_contact.phone.description_text") } + /// BY NUMBER + static var searchContactPhoneScreenTitle: String { return localizable.tr("Localizable", "search_contact.phone.screen_title") } + /// Enter Phone Number + static var searchContactPhoneSubtitle: String { return localizable.tr("Localizable", "search_contact.phone.subtitle") } + /// Username + static var searchContactUsernamePlaceholder: String { return localizable.tr("Localizable", "search_contact.username.placeholder") } + /// BY USERNAME + static var searchContactUsernameScreenTitle: String { return localizable.tr("Localizable", "search_contact.username.screen_title") } + /// Enter Username + static var searchContactUsernameSubtitle: String { return localizable.tr("Localizable", "search_contact.username.subtitle") } } } diff --git a/Nynja/Library/UI/Alert/Alert+Defaults.swift b/Nynja/Library/UI/Alert/Alert+Defaults.swift new file mode 100644 index 000000000..13920cb1f --- /dev/null +++ b/Nynja/Library/UI/Alert/Alert+Defaults.swift @@ -0,0 +1,19 @@ +// +// Alert+Defaults.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import NynjaUIKit + +extension Alert { + + static func makeDefault(title: String = "", message: String) -> Alert { + let actions: [Action] = [ + .init(title: String.localizable.ok, style: .default, handler: nil) + ] + return Alert(title: title, message: message, actions: actions) + } +} diff --git a/Nynja/Library/UI/BaseVC/BaseVC.swift b/Nynja/Library/UI/BaseVC/BaseVC.swift index 77f988986..0f6da7618 100644 --- a/Nynja/Library/UI/BaseVC/BaseVC.swift +++ b/Nynja/Library/UI/BaseVC/BaseVC.swift @@ -57,6 +57,10 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U } } } + + var solidBackgroundColor: UIColor { + return UIColor.nynja.backgroundColor + } // MARK: - Views diff --git a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift index af231e306..a9b4f2978 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift @@ -66,6 +66,8 @@ class BaseNynjaButton: UIButton { // MARK: - Setup func baseSetup() { + setContentHuggingPriority(.required, for: .vertical) + setContentCompressionResistancePriority(.required, for: .vertical) backgroundColor = defaultColor setupTextColor() diff --git a/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift index b3dfa5c76..ed3a35e33 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift @@ -19,24 +19,19 @@ final class NynjaCellButton: BaseNynjaButton { setContentHuggingPriority(.required, for: .horizontal) setContentCompressionResistancePriority(.required, for: .horizontal) - layer.cornerRadius = Constraints.cornerRadius.adjustedByWidth layer.masksToBounds = false - - adjustShadow(with: Constraints.shadow.radius) - layer.shadowOpacity = 0.3 - layer.shadowOffset = Constraints.shadow.offset.adjustedByWidth + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = Constraints.cornerRadius.adjustedByWidth } } extension NynjaCellButton { enum Constraints { - static let cornerRadius: CGFloat = 6 - static let contentEdgeInsets = UIEdgeInsets(top: 6, left: 5, bottom: 6, right: 5) - - enum shadow { - static let radius: CGFloat = 4 - static let offset = CGSize(width: 0, height: 3) - } + static let cornerRadius: CGFloat = 8 + static let contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6) } } diff --git a/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift b/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift index 9dcddc9a7..2122f4426 100644 --- a/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift +++ b/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift @@ -86,8 +86,6 @@ final class NynjaControlContainerView: UIView { func addGradientView(with colors: [UIColor] = [UIColor.nynja.backgroundColor.withAlphaComponent(0), UIColor.nynja.backgroundColor]) { let gradientView = GradientView(colors: colors) - // Disable interaction in order to handle taps through gradient, for example tap on table cells below gradient. - gradientView.isUserInteractionEnabled = false addSubview(gradientView) gradientView.snp.makeConstraints { maker in @@ -103,7 +101,6 @@ final class NynjaControlContainerView: UIView { @objc private func actionCloseButtonTapped(sender: UIButton) { closeButtonHandler?(sender) } - } // MARK: - Layout @@ -125,5 +122,4 @@ extension NynjaControlContainerView { static let defaultHeight = CGFloat(28.0).adjustedByWidth } } - } diff --git a/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift index e87f216a1..da07fbe74 100644 --- a/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift +++ b/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift @@ -47,7 +47,7 @@ extension LengthValidator { return self case let .both(max: _, min: length): return length - default: + case .max: return nil } } @@ -58,7 +58,7 @@ extension LengthValidator { return self case let .both(max: length, min: _): return length - default: + case .min: return nil } } @@ -69,7 +69,7 @@ extension LengthValidator { return provider(value) case let .max(value, provider): return provider(value) - default: + case .both: fatalError("Should not be called") } } @@ -80,7 +80,7 @@ extension LengthValidator { return value case let .max(value, _): return value - default: + case .both: fatalError("Should not be called") } } diff --git a/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift index de8b02eb1..d5e030177 100644 --- a/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift +++ b/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift @@ -7,5 +7,14 @@ // protocol MTIValidator { + var validationHandler: ((Bool) -> Void)? { get set } func validate(text: String) -> InputInfo? } + +extension MTIValidator { + + var validationHandler: ((Bool) -> Void)? { + get { return nil } + set { } + } +} diff --git a/Nynja/Library/UI/View/GradientContainerView.swift b/Nynja/Library/UI/View/GradientContainerView.swift new file mode 100644 index 000000000..bbc882bb1 --- /dev/null +++ b/Nynja/Library/UI/View/GradientContainerView.swift @@ -0,0 +1,121 @@ +// +// GradientContainerView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class GradientContainerView: UIView { + + var verticalInset: CGFloat { + return topGradientView.bounds.height + } + + // MARK: - Color + + var contentbackgroundColor: UIColor? { + didSet { + setupColors() + } + } + + private var topGradientColors: [UIColor] { + let backgroundColor = contentbackgroundColor ?? .clear + return [backgroundColor.withAlphaComponent(0), backgroundColor] + } + + private var bottomGradientColors: [UIColor] { + return topGradientColors.reversed() + } + + + // MARK: - Views + + private lazy var topGradientView: GradientView = { + let gradient = makeGradient(on: self, colors: topGradientColors) + + gradient.snp.makeConstraints { maker in + maker.top.equalToSuperview() + } + + return gradient + }() + + private lazy var bottomGradientView: GradientView = { + let gradient = makeGradient(on: self, colors: bottomGradientColors) + + gradient.snp.makeConstraints { maker in + maker.bottom.equalToSuperview() + } + + return gradient + }() + + private lazy var contentContainerView: UIView = { + let containerView = UIView() + addSubview(containerView) + return containerView + }() + + private let contentView: ContentView + + + // MARK: - Init + + init(contentView: ContentView, horizontalInset: CGFloat, backgroundColor: UIColor) { + self.contentView = contentView + super.init(frame: .zero) + self.backgroundColor = .clear + self.contentbackgroundColor = backgroundColor + setupColors() + setup(with: horizontalInset) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Setup + + private func setup(with horizontalInset: CGFloat) { + contentContainerView.snp.makeConstraints { maker in + maker.top.equalTo(topGradientView.snp.bottom) + maker.left.equalToSuperview() + maker.right.equalToSuperview() + maker.bottom.equalTo(bottomGradientView.snp.top) + } + + contentContainerView.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.top.equalToSuperview() + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + maker.bottom.equalToSuperview() + } + } + + private func setupColors() { + contentContainerView.backgroundColor = contentbackgroundColor + topGradientView.colors = topGradientColors + bottomGradientView.colors = bottomGradientColors + } + + + // MARK: - Gradient + + private func makeGradient(on view: UIView, colors: [UIColor]) -> GradientView { + let gradientView = GradientView(colors: colors) + + view.addSubview(gradientView) + gradientView.snp.makeConstraints { maker in + maker.left.right.equalToSuperview() + } + + return gradientView + } +} diff --git a/Nynja/Library/UI/View/GradientView.swift b/Nynja/Library/UI/View/GradientView.swift index b3bd9aaf7..cd4ee2f10 100644 --- a/Nynja/Library/UI/View/GradientView.swift +++ b/Nynja/Library/UI/View/GradientView.swift @@ -8,7 +8,7 @@ import UIKit -class GradientView: UIView { +final class GradientView: UIView { var colors: [UIColor] = [] { didSet { @@ -16,18 +16,35 @@ class GradientView: UIView { } } - // MARK: Init + override var intrinsicContentSize: CGSize { + var size = super.intrinsicContentSize + size.height = CGFloat(28.0).adjustedByWidth + return size + } + + + // MARK: - Init + init(colors: [UIColor]) { self.colors = colors super.init(frame: CGRect.zero) - self.backgroundColor = UIColor.nynja.clear + + backgroundColor = UIColor.nynja.clear + + setContentHuggingPriority(.required, for: .vertical) + setContentCompressionResistancePriority(.required, for: .vertical) + + // Disable interaction in order to handle taps through gradient, for example tap on table cells below gradient. + isUserInteractionEnabled = false } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - // MARK: Draw + + // MARK: - Draw + override func draw(_ rect: CGRect) { if !colors.isEmpty { let context = UIGraphicsGetCurrentContext() @@ -35,5 +52,4 @@ class GradientView: UIView { drawLinearGradient(in: context, colors: cgColors, direction: .fromTop) } } - } diff --git a/Nynja/Modules/Call/View/CallInProgressView.swift b/Nynja/Modules/Call/View/CallInProgressView.swift index 9fb80ebf1..a0ef90f84 100644 --- a/Nynja/Modules/Call/View/CallInProgressView.swift +++ b/Nynja/Modules/Call/View/CallInProgressView.swift @@ -75,8 +75,8 @@ class CallInProgressView: UIView, MultiPageCollectionViewDelegate { lazy var backgroundGradientView: UIView = { - let view = GradientView(colors: [UIColor.nynja.callGradientStart, - UIColor.nynja.callGradientEnd]) + let view = GradientView(colors: [UIColor.nynja.gradientStart, + UIColor.nynja.gradientEnd]) self.addSubview(view) view.snp.makeConstraints({ (make) in diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift index 04d0422f6..7b1bbb63b 100644 --- a/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift @@ -26,7 +26,6 @@ protocol AuthProviderViewInput: LoadingInteractive where Self: UIViewController func setNextActionEnabled(_ isEnabled: Bool) func select(country: Country) - func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) } // MARK: - Presenter diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift index d2e41c0da..d14e8dccc 100644 --- a/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift @@ -70,23 +70,24 @@ final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, } private func makePhoneNumberController(with country: Country) -> PhoneNumberTextController { - let controller = PhoneNumberTextController(country: country) + let controller = PhoneNumberTextController(countryProvider: CountriesProvider()) + controller.country = country controller.validationAction = { [weak view] result in view?.setNextActionEnabled(result) } - controller.autofillHandler = { [weak self] autofillInfo in - self?.processPhoneAutoFillInfo(autofillInfo) - } - return controller } private func makeEmailController() -> EmailTextController { - return EmailTextController(validator: EmailValidator()) { [weak view] result in + let controller = EmailTextController(validator: EmailValidator()) + + controller.validationAction = { [weak view] result in view?.setNextActionEnabled(result) } + + return controller } func setAvailableForSearch(_ isAvailable: Bool) { @@ -127,16 +128,6 @@ final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, wireframe.dismiss() } - private func processPhoneAutoFillInfo(_ autofillInfo: PhoneAutoFillInfo) { - guard let country = interactor.fetchCountry(by: autofillInfo.countryCode) else { - return - } - phoneNumberController?.country = country - - let phoneNumber = PhoneNumberInfo(country: country, number: autofillInfo.phoneNumber) - view?.update(phone: phoneNumber) - } - // MARK: - Interactor Output diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift index 658781f33..0b2eddae7 100644 --- a/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift @@ -26,8 +26,10 @@ final class AuthProviderViewController: BaseVC, AuthProviderViewInput, LoadingDi view.addSubview(container) container.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + maker.top.equalTo(navigationView.snp.bottom).offset(top) - maker.left.right.equalToSuperview() + maker.left.right.equalToSuperview().inset(horizontal) } return container @@ -187,10 +189,6 @@ final class AuthProviderViewController: BaseVC, AuthProviderViewInput, LoadingDi func select(country: Country) { phoneNumberView?.selectCountry(country) } - - func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) { - phoneNumberView?.updatePhone(autofillPhoneNumberInfo) - } } // MARK: - Layout diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift new file mode 100644 index 000000000..33f86a9e6 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift @@ -0,0 +1,41 @@ +// +// ContactInfoManagementProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +// MARK: - Wireframe + +protocol ContactInfoManagementWireframeProtocol: AlertDisplayable { + func dismiss() +} + +// MARK: - View + +protocol ContactInfoManagementViewInput: LoadingInteractive { +} + +// MARK: - Presenter + +protocol ContactInfoManagementPresenterProtocol: BasePresenterProtocol, NavigationProtocol { + func delete() + func save() +} + +// MARK: - Interactor + +// MARK: Input +protocol ContactInfoManagementInteractorInput: class { + func save(_ contactInfo: ContactInfoInputModel.ContactInfo) +} + +// MARK: Output +protocol ContactInfoManagementInteractorOutput: class { + func didDeleteContactInfo() + func didSaveContactInfo() + func didReceiveFailure(_ error: Error?) +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift new file mode 100644 index 000000000..591fde60f --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift @@ -0,0 +1,53 @@ +// +// ContactInfoInputModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/7/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct ContactInfoInputModel { + enum InfoType { + case phoneNumber + case email + case social + } + + enum Data { + case empty + case data(ContactInfo) + } + + enum ContactInfo { + case phoneNumber(PhoneNumber) + case email(String) + case social(SocialProfile) + + struct PhoneNumber { + enum Label { + case mobile + case home + case work + case custom(String) + } + + let numberInfo: PhoneNumberInfo + let label: Label + } + + struct SocialProfile { + enum Provider { + case facebook + case google + case twitter + } + let provider: Provider + let link: String + } + } + + let type: InfoType + let data: Data +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift new file mode 100644 index 000000000..25413beac --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift @@ -0,0 +1,45 @@ +// +// ContactInfoManagementInteractor.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaSDK + +final class ContactInfoManagementInteractor: BaseInteractor, ContactInfoManagementInteractorInput { + + private weak var presenter: ContactInfoManagementInteractorOutput? + + private let accountId: String + + + // MARK: - Services + + private let accountService: AccountService + + + // MARK: - Init + + struct Dependencies { + let presenter: ContactInfoManagementInteractorOutput + let accountId: String + let accountService: AccountService + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + accountId = dependencies.accountId + accountService = dependencies.accountService + } + + + // MARK: - Interactor Input + + func save(_ contactInfo: ContactInfoInputModel.ContactInfo) { +// let contactDetails = NYNContactDetails +// accountService.addContactInfo(to: "", contactDetails: ) + } +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift new file mode 100644 index 000000000..f5571c264 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift @@ -0,0 +1,63 @@ +// +// ContactInfoManagementPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class ContactInfoManagementPresenter: BasePresenter, ContactInfoManagementPresenterProtocol, ContactInfoManagementInteractorOutput { + + private weak var view: ContactInfoManagementViewInput? + private var interactor: ContactInfoManagementInteractorInput! + private var wireframe: ContactInfoManagementWireframeProtocol! + + + // MARK: - Presenter + + func back() { + wireframe.dismiss() + } + + func delete() { + + } + + func save() { + + } + + // MARK: - Interactor Output + + func didDeleteContactInfo() { + + } + + func didSaveContactInfo() { + + } + + func didReceiveFailure(_ error: Error?) { + + } +} + +// MARK: - Injection + +extension ContactInfoManagementPresenter: SetInjectable { + + struct Dependencies { + let view: ContactInfoManagementViewInput + let interactor: ContactInfoManagementInteractorInput + let wireframe: ContactInfoManagementWireframeProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift new file mode 100644 index 000000000..3bfe23f67 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift @@ -0,0 +1,156 @@ +// +// ContactInfoManagementContainerViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class ContactInfoManagementContainerViewController: BaseVC, ContactInfoManagementViewInput, KeyboardInteractive, LoadingDisplayable { + + private let presenter: ContactInfoManagementPresenterProtocol + + + // MARK: - Views + + private let bottomInset = Constraints.saveButton.bottom + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + private(set) lazy var contentView: UIView = { + let contentView = UIView() + + view.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom) + maker.left.right.equalToSuperview() + } + + return contentView + }() + + private(set) lazy var deleteButton: UIButton = { + let height = Constraints.deleteButton.height + let horizontal = Constraints.deleteButton.horizontal + + let button = UIButton() + + view.addSubview(button) + button.snp.makeConstraints { maker in + maker.top.equalTo(contentView.snp.bottom) + maker.left.right.equalToSuperview().inset(horizontal) + maker.height.equalTo(height) + } + + return button + }() + + private(set) lazy var saveButton: BaseNynjaButton = { + let height = Constraints.saveButton.height + let horizontal = Constraints.saveButton.horizontal + + let button = BaseNynjaButton() + + view.addSubview(button) + button.snp.makeConstraints { maker in + maker.left.right.equalToSuperview().inset(horizontal) + maker.height.equalTo(height) + adjustVerticalInset(.bottom, make: maker, offset: -bottomInset) + } + + return button + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: ContactInfoManagementPresenterProtocol + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + super.init(nibName: nil, bundle: nil) + _presenter = presenter + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Life Cycle + + override func initialize() { + super.initialize() + setupUI() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unregisterForKeyboardNotifications() + } + + + // MARK: - UI Setup + + private func setupUI() { + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) + + deleteButton.addTarget(self, action: #selector(delete(sender:)), for: .touchUpInside) + saveButton.addTarget(self, action: #selector(save(sender:)), for: .touchUpInside) + } + + + // MARK: - Actions + + @objc private func delete(sender: Any) { + presenter.delete() + } + + @objc private func save(sender: Any) { + presenter.save() + } + + + // MARK: - Layout + + private enum Constraints { + + enum deleteButton { + static let height: CGFloat = CGFloat(44).adjustedByWidth + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + } + + enum saveButton { + static let height: CGFloat = CGFloat(44).adjustedByWidth + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + static let bottom: CGFloat = CGFloat(28).adjustedByWidth + } + } + + + // MARK: - Keyboard + + func keyboardNotified(endFrame: CGRect) { + if endFrame.origin.y >= UIScreen.main.bounds.size.height { + updateToHide(view: saveButton, offset: -bottomInset) + } else { + updateToShow(view: saveButton, offset: -bottomInset - endFrame.height) + } + } +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift new file mode 100644 index 000000000..519bb6d0b --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift @@ -0,0 +1,61 @@ +// +// ContactInfoManagementWireframe.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import UIKit.UIViewController +import NynjaUIKit + +protocol ContactInfoManagementCoordinatorProtocol: AlertDisplayable { + func wireframe(_ wireframe: ContactInfoManagementWireframe, didEndWithState state: ContactInfoManagementWireframe.State) +} + +final class ContactInfoManagementWireframe: Wireframe, ContactInfoManagementWireframeProtocol { + + private let coordinator: ContactInfoManagementCoordinatorProtocol + + init(coordinator: ContactInfoManagementCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let accountId: String + let inputData: ContactInfoInputModel + } + + struct Dependencies { + let accountService: AccountService + } + + enum State { + case dismiss + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = ContactInfoManagementPresenter() + + let view = ContactInfoManagementContainerViewController(dependencies: .init(presenter: presenter)) + + let interactor = ContactInfoManagementInteractor(dependencies: .init( + presenter: presenter, + accountId: parameters.accountId, + accountService: dependencies.accountService) + ) + + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + + return view + } + + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } +} diff --git a/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift index 6f214274e..68c59ffbb 100644 --- a/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift @@ -231,6 +231,18 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { } } +// MARK: - Contact Info Management + +extension AccountSettingsCoordinator: ContactInfoManagementCoordinatorProtocol { + + func wireframe(_ wireframe: ContactInfoManagementWireframe, didEndWithState state: ContactInfoManagementWireframe.State) { + switch state { + case .dismiss: + navigation.popViewController(animated: true) + } + } +} + // MARK: - Delete Account extension AccountSettingsCoordinator: DeleteAccountCoordinatorProtocol { diff --git a/Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift index 7d500ba5b..93c9569ff 100644 --- a/Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift +++ b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift @@ -44,7 +44,7 @@ final class DeleteAccountInteractor: BaseInteractor, DeleteAccountInteractorInpu func deleteAccount() { // TODO: Use deleteProfile for now, because of request from BE team. In the future must be replaced with: // accountService.deleteAccount(accountId) { [weak self] result in - accountService.deleteProfile(identityId) { [weak self] result in + accountService.deleteIdentity(identityId) { [weak self] result in switch result { case let .success(status): print(status) diff --git a/Nynja/Modules/Flows/AppCoordinator.swift b/Nynja/Modules/Flows/AppCoordinator.swift index decb2a3c2..aefdd82ad 100644 --- a/Nynja/Modules/Flows/AppCoordinator.swift +++ b/Nynja/Modules/Flows/AppCoordinator.swift @@ -27,16 +27,21 @@ final class AppCoordinator: AppCoordinatorInput { } func start() { - let authCoordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) - authCoordinator.delegate = self + let authCoordinator = makeAuthCoordinator() authCoordinator.start() } func logout() { - start() + let authCoordinator = makeAuthCoordinator() + authCoordinator.restart() } - func end() { + func end() { } + + private func makeAuthCoordinator() -> AuthCoordinatorInput { + let authCoordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) + authCoordinator.delegate = self + return authCoordinator } } @@ -48,13 +53,3 @@ extension AppCoordinator: AuthCoordinatorDelegate { MainWireFrame().presentMain(navigation: navigation, isRegistered: true) } } - -// MARK: - Account Settings Delegate - -// Should be handled here when new navigation will be implemented -extension AppCoordinator: AccountSettingsCoordinatorDelegate { - - func accountSettingsCoordinator(_ coordinator: AccountSettingsCoordinator, didFinishWithState state: AccountSettingsCoordinator.State) { - - } -} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift index 004a070b2..8da21093e 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift @@ -13,11 +13,11 @@ protocol AuthCoordinatorDelegate: class { func authCoordinatorDidFinish(_ coordinator: AuthCoordinator) } -protocol AuthCoordinatorInput: class { +protocol AuthCoordinatorInput: Coordinator { func restart() } -final class AuthCoordinator: Coordinator, NavigationContainer { +final class AuthCoordinator: AuthCoordinatorInput, NavigationContainer { weak var delegate: AuthCoordinatorDelegate? diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift index b5cca653e..11bf72a47 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift @@ -24,15 +24,12 @@ protocol AuthWireframeProtocol: AlertDisplayable { protocol AuthViewInput: LoadingInteractive where Self: UIViewController { func select(country: Country) - func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) } // MARK: - Presenter protocol AuthPresenterProtocol: class { var loginOption: PlainLoginOption { get } - - var selectedCountry: Country { get } func switchLoginOption() @@ -42,7 +39,6 @@ protocol AuthPresenterProtocol: class { func loginViaPhoneNumber(_ phoneNumber: String, country: Country) func selectCountry() - func processPhoneAutoFillInfo(_ autofillInfo: PhoneAutoFillInfo) } // MARK: - Interactor @@ -50,7 +46,6 @@ protocol AuthPresenterProtocol: class { // MARK: Input protocol AuthInteractorInput: class { func fetchDefaultCountry() -> Country - func fetchCountry(by code: String) -> Country? func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) func loginViaEmail(_ email: String) diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift index 7ece50fa9..fea133f14 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift @@ -11,18 +11,18 @@ import Foundation final class EmailTextController { private let validator: Validator - private let validationAction: (Bool) -> Void + + var validationAction: ((Bool) -> Void)? private(set) var isValid: Bool = false - init(validator: Validator, validationAction: @escaping (Bool) -> Void) { + init(validator: Validator) { self.validator = validator - self.validationAction = validationAction } func textDidChange(_ textInput: MaterialTextInput) { isValid = validator.isValid(text: textInput.text.trimmed()) - validationAction(isValid) + validationAction?(isValid) } func textInputShouldReturn(_ textInput: MaterialTextField) -> Bool { diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift index c2182a678..1d2c87065 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift @@ -11,8 +11,7 @@ import NynjaUIKit import libPhoneNumber_iOS struct PhoneAutoFillInfo { - let countryCode: String - let phoneNumber: String + let country: Country } final class PhoneNumberTextController: NSObject, UITextFieldDelegate { @@ -32,6 +31,8 @@ final class PhoneNumberTextController: NSObject, UITextFieldDelegate { // MARK: - Dependencies + private let countryProvider: CountrySearchProviding + private let phoneNumberUtil = NBPhoneNumberUtil.sharedInstance()! private var formatter: PhoneNumberFormatter @@ -39,8 +40,9 @@ final class PhoneNumberTextController: NSObject, UITextFieldDelegate { // MARK: - Init - init(country: Country) { - self.country = country + init(countryProvider: CountrySearchProviding & LocalCountryProviding) { + self.country = countryProvider.fetchDefaultCountry() + self.countryProvider = countryProvider self.formatter = CountryMaskFormatter(country: country) } @@ -60,7 +62,9 @@ final class PhoneNumberTextController: NSObject, UITextFieldDelegate { // 971231212 - without country code let nationalNumber = try extractNationalNumber(from: possibleNumber, countryCode: countryCode) - let autofillInfo = PhoneAutoFillInfo(countryCode: countryCode, phoneNumber: nationalNumber) + country = countryProvider.fetchCountry(by: countryCode) ?? country + + let autofillInfo = PhoneAutoFillInfo(country: country) autofillHandler?(autofillInfo) isValid = check(nationalNumber) diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift index eefae8801..29110b89b 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift @@ -48,10 +48,6 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { return countriesProvider.fetchDefaultCountry() } - func fetchCountry(by code: String) -> Country? { - return countriesProvider.fetchCountry(by: code) - } - func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) { authService.login(by: phoneNumberInfo, confirmVia: .sms) { [weak self] result in switch result { @@ -125,10 +121,9 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { let accountId = authResponse.accountId let authType = authResponse.authenticationType - switch authType { case .login: - accountService.getAccount(by: accountId) { [weak self] accountLoadingResult in + accountService.getAccount(accountId: accountId) { [weak self] accountLoadingResult in guard let self = self else { return } switch accountLoadingResult { diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift index 862f799f5..597aaee4a 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift @@ -19,10 +19,6 @@ final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAu private(set) var loginOption: PlainLoginOption = .phoneNumber("") - private(set) lazy var selectedCountry: Country = { - return interactor.fetchDefaultCountry() - }() - func switchLoginOption() { switch loginOption { case .email: @@ -74,19 +70,10 @@ final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAu guard case let .success(country) = result else { return } - self?.selectedCountry = country self?.view?.select(country: country) } } - func processPhoneAutoFillInfo(_ autofillInfo: PhoneAutoFillInfo) { - guard let country = interactor.fetchCountry(by: autofillInfo.countryCode) else { - return - } - let phoneNumberInfo = PhoneNumberInfo(country: country, number: autofillInfo.phoneNumber) - view?.update(phone: phoneNumberInfo) - } - private func confirmInputData(for loginOption: PlainLoginOption, completion: @escaping (_ isConfirmed: Bool) -> Void) { let title = titleForPopup(loginOption: loginOption) let message = messageForPopup(loginOption: loginOption) @@ -94,7 +81,7 @@ final class AuthPresenter: AuthPresenterProtocol, AuthInteractorOutput, GoogleAu let modifyAction = Alert.Action(title: String.localizable.authPopupModifyAction, style: .default) { _ in completion(false) } - let confirmAction = Alert.Action(title: String.localizable.authPopupModifyAction, style: .default) { _ in + let confirmAction = Alert.Action(title: String.localizable.authPopupConfirmAction, style: .default) { _ in completion(true) } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift index 9f5a92bc3..865dc2fcc 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift @@ -13,6 +13,10 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec private let presenter: AuthPresenterProtocol + private let phoneNumberTextController: PhoneNumberTextController + + private let emailTextController: EmailTextController + // MARK: - Views @@ -77,8 +81,10 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec contentView.addSubview(containerView) containerView.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + maker.top.equalTo(headerView.snp.bottom) - maker.left.right.equalToSuperview() + maker.left.right.equalToSuperview().inset(horizontal) } return containerView @@ -86,7 +92,7 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec private lazy var emailContainerView = makeEmailLoginView(on: loginContainerView) - private lazy var phoneContainerView = makePhoneNumberLoginView(on: loginContainerView, country: presenter.selectedCountry) + private lazy var phoneContainerView = makePhoneNumberLoginView(on: loginContainerView, country: phoneNumberTextController.country) private var emailLoginView: EmailLoginView { return emailContainerView.contentView @@ -106,9 +112,10 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec contentView.addSubview(button) button.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + maker.top.equalTo(loginContainerView.snp.bottom).offset(24) - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.left.right.equalToSuperview().inset(horizontal) maker.height.equalTo(44) } @@ -150,8 +157,6 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec self.showEmailLogin(animated: true) case .phoneNumber: self.showPhoneNumberLogin(animated: true) - default: - break } return loginOption @@ -162,8 +167,10 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec contentView.addSubview(bottomView) bottomView.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + maker.top.equalTo(alternativeLabel.snp.bottom).offset(32) - maker.left.right.equalToSuperview() + maker.left.right.equalToSuperview().inset(horizontal) maker.bottom.equalToSuperview().inset(30 + UIWindow.safeAreaBottomPadding()) } @@ -171,38 +178,18 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec }() - // MARK: - Validators - - private lazy var emailTextController: EmailTextController = { - return EmailTextController(validator: EmailValidator()) { [weak self] result in - self?.nextButton.isEnabled = result - } - }() - - private lazy var phoneNumberTextController: PhoneNumberTextController = { - let controller = PhoneNumberTextController(country: presenter.selectedCountry) - - controller.validationAction = { [weak self] result in - self?.nextButton.isEnabled = result - } - - controller.autofillHandler = { [weak presenter] autofillInfo in - presenter?.processPhoneAutoFillInfo(autofillInfo) - } - - return controller - }() - - // MARK: - Init struct Dependencies { let presenter: AuthPresenterProtocol + let phoneNumberController: PhoneNumberTextController + let emailController: EmailTextController } init(dependencies: Dependencies) { presenter = dependencies.presenter - + phoneNumberTextController = dependencies.phoneNumberController + emailTextController = dependencies.emailController super.init(nibName: nil, bundle: nil) } @@ -236,6 +223,9 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec } enableKeyboardHidingWhenTappedAround() + + setupPhoneNumberController() + setupEmailController() } override func viewWillAppear(_ animated: Bool) { @@ -247,6 +237,21 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec super.viewDidDisappear(animated) unregisterForKeyboardNotifications() } + + + // MARK: - Setup + + private func setupPhoneNumberController() { + phoneNumberTextController.validationAction = { [weak self] result in + self?.nextButton.isEnabled = result + } + } + + private func setupEmailController() { + emailTextController.validationAction = { [weak self] result in + self?.nextButton.isEnabled = result + } + } } // MARK: - View Input @@ -257,11 +262,6 @@ extension AuthViewController { phoneNumberTextController.country = country phoneNumberLoginView.selectCountry(country) } - - func update(phone autofillPhoneNumberInfo: PhoneNumberInfo) { - phoneNumberTextController.country = autofillPhoneNumberInfo.country - phoneNumberLoginView.updatePhone(autofillPhoneNumberInfo) - } } // MARK: - KeyboardInteractive @@ -285,7 +285,7 @@ private extension AuthViewController { case .phoneNumber: let inputText = phoneNumberLoginView.phoneNumberTextField.text ?? "" - presenter.loginViaPhoneNumber(inputText, country: presenter.selectedCountry) + presenter.loginViaPhoneNumber(inputText, country: phoneNumberTextController.country) } } @@ -328,10 +328,10 @@ private extension AuthViewController { } } - func makeEmailLoginView(on view: UIView) -> LoginContainerView { + func makeEmailLoginView(on view: UIView) -> DetailContainerView { let loginView = EmailLoginView(textController: emailTextController) - let container = LoginContainerView(contentView: loginView) + let container = DetailContainerView(contentView: loginView) container.detailsLabel.text = String.localizable.authEnterEmailAddressComment view.addSubview(container) @@ -342,10 +342,10 @@ private extension AuthViewController { return container } - func makePhoneNumberLoginView(on view: UIView, country: Country) -> LoginContainerView { + func makePhoneNumberLoginView(on view: UIView, country: Country) -> DetailContainerView { let loginView = PhoneNumberLoginView(textController: phoneNumberTextController) - let container = LoginContainerView(contentView: loginView) + let container = DetailContainerView(contentView: loginView) container.detailsLabel.text = String.localizable.authEnterPhoneNumberComment view.addSubview(container) @@ -363,3 +363,12 @@ private extension AuthViewController { return container } } + +// MARK: - Layout + +private extension AuthViewController { + + enum Constraints { + static let horizontal: CGFloat = CGFloat(16) + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/DetailContainerView.swift similarity index 88% rename from Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift rename to Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/DetailContainerView.swift index c05f11b85..1a77de3f0 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginContainerView.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/DetailContainerView.swift @@ -1,5 +1,5 @@ // -// LoginContainerView.swift +// DetailContainerView.swift // Nynja // // Created by Anton Poltoratskyi on 11/26/18. @@ -10,7 +10,7 @@ import UIKit import SnapKit import NynjaUIKit -final class LoginContainerView: BaseView { +final class DetailContainerView: BaseView { let contentView: T @@ -57,8 +57,8 @@ final class LoginContainerView: BaseView { addSubview(label) label.snp.makeConstraints { maker in maker.top.equalTo(contentView.snp.bottom) - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.left.equalToSuperview() + maker.right.equalToSuperview() maker.bottom.lessThanOrEqualToSuperview() maker.bottom.equalToSuperview().priority(.high) } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift index 008a7f6fd..209128992 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift @@ -70,8 +70,8 @@ private extension EmailLoginView { addSubview(textField) textField.snp.makeConstraints { maker in maker.top.bottom.equalToSuperview() - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.left.equalToSuperview() + maker.right.equalToSuperview() maker.height.equalTo(64) } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift index f32f5b657..6a045ec91 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift @@ -102,8 +102,8 @@ private extension LoginOptionsView { button.snp.makeConstraints { maker in maker.top.equalToSuperview() maker.bottom.equalTo(loginWithFacebook.snp.top).offset(-16) - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.left.equalToSuperview() + maker.right.equalToSuperview() maker.height.equalTo(44) } @@ -125,8 +125,8 @@ private extension LoginOptionsView { addSubview(button) button.snp.makeConstraints { maker in maker.bottom.equalTo(loginWithGoogle.snp.top).offset(-16) - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.left.equalToSuperview() + maker.right.equalToSuperview() maker.height.equalTo(44) } @@ -148,8 +148,8 @@ private extension LoginOptionsView { addSubview(button) button.snp.makeConstraints { maker in maker.bottom.equalToSuperview() - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.left.equalToSuperview() + maker.right.equalToSuperview() maker.height.equalTo(44) } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift index 0bec7f77a..b6490367c 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -44,7 +44,7 @@ final class PhoneNumberLoginView: UIView, Configurable { extension PhoneNumberLoginView { struct Config { let country: Country - let countrySelectorAction: () -> Void + let countrySelectorAction: (() -> Void)? } func configure(config: Config) { @@ -53,6 +53,10 @@ extension PhoneNumberLoginView { phoneNumberTextField.delegate = textController selectCountry(config.country) + textController.autofillHandler = { [weak self] autofillInfo in + self?.updatePhoneCountry(autofillInfo.country) + } + _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField] } } @@ -64,6 +68,8 @@ extension PhoneNumberLoginView { private func updateCountryInfo(for country: Country) { self.country = country + textController.country = country + countrySelector.setTitle(country.name, for: .normal) countryCodeField.setTitle("+\(country.code)", for: .normal) @@ -72,20 +78,19 @@ extension PhoneNumberLoginView { phoneNumberTextField.attributedPlaceholder = NSAttributedString( string: placeholder, attributes: [ - .font: phoneNumberTextField.font, - .foregroundColor: phoneNumberTextField.textColor + .font: phoneNumberTextField.font!, + .foregroundColor: phoneNumberTextField.textColor! ] ) } func selectCountry(_ country: Country) { - updateCountryInfo(for: country) + updatePhoneCountry(country) phoneNumberTextField.text = "" } - func updatePhone(_ autofillPhoneNumberInfo: PhoneNumberInfo) { - updateCountryInfo(for: autofillPhoneNumberInfo.country) - phoneNumberTextField.text = autofillPhoneNumberInfo.number + private func updatePhoneCountry(_ country: Country) { + updateCountryInfo(for: country) } } @@ -115,8 +120,8 @@ private extension PhoneNumberLoginView { addSubview(button) button.snp.makeConstraints { maker in maker.top.equalToSuperview() - maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.left.equalToSuperview() + maker.right.equalToSuperview() maker.height.equalTo(64) } @@ -131,7 +136,7 @@ private extension PhoneNumberLoginView { container.snp.makeConstraints { maker in maker.top.equalTo(countrySelector.snp.bottom) maker.left.bottom.equalToSuperview() - maker.width.equalTo(100) + maker.width.equalTo(84) maker.height.equalTo(64) } @@ -153,7 +158,7 @@ private extension PhoneNumberLoginView { countryCodeContainer.addSubview(field) field.snp.makeConstraints { maker in maker.centerY.equalToSuperview() - maker.left.equalToSuperview().offset(16) + maker.left.equalToSuperview() maker.right.equalToSuperview().inset(16) } @@ -194,7 +199,7 @@ private extension PhoneNumberLoginView { textField.snp.makeConstraints { maker in maker.centerY.equalToSuperview() maker.left.equalToSuperview().offset(16) - maker.right.equalToSuperview().inset(16) + maker.right.equalToSuperview() } return textField diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift index 47dd06564..fea9e977e 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift @@ -40,7 +40,13 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let presenter = AuthPresenter() - let view = AuthViewController(dependencies: .init(presenter: presenter)) + let view = AuthViewController( + dependencies: .init( + presenter: presenter, + phoneNumberController: PhoneNumberTextController(countryProvider: dependencies.countriesProvider), + emailController: EmailTextController(validator: EmailValidator()) + ) + ) let interactor = AuthInteractor(dependencies: .init( presenter: presenter, diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift index c7c83ccd1..6f24ab621 100644 --- a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift @@ -10,7 +10,7 @@ import Foundation // MARK: - Wireframe -protocol CodeConfirmationWireframeProtocol: class { +protocol CodeConfirmationWireframeProtocol: AlertDisplayable { func continueSignUpFlow(with accountId: String) func continueSuccessAuthentication() func back() @@ -58,5 +58,5 @@ protocol CodeConfirmationInteractorOutput: class { func didConfirmCode(response: AuthResponse) func didSaveAccount() - func didReceiveFailure(_ error: Error) + func didReceiveFailure(_ error: Error?) } diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift index 3e16b7958..c5cb65028 100644 --- a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -66,7 +66,7 @@ final class CodeConfirmationInteractor: CodeConfirmationInteractorInput, Initial } func loadAccount(by accountId: String) { - accountService.getAccount(by: accountId) { [weak self] result in + accountService.getAccount(accountId: accountId) { [weak self] result in guard let self = self else { return } switch result { diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift index 3a02e1e9e..30c9f417c 100644 --- a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -7,6 +7,7 @@ // import Foundation +import NynjaUIKit final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeConfirmationInteractorOutput, SetInjectable { @@ -133,12 +134,14 @@ extension CodeConfirmationPresenter { wireframe.continueSuccessAuthentication() } - func didReceiveFailure(_ error: Error) { + func didReceiveFailure(_ error: Error?) { view?.hideLoading() -// let actions = [UIAlertAction(title: "OK", style: .default, handler: nil)] -// presentAlert(title: "Failure", message: "Code is invalid or internal error", actions: actions) - // FIXME: check if it is internal error or real wrong code + let actions = [Alert.Action(title: "OK", style: .default)] + let message: String = error.flatMap { "\(($0 as NSError).code): \($0.localizedDescription)" } ?? "Something went wrong" + let alert = Alert(title: "Failure", message: message, actions: actions) + + wireframe.present(alert) } } diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift index 002b65588..4250610a4 100644 --- a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -7,8 +7,9 @@ // import Foundation +import NynjaUIKit -protocol CodeConfirmationCoordinatorProtocol: class { +protocol CodeConfirmationCoordinatorProtocol: AlertDisplayable { func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) } @@ -55,6 +56,10 @@ final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProto return view } + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) + } + func continueSignUpFlow(with accountId: String) { coordinator.wireframe(self, didEndWith: .registered(accountId: accountId)) } diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift index 5ca4a4411..d735568ba 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -114,7 +114,7 @@ final class CreateProfileInteractor: CreateProfileInteractorInput, InitializeInj username: userName, accountStatus: .enabled, roles: nil, - qrCode: nil, + qrCode: accountId, birthday: nil) accountService.completePendingAccountCreation(accountInfo) { [weak self] result in diff --git a/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift index 6c692cd67..63e692193 100644 --- a/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift @@ -68,7 +68,9 @@ final class SplashInteractor: SplashInteractorInput, InitializeInjectable { badgeService.observeBadgeNumber(appDelegate) { badgeNumber in application.applicationIconBadgeNumber = Int(badgeNumber) } - mqttService.connect() + + // FIXME: connect when +// mqttService.connect() callService.initialize() MediaDownloadManager.setupAppDataUsageSettingsIfNeeded() diff --git a/Nynja/Modules/Flows/Search Flow/Entities/SearchContactResponse.swift b/Nynja/Modules/Flows/Search Flow/Entities/SearchContactResponse.swift new file mode 100644 index 000000000..c2358187d --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/Entities/SearchContactResponse.swift @@ -0,0 +1,94 @@ +// +// SearchContactResponse.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import NynjaSDK + +struct SearchContactResponse { + enum AccountInfo { + case accountId(String, avatar: String?, firstName: String, lastName: String) + case account(DBAccount) + } + let accountInfo: AccountInfo + + /// The field by which result was found + enum InputField { + case phoneNumber(PhoneNumberInfo) + case email(String) + case username(String) + case qrCode + } + let inputField: InputField + + var accountId: String { + switch accountInfo { + case let .accountId(accountId, _, _, _): + return accountId + case let .account(account): + return account.accountId + } + } + + var avatar: String? { + switch accountInfo { + case let .accountId(_, avatar, _, _): + return avatar + case let .account(account): + return account.avatar + } + } + + var firstName: String { + switch accountInfo { + case let .accountId(_, _, firstName, _): + return firstName + case let .account(account): + return account.firstName + } + } + + var lastName: String? { + switch accountInfo { + case let .accountId(_, _, _, lastName): + return lastName + case let .account(account): + return account.lastName + } + } + + var fullName: String { + return lastName.flatMap { !$0.isEmpty ? "\(firstName) \($0)" : firstName } ?? firstName + } + + var details: String { + switch inputField { + case let .phoneNumber(phoneNumber): + return phoneNumber.displayString + case let .email(email): + return email + case let .username(username): + return username + case .qrCode: + // should never happen + return "" + } + } + + init(result: NYNSearchResultDetails, account: Account?, inputField: InputField) { + if let account = account?.databaseModel as? DBAccount { + self.accountInfo = .account(account) + } else { + self.accountInfo = .accountId(result.accountId, avatar: result.avatar, firstName: result.firstName, lastName: result.lastName) + } + self.inputField = inputField + } + + init(account: DBAccount, inputField: InputField) { + self.accountInfo = .account(account) + self.inputField = inputField + } +} diff --git a/Nynja/Modules/Flows/Search Flow/Entities/SearchContactResult.swift b/Nynja/Modules/Flows/Search Flow/Entities/SearchContactResult.swift new file mode 100644 index 000000000..af1c1b049 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/Entities/SearchContactResult.swift @@ -0,0 +1,14 @@ +// +// SearchContactResult.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum SearchContactResult { + case inAppAccount(SearchContactResponse) + case phonebook([PhoneContact]) +} diff --git a/Nynja/Modules/Flows/Search Flow/Entities/SearchInputMode.swift b/Nynja/Modules/Flows/Search Flow/Entities/SearchInputMode.swift new file mode 100644 index 000000000..f6f34cbf4 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/Entities/SearchInputMode.swift @@ -0,0 +1,13 @@ +// +// SearchInputMode.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum SearchInputMode { + case phoneNumber + case username + case email +} diff --git a/Nynja/Modules/Flows/Search Flow/Interactor/SearchContactInteractor.swift b/Nynja/Modules/Flows/Search Flow/Interactor/SearchContactInteractor.swift new file mode 100644 index 000000000..6259f25cd --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/Interactor/SearchContactInteractor.swift @@ -0,0 +1,100 @@ +// +// SearchContactInteractor.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class SearchContactInteractor: BaseInteractor, SearchContactInteractorInput { + + private weak var presenter: SearchContactInteractorOutput? + + // MARK: - Services + + private let accountService: AccountService + + private let contactManager: ContactManager + + + // MARK: - Init + + struct Dependencies { + let presenter: SearchContactInteractorOutput + let accountService: AccountService + let contactManager: ContactManager + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + accountService = dependencies.accountService + contactManager = dependencies.contactManager + } + + + // MARK: - Interactor Input + + func searchByEmail(_ email: String) { + accountService.searchByEmail(email) { [weak self] result in + switch result { + case let .success(searchResult): + self?.processSearchResult(searchResult, for: .email(email)) + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + } + + func searchByPhoneNumber(_ phoneNumber: PhoneNumberInfo) { + accountService.searchByPhone(phoneNumber) { [weak self] result in + switch result { + case let .success(searchResult): + self?.processSearchResult(searchResult, for: .phoneNumber(phoneNumber)) + + case let .failure(error): + if let error = error as? AccountError, case .accountNotFound = error { + self?.searchPhoneBook(by: phoneNumber) { [weak self] result in + switch result { + case let .success(phonebookContacts): + self?.presenter?.didReceiveSearchResult(.phonebook(phonebookContacts)) + case .failure: + self?.presenter?.didReceiveFailure(AccountError.accountNotFound) + } + } + } else { + self?.presenter?.didReceiveFailure(error) + } + } + } + } + + func searchByUsername(_ username: String) { + accountService.searchByUsername(username) { [weak self] result in + switch result { + case let .success(searchResult): + self?.processSearchResult(searchResult, for: .username(username)) + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + } + + private func processSearchResult(_ searchResult: NYNSearchResultDetails, for inputField: SearchContactResponse.InputField) { + accountService.getAccount(accountId: searchResult.accountId) { [weak self] result in + switch result { + case let .success(account): + let result = SearchContactResponse(result: searchResult, account: account, inputField: inputField) + self?.presenter?.didReceiveSearchResult(.inAppAccount(result)) + + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + } + + private func searchPhoneBook(by phoneNumber: PhoneNumberInfo, completion: @escaping (Result<[PhoneContact]>) -> Void) { + contactManager.getContactsByPhoneNumber(phoneNumber.fullNumber, completion: completion) + } +} diff --git a/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift b/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift new file mode 100644 index 000000000..cb7a7a19b --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift @@ -0,0 +1,243 @@ +// +// SearchContactPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class SearchContactPresenter: BasePresenter, SearchContactPresenterProtocol, SearchContactInteractorOutput { + + override var itemsFactory: WCItemsFactory? { + return ContactsExpandedItemsFactory() + } + + private weak var view: SearchContactViewInput? + private var interactor: SearchContactInteractorInput! + private var wireframe: SearchContactWireframeProtocol! + + private let inputMode: SearchInputMode + + private var phoneTextController: PhoneNumberTextController! + + private lazy var contentViewModel: SearchContactViewModel = makeContentViewModel() + + + // MARK: - Init + + init(inputMode: SearchInputMode) { + self.inputMode = inputMode + super.init() + } + + + // MARK: - ViewModel + + private func makeContentViewModel() -> SearchContactViewModel { + let title: String + let subtitle: String + let viewModel: ContentViewModel + + switch inputMode { + case .phoneNumber: + title = String.localizable.searchContactPhoneScreenTitle + subtitle = String.localizable.searchContactPhoneSubtitle + viewModel = makePhoneNumberViewModel() + + case .username: + title = String.localizable.searchContactUsernameScreenTitle + subtitle = String.localizable.searchContactUsernameSubtitle + viewModel = makeUsernameViewModel() + + case .email: + title = String.localizable.searchContactEmailScreenTitle + subtitle = String.localizable.searchContactEmailSubtitle + viewModel = makeEmailViewModel() + } + + return SearchContactViewModel(title: title, subtitle: subtitle, content: viewModel) + } + + private func makePhoneNumberViewModel() -> ContentViewModel { + let viewModel = PhoneNumberContentViewModel( + controller: phoneTextController, + descriptionText: String.localizable.searchContactPhoneDescriptionText + ) + + viewModel.countrySelectionHandler = { [weak wireframe, weak viewModel] in + wireframe?.selectCountry { result in + guard case let .success(country) = result else { + return + } + viewModel?.selectCountry(country) + } + } + + viewModel.validationHandler = { [weak view] isEnabled in + view?.setActionsEnabled(isEnabled) + } + + return viewModel + } + + private func makeEmailViewModel() -> ContentViewModel { + var isValid = false + + let viewModel = EmailContentViewModel(validator: EmailTextValidator()) + + viewModel.returnHandler = { [weak self] in + if isValid { + self?.search() + } + } + + viewModel.validationHandler = { [weak view] isEnabled in + isValid = isEnabled + view?.setActionsEnabled(isEnabled) + } + + return viewModel + } + + private func makeUsernameViewModel() -> ContentViewModel { + var isValid = false + + let viewModel = UsernameContentViewModel(validator: UsernameTextValidator()) + + viewModel.returnHandler = { [weak self] in + if isValid { + self?.search() + } + } + + viewModel.validationHandler = { [weak view] isEnabled in + isValid = isEnabled + view?.setActionsEnabled(isEnabled) + } + + return viewModel + } + + + // MARK: - Presenter + + func viewDidLoad() { + view?.setupContent(contentViewModel) + view?.setActionsEnabled(false) + } + + func search() { + view?.showLoading() + switch contentViewModel.content { + case let viewModel as PhoneNumberContentViewModel: + interactor.searchByPhoneNumber(viewModel.inputNumber) + + case let viewModel as UsernameContentViewModel: + interactor.searchByUsername(viewModel.inputText) + + case let viewModel as EmailContentViewModel: + interactor.searchByEmail(viewModel.inputText) + + default: + view?.hideLoading() + } + } + + func back() { + wireframe.dismiss() + } + + + // MARK: - Interactor Output + + func didReceiveSearchResult(_ searchResult: SearchContactResult) { + view?.hideLoading() + + switch searchResult { + case let .inAppAccount(searchResponse): + openAccountDetails(for: searchResponse) + + case let .phonebook(contacts): + handlePhonebookResponse(contacts) + } + } + + func didReceiveFailure(_ error: Error) { + view?.hideLoading() + + func presentDefaultAlert() { + let title = "Failure" + let message = "Something went wrong" + let actions: [Alert.Action] = [ + .init(title: String.localizable.ok, style: .default, handler: nil) + ] + + let alert = Alert(title: title, message: message, actions: actions) + wireframe.present(alert) + } + + guard let error = error as? AccountError else { + presentDefaultAlert() + return + } + switch error { + case .invalidResponse: + presentDefaultAlert() + case .accountNotFound: + handleEmptySearchResult() + } + } + + private func openInviteToNynja(for contact: PhoneContact) { + wireframe.showInvite(contact) + } + + private func openAccountDetails(for searchResponse: SearchContactResponse) { + wireframe.showAccountDetails(for: searchResponse) + } + + private func handlePhonebookResponse(_ phonebookContacts: [PhoneContact]) { + guard !phonebookContacts.isEmpty else { + handleEmptySearchResult() + return + } + + let data: [SearchResultCellModel] = phonebookContacts.map { contact in + SearchResultCellModel( + contact: contact, + selectionHandler: nil, + actionHandler: { [weak self] in self?.openInviteToNynja(for: contact) } + ) + } + view?.showSearchResult(.filled(data: data)) + } + + private func handleEmptySearchResult() { + let emptyModel = EmptyStateViewModel(image: UIImage.nynja.icSearchEmpty.image, + descriptionText: String.localizable.noSearchResult) + + view?.showSearchResult(.empty(emptyModel)) + } +} + +// MARK: - Injection + +extension SearchContactPresenter: SetInjectable { + + struct Dependencies { + let view: SearchContactViewInput + let interactor: SearchContactInteractorInput + let wireframe: SearchContactWireframeProtocol + let phoneTextController: PhoneNumberTextController + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + phoneTextController = dependencies.phoneTextController + } +} diff --git a/Nynja/Modules/Flows/Search Flow/SearchContactCoordinator.swift b/Nynja/Modules/Flows/Search Flow/SearchContactCoordinator.swift new file mode 100644 index 000000000..ac35ed049 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/SearchContactCoordinator.swift @@ -0,0 +1,131 @@ +// +// SearchContactCoordinator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import MessageUI +import NynjaUIKit + +final class SearchContactCoordinator: NSObject, Coordinator, NavigationContainer { + + private let inputMode: SearchInputMode + + private(set) weak var navigation: UINavigationController! + + private let serviceFactory: ServiceFactoryProtocol + + private var selectCountryCallback: ((Result) -> Void)? + + init(inputMode: SearchInputMode, navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { + self.inputMode = inputMode + self.navigation = navigation + self.serviceFactory = serviceFactory + } + + func start() { + let wireframe = SearchContactWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init( + inputMode: inputMode + ), + dependencies: .init( + accountService: serviceFactory.makeAccountService(), + contactManager: serviceFactory.makeContactManager(), + phoneTextController: serviceFactory.makePhoneNumberTextController() + ) + ) + navigation?.pushViewController(view, animated: true) + } + + func end() { + + } +} + +// MARK: - Search + +extension SearchContactCoordinator: SearchContactCoordinatorProtocol { + + func wireframe(_ wireframe: SearchContactWireframe, didEndWithState state: SearchContactWireframe.State) { + switch state { + case .dismiss: + navigation?.popViewController(animated: true) + + case let .selectCountry(callback): + selectCountryCallback = callback + showCountrySelector() + + case let .showInvite(contact): + if MFMessageComposeViewController.canSendText() { + showMessageComposer(with: [contact]) + } else { + let alert = Alert.makeDefault(message: String.localizable.smsFunctionalityIsNotAvailable) + present(alert) + } + case let .showAccountDetails(searchResponse): + break + } + } +} + +// MARK: - Country Selector + +extension SearchContactCoordinator: CountrySelectorCoordinatorProtocol { + + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { + switch state { + case .selected(let country): + selectCountryCallback?(.success(country)) + case .dismiss: + selectCountryCallback?(.failure(NavigationError.dismissed)) + } + selectCountryCallback = nil + navigation.popViewController(animated: true) + } +} + +// MARK: - MFMessageComposeViewControllerDelegate + +extension SearchContactCoordinator: MFMessageComposeViewControllerDelegate { + + func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { + controller.dismiss(animated: true) { + switch result { + case .sent, .cancelled: + break + case .failed: + // TODO: do we need to handler such error ? + break + } + } + } +} + +// MARK: - Presentation + +private extension SearchContactCoordinator { + + func showCountrySelector() { + let wireframe = SelectCountryWireFrame(coordinator: self) + let view = wireframe.prepareModule( + dependencies: .init( + countriesProvider: serviceFactory.makeCountriesProvider() + ) + ) + navigation.pushViewController(view, animated: true) + } + + func showMessageComposer(with recepients: [PhoneContact]) { + let controller = MFMessageComposeViewController() + + controller.body = String.localizable.nynjaShareString + controller.recipients = recepients.map { $0.phoneNumber } + controller.messageComposeDelegate = self + + navigation.present(controller, animated: true, completion: nil) + } +} diff --git a/Nynja/Modules/Flows/Search Flow/SearchContactProtocols.swift b/Nynja/Modules/Flows/Search Flow/SearchContactProtocols.swift new file mode 100644 index 000000000..f35eaa615 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/SearchContactProtocols.swift @@ -0,0 +1,48 @@ +// +// SearchContactProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +// MARK: - Wireframe + +protocol SearchContactWireframeProtocol: AlertDisplayable { + func selectCountry(completion: @escaping (Result) -> Void) + func showAccountDetails(for searchResponse: SearchContactResponse) + func showInvite(_ contact: PhoneContact) + func dismiss() +} + +// MARK: - View + +protocol SearchContactViewInput: LoadingInteractive { + func setupContent(_ viewModel: SearchContactViewModel) + func showSearchResult(_ searchResults: CollectionState<[SearchResultCellModel]>) + func setActionsEnabled(_ isEnabled: Bool) +} + +// MARK: - Presenter + +protocol SearchContactPresenterProtocol: BasePresenterProtocol, NavigationProtocol { + func viewDidLoad() + func search() +} + +// MARK: - Interactor + +// MARK: Input +protocol SearchContactInteractorInput: class { + func searchByEmail(_ email: String) + func searchByPhoneNumber(_ phoneNumber: PhoneNumberInfo) + func searchByUsername(_ username: String) +} + +// MARK: Output +protocol SearchContactInteractorOutput: class { + func didReceiveSearchResult(_ result: SearchContactResult) + func didReceiveFailure(_ error: Error) +} diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/ContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/InputView/ContentViewModel.swift new file mode 100644 index 000000000..c384250bd --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/InputView/ContentViewModel.swift @@ -0,0 +1,13 @@ +// +// ContentViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol ContentViewModel: class { + func makeContentView() -> UIView +} diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/EmailContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/InputView/EmailContentViewModel.swift new file mode 100644 index 000000000..fa8df777f --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/InputView/EmailContentViewModel.swift @@ -0,0 +1,39 @@ +// +// EmailContentViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +struct EmailTextValidator: MTIValidator { + + private let validator = EmailValidator() + + var validationHandler: ((Bool) -> Void)? + + func validate(text: String) -> InputInfo? { + let isValid = validator.isValid(text: text) + + validationHandler?(isValid) + + // FIXME: return errors + return nil + } +} + +final class EmailContentViewModel: TextFieldContentViewModel, ContentViewModel { + + func makeContentView() -> UIView { + let textField = makeInputField() + + textField.placeholder = String.localizable.searchContactEmailPlaceholder + textField.textContentType = .emailAddress + textField.keyboardType = .emailAddress + textField.returnKeyType = .search + + return textField + } +} diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/PhoneNumberContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/InputView/PhoneNumberContentViewModel.swift new file mode 100644 index 000000000..1c032aae0 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/InputView/PhoneNumberContentViewModel.swift @@ -0,0 +1,61 @@ +// +// PhoneNumberContentViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class PhoneNumberContentViewModel: ContentViewModel { + + private let controller: PhoneNumberTextController + + private let descriptionText: String + + var countrySelectionHandler: (() -> Void)? + + var validationHandler: ((Bool) -> Void)? { + didSet { + controller.validationAction = validationHandler + } + } + + var inputNumber: PhoneNumberInfo { + guard let view = view else { fatalError("view = nil") } + + let country = controller.country + + let text = view.contentView.phoneNumberTextField.text ?? "" + let phoneNumber = text.replacingOccurrences(of: " ", with: "") + + return PhoneNumberInfo(country: country, number: phoneNumber) + } + + private weak var view: DetailContainerView? + + init(controller: PhoneNumberTextController, descriptionText: String) { + self.controller = controller + self.descriptionText = descriptionText + } + + func selectCountry(_ country: Country) { + view?.contentView.selectCountry(country) + } + + func makeContentView() -> UIView { + let phoneNumberInputView = PhoneNumberLoginView(textController: controller) + phoneNumberInputView.configure(config: .init( + country: controller.country, + countrySelectorAction: countrySelectionHandler + )) + + let container = DetailContainerView(contentView: phoneNumberInputView) + container.detailsLabel.text = descriptionText + + self.view = container + + return container + } +} diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift new file mode 100644 index 000000000..7e4851b55 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift @@ -0,0 +1,58 @@ +// +// TextFieldContentViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class TextFieldContentViewModel { + + private var validator: MTIValidator + + var returnHandler: (() -> Void)? + + var inputText: String { + guard let view = view else { fatalError("view = nil") } + return view.text.trimmed() + } + + var validationHandler: ((Bool) -> Void)? { + didSet { + validator.validationHandler = validationHandler + } + } + + private weak var view: MaterialTextField? + + init(validator: MTIValidator) { + self.validator = validator + } + + func makeInputField() -> MaterialTextField { + let textField = MaterialTextField() + self.view = textField + + textField.textColor = UIColor.nynja.white + textField.placeholderColor = UIColor.nynja.dustyGray + textField.separatorColor = UIColor.nynja.dustyGray + + textField.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 22.0.adjustedByWidth) + + textField.snp.makeConstraints { maker in + maker.height.equalTo(64.0.adjustedByWidth) + } + + textField.validators = [validator] + + textField.returnHandler = { [weak self, weak textField] textInput in + self?.returnHandler?() + textField?.endEditing(true) + return false + } + + return textField + } +} diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/UsernameContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/InputView/UsernameContentViewModel.swift new file mode 100644 index 000000000..4b2f85ec1 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/InputView/UsernameContentViewModel.swift @@ -0,0 +1,39 @@ +// +// UsernameContentViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +struct UsernameTextValidator: MTIValidator { + + private let regexp = "^([a-zA-Z]|[0-9]|_){2,}$" + + var validationHandler: ((Bool) -> Void)? + + func validate(text: String) -> InputInfo? { + let predicate = NSPredicate(format: "SELF MATCHES %@", regexp) + let isValid = predicate.evaluate(with: text) + + validationHandler?(isValid) + + // FIXME: return errors + return nil + } +} + +final class UsernameContentViewModel: TextFieldContentViewModel, ContentViewModel { + + func makeContentView() -> UIView { + let textField = makeInputField() + + textField.placeholder = String.localizable.searchContactUsernamePlaceholder + textField.keyboardType = .default + textField.returnKeyType = .search + + return textField + } +} diff --git a/Nynja/Modules/Flows/Search Flow/View/SearchContactViewController.swift b/Nynja/Modules/Flows/Search Flow/View/SearchContactViewController.swift new file mode 100644 index 000000000..38e48137f --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/SearchContactViewController.swift @@ -0,0 +1,240 @@ +// +// SearchContactViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +final class SearchContactViewController: BaseVC, SearchContactViewInput, LoadingDisplayable { + + private let presenter: SearchContactPresenterProtocol + + private var emptyStateDataSource: EmptyStateTableViewDS! + + private var dataSource: TableViewDataSource! { + didSet { + emptyStateDataSource = EmptyStateTableViewDS(dataSource: dataSource) + } + } + + + // MARK: - Views + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + private lazy var subtitleLabel: UILabel = { + let fontHeight = Constraints.subtitleLabel.fontHeight + let top = Constraints.subtitleLabel.top + let horizontal = Constraints.horizontal + let color = UIColor.nynja.manatee + + let label = UILabel(height: fontHeight, color: color, font: FontFamily.NotoSans.medium) + label.textAlignment = .center + label.setContentHuggingPriority(.required, for: .vertical) + + view.addSubview(label) + label.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom).offset(top) + maker.left.equalToSuperview().offset(horizontal) + maker.right.equalToSuperview().inset(horizontal) + maker.centerX.equalToSuperview() + } + + return label + }() + + private lazy var inputContainerView: UIView = { + let top = Constraints.subtitleLabel.bottom + + let container = UIView() + + view.addSubview(container) + container.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + + maker.top.equalTo(subtitleLabel.snp.bottom).offset(top) + maker.left.right.equalToSuperview().inset(horizontal) + } + + return container + }() + + private lazy var searchButton: RoundNynjaButton = { + let button = RoundNynjaButton() + + button.setTitle(String.localizable.search.uppercased(), for: .normal) + + view.addSubview(button) + button.snp.makeConstraints { maker in + let top = Constraints.searchButton.vertical + let horizontal = Constraints.horizontal + let height = Constraints.searchButton.height + + maker.top.equalTo(inputContainerView.snp.bottom).offset(top) + maker.left.equalToSuperview().offset(horizontal) + maker.right.equalToSuperview().inset(horizontal) + maker.height.equalTo(height) + } + + return button + }() + + private lazy var tableView: UITableView = { + let tableView = UITableView() + + tableView.backgroundColor = UIColor.nynja.clear + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + tableView.isScrollEnabled = false + tableView.showsVerticalScrollIndicator = false + + tableView.rowHeight = SearchResultCellModel.Cell.Constraints.height + tableView.estimatedRowHeight = tableView.rowHeight + + tableView.contentInset.top = Constraints.tableView.topInset + tableView.separatorStyle = .none + + view.addSubview(tableView) + tableView.snp.makeConstraints { maker in + let top = Constraints.searchButton.vertical + maker.top.equalTo(searchButton.snp.bottom).offset(top) + maker.left.right.equalToSuperview() + maker.bottom.equalToSuperview() + } + + return tableView + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: SearchContactPresenterProtocol + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + super.init(nibName: nil, bundle: nil) + _presenter = presenter + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Life Cycle + + override func initialize() { + super.initialize() + setupUI() + } + + override func viewDidLoad() { + super.viewDidLoad() + presenter.viewDidLoad() + } + + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + view.endEditing(true) + } + + + // MARK: - UI Setup + + private func setupUI() { + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: false, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) + searchButton.isHidden = false + searchButton.addTarget(self, action: #selector(search(sender:)), for: .touchUpInside) + + dataSource = TableViewDataSource(tableView: tableView) + + tableView.backgroundColor = UIColor.nynja.clear + tableView.register(viewModel: SearchResultCellModel.self) + tableView.dataSource = emptyStateDataSource + } + + + // MARK: - Actions + + @objc private func search(sender: Any) { + presenter.search() + } + + + // MARK: - View Input + + func setActionsEnabled(_ isEnabled: Bool) { + searchButton.isEnabled = isEnabled + } + + func setupContent(_ viewModel: SearchContactViewModel) { + screenTitle = viewModel.title + subtitleLabel.text = viewModel.subtitle + + let contentView = viewModel.content.makeContentView() + + inputContainerView.addSubview(contentView) + contentView.snp.makeConstraints { maker in + let bottom = Constraints.inputContainerView.bottom + maker.top.left.right.equalToSuperview() + maker.bottom.equalToSuperview().offset(-bottom) + } + } + + func showSearchResult(_ searchResults: CollectionState<[SearchResultCellModel]>) { + let data: [SearchResultCellModel] + let emptyStateViewModel: EmptyStateViewModel? + + switch searchResults { + case let .empty(viewModel): + emptyStateViewModel = viewModel + data = [] + case let .filled(result): + emptyStateViewModel = nil + data = result + } + + emptyStateDataSource.emptyStateViewModel = emptyStateViewModel + dataSource.data = data + } + + + // MARK: - Layout + + private enum Constraints { + + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + + enum subtitleLabel { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + static let top: CGFloat = CGFloat(32).adjustedByWidth + static let bottom: CGFloat = CGFloat(16).adjustedByWidth + } + + enum inputContainerView { + static let bottom: CGFloat = CGFloat(8).adjustedByWidth + } + + enum searchButton { + static let vertical: CGFloat = CGFloat(28).adjustedByWidth + static let height: CGFloat = CGFloat(44).adjustedByWidth + } + + enum tableView { + static let topInset: CGFloat = CGFloat(16).adjustedByWidth + } + } +} diff --git a/Nynja/Modules/Flows/Search Flow/View/SearchContactViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/SearchContactViewModel.swift new file mode 100644 index 000000000..cab8ad17c --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/SearchContactViewModel.swift @@ -0,0 +1,13 @@ +// +// SearchContactViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct SearchContactViewModel { + let title: String + let subtitle: String + let content: ContentViewModel +} diff --git a/Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultCellModel.swift b/Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultCellModel.swift new file mode 100644 index 000000000..87e8b4d02 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultCellModel.swift @@ -0,0 +1,44 @@ +// +// SearchResultCellModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class SearchResultCellModel: CellViewModel, InteractiveCellViewModel { + + var accessibilityIdentifier: String { + return "search_result_cell" + } + + private let contact: PhoneContact + + private let actionHandler: (() -> Void)? + + var select: (() -> Void)? + + init(contact: PhoneContact, selectionHandler: (() -> Void)?, actionHandler: (() -> Void)?) { + self.contact = contact + self.select = selectionHandler + self.actionHandler = actionHandler + } + + func setup(cell: SearchResultTableViewCell) { + let avatarPlaceholder = UIImage.nynja.Contacts.avaPlaceholder.image + let avatar = contact.imageData.flatMap { UIImage(data: $0) } ?? avatarPlaceholder + + cell.avatarImageView.image = avatar + cell.titleLabel.text = contact.name + cell.subtitleLabel.text = contact.phoneNumber + cell.actionButton.setTitle(String.localizable.searchContactInviteButtonTitle, for: .normal) + cell.actionButton.addTarget(self, action: #selector(actionButtonTapped(sender:)), for: .touchUpInside) + } + + @objc private func actionButtonTapped(sender: Any){ + actionHandler?() + } +} diff --git a/Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultTableViewCell.swift b/Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultTableViewCell.swift new file mode 100644 index 000000000..fd8df226c --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/TableView/SearchResultTableViewCell.swift @@ -0,0 +1,136 @@ +// +// SearchResultTableViewCell.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +final class SearchResultTableViewCell: UITableViewCell { + + // MARK: - Views + + private(set) lazy var avatarImageView: RoundImageView = { + let imageView = RoundImageView() + contentView.addSubview(imageView) + return imageView + }() + + private lazy var contentContainerView: UIView = { + let containerView = UIView() + contentView.addSubview(containerView) + return containerView + }() + + private(set) lazy var titleLabel: UILabel = { + let fontHeight = Constraints.titleLabel.fontHeight + let label = UILabel(height: fontHeight, color: UIColor.nynja.white, font: FontFamily.NotoSans.medium) + contentContainerView.addSubview(label) + return label + }() + + private(set) lazy var subtitleLabel: UILabel = { + let fontHeight = Constraints.titleLabel.fontHeight + let label = UILabel(height: fontHeight, color: UIColor.nynja.manatee, font: FontFamily.NotoSans.regular) + contentContainerView.addSubview(label) + return label + }() + + private(set) lazy var actionButton: NynjaCellButton = { + let button = NynjaCellButton() + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: Constraints.actionButton.fontHeight) + contentContainerView.addSubview(button) + return button + }() + + + // MARK: - Init + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + backgroundColor = .clear + contentView.backgroundColor = .clear + selectionStyle = .none + + avatarImageView.snp.makeConstraints { maker in + let left = Constraints.avatarImageView.left + let size = Constraints.avatarImageView.size + + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(left) + maker.width.height.equalTo(size) + } + + contentContainerView.snp.makeConstraints { maker in + let horizontal = Constraints.contentContainer.horizontal + maker.centerY.equalToSuperview() + maker.left.equalTo(avatarImageView.snp.right).offset(horizontal) + maker.right.equalToSuperview().inset(horizontal) + } + + titleLabel.snp.makeConstraints { maker in + maker.top.left.equalToSuperview() + maker.right.equalTo(subtitleLabel.snp.right) + } + + subtitleLabel.snp.makeConstraints { maker in + let top = Constraints.subtitleLabel.top + let right = Constraints.subtitleLabel.right + maker.top.equalTo(titleLabel.snp.bottom).offset(top) + maker.left.bottom.equalToSuperview() + maker.right.equalTo(actionButton.snp.left).offset(-right) + } + + actionButton.setContentHuggingPriority(.required, for: .horizontal) + actionButton.setContentCompressionResistancePriority(.required, for: .horizontal) + actionButton.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.right.equalToSuperview() + } + } + + + // MARK: - Layout + + enum Constraints { + static let height: CGFloat = avatarImageView.size + CGFloat(8.0).adjustedByWidth * 2 + + fileprivate enum avatarImageView { + static let left: CGFloat = CGFloat(16).adjustedByWidth + static let size: CGFloat = CGFloat(48).adjustedByWidth + } + + fileprivate enum contentContainer { + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + } + + fileprivate enum titleLabel { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + } + + fileprivate enum subtitleLabel { + static let fontHeight: CGFloat = CGFloat(20).adjustedByWidth + static let top: CGFloat = 1 + static let right: CGFloat = CGFloat(16).adjustedByWidth + } + + fileprivate enum actionButton { + static let fontHeight: CGFloat = CGFloat(20).adjustedByWidth + } + } +} diff --git a/Nynja/Modules/Flows/Search Flow/Wireframe/SearchContactWireframe.swift b/Nynja/Modules/Flows/Search Flow/Wireframe/SearchContactWireframe.swift new file mode 100644 index 000000000..dc752b306 --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/Wireframe/SearchContactWireframe.swift @@ -0,0 +1,86 @@ +// +// SearchContactWireframe.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import UIKit.UIViewController +import NynjaUIKit + +protocol SearchContactCoordinatorProtocol: AlertDisplayable { + func wireframe(_ wireframe: SearchContactWireframe, didEndWithState state: SearchContactWireframe.State) +} + +final class SearchContactWireframe: Wireframe, SearchContactWireframeProtocol { + + private let coordinator: SearchContactCoordinatorProtocol + + init(coordinator: SearchContactCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let inputMode: SearchInputMode + } + + struct Dependencies { + let accountService: AccountService + let contactManager: ContactManager + let phoneTextController: PhoneNumberTextController + } + + enum State { + case dismiss + case selectCountry(callback: (Result) -> Void) + case showInvite(PhoneContact) + case showAccountDetails(SearchContactResponse) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = SearchContactPresenter(inputMode: parameters.inputMode) + + let view = SearchContactViewController(dependencies: .init(presenter: presenter)) + + let interactor = SearchContactInteractor(dependencies: + .init( + presenter: presenter, + accountService: dependencies.accountService, + contactManager: dependencies.contactManager + ) + ) + + presenter.inject(dependencies: + .init( + view: view, + interactor: interactor, + wireframe: self, + phoneTextController: dependencies.phoneTextController + ) + ) + + return view + } + + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) + } + + func selectCountry(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) + } + + func showAccountDetails(for searchResponse: SearchContactResponse) { + coordinator.wireframe(self, didEndWithState: .showAccountDetails(searchResponse)) + } + + func showInvite(_ contact: PhoneContact) { + coordinator.wireframe(self, didEndWithState: .showInvite(contact)) + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } +} diff --git a/Nynja/Modules/InviteFriends/Entity/PhoneContact.swift b/Nynja/Modules/InviteFriends/Entity/PhoneContact.swift index e37ca11fc..0335caf1f 100644 --- a/Nynja/Modules/InviteFriends/Entity/PhoneContact.swift +++ b/Nynja/Modules/InviteFriends/Entity/PhoneContact.swift @@ -9,15 +9,15 @@ import Foundation import Contacts -class PhoneContact: NSObject { +final class PhoneContact { - var needToRemove: Bool = false - var name: String? - var phoneNumber: String? - var imageData: Data? + let name: String + let phoneNumber: String + let email: String? + private(set) var imageData: Data? var firstLetter: String? { - if let f = name?.uppercased().unicodeScalars.first { + if let f = name.uppercased().unicodeScalars.first { if NSCharacterSet.letters.contains(f) { return "\(f)" } else { @@ -28,19 +28,26 @@ class PhoneContact: NSObject { } } - static func readContact(contact: CNContact) -> PhoneContact? { + init?(contact: CNContact) { + guard let name = CNContactFormatter.string(from: contact, style: .fullName) else { + return nil + } + self.name = name - if let pN = contact.phoneNumbers.first?.value.value(forKey: "digits") as? String { - let phoneContact = PhoneContact() - phoneContact.phoneNumber = pN - phoneContact.name = CNContactFormatter.string(from: contact, style: .fullName) ?? pN - if let id = contact.thumbnailImageData, contact.imageDataAvailable { - phoneContact.imageData = id - } - return phoneContact + guard contact.isKeyAvailable(CNContactPhoneNumbersKey), + let number = contact.phoneNumbers.first?.value.value(forKey: "digits") as? String else { + return nil } + self.phoneNumber = number - return nil + if contact.isKeyAvailable(CNContactEmailAddressesKey) { + self.email = contact.emailAddresses.first.map { $0.value as String } + } else { + self.email = nil + } + + if contact.imageDataAvailable, let imageData = contact.thumbnailImageData { + self.imageData = imageData + } } - } diff --git a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift index 72a3df200..aacbdbdb8 100644 --- a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift +++ b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift @@ -34,7 +34,7 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto } func selectContact(_ contact: PhoneContact) { - if let index = selectedContacts.index(of: contact) { + if let index = selectedContacts.index(where: { $0 === contact }) { selectedContacts.remove(at: index) } else { selectedContacts.append(contact) @@ -76,7 +76,7 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto } func isSelected(contact: PhoneContact) -> Bool { - return selectedContacts.contains(contact) + return selectedContacts.contains { $0 === contact } } func hasSelectedContacts() -> Bool { @@ -88,41 +88,37 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto } private func searchContactsInSystem() { - self.contactManager.getContactsForInvites { [weak self] (success, resultDict) in - guard let result = resultDict else { + self.contactManager.getContactsForInvites { [weak self] result in + switch result { + case let .success(dictionary): + self?.contactsList = dictionary + + let listOfPhones = dictionary.keys.compactMap { + $0.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + } + self?.mqttService.searchContacts(numbers: listOfPhones) + case .failure: self?.presenter.showError(String.localizable.contactsWereNotFetched) - return - } - self?.contactsList = result - - let listOfPhones = result.keys.compactMap{ - $0.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() } - self?.mqttService.searchContacts(numbers: listOfPhones) } } private func fetchContacts(excluding inAppContacts: [Contact]) { - let finalList = self.removeAllInAppContacts(removeContacts: inAppContacts, from: self.contactsList) - self.prepareGroupedContacts(contacts: finalList) - self.presenter.updateUI() + let finalList = self.removeAllInAppContacts(removeContacts: inAppContacts, from: self.contactsList) + self.prepareGroupedContacts(contacts: finalList) + self.presenter.updateUI() } private func removeAllInAppContacts(removeContacts: [Contact], from contacts: [String: PhoneContact]) -> [PhoneContact] { - var result = [PhoneContact]() + var contacts = contacts for nynjaContact in removeContacts { - if let pN = nynjaContact.phoneNumber, let c = contacts["+" + pN] { - c.needToRemove = true + if let number = nynjaContact.phoneNumber, case let fullPhoneNumber = "+" + number { + contacts[fullPhoneNumber] = nil } } - for (_, contact) in contacts { - if !contact.needToRemove { - result.append(contact) - } - } - return result + return contacts.values.map { $0 } } private func prepareGroupedContacts(contacts: [PhoneContact]) { diff --git a/Nynja/Modules/InviteFriends/View/Cell/InviteFriendsCell.swift b/Nynja/Modules/InviteFriends/View/Cell/InviteFriendsCell.swift index 100f8ebbf..fc6a27404 100644 --- a/Nynja/Modules/InviteFriends/View/Cell/InviteFriendsCell.swift +++ b/Nynja/Modules/InviteFriends/View/Cell/InviteFriendsCell.swift @@ -79,7 +79,7 @@ class InviteFiendsCell: UITableViewCell { func setupWith(contact: PhoneContact, isSelected: Bool) { separatorView.backgroundColor = UIColor.nynja.backgroundGray nameLabel.text = contact.name - phoneLabel.text = contact.phoneNumber?.stringAsPhone() ?? "" + phoneLabel.text = contact.phoneNumber.stringAsPhone() if let id = contact.imageData { avatarImageView.image = UIImage(data: id) } else { diff --git a/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift b/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift index 2f0508d10..f12f32572 100644 --- a/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift +++ b/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift @@ -46,10 +46,10 @@ class InviteFriendsWireFrame: InviteFriendsWireFrameProtocol { } func sendInvite(contacts: [PhoneContact]) { - if (MFMessageComposeViewController.canSendText()) { + if MFMessageComposeViewController.canSendText() { let controller = MFMessageComposeViewController() controller.body = String.localizable.nynjaShareString - controller.recipients = contacts.map { $0.phoneNumber ?? "" } + controller.recipients = contacts.map { $0.phoneNumber } controller.messageComposeDelegate = presenter?.messageComposeDelegateHandler viewController?.present(controller, animated: true, completion: nil) } else { diff --git a/Nynja/Modules/LeaveVoiceMessage/View/LeaveVoiceMessageViewController.swift b/Nynja/Modules/LeaveVoiceMessage/View/LeaveVoiceMessageViewController.swift index e2e16327c..d0964bc21 100644 --- a/Nynja/Modules/LeaveVoiceMessage/View/LeaveVoiceMessageViewController.swift +++ b/Nynja/Modules/LeaveVoiceMessage/View/LeaveVoiceMessageViewController.swift @@ -55,8 +55,8 @@ class LeaveVoiceMessageViewController: BaseVC, LeaveVoiceMessageViewProtocol { private lazy var backgroundGradientView: UIView = { - let view = GradientView(colors: [UIColor.nynja.callGradientStart, - UIColor.nynja.callGradientEnd]) + let view = GradientView(colors: [UIColor.nynja.gradientStart, + UIColor.nynja.gradientEnd]) self.view.addSubview(view) view.snp.makeConstraints({ (make) in diff --git a/Nynja/Modules/Main/MainProtocols.swift b/Nynja/Modules/Main/MainProtocols.swift index 64a003eb0..a6cacccd8 100644 --- a/Nynja/Modules/Main/MainProtocols.swift +++ b/Nynja/Modules/Main/MainProtocols.swift @@ -82,6 +82,7 @@ protocol MainWireFrameProtocol: class { func showMySelfChat(contact: Contact) func showFavorites() func showQRGenerator() + func showAddContactByEmail() func showAddContactByUserName() func showWallet(for profile: Profile) func getRecentsLocation() -> [LocationType] @@ -189,6 +190,7 @@ protocol MainPresenterProtocol: BasePresenterProtocol { func showAccountSettings() func showLoginOptions() + func showAddContactByEmail() func showAddContactByUserName() func conferenceVoiceCall() diff --git a/Nynja/Modules/Main/Presenter/MainPresenter.swift b/Nynja/Modules/Main/Presenter/MainPresenter.swift index 9f58be939..95d1d450a 100644 --- a/Nynja/Modules/Main/Presenter/MainPresenter.swift +++ b/Nynja/Modules/Main/Presenter/MainPresenter.swift @@ -260,6 +260,10 @@ final class MainPresenter: BasePresenter, MainPresenterProtocol, MainInteractorO func showQRGenerator() { self.wireFrame.showQRGenerator() } + + func showAddContactByEmail() { + wireFrame.showAddContactByEmail() + } func showAddContactByUserName() { self.wireFrame.showAddContactByUserName() diff --git a/Nynja/Modules/Main/View/MainNavigationItem.swift b/Nynja/Modules/Main/View/MainNavigationItem.swift index 209cf7424..c7a7cfe1b 100644 --- a/Nynja/Modules/Main/View/MainNavigationItem.swift +++ b/Nynja/Modules/Main/View/MainNavigationItem.swift @@ -77,6 +77,7 @@ enum MainNavigationItem: String { case history = "wheel_item_history" // New Contact section + case byEmail = "wheel_item_byEmail" case byUsername = "wheel_item_byUsername" case byNumber = "wheel_item_byNumber" case byQRCode = "wheel_item_byQRCode" @@ -102,7 +103,7 @@ enum MainNavigationItem: String { case send = "wheel_item_send" var localized: String { - return self.rawValue.localized + return rawValue.localized } var factoryType: WCItemsFactory.Type? { diff --git a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift index fe98acf09..42af39271 100644 --- a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift @@ -224,6 +224,11 @@ extension MainViewController: NavigateProtocol { expand(by: indexPath) } + func showSearchByEmail(indexPath: IndexPath?) { + presenter.showAddContactByEmail() + closeWheel(indexPath: indexPath) + } + func showByUserName(indexPath: IndexPath?) { presenter.showAddContactByUserName() closeWheel(indexPath: indexPath) diff --git a/Nynja/Modules/Main/View/NavigateProtocol.swift b/Nynja/Modules/Main/View/NavigateProtocol.swift index 1655cf33e..fd66e458f 100644 --- a/Nynja/Modules/Main/View/NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/NavigateProtocol.swift @@ -77,6 +77,7 @@ protocol SecondLevelNavigateProtocol: class { func showHistory(indexPath: IndexPath?) func showNewContact(indexPath: IndexPath?) + func showSearchByEmail(indexPath: IndexPath?) func showByUserName(indexPath: IndexPath?) func showByNumber(indexPath: IndexPath?) func showByQR(indexPath: IndexPath?) diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 9cdfe6fed..2cda656bb 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -134,12 +134,6 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega } } - func showAddContactByUserName() { - if let navigation = self.contentNavigation { - AddContactByUsernameWireFrame().presentAddContactByUsername(navigation:navigation, main: self) - } - } - func showMapSearch(delegate: MapSearchDelegate?) { if let navigation = self.navigation { MapSearchWireFrame().presentMapSearch(navigation: navigation, delegate: delegate!) @@ -372,9 +366,27 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega } func showAddContactViaPhoneNumber() { - if let navigation = contentNavigation { - AddContactViaPhoneWireFrame().presentAddContactViaPhone(navigation: navigation, main: self) + guard let navigation = contentNavigation else { + return + } + let coordinator = SearchContactCoordinator(inputMode: .phoneNumber, navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.start() + } + + func showAddContactByEmail() { + guard let navigation = contentNavigation else { + return } + let coordinator = SearchContactCoordinator(inputMode: .email, navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.start() + } + + func showAddContactByUserName() { + guard let navigation = contentNavigation else { + return + } + let coordinator = SearchContactCoordinator(inputMode: .username, navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.start() } // MARK: Options diff --git a/Nynja/Modules/QRCodeGenerator/Interactor/QRCodeGeneratorInteractor.swift b/Nynja/Modules/QRCodeGenerator/Interactor/QRCodeGeneratorInteractor.swift index 48393decb..f22b7945c 100644 --- a/Nynja/Modules/QRCodeGenerator/Interactor/QRCodeGeneratorInteractor.swift +++ b/Nynja/Modules/QRCodeGenerator/Interactor/QRCodeGeneratorInteractor.swift @@ -6,12 +6,20 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class QRCodeGeneratorInteractor: QRCodeGeneratorInteractorInputProtocol { - - weak var presenter: QRCodeGeneratorInteractorOutputProtocol! - - var contact: Contact? { - return ContactDAO.currentContact +final class QRCodeGeneratorInteractor: QRCodeGeneratorInteractorInput { + + weak var presenter: QRCodeGeneratorInteractorOutput! + + private let accountId: String + + init(accountId: String) { + self.accountId = accountId } + func generateQRCode() { + guard let qrCode = accountId.data(using: .utf8) else { + return + } + presenter.didGenerateQRCode(qrCode) + } } diff --git a/Nynja/Modules/QRCodeGenerator/Presenter/QRCodeGeneratorPresenter.swift b/Nynja/Modules/QRCodeGenerator/Presenter/QRCodeGeneratorPresenter.swift index 63c54a892..57b80fc0b 100644 --- a/Nynja/Modules/QRCodeGenerator/Presenter/QRCodeGeneratorPresenter.swift +++ b/Nynja/Modules/QRCodeGenerator/Presenter/QRCodeGeneratorPresenter.swift @@ -6,16 +6,17 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class QRCodeGeneratorPresenter: QRCodeGeneratorPresenterProtocol, QRCodeGeneratorInteractorOutputProtocol { +final class QRCodeGeneratorPresenter: QRCodeGeneratorPresenterProtocol, QRCodeGeneratorInteractorOutput { - weak var view: QRCodeGeneratorViewProtocol! - var interactor: QRCodeGeneratorInteractorInputProtocol! + weak var view: QRCodeGeneratorViewInput? + var interactor: QRCodeGeneratorInteractorInput! var wireFrame: QRCodeGeneratorWireFrameProtocol! + + // MARK: - Presenter + func showed() { - if let data = interactor.contact?.phoneNumber?.data(using: String.Encoding.utf8) { - view.setup(data: data) - } + interactor.generateQRCode() } func showQRCodeReader() { @@ -26,4 +27,10 @@ class QRCodeGeneratorPresenter: QRCodeGeneratorPresenterProtocol, QRCodeGenerato wireFrame.dismiss() } + + // MARK: - Interactor Output + + func didGenerateQRCode(_ qrCode: Data) { + view?.setup(data: qrCode) + } } diff --git a/Nynja/Modules/QRCodeGenerator/QRCodeGeneratorProtocols.swift b/Nynja/Modules/QRCodeGenerator/QRCodeGeneratorProtocols.swift index e40f87369..583854e8d 100644 --- a/Nynja/Modules/QRCodeGenerator/QRCodeGeneratorProtocols.swift +++ b/Nynja/Modules/QRCodeGenerator/QRCodeGeneratorProtocols.swift @@ -8,6 +8,8 @@ import UIKit +// MARK: - Wireframe + protocol QRCodeGeneratorWireFrameProtocol: class { func presentQRCodeGenerator(navigation: UINavigationController, main: MainWireFrame?, mode: CodeGeneratorPresentationMode) @@ -19,43 +21,24 @@ protocol QRCodeGeneratorWireFrameProtocol: class { func dismiss() } -protocol QRCodeGeneratorViewProtocol: class { - - var presenter: QRCodeGeneratorPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ +// MARK: - View + +protocol QRCodeGeneratorViewInput: class { func setup(data: Data) } -protocol QRCodeGeneratorPresenterProtocol: class { - - var view: QRCodeGeneratorViewProtocol! { get set } - var interactor: QRCodeGeneratorInteractorInputProtocol! { get set } - var wireFrame: QRCodeGeneratorWireFrameProtocol! { get set } +// MARK: - Presenter - /** - * Add here your methods for communication VIEW -> PRESENTER - */ +protocol QRCodeGeneratorPresenterProtocol: class { func showed() func showQRCodeReader() func dismiss() } -protocol QRCodeGeneratorInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ +protocol QRCodeGeneratorInteractorOutput: class { + func didGenerateQRCode(_ qrCode: Data) } -protocol QRCodeGeneratorInteractorInputProtocol: class { - - var presenter: QRCodeGeneratorInteractorOutputProtocol! { get set } - - var contact: Contact? { get } - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ +protocol QRCodeGeneratorInteractorInput: class { + func generateQRCode() } diff --git a/Nynja/Modules/QRCodeGenerator/View/QRCodeGeneratorViewController.swift b/Nynja/Modules/QRCodeGenerator/View/QRCodeGeneratorViewController.swift index 1fd0fa81f..08d601b9a 100644 --- a/Nynja/Modules/QRCodeGenerator/View/QRCodeGeneratorViewController.swift +++ b/Nynja/Modules/QRCodeGenerator/View/QRCodeGeneratorViewController.swift @@ -9,14 +9,18 @@ import UIKit import QRCode -class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { - enum Constraints { +final class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewInput { + + private enum Constraints { static let qrCodeSize = 250.0.adjustedByWidth } var presenter: QRCodeGeneratorPresenterProtocol! - lazy var imageView: UIImageView = { + + // MARK: - Views + + private lazy var imageView: UIImageView = { var imgv = UIImageView() self.view.addSubview(imgv) imgv.snp.makeConstraints({ (make) in @@ -27,7 +31,7 @@ class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { return imgv }() - lazy var showQRReaderButton: UIButton = { + private lazy var showQRReaderButton: UIButton = { var button = UIButton() self.view.addSubview(button) button.addTarget(self, action: #selector(onShowQRReaderTapped), for: .touchUpInside) @@ -41,7 +45,7 @@ class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { return button }() - lazy var closeButton: UIButton = { + private lazy var closeButton: UIButton = { var button = UIButton() self.view.addSubview(button) button.addTarget(self, action: #selector(onCloseButtonTapped), for: .touchUpInside) @@ -56,7 +60,7 @@ class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { return button }() - lazy var appLogo: UIImageView = { + private lazy var appLogo: UIImageView = { var imageView = UIImageView() self.view.addSubview(imageView) @@ -70,7 +74,7 @@ class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { return imageView }() - lazy var topLabel: UILabel = { + private lazy var topLabel: UILabel = { var label = UILabel() self.view.addSubview(label) @@ -82,6 +86,9 @@ class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { return label }() + + + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() @@ -90,9 +97,12 @@ class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { backImage.removeFromSuperview() view.backgroundColor = UIColor.nynja.darkLight - self.view.isUserInteractionEnabled = true + view.isUserInteractionEnabled = true } + + // MARK: - View Input + func setup(data: Data) { var qrCode = QRCode(data) qrCode.color = CIColor(color: UIColor.nynja.white) @@ -114,16 +124,18 @@ class QRCodeGeneratorViewController: BaseVC, QRCodeGeneratorViewProtocol { topLabel.font = UIFont(name: FontFamily.NotoSans.regular.name, size: 11.0) } + + // MARK: - Actions + @objc private func onCloseButtonTapped() { presenter.dismiss() } @objc private func onShowQRReaderTapped() { presenter.showQRCodeReader() - self.showQRReaderButton.isEnabled = false + showQRReaderButton.isEnabled = false DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.showQRReaderButton.isEnabled = true } } } - diff --git a/Nynja/Modules/QRCodeGenerator/WireFrame/QRCodeGeneratorWireframe.swift b/Nynja/Modules/QRCodeGenerator/WireFrame/QRCodeGeneratorWireframe.swift index 71400cb8a..043ec4ff5 100644 --- a/Nynja/Modules/QRCodeGenerator/WireFrame/QRCodeGeneratorWireframe.swift +++ b/Nynja/Modules/QRCodeGenerator/WireFrame/QRCodeGeneratorWireframe.swift @@ -13,31 +13,48 @@ enum CodeGeneratorPresentationMode { case fromCodeReader } -class QRCodeGeneratorWireFrame: QRCodeGeneratorWireFrameProtocol { +final class QRCodeGeneratorWireFrame: Wireframe, QRCodeGeneratorWireFrameProtocol { weak var navigation : UINavigationController? weak var main: MainWireFrame? var mode: CodeGeneratorPresentationMode = .fromMainScreen - func presentQRCodeGenerator(navigation: UINavigationController, main: MainWireFrame?, mode: CodeGeneratorPresentationMode) { + struct Parameters { + let accountId: String + } + + enum State { + case dismiss + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let view = QRCodeGeneratorViewController() let presenter = QRCodeGeneratorPresenter() - let interactor = QRCodeGeneratorInteractor() + let interactor = QRCodeGeneratorInteractor(accountId: parameters.accountId) - self.navigation = navigation - self.main = main - self.mode = mode - - // Connecting view.presenter = presenter presenter.view = view presenter.wireFrame = self presenter.interactor = interactor interactor.presenter = presenter + return view + } + + func presentQRCodeGenerator(navigation: UINavigationController, main: MainWireFrame?, mode: CodeGeneratorPresentationMode) { + self.navigation = navigation + self.main = main + self.mode = mode + + guard let accountId = StorageService.sharedInstance.accountId else { + return + } + let view = prepareModule(parameters: .init(accountId: accountId)) + view.modalTransitionStyle = .crossDissolve view.modalPresentationStyle = .overCurrentContext - navigation.pushViewController(view as UIViewController, animated: true) + + navigation.pushViewController(view, animated: true) } func showQRCodeReader() { diff --git a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift index 7eecf68db..d518992e7 100644 --- a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift +++ b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift @@ -6,35 +6,65 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class QRCodeReaderInteractor: QRCodeReaderInteractorInputProtocol, IoHandlerDelegate { +final class QRCodeReaderInteractor: QRCodeReaderInteractorInput, IoHandlerDelegate, InitializeInjectable { - weak var presenter: QRCodeReaderInteractorOutputProtocol! + private weak var presenter: QRCodeReaderInteractorOutput? + + // MARK: - Services - var currentNumer = "" - var status = "" + private let accountId: String - init() { - IoHandler.shared.delegate = self - } + private let accountService: AccountService - func getContactByPhone(number: String) { - currentNumer = number - if StorageService.sharedInstance.phone == number { - self.presenter.getMyProfile() - } else if let contact = ContactDAO.findPlainContactBy(phone: number) { - self.presenter.getContactSuccess(contact: contact) - } else { - MQTTService.sharedInstance.tryFindContact(number: number, modelReference: .QRCODE) - } - } - - // MARK: IoHandlerDelegate - func getContactQRSuccess(contact: Contact) { - self.presenter.getContactSuccess(contact: contact) + private let accountDAO: AccountDAOProtocol + + + // MARK: - Init + + struct Dependencies { + let presenter: QRCodeReaderInteractorOutput + let accountId: String + let accountService: AccountService + let accountDAO: AccountDAOProtocol } - func contactQRNotFound() { - self.presenter.getContactFailed() + init(dependencies: Dependencies) { + presenter = dependencies.presenter + accountId = dependencies.accountId + accountService = dependencies.accountService + accountDAO = dependencies.accountDAO } + + // MARK: - Interactor Input + + func search(by qrCode: String) { + guard qrCode != accountId else { + presenter?.didReceiveOwnAccount() + return + } + + if let account = accountDAO.fetchAccount(by: qrCode) { + let result = SearchContactResponse(account: account, inputField: .qrCode) + presenter?.didReceiveSearchResult(result) + return + } + + accountService.searchByQRCode(qrCode) { [weak self] result in + switch result { + case let .success(searchResult): + self?.accountService.getAccount(accountId: searchResult.accountId) { [weak self] result in + switch result { + case let .success(account): + let result = SearchContactResponse(result: searchResult, account: account, inputField: .qrCode) + self?.presenter?.didReceiveSearchResult(result) + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + } } diff --git a/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift b/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift index 89b5414d2..984bf9626 100644 --- a/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift +++ b/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift @@ -6,26 +6,36 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class QRCodeReaderPresenter: QRCodeReaderPresenterProtocol, QRCodeReaderInteractorOutputProtocol { +final class QRCodeReaderPresenter: QRCodeReaderPresenterProtocol, QRCodeReaderInteractorOutput { - weak var view: QRCodeReaderViewProtocol! - var interactor: QRCodeReaderInteractorInputProtocol! + weak var view: QRCodeReaderViewInput! + var interactor: QRCodeReaderInteractorInput! var wireFrame: QRCodeReaderWireFrameProtocol! - func scanned(number: String) { - self.interactor.getContactByPhone(number: number) + + // MARK: - Presenter + + func didScan(qrcode: String) { + interactor.search(by: qrcode) } - func getContactSuccess(contact: Contact) { - wireFrame.showAddContact(contact: contact) + func showOwnQRCode() { + wireFrame.showMyQRCode() } - func getContactFailed() { - AlertManager.sharedInstance.showAlertOk(message: String.localizable.profileNotFound) + + // MARK: - Interactor Output + + func didReceiveSearchResult(_ result: SearchContactResponse) { + // TODO: open other user profile screen } - func getMyProfile() { + func didReceiveOwnAccount() { wireFrame.showMyProfile() } + func didReceiveFailure(_ error: Error) { + // TODO: handle failure code from SDK + AlertManager.sharedInstance.showAlertOk(message: String.localizable.profileNotFound) + } } diff --git a/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift b/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift index 6f1c4e226..83a811cab 100644 --- a/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift +++ b/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift @@ -8,6 +8,8 @@ import UIKit +// MARK: - Wireframe + protocol QRCodeReaderWireFrameProtocol: class { func presentQRCodeReader(navigation: UINavigationController, main: MainWireFrame?, mode: CodeReaderPresentationMode) @@ -21,44 +23,28 @@ protocol QRCodeReaderWireFrameProtocol: class { func showMyQRCode() } -protocol QRCodeReaderViewProtocol: class { - - var presenter: QRCodeReaderPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ +// MARK: - View + +protocol QRCodeReaderViewInput: class { } -protocol QRCodeReaderPresenterProtocol: class { - - var view: QRCodeReaderViewProtocol! { get set } - var interactor: QRCodeReaderInteractorInputProtocol! { get set } - var wireFrame: QRCodeReaderWireFrameProtocol! { get set } +// MARK: - Presenter - /** - * Add here your methods for communication VIEW -> PRESENTER - */ - /* func getStarted(contact: Contact) */ - func scanned(number: String) +protocol QRCodeReaderPresenterProtocol: class { + func didScan(qrcode: String) + func showOwnQRCode() } -protocol QRCodeReaderInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ - func getContactSuccess(contact: Contact) - func getContactFailed() - func getMyProfile() +// MARK: - Interactor + +// MARK: Input +protocol QRCodeReaderInteractorInput: class { + func search(by qrCode: String) } -protocol QRCodeReaderInteractorInputProtocol: class { - - var presenter: QRCodeReaderInteractorOutputProtocol! { get set } - - func getContactByPhone(number: String) - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ +// MARK: Output +protocol QRCodeReaderInteractorOutput: class { + func didReceiveSearchResult(_ result: SearchContactResponse) + func didReceiveFailure(_ error: Error) + func didReceiveOwnAccount() } diff --git a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift index eeae4ad95..e35716733 100644 --- a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift +++ b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift @@ -9,20 +9,21 @@ import UIKit import AVFoundation - -class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { +final class QRCodeReaderViewController: BaseVC, QRCodeReaderViewInput { var presenter: QRCodeReaderPresenterProtocol! - fileprivate var captureSession: AVCaptureSession? - fileprivate var videoPreviewLayer: AVCaptureVideoPreviewLayer? - fileprivate var captureMetadataOutput: AVCaptureMetadataOutput? + private var captureSession: AVCaptureSession? + private var videoPreviewLayer: AVCaptureVideoPreviewLayer? + private var captureMetadataOutput: AVCaptureMetadataOutput? private var isFlashlightOn = false private var isCursorAnimated = false + // MARK: - Views - lazy var videoView: UIView = { + + private lazy var videoView: UIView = { let view = UIView() self.view.addSubview(view) let scwidth = UIScreen.main.bounds.width @@ -37,7 +38,7 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { return view }() - lazy var captureContainer: UIView = { + private lazy var captureContainer: UIView = { let view = UIView() self.view.addSubview(view) let scwidth = UIScreen.main.bounds.width @@ -52,7 +53,7 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { return view }() - lazy var captureImageView: UIImageView = { + private lazy var captureImageView: UIImageView = { let view = UIImageView() self.captureContainer.addSubview(view) @@ -68,7 +69,7 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { return view }() - lazy var captureCursor: UIView = { + private lazy var captureCursor: UIView = { let height = Constraints.captureCursor.height let frame = CGRect(x: 0, y: 0, width: captureContainer.frame.width, height: height) let view = UIView(frame: frame) @@ -83,7 +84,7 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { return view }() - lazy var closeButton: UIButton = { + private lazy var closeButton: UIButton = { var button = UIButton() button.setImage(UIImage.nynja.icClose.image, for: .normal) self.view.addSubview(button) @@ -99,7 +100,7 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { return button }() - lazy var flashlightButton: UIButton = { + private lazy var flashlightButton: UIButton = { var button = UIButton() self.view.addSubview(button) button.setImage(UIImage.nynja.CameraItems.icFlashlightInactive.image, for: .normal) @@ -117,7 +118,7 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { return button }() - lazy var showMyQRCodeButton: UIButton = { + private lazy var showMyQRCodeButton: UIButton = { var button = UIButton() button.setTitle(String.localizable.myQrCode, for: .normal) self.view.addSubview(button) @@ -133,21 +134,30 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { return button }() - // MARK: - View lifecycle + + // MARK: - Life Cycle + override func viewDidLoad() { super.viewDidLoad() ConnectionService.shared.addSubscriber(self) screenTitle = Constants.LocalizableKeys.byQRCode.localized.uppercased() - self.setupAppStateNotifications() - self.setupUI() + setupAppStateNotifications() + setupUI() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + startCursorAnimation() } deinit { ConnectionService.shared.removeSubscriber(self) - self.removeNotificationObservers() + removeNotificationObservers() } + // MARK: - Setup + private func setupUI() { videoView.isHidden = false captureImageView.isHidden = false @@ -158,55 +168,55 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { isFlashlightOn = false view.backgroundColor = UIColor.nynja.darkLight - self.setupScanner() + setupScanner() - self.videoView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapScreen))) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - startCursorAnimation() + videoView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapScreen))) } private func setupScanner() { + guard let captureDevice = AVCaptureDevice.default(for: .video) else { + return + } - if let captureDevice = AVCaptureDevice.default(for: .video) { - - self.flashlightButton.isHidden = !captureDevice.hasTorch + flashlightButton.isHidden = !captureDevice.hasTorch + + do { + let input = try AVCaptureDeviceInput(device: captureDevice) - do { - let input = try AVCaptureDeviceInput(device: captureDevice) - - captureSession = AVCaptureSession() - captureSession?.addInput(input) - - captureMetadataOutput = AVCaptureMetadataOutput() - captureSession?.addOutput(captureMetadataOutput!) - - captureMetadataOutput?.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - captureMetadataOutput?.metadataObjectTypes = [AVMetadataObject.ObjectType.qr] - - videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession!) - videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill - - let height = UIScreen.main.bounds.width * 0.68 - videoPreviewLayer?.frame = CGRect(x: 0.0, y: 0.0, width: height, height: height) - videoView.layer.addSublayer(videoPreviewLayer!) - - captureSession?.startRunning() - } catch { - self.flashlightButton.isHidden = true - return - } + captureSession = AVCaptureSession() + captureSession?.addInput(input) + + captureMetadataOutput = AVCaptureMetadataOutput() + captureSession?.addOutput(captureMetadataOutput!) + + captureMetadataOutput?.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + captureMetadataOutput?.metadataObjectTypes = [AVMetadataObject.ObjectType.qr] + + videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession!) + videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill + + let height = UIScreen.main.bounds.width * 0.68 + videoPreviewLayer?.frame = CGRect(x: 0.0, y: 0.0, width: height, height: height) + videoView.layer.addSublayer(videoPreviewLayer!) + + captureSession?.startRunning() + } catch { + flashlightButton.isHidden = true } } + // MARK: - Actions - @objc func tapScreen(gestureRecognizer: UITapGestureRecognizer) { - guard let devicePoint: CGPoint = self.videoPreviewLayer?.captureDevicePointConverted(fromLayerPoint: gestureRecognizer.location(in: gestureRecognizer.view)) else { return } - - guard let device: AVCaptureDevice = AVCaptureDevice.default(for: .video) else { return } + + @objc private func tapScreen(recognizer: UITapGestureRecognizer) { + guard let device = AVCaptureDevice.default(for: .video) else { + return + } + let point = recognizer.location(in: recognizer.view) + guard let devicePoint = videoPreviewLayer?.captureDevicePointConverted(fromLayerPoint: point) else { + return + } do { try device.lockForConfiguration() let fMode: AVCaptureDevice.FocusMode = .autoFocus @@ -222,20 +232,19 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { } @objc private func onCloseButtonTapped() { - self.navigationController?.popToRootViewController(animated: true) + navigationController?.popToRootViewController(animated: true) } @objc private func onShowMyQRCodeTapped() { - self.presenter.wireFrame.showMyQRCode() - self.showMyQRCodeButton.isEnabled = false + presenter.showOwnQRCode() + showMyQRCodeButton.isEnabled = false DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.showMyQRCodeButton.isEnabled = true } } @objc private func onFlashlightTapped() { - guard let device: AVCaptureDevice = AVCaptureDevice.default(for: .video) else - { + guard let device = AVCaptureDevice.default(for: .video) else { return } if device.hasTorch { @@ -266,9 +275,9 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { } } - // MARK: Private + // MARK: - Private - func startCursorAnimation () { + private func startCursorAnimation() { if !isCursorAnimated { let height = Constraints.captureCursor.height let width = captureContainer.frame.width @@ -293,7 +302,9 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { } } - //MARK: 📩 Notification Handlers + + // MARK: - Notifications + private func setupAppStateNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(startCursorAnimationOnEnterForeground), @@ -312,17 +323,18 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { } @objc private func startCursorAnimationOnEnterForeground() { - self.startCursorAnimation() + startCursorAnimation() } @objc private func startCursorAnimationOnResignActive() { - self.isCursorAnimated = false + isCursorAnimated = false } - // MARK: Constants - struct Constraints { - struct captureCursor { + // MARK: - Constants + + private enum Constraints { + enum captureCursor { static let height = CGFloat(10.adjustedByWidth) static let animationDuration = Double(3.adjustedByWidth) // Scale time dy width for constant movement speed } @@ -330,24 +342,20 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { } // MARK: - AVCaptureMetadataOutputObjectsDelegate + extension QRCodeReaderViewController: AVCaptureMetadataOutputObjectsDelegate { func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { - if metadataObjects.count == 0 { - return - } - let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject - if metadataObj.type == AVMetadataObject.ObjectType.qr { - if metadataObj.stringValue != nil { - captureSession?.stopRunning() - if let number = metadataObj.stringValue { - self.presenter.scanned(number: number) - } - } - } + guard let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject else { return } + guard metadata.type == .qr, let qrCode = metadata.stringValue else { return } + + captureSession?.stopRunning() + presenter.didScan(qrcode: qrCode) } } +// MARK: - ConnectionServiceDelegate + extension QRCodeReaderViewController: ConnectionServiceDelegate { func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { @@ -358,5 +366,3 @@ extension QRCodeReaderViewController: ConnectionServiceDelegate { } } } - - diff --git a/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift b/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift index c5dad302b..369480d15 100644 --- a/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift +++ b/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift @@ -13,29 +13,67 @@ enum CodeReaderPresentationMode { case fromMyCode } -class QRCodeReaderWireFrame: QRCodeReaderWireFrameProtocol { +final class QRCodeReaderWireFrame: Wireframe, QRCodeReaderWireFrameProtocol { weak var navigation : UINavigationController? weak var main: MainWireFrame? var mode: CodeReaderPresentationMode = .fromMainScreen - func presentQRCodeReader(navigation: UINavigationController, main: MainWireFrame?, mode: CodeReaderPresentationMode) { + struct Parameters { + let accountId: String + } + + struct Dependencies { + let accountService: AccountService + let accountDAO: AccountDAOProtocol + } + + enum State { + case dismiss + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { let view = QRCodeReaderViewController() let presenter = QRCodeReaderPresenter() - let interactor = QRCodeReaderInteractor() - self.navigation = navigation - self.main = main - self.mode = mode + let interactor = QRCodeReaderInteractor( + dependencies: .init( + presenter: presenter, + accountId: parameters.accountId, + accountService: dependencies.accountService, + accountDAO: dependencies.accountDAO + ) + ) - // Connecting view.presenter = presenter presenter.view = view presenter.wireFrame = self presenter.interactor = interactor - interactor.presenter = presenter - navigation.pushViewController(view as UIViewController, animated: true) + return view + } + + func presentQRCodeReader(navigation: UINavigationController, main: MainWireFrame?, mode: CodeReaderPresentationMode) { + self.navigation = navigation + self.main = main + self.mode = mode + + guard let accountId = StorageService.sharedInstance.accountId else { + return + } + let serviceFactory = ServiceFactory() + + let view = prepareModule( + parameters: .init( + accountId: accountId + ), + dependencies: .init( + accountService: serviceFactory.makeAccountService(), + accountDAO: serviceFactory.makeAccountDAO() + ) + ) + + navigation.pushViewController(view, animated: true) } func showAddContact(contact: Contact) { diff --git a/Nynja/Resources/Colors.json b/Nynja/Resources/Colors.json index 454ea75fd..03566edd3 100644 --- a/Nynja/Resources/Colors.json +++ b/Nynja/Resources/Colors.json @@ -2,7 +2,7 @@ "mainRed": "#c90010", "wheelBackHighlitedGray": "#45484D", "contentDisabledGray": "#505255", - "backgroundColor": "#272a30", + "backgroundColor": "#2c2d32", "gray": "#666666", "manatee": "#959699", "lightGray": "#63666a", @@ -48,6 +48,6 @@ "lightTransparentBlack": "#0000001A", "callBackground": "#2c2e33", "separatorGrayColor": "#3f3f3f", - "callGradientStart": "#2c2e33ff", - "callGradientEnd": "#2c2e3300" + "gradientStart": "#2c2e33ff", + "gradientEnd": "#2c2e3300" } diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 1f1075b14..e8a271ef2 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -2,30 +2,6 @@ - NYNJA_API - - Endpoints - - Auth - - host - $(AUTH_SERVER_HOST) - port - $(AUTH_SERVER_PORT) - secure - $(AUTH_SERVER_SECURE) - - Account - - host - $(ACCOUNT_SERVER_HOST) - port - $(ACCOUNT_SERVER_PORT) - secure - $(ACCOUNT_SERVER_SECURE) - - - AppGroup $(AppGroup) AssociatedDomain @@ -58,7 +34,7 @@ CFBundleVersion - 0.5.5 + 0.5.5.multi-acc ConfServerAddress $(ConfServerAddress) ConfServerPort @@ -115,6 +91,30 @@ NYNJA needs it to save photos and video to your device. NSPhotoLibraryUsageDescription NYNJA needs it so that you can use your local images. + NYNJA_API + + Endpoints + + Account + + host + $(ACCOUNT_SERVER_HOST) + port + $(ACCOUNT_SERVER_PORT) + secure + $(ACCOUNT_SERVER_SECURE) + + Auth + + host + $(AUTH_SERVER_HOST) + port + $(AUTH_SERVER_PORT) + secure + $(AUTH_SERVER_SECURE) + + + ReversedGoogleClientId $(ReversedGoogleClientId) ServerPort diff --git a/Nynja/Resources/LaunchScreen.storyboard b/Nynja/Resources/LaunchScreen.storyboard index ac3b99c6d..4aa5ed7f1 100644 --- a/Nynja/Resources/LaunchScreen.storyboard +++ b/Nynja/Resources/LaunchScreen.storyboard @@ -1,13 +1,11 @@ - + - - - + @@ -27,16 +25,16 @@ - + - + - - - + + + diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index eb7fea0fe..ed3e7f727 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -579,11 +579,12 @@ "wheel_item_group"="Group"; "wheel_item_newContact"="New Contact"; "wheel_item_history"="History"; +"wheel_item_byEmail"="By Email"; "wheel_item_byUsername"="By Username"; "wheel_item_byNumber"="By Number"; "wheel_item_byQRCode"="By QR Code"; "wheel_item_byPassword"="By Password"; -"wheel_item_byContacts"="By Contacts"; +"wheel_item_byContacts"="In Contacts"; "wheel_item_options"="Settings"; "wheel_item_logOut"="Log Out"; "wheel_item_about"="About"; @@ -1084,6 +1085,24 @@ "delete_account.delete_account_button" = "DELETE ACCOUNT"; +// MARK - Search (Add New Contact) Flow + +"search_contact.phone.screen_title" = "BY NUMBER"; +"search_contact.email.screen_title" = "BY EMAIL"; +"search_contact.username.screen_title" = "BY USERNAME"; + +"search_contact.phone.subtitle" = "Enter Phone Number"; +"search_contact.email.subtitle" = "Enter Email"; +"search_contact.username.subtitle" = "Enter Username"; + +"search_contact.phone.description_text" = "Please choose a country code and enter a phone number."; + +"search_contact.email.placeholder" = "Email"; +"search_contact.username.placeholder" = "Username"; + +"search_contact.invite_button_title" = "Invite to NYNJA"; + + // MARK: - General Alerts "alert_action_take_from_camera" = "Take from Camera"; diff --git a/Nynja/SDK/Account/Entities/AccountError.swift b/Nynja/SDK/Account/Entities/AccountError.swift deleted file mode 100644 index a55ff6a01..000000000 --- a/Nynja/SDK/Account/Entities/AccountError.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// AccountError.swift -// Nynja -// -// Created by Anton Poltoratskyi on 20.11.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -enum AccountError: Error { - case invalidResponse -} diff --git a/Nynja/SDK/Account/Entities/AccountInfo.swift b/Nynja/SDK/Account/Entities/AccountInfo.swift index 36e73d196..4ca08bb24 100644 --- a/Nynja/SDK/Account/Entities/AccountInfo.swift +++ b/Nynja/SDK/Account/Entities/AccountInfo.swift @@ -8,20 +8,21 @@ import Foundation -final class AccountInfo { - let accountId: String - let avatar: String? - let accountMark: String? - let accountName: String? - let firstName: String? - let lastName: String? - let username: String? - let accountStatus: NYNAccountAccessStatus - let roles: NYNAccountRoles? - let qrCode: String? - let birthday: NYNDate? +public final class AccountInfo { - init(accountId: String, + public let accountId: String + public let avatar: String? + public let accountMark: String? + public let accountName: String? + public let firstName: String? + public let lastName: String? + public let username: String? + public let accountStatus: NYNAccountAccessStatus + public let roles: NYNAccountRoles? + public let qrCode: String? + public let birthday: NYNDate? + + public init(accountId: String, avatar: String?, accountMark: String?, accountName: String?, diff --git a/Nynja/SDK/Account/Service/AccountService.swift b/Nynja/SDK/Account/Service/AccountService.swift index 1b89e5222..becd73d52 100644 --- a/Nynja/SDK/Account/Service/AccountService.swift +++ b/Nynja/SDK/Account/Service/AccountService.swift @@ -9,28 +9,39 @@ import Foundation import NynjaSDK +public enum AccountError: Error { + case invalidResponse + case accountNotFound +} + +public struct UpdateInfo { + let oldValue: T + let newValue: T +} + protocol AccountService: class { - typealias ProfileCompletion = (Result) -> Void + typealias IdentityCompletion = (Result) -> Void typealias AccountCompletion = (Result) -> Void typealias AccountListCompletion = (Result<[NYNAccountDetails]>) -> Void typealias StatusCompletion = (Result) -> Void + typealias SearchCompletion = (Result) -> Void // MARK: - Profile (Identity) - func updateProfile(_ profileId: String, passcode: String?, defaultAccountId: String?, completion: @escaping ProfileCompletion) + func getIdentity(by identityId: String, completion: @escaping IdentityCompletion) - func deleteProfile(_ profileId: String, completion: @escaping StatusCompletion) + func deleteIdentity(_ identityId: String, completion: @escaping StatusCompletion) - func addAuthenticationProvider(to profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) + // MARK: Login Option - func deleteAuthenticationProvider(from profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) + func addAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, to identityId: String, completion: @escaping StatusCompletion) + func deleteAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, from identityId: String, completion: @escaping StatusCompletion) - // MARK: - Account - func createAccount(with authenticationProvider: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) + // MARK: - Account func completePendingAccountCreation(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) @@ -38,16 +49,33 @@ protocol AccountService: class { func deleteAccount(_ accountId: String, completion: @escaping StatusCompletion) - func getAccount(by accountId: String, completion: @escaping AccountCompletion) + func getAccount(accountId: String, completion: @escaping AccountCompletion) + + func getAccount(qrCode: String, completion: @escaping AccountCompletion) + + func getAccount(username: String, completion: @escaping AccountCompletion) - func getAccount(by authenticationIdentifier: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) + func getAccount(authenticationIdentifier: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) - func getAllAccounts(by profileId: String, completion: @escaping AccountListCompletion) + func getAllAccounts(by identityId: String, completion: @escaping AccountListCompletion) // MARK: - Account's Contact Info func addContactInfo(to accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) + func editContactInfo(_ editInfo: UpdateInfo, in accountId: String, completion: @escaping StatusCompletion) + func deleteContactInfo(from accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) + + + // MARK: - Search + + func searchByPhone(_ phoneNumber: PhoneNumberInfo, completion: @escaping SearchCompletion) + + func searchByEmail(_ email: String, completion: @escaping SearchCompletion) + + func searchByUsername(_ username: String, completion: @escaping SearchCompletion) + + func searchByQRCode(_ qrCode: String, completion: @escaping SearchCompletion) } diff --git a/Nynja/SDK/Account/Service/AccountServiceImpl.swift b/Nynja/SDK/Account/Service/AccountServiceImpl.swift index 16b6e6cd8..bccc4e582 100644 --- a/Nynja/SDK/Account/Service/AccountServiceImpl.swift +++ b/Nynja/SDK/Account/Service/AccountServiceImpl.swift @@ -22,22 +22,35 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, // MARK: - Handlers - private var updateProfileCompletion: ProfileCompletion? - private var deleteProfileCompletion: StatusCompletion? - private var addAuthProviderToProfileCompletion: StatusCompletion? - private var deleteAuthProviderFromProfileCompletion: StatusCompletion? + // MARK: Identity + + private var getIdentityCompletion: IdentityCompletion? + private var deleteIdentityCompletion: StatusCompletion? + private var addAuthProviderToIdentityCompletion: StatusCompletion? + private var deleteAuthProviderFromIdentityCompletion: StatusCompletion? + + // MARK: Account - private var createAccountCompletion: AccountCompletion? private var completePendingAccountCompletion: AccountCompletion? private var updateAccountCompletion: AccountCompletion? private var deleteAccountCompletion: StatusCompletion? - private var getAccountCompletion: AccountCompletion? + private var getAccountByIdCompletion: AccountCompletion? + private var getAccountByUsernameCompletion: AccountCompletion? + private var getAccountByQRCodeCompletion: AccountCompletion? private var getAccountByAuthProviderCompletion: AccountCompletion? private var getAllAccountsCompletion: AccountListCompletion? private var addContactInfoCompletion: StatusCompletion? + private var editContactInfoCompletion: StatusCompletion? private var deleteContactInfoCompletion: StatusCompletion? + // MARK: Search + + private var searchByPhoneCompletion: SearchCompletion? + private var searchByEmailCompletion: SearchCompletion? + private var searchByUsernameCompletion: SearchCompletion? + private var searchByQRCodeCompletion: SearchCompletion? + private var token: String? { return storage.token } @@ -45,13 +58,13 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, // MARK: - Init - struct Dependencies { + public struct Dependencies { let accountManager: AccountManager let storage: SessionStorage let processingQueue: DispatchQueue = .main } - init(dependencies: Dependencies) { + public init(dependencies: Dependencies) { accountManager = dependencies.accountManager storage = dependencies.storage processingQueue = dependencies.processingQueue @@ -64,62 +77,46 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, // MARK: - Profile (Identity) - func updateProfile(_ profileId: String, passcode: String?, defaultAccountId: String?, completion: @escaping ProfileCompletion) { + public func getIdentity(by identityId: String, completion: @escaping IdentityCompletion) { guard let token = token else { return } - bind(completion, to: \AccountServiceImpl.updateProfileCompletion) + bind(completion, to: \AccountServiceImpl.getIdentityCompletion) - accountManager.sendUpdateProfile(withProfileId: profileId, - withAccessToken: token, - withPasscode: passcode, - withAccountId: defaultAccountId) + accountManager.sendGetIdentity(identityId, withAccessToken: token) } - func deleteProfile(_ profileId: String, completion: @escaping StatusCompletion) { + public func deleteIdentity(_ identityId: String, completion: @escaping StatusCompletion) { guard let token = token else { return } - bind(completion, to: \AccountServiceImpl.deleteProfileCompletion) + bind(completion, to: \AccountServiceImpl.deleteIdentityCompletion) - accountManager.sendDeleteProfile(withProfileId: profileId, withAccessToken: token) + accountManager.sendDeleteIdentity(identityId, withAccessToken: token) } - func addAuthenticationProvider(to profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) { + public func addAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, to identityId: String, completion: @escaping StatusCompletion) { guard let token = token else { return } - bind(completion, to: \AccountServiceImpl.addAuthProviderToProfileCompletion) + bind(completion, to: \AccountServiceImpl.addAuthProviderToIdentityCompletion) - accountManager.sendAddAuthenticationProviderToProfile(withProfileId: profileId, - withAccessToken: token, - with: authProviderDetails) + accountManager.sendAddAuthenticationProvider(toIdentity: identityId, withAccessToken: token, with: authProviderDetails) } - func deleteAuthenticationProvider(from profileId: String, authProviderDetails: NYNAuthProviderDetails, completion: @escaping StatusCompletion) { + public func deleteAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, from identityId: String, completion: @escaping StatusCompletion) { guard let token = token else { return } - bind(completion, to: \AccountServiceImpl.deleteAuthProviderFromProfileCompletion) + bind(completion, to: \AccountServiceImpl.deleteAuthProviderFromIdentityCompletion) - accountManager.sendDeleteAuthenticationProviderFromProfile(withProfileId: profileId, - withAccessToken: token, - with: authProviderDetails) + accountManager.sendDeleteAuthenticationProvider(fromIdentity: identityId, withAccessToken: token, with: authProviderDetails) } // MARK: - Account - func createAccount(with authenticationProvider: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) { - guard let token = token else { - return - } - bind(completion, to: \AccountServiceImpl.createAccountCompletion) - - accountManager.sendCreateAccount(withAuthProvider: authenticationProvider, withAccessToken: token, with: authType) - } - - func completePendingAccountCreation(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) { + public func completePendingAccountCreation(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) { guard let token = token else { return } @@ -138,7 +135,7 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, with: accountInfo.roles) } - func updateAccount(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) { + public func updateAccount(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) { guard let token = token else { return } @@ -157,7 +154,7 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, withBirthday: accountInfo.birthday) } - func deleteAccount(_ accountId: String, completion: @escaping StatusCompletion) { + public func deleteAccount(_ accountId: String, completion: @escaping StatusCompletion) { guard let token = token else { return } @@ -166,16 +163,34 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, accountManager.sendDeleteAccount(withAccountId: accountId, withAccessToken: token) } - func getAccount(by accountId: String, completion: @escaping AccountCompletion) { + public func getAccount(accountId: String, completion: @escaping AccountCompletion) { guard let token = token else { return } - bind(completion, to: \AccountServiceImpl.getAccountCompletion) + bind(completion, to: \AccountServiceImpl.getAccountByIdCompletion) accountManager.sendGetAccount(withAccountId: accountId, withAccessToken: token) } - func getAccount(by authenticationIdentifier: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) { + public func getAccount(qrCode: String, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getAccountByQRCodeCompletion) + + accountManager.sendGetAccount(withQrCode: qrCode, withAccessToken: token) + } + + public func getAccount(username: String, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getAccountByUsernameCompletion) + + accountManager.sendGetAccount(withUsername: username, withAccessToken: token) + } + + public func getAccount(authenticationIdentifier: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) { guard let token = token else { return } @@ -184,19 +199,19 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, accountManager.sendGetAccount(withAuthProvider: authenticationIdentifier, withAccessToken: token, with: authType) } - func getAllAccounts(by profileId: String, completion: @escaping AccountListCompletion) { + public func getAllAccounts(by identityId: String, completion: @escaping AccountListCompletion) { guard let token = token else { return } bind(completion, to: \AccountServiceImpl.getAllAccountsCompletion) - accountManager.sendGetAllAccounts(withProfileId: profileId, withAccessToken: token) + accountManager.sendGetAllAccounts(withIdentity: identityId, withAccessToken: token) } // MARK: - Account's Contact Info - func addContactInfo(to accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { + public func addContactInfo(to accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { guard let token = token else { return } @@ -205,7 +220,19 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, accountManager.sendAddContactInfoToAccount(withAccountId: accountId, withAccessToken: token, with: contactDetails) } - func deleteContactInfo(from accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { + public func editContactInfo(_ editInfo: UpdateInfo, in accountId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.editContactInfoCompletion) + + accountManager.sendEditContactInfoForAccount(withAccountId: accountId, + withAccessToken: token, + withOldContactDetails: editInfo.oldValue, + withEditedContactDetails: editInfo.newValue) + } + + public func deleteContactInfo(from accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { guard let token = token else { return } @@ -214,6 +241,49 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, accountManager.sendDeleteContactInfoFromAccount(withAccountId: accountId, withAccessToken: token, with: contactDetails) } + + // MARK: - Search + + func searchByPhone(_ phoneNumber: PhoneNumberInfo, completion: @escaping SearchCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.searchByPhoneCompletion) + + let phone = phoneNumber.formattedForRequest() + accountManager.sendSearch(byPhoneNumber: phone, withAccessToken: token) + } + + public func searchByEmail(_ email: String, completion: @escaping SearchCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.searchByEmailCompletion) + + accountManager.sendSearch(byEmail: email, withAccessToken: token) + } + + public func searchByUsername(_ username: String, completion: @escaping SearchCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.searchByUsernameCompletion) + + accountManager.sendSearch(byUsername: username, withAccessToken: token) + } + + public func searchByQRCode(_ qrCode: String, completion: @escaping SearchCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.searchByQRCodeCompletion) + + accountManager.sendSearch(byQrCode: qrCode, withAccessToken: token) + } + + + // MARK: - Utils + private func bind(_ value: T, to keyPath: ReferenceWritableKeyPath) { processingQueue.async { self[keyPath: keyPath] = value @@ -227,6 +297,18 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, handler(completion) } } + + private func processResponseBody(_ body: T?, error: Error?, completion: ((Result) -> Void)?) { + if let error = error { + completion?(.failure(error)) + + } else if let body = body { + completion?(.success(body)) + + } else { + completion?(.failure(AccountError.invalidResponse)) + } + } } @@ -234,174 +316,136 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, extension AccountServiceImpl { - // MARK: Profile (Identity) + // MARK: Identity - public func updateProfileDidFinish(with profileDetails: NYNProfileDetails?, withError error: Error?) { - handleResponse(nil, to: \AccountServiceImpl.updateProfileCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - if let profileDetails = profileDetails { - completion?(.success(profileDetails)) - } - completion?(.failure(AccountError.invalidResponse)) + public func getIdentityDidFinish(with profileDetails: NYNProfileDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getIdentityCompletion) { completion in + self.processResponseBody(profileDetails, error: error, completion: completion) } } - public func deleteProfileDidFinish(withStatus status: String, withError error: Error?) { - handleResponse(nil, to: \AccountServiceImpl.deleteProfileCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - completion?(.success(status)) + @available(*, deprecated, message: "will be removed") + public func updateIdentityDidFinish(with profileDetails: NYNProfileDetails?, withError error: Error?) { + // FIXME: deprecated + } + + public func deleteIdentityDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.deleteIdentityCompletion) { completion in + self.processResponseBody(status, error: error, completion: completion) } } - func addAuthenticationProviderToProfileDidFinish(withStatus status: String, withError error: Error?, withProfileId profileId: String) { - handleResponse(nil, to: \AccountServiceImpl.addAuthProviderToProfileCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - completion?(.success(status)) + public func addAuthenticationProviderToIdentityDidFinish(withStatus status: String, withError error: Error?, withId profileId: String) { + handleResponse(nil, to: \AccountServiceImpl.addAuthProviderToIdentityCompletion) { completion in + self.processResponseBody(status, error: error, completion: completion) } } - public func deleteAuthenticationProviderFromProfileDidFinish(withStatus status: String, withError error: Error?) { - handleResponse(nil, to: \AccountServiceImpl.deleteAuthProviderFromProfileCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - completion?(.success(status)) + public func deleteAuthenticationProviderFromIdentityDidFinish(withStatus status: String, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.deleteAuthProviderFromIdentityCompletion) { completion in + self.processResponseBody(status, error: error, completion: completion) } } // MARK: Account + @available(*, deprecated, message: "will be removed") public func createAccountDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { - handleResponse(nil, to: \AccountServiceImpl.createAccountCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - if let accountDetails = accountDetails { - completion?(.success(accountDetails)) - } - completion?(.failure(AccountError.invalidResponse)) - } + // FIXME: deprecated } public func completePendingAccountCreationDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { handleResponse(nil, to: \AccountServiceImpl.completePendingAccountCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - if let accountDetails = accountDetails { - completion?(.success(accountDetails)) - } - completion?(.failure(AccountError.invalidResponse)) + self.processResponseBody(accountDetails, error: error, completion: completion) } } public func updateAccountDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { handleResponse(nil, to: \AccountServiceImpl.updateAccountCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - if let accountDetails = accountDetails { - completion?(.success(accountDetails)) - } - completion?(.failure(AccountError.invalidResponse)) + self.processResponseBody(accountDetails, error: error, completion: completion) } } func deleteAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { handleResponse(nil, to: \AccountServiceImpl.deleteAccountCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - completion?(.success(status)) + self.processResponseBody(status, error: error, completion: completion) } } public func getAccountByIdDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { - handleResponse(nil, to: \AccountServiceImpl.getAccountCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - if let accountDetails = accountDetails { - completion?(.success(accountDetails)) - } - completion?(.failure(AccountError.invalidResponse)) + handleResponse(nil, to: \AccountServiceImpl.getAccountByIdCompletion) { completion in + self.processResponseBody(accountDetails, error: error, completion: completion) + } + } + + public func getAccountByUsernameDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getAccountByUsernameCompletion) { completion in + self.processResponseBody(accountDetails, error: error, completion: completion) + } + } + + public func getAccountByQrCodeDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getAccountByQRCodeCompletion) { completion in + self.processResponseBody(accountDetails, error: error, completion: completion) } } public func getAccountByAuthProviderDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { handleResponse(nil, to: \AccountServiceImpl.getAccountByAuthProviderCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - if let accountDetails = accountDetails { - completion?(.success(accountDetails)) - } - completion?(.failure(AccountError.invalidResponse)) + self.processResponseBody(accountDetails, error: error, completion: completion) } } - public func getAllAccountsByProfileIdDidFinish(withDetails accountDetailsArray: [NYNAccountDetails]?, withError error: Error?) { + public func getAllAccountsByIdentityDidFinish(withDetails accountDetailsArray: [NYNAccountDetails]?, withError error: Error?) { handleResponse(nil, to: \AccountServiceImpl.getAllAccountsCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - if let accountDetailsArray = accountDetailsArray { - completion?(.success(accountDetailsArray)) - } - completion?(.failure(AccountError.invalidResponse)) + self.processResponseBody(accountDetailsArray, error: error, completion: completion) } } // MARK: Account's Contact Info - func addContactInfoToAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { + public func addContactInfoToAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { handleResponse(nil, to: \AccountServiceImpl.addContactInfoCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - completion?(.success(status)) + self.processResponseBody(status, error: error, completion: completion) + } + } + + public func editContactInfoForAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { + handleResponse(nil, to: \AccountServiceImpl.editContactInfoCompletion) { completion in + self.processResponseBody(status, error: error, completion: completion) } } - public func deleteContactInfoFromAccountDidFinish(withStatus status: String, withError error: Error?) { + public func deleteContactInfoFromAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { handleResponse(nil, to: \AccountServiceImpl.deleteContactInfoCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } - completion?(.success(status)) + self.processResponseBody(status, error: error, completion: completion) } } // MARK: Search - func searchByPhoneNumberDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { - + public func searchByPhoneNumberDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.searchByPhoneCompletion) { completion in + self.processResponseBody(searchResultDetails, error: error, completion: completion) + } } - func searchByQrCodeDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { - - } - - func searchByEmailDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { - - } - - func searchByUsernameDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { - + public func searchByEmailDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.searchByEmailCompletion) { completion in + self.processResponseBody(searchResultDetails, error: error, completion: completion) + } } - func getAccountByUsernameDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { - + public func searchByUsernameDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.searchByUsernameCompletion) { completion in + self.processResponseBody(searchResultDetails, error: error, completion: completion) + } } - func getAccountByQrCodeDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { - + public func searchByQrCodeDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.searchByQRCodeCompletion) { completion in + self.processResponseBody(searchResultDetails, error: error, completion: completion) + } } } diff --git a/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift b/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift index ca5f622a3..1779af37f 100644 --- a/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift +++ b/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift @@ -15,10 +15,18 @@ struct PhoneNumberInfo { /// - Example: "+380 97 123 12 12" -> "971231212" let number: String + var fullNumber: String { + return "+\(country.code)\(number)" + } + var displayString: String { return "+\(country.code) \(formattedNumber(number, with: country.placeHolder))" } + func formattedForRequest() -> String { + return "\(country.ISO):\(country.code)\(number)" + } + private func formattedNumber(_ number: String, with mask: String?) -> String { let maskLength = mask?.replacingOccurrences(of: " ", with: "").count guard let mask = mask, number.count == maskLength else { diff --git a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift index b6999b493..4b493ef70 100644 --- a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -11,6 +11,10 @@ import NynjaSDK final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLoginManagerDelegate { + enum AuthError: Error { + case invalidResponse + } + // MARK: - Dependencies private let loginManager: LoginManager @@ -77,11 +81,10 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func login(by numberInfo: PhoneNumberInfo, confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) { - let country = numberInfo.country - let numberFormat = "\(country.ISO):\(country.code)\(numberInfo.number)" + let phone = numberInfo.formattedForRequest() bind(completion, to: \AuthServiceImpl.loginByPhoneCompletion) - loginManager.sendLogin(byPhone: numberFormat, withAppToken: appToken, withSendTokenVia: authConfirmationType.sdkValue) + loginManager.sendLogin(byPhone: phone, withAppToken: appToken, withSendTokenVia: authConfirmationType.sdkValue) } func loginByFacebook(serverCode: String, completion: @escaping CodeConfirmationCompletion) { @@ -117,7 +120,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } func confirmNynjaCode(_ code: String, completion: @escaping CodeConfirmationCompletion) { - confirm(code: code, with: nil, completion: completion) + confirm(code: code, with: "", completion: completion) } func confirmSocialServerAuthCode(_ code: String, completion: @escaping CodeConfirmationCompletion) { @@ -143,6 +146,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } storage.setupDatabase(with: passcode, application: UIApplication.shared) + storage.wasLogined = true storage.clientId = appConfigurationProvider.sdkCredentials.deviceId storage.identityId = account.profileId storage.accountId = account.accountId @@ -163,41 +167,25 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog func sendLogin(byEmailDidFinish error: Error?) { handleResponse(nil, to: \AuthServiceImpl.loginByEmailCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } else { - completion?(.success(())) - } + self.processResponseBody((), error: error, completion: completion) } } func sendLogin(byPhoneDidFinish error: Error?) { handleResponse(nil, to: \AuthServiceImpl.loginByPhoneCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } else { - completion?(.success(())) - } + self.processResponseBody((), error: error, completion: completion) } } func sendLogin(byFacebookDidFinish error: Error?) { handleResponse(nil, to: \AuthServiceImpl.loginByFacebookCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } else { - completion?(.success(())) - } + self.processResponseBody((), error: error, completion: completion) } } func sendLogin(byGooglePlusDidFinish error: Error?) { handleResponse(nil, to: \AuthServiceImpl.loginByGoogleCompletion) { completion in - if let error = error { - completion?(.failure(error)) - } else { - completion?(.success(())) - } + self.processResponseBody((), error: error, completion: completion) } } @@ -206,6 +194,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog withRefreshToken refreshToken: String, withExpiration expiration: NSNumber?, isPendingAccount pending: Bool, + with socialLoginDetails: NYNSocialLoginDetails?, withError error: Error?) { handleResponse(nil, to: \AuthServiceImpl.confirmCodeCompletion) { completion in if let error = error { @@ -214,7 +203,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog return } self.storage.save(accessToken: accessToken, refreshToken: refreshToken) - + let response = AuthResponse(accountId: accountId, authenticationType: pending ? .register : .login) completion?(.success(response)) @@ -257,6 +246,18 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog handler(completion) } } + + private func processResponseBody(_ body: T?, error: Error?, completion: ((Result) -> Void)?) { + if let error = error { + completion?(.failure(error)) + + } else if let body = body { + completion?(.success(body)) + + } else { + completion?(.failure(AuthError.invalidResponse)) + } + } } // MARK: - Extensions diff --git a/Nynja/SearchModel.swift b/Nynja/SearchModel.swift index c5cc6b6fb..bb8d5e549 100644 --- a/Nynja/SearchModel.swift +++ b/Nynja/SearchModel.swift @@ -11,7 +11,6 @@ import Foundation enum SearchModelReference: String { case PHONEBOOK = "phonebook" case PHONE = "phone" - case QRCODE = "qrcode" case USERNAME = "username" } diff --git a/Nynja/Services/ContactManager.swift b/Nynja/Services/ContactManager.swift index 9404d4ce5..3b87b1aa0 100644 --- a/Nynja/Services/ContactManager.swift +++ b/Nynja/Services/ContactManager.swift @@ -10,7 +10,7 @@ import Contacts final class ContactManager { - static var shared = ContactManager() + static let shared = ContactManager() private init() {} @@ -22,12 +22,14 @@ final class ContactManager { var contacts = [CNContact]() do { - let keys = [CNContactGivenNameKey as CNKeyDescriptor, - CNContactFamilyNameKey as CNKeyDescriptor, - CNContactImageDataKey as CNKeyDescriptor, - CNContactImageDataAvailableKey as CNKeyDescriptor, - CNContactPhoneNumbersKey as CNKeyDescriptor, - CNContactEmailAddressesKey as CNKeyDescriptor] + let keys = [ + CNContactGivenNameKey as CNKeyDescriptor, + CNContactFamilyNameKey as CNKeyDescriptor, + CNContactImageDataKey as CNKeyDescriptor, + CNContactImageDataAvailableKey as CNKeyDescriptor, + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor + ] let contactsFetchRequest = CNContactFetchRequest(keysToFetch: keys) @@ -49,37 +51,100 @@ final class ContactManager { } } - func getContactsForInvites(missEmpty: Bool = false, completion: @escaping (_ success: Bool, _ contacts: [String: PhoneContact]?) -> Void) { + func getContactsForInvites(completion: @escaping (Result<[String: PhoneContact]>) -> Void) { DispatchQueue.global(qos: .background).async { do { var phoneToContact = [String: PhoneContact]() - let keys = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName) as CNKeyDescriptor, - CNContactImageDataAvailableKey as CNKeyDescriptor, - CNContactThumbnailImageDataKey as CNKeyDescriptor, - CNContactPhoneNumbersKey as CNKeyDescriptor] + let keys: [CNKeyDescriptor] = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName) as CNKeyDescriptor, + CNContactImageDataAvailableKey as CNKeyDescriptor, + CNContactThumbnailImageDataKey as CNKeyDescriptor, + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor + ] let contactsFetchRequest = CNContactFetchRequest(keysToFetch: keys) + + try self.contactStore.enumerateContacts(with: contactsFetchRequest) { contact, error in + guard let phoneContact = PhoneContact(contact: contact) else { + return + } + let phoneNumber = phoneContact.phoneNumber.first == "+" + ? phoneContact.phoneNumber + : phoneContact.phoneNumber.stringAsPhone(format: .E164) + + phoneToContact[phoneNumber] = phoneContact + } + DispatchQueue.main.async { + completion(.success(phoneToContact)) + } + } catch { + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + } + + + /// Returns contact array from device contacts book that matches specific phone number + /// + /// - Parameters: + /// - phoneNumber: Number with country code prefix and '+' sign, ex: '+380971231212' + /// - completion: Completion block + func getContactsByPhoneNumber(_ phoneNumber: String, completion: @escaping (Result<[PhoneContact]>) -> Void) { + DispatchQueue.global(qos: .background).async { + do { + let keysToFetch: [CNKeyDescriptor] = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName) as CNKeyDescriptor, + CNContactImageDataAvailableKey as CNKeyDescriptor, + CNContactThumbnailImageDataKey as CNKeyDescriptor, + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor + ] + + var result: [PhoneContact] - try self.contactStore.enumerateContacts(with: contactsFetchRequest, usingBlock: { (contact, error) in + if #available(iOS 11.0, *) { + let cnPhoneNumber = CNPhoneNumber(stringValue: phoneNumber) + let predicate = CNContact.predicateForContacts(matching: cnPhoneNumber) + let contacts = try self.contactStore.unifiedContacts(matching: predicate, keysToFetch: keysToFetch) - if let c = PhoneContact.readContact(contact: contact) { - if c.phoneNumber?.first == "+" { - phoneToContact[c.phoneNumber!] = c - } else { - phoneToContact[c.phoneNumber!.stringAsPhone(format: .E164)] = c + result = contacts.compactMap { contact -> PhoneContact? in + guard let phoneContact = PhoneContact(contact: contact) else { + return nil } + return phoneContact } - }) + + } else { + result = [] + + let fetchRequest = CNContactFetchRequest(keysToFetch: keysToFetch) + + try self.contactStore.enumerateContacts(with: fetchRequest) { contact, error in + guard let phoneContact = PhoneContact(contact: contact) else { + return + } + let number = phoneContact.phoneNumber.first == "+" + ? phoneContact.phoneNumber + : phoneContact.phoneNumber.stringAsPhone(format: .E164) + + if phoneNumber == number { + result.append(phoneContact) + } + } + } + DispatchQueue.main.async { - completion(true, phoneToContact) + completion(.success(result)) } } catch { DispatchQueue.main.async { - completion(false, nil) + completion(.failure(error)) } } } } - } diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 1d106d770..2b6dfec2e 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -111,6 +111,10 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return PermissionManager() } + func makeContactManager() -> ContactManager { + return ContactManager.shared + } + func makeTextInputValidationService() -> TextInputValidationServiceProtocol { return TextInputValidationService() } @@ -234,6 +238,10 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { func makeLocationService() -> LocationService { return LocationService.sharedInstance } + + func makePhoneNumberTextController() -> PhoneNumberTextController { + return PhoneNumberTextController(countryProvider: makeCountriesProvider()) + } } // MARK: - MQTT Handlers diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift index 4963ebdb8..2db6889e2 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -35,7 +35,9 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtoc func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol func makeWalletOpeningTextInputValidationService() -> WalletOpeningTextInputValidationServiceProtocol func makeWalletFundingNetworkService() -> WalletFundingNetworkService + func makePermissionManager() -> PermissionManager + func makeContactManager() -> ContactManager func makeWalletService() -> WalletService @@ -78,4 +80,6 @@ protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtoc func makeValidatorFactory() -> ValidatorFactory func makeLocationService() -> LocationService + + func makePhoneNumberTextController() -> PhoneNumberTextController } diff --git a/Podfile b/Podfile index 7bd55a6f3..c01f520e6 100644 --- a/Podfile +++ b/Podfile @@ -40,7 +40,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.6' # pod 'NynjaSDK', '= 1.8' - pod 'NynjaSDK-MultiAcc', '= 0.5.6.3' + pod 'NynjaSDK-MultiAcc', '= 0.5.6.4' pod 'CryptoSwift', '= 0.13.0' diff --git a/Podfile.lock b/Podfile.lock index ceedf7794..c6571b3df 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -91,7 +91,7 @@ PODS: - MQTTClient/Min - SocketRocket - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.6.3) + - NynjaSDK-MultiAcc (0.5.6.4) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -126,7 +126,7 @@ DEPENDENCIES: - MaterialComponents/FlexibleHeader (= 55.3.0) - MQTTClient/Websocket (= 0.15.2) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.6.3) + - NynjaSDK-MultiAcc (= 0.5.6.4) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.2.0) @@ -215,7 +215,7 @@ SPEC CHECKSUMS: MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MQTTClient: 902c7bcac1501595f3d0b15178c7205b40331fb0 MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: 825314ad71cf1b39ea79ec9015d4768f51fa8b90 + NynjaSDK-MultiAcc: ead0f0eb472eccddf71cc40cd155184bd9925dbf QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a @@ -224,6 +224,6 @@ SPEC CHECKSUMS: SwiftyJSON: c4bcba26dd9ec7a027fc8eade48e2c911f229e96 TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: d79872a5a9599e7527c52e75f89eafbbff8ef0c1 +PODFILE CHECKSUM: 1d41f8e1e45db54475f314e6bddc09e3048fd03a COCOAPODS: 1.5.3 diff --git a/Shared/Services/Handlers/IoHandler/IoHandler.swift b/Shared/Services/Handlers/IoHandler/IoHandler.swift index 8fdf65f51..86556ce04 100644 --- a/Shared/Services/Handlers/IoHandler/IoHandler.swift +++ b/Shared/Services/Handlers/IoHandler/IoHandler.swift @@ -85,14 +85,6 @@ final class IoHandler: BaseHandler, StaticDelegating { delegate { $0.contactsNotFound() } } } - case "qrcode": - if let roster = IO.data as? Roster { - if let contact = roster.userlist?.first { - delegate { $0.getContactQRSuccess(contact: contact) } - } else { - delegate { $0.contactQRNotFound() } - } - } case "nick": delegate { $0.usernameIsBusy() } case "username": diff --git a/Shared/Services/Handlers/IoHandler/IoHandlerDelegate.swift b/Shared/Services/Handlers/IoHandler/IoHandlerDelegate.swift index 7620fa52b..e9fff4b81 100644 --- a/Shared/Services/Handlers/IoHandler/IoHandlerDelegate.swift +++ b/Shared/Services/Handlers/IoHandler/IoHandlerDelegate.swift @@ -25,12 +25,10 @@ protocol IoHandlerDelegate: class { func sessionDeleted() func getContactSuccess(contact: Contact) - func getContactQRSuccess(contact: Contact) func getContactByUsernameSucces(contact: Contact) func getContactsSuccess(contacts: [Contact]) func contactNotFound() func contactsNotFound() - func contactQRNotFound() func contactByUsernameNotFound() func usernameIsBusy() } @@ -51,12 +49,10 @@ extension IoHandlerDelegate { func sessionDeleted() {} func getContactSuccess(contact: Contact) {} - func getContactQRSuccess(contact: Contact) {} func getContactByUsernameSucces(contact: Contact) {} func getContactsSuccess(contacts: [Contact]) {} func contactNotFound() {} func contactsNotFound() {} - func contactQRNotFound() {} func contactByUsernameNotFound() {} func usernameIsBusy() {} -- GitLab From 9718a09048f214068b535c4783d2146233ecb8f6 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 21 Dec 2018 15:28:09 +0200 Subject: [PATCH 135/138] [Multiple Accounts] Account Settings. Contact Info flow. (#1557) * [NY-3852] Added base module skeleton for contact info from template. * Fixed auth and account services. * Update app version for build. * [NY-5507] Updated SDK for the latest version. * Refactored phone number text controller * Add error handling popups to code confirmation screen. * [NY-3852] Implemented UI for base container. * Update input model * [NY-5178] Added search module from xcode template * [NY-5187] Implemented base layout for search screen. * [NY-5187] Handle empty state. * [NY-5187] Implemented search UI. * Fixed auth * [NY-5187] Minor updates. * [NY-5187] Implemented search results table UI. Minor UI fixes. * [NY-5187] Fixed search result UI. * [NY-5187] Fixed validators * [Multiple accounts] Search by QR Code (#1536) * [NY-5178] Minor refactoring in QR Code reader / generator modules. * [NY-5178] Implemented new logic for search by QR code. * [NY-5178] Refactored search result processing logic. * [NY-5178] Update SDK version * [NY-5178] Implemented full search logic * [NY-5178] Handle empty phone book response * [NY-5178] Minor UI fix. * [NY-5178] Setup proper wheel factory * [NY-3852] Implemented UI for phone number contact info screen. * [NY-3852] Implemented email UI * [NY-3852] UI updates * [NY-3852] Fixed UI * [NY-3852] Implemented social UI. * [NY-3852] SDK integration for contact info flow. * [NY-3852] Save DBContactInfo to database * [NY-3852] Pass data flow to coordinator * [NY-3852] Update SDK version. * Hide tutorial wireframe from auth flow. * [NY-3852] Update UI logic on Account Settings. * [NY-3852] Remove unused models. * [NY-3852] Implemented new navigation logic for account settings. Fixed localization and minor issues. * [NY-3852] Fill AccountSettingsViewModel. * [NY-3852] Implemented account settings editing logic without contact info section. * [NY-3852] Implemented contact info section. * Fixed minor issues. * Requested changes. * Requested changes in DBAccount model. * Remove unused files after merge. * Requested changes. * Requested changes. --- Nynja.xcodeproj/project.pbxproj | 210 +++++------ Nynja/DB/Models/DBAccount.swift | 21 +- Nynja/DBObserver.swift | 44 ++- Nynja/Generated/AssetsConstants.swift | 6 + Nynja/Generated/LocalizableConstants.swift | 72 ++++ Nynja/ImageSelector.swift | 2 +- Nynja/Improvements/StorageSubscriber.swift | 2 + .../Buttons/NynjaButton/BaseNynjaButton.swift | 4 + .../NynjaButton/DestructiveNynjaButton.swift | 65 ++++ .../NynjaButton/RoundNynjaButton.swift | 1 + .../NynjaControlContainerView.swift | 2 + .../UI/Extensions/UI/UIFontExtension.swift | 12 + .../UI/SeparatorView/SeparatorView.swift | 14 +- ...dator.swift => ChannelLinkValidator.swift} | 4 +- .../{LinkField.swift => NynjaLinkField.swift} | 6 +- .../NewChannel/NewChannelProtocols.swift | 2 +- .../Presenter/NewChannelPresenter.swift | 2 +- .../View/NewChannelViewController.swift | 6 +- .../AccountSettingsProtocols.swift | 55 +-- .../Entities/AccountSettingsViewModel.swift | 26 ++ .../Entities/AccountStatus.swift | 28 ++ .../Entities/AccountTimeout.swift | 25 ++ .../Entities/AddContactCellModel.swift | 19 - .../Entities/ContactInfoSectionItem.swift | 29 ++ .../Entities/ContactInfoViewModel.swift | 207 +++++++++++ .../Entities/ContactTVCellModel.swift | 26 -- .../Entities/DescriptionCellModel.swift | 28 -- .../Entities/DestructiveActionCellModel.swift | 28 -- .../Entities/MaterialTextFieldCellModel.swift | 29 -- .../Entities/SettingsSectionHeader.swift | 20 - .../Entities/SettingsSelectorCellModel.swift | 27 -- .../Entities/SettingsSetAvatarCellModel.swift | 23 -- .../AccountSettings/Entities/Sizeble.swift | 20 - .../Entities/StatusTimeout.swift | 17 - .../Entities/UserAccount.swift | 24 -- .../Entities/UserContact.swift | 31 -- .../Entities/UserContactAction.swift | 16 - .../AccountSettings/Entities/UserStatus.swift | 16 - .../AccountSettingsInteractor.swift | 115 +++--- .../Presenter/AccountSettingsPresenter.swift | 342 ++++++++++++------ .../View/AccountSettingsViewController.swift | 314 ++++++---------- .../View/Cells/AddContactCell.swift | 64 ---- .../View/Cells/ContactTVCell.swift | 98 ----- .../View/Cells/DescriptionTVCell.swift | 41 --- .../DestructiveActionTableViewCell.swift | 63 ---- .../View/Cells/MaterialTextFieldTVCell.swift | 47 --- .../View/Cells/SettingsSelectorTVCell.swift | 69 ---- .../View/Cells/SettingsSetAvatarTVCell.swift | 59 --- .../ContactInfoSectionViewController.swift | 67 ++++ .../Header/SettingsSectionHeaderView.swift | 38 -- .../Wireframe/AccountSettingsWireframe.swift | 37 +- .../ContactInfoManagementProtocols.swift | 14 +- .../Entities/ContactInfoInputModel.swift | 70 ++-- .../Entities/LinkValidator.swift | 13 + .../Entities/SocialLinkValidator.swift | 34 ++ .../ContactInfoManagementInteractor.swift | 103 +++++- .../ContactInfoManagementPresenter.swift | 290 ++++++++++++++- .../ContactInfoManagementViewController.swift | 59 ++- .../View/ContactInfoManagementViewModel.swift | 18 + .../PhoneNumberContactInfoViewModel.swift | 126 +++++++ .../ContactInfoManagementWireframe.swift | 24 +- .../AccountSettingsCoordinator.swift | 226 +++++------- .../Forms/FieldRowItem/AnyFieldRowItem.swift | 27 +- .../Forms/FieldRowItem/FieldRowItem.swift | 13 +- .../LoginOptions/View/Forms/Form.swift | 9 +- .../View/Forms/FormHeaderView.swift | 59 +++ .../View/Forms/Items/ActionRowItemView.swift | 17 +- .../View/Forms/Items/AvatarRowItemView.swift | 113 ++++++ .../Items/DestructiveActionRowItem.swift | 77 ++++ .../View/Forms/Items/PickerRowItemView.swift | 140 +++++++ .../Forms/Items/TextFieldRowItemView.swift | 119 +++--- .../View/Forms/Items/TextRowItemView.swift | 22 +- .../LoginOptionSwitchRowItemView.swift | 2 +- Nynja/Modules/Flows/AppCoordinator.swift | 1 + .../Flows/Auth Flow/AuthCoordinator.swift | 12 +- .../View/Subviews/PhoneNumberLoginView.swift | 9 +- .../EmailTextValidator.swift} | 16 +- .../UsernameTextValidator.swift} | 15 +- .../Presenter/SearchContactPresenter.swift | 25 +- .../InputView/TextFieldContentViewModel.swift | 73 +++- .../SelectAvatarCoordinator.swift | 6 +- .../Interactor/QRCodeReaderInteractor.swift | 10 +- .../Presenter/QRCodeReaderPresenter.swift | 4 +- .../QRCodeReader/QRCodeReaderProtocols.swift | 2 +- .../View/QRCodeReaderViewController.swift | 2 +- .../WireFrame/QRCodeReaderWireframe.swift | 6 +- .../Contents.json | 12 + .../Icons_General_ic_mail.pdf | Bin 0 -> 4895 bytes .../ic_fb.imageset/Contents.json | 12 + .../Assets.xcassets/ic_fb.imageset/ic_fb.pdf | Bin 0 -> 4064 bytes .../ic_twitter.imageset/Contents.json | 12 + .../ic_twitter.imageset/ic_twitter.pdf | Bin 0 -> 4436 bytes Nynja/Resources/en.lproj/Localizable.strings | 50 +++ .../SDK/Account/Service/AccountService.swift | 4 +- .../Account/Service/AccountServiceImpl.swift | 4 +- .../SDK/Auth/Entities/PhoneNumberLabel.swift | 27 ++ Nynja/SDK/Auth/Service/AuthService.swift | 1 + Nynja/SDK/Auth/Service/AuthServiceImpl.swift | 12 + .../Storage/DAO/Account/AccountDAO.swift | 55 ++- .../DAO/Account/AccountDAOProtocol.swift | 9 +- Podfile | 2 +- Podfile.lock | 8 +- 102 files changed, 2751 insertions(+), 1736 deletions(-) create mode 100644 Nynja/Library/UI/Buttons/NynjaButton/DestructiveNynjaButton.swift rename Nynja/Library/UI/TextInput/InputField/LinkField/{LinkValidator.swift => ChannelLinkValidator.swift} (95%) rename Nynja/Library/UI/TextInput/InputField/LinkField/{LinkField.swift => NynjaLinkField.swift} (95%) create mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountSettingsViewModel.swift create mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountStatus.swift create mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountTimeout.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AddContactCellModel.swift create mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoSectionItem.swift create mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoViewModel.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/Sizeble.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/StatusTimeout.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserAccount.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContact.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContactAction.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserStatus.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/AddContactCell.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift create mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/ContactInfoSectionViewController.swift delete mode 100644 Nynja/Modules/Flows/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/LinkValidator.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewModel.swift create mode 100644 Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/InputView/PhoneNumberContactInfoViewModel.swift create mode 100644 Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormHeaderView.swift create mode 100644 Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/AvatarRowItemView.swift create mode 100644 Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/DestructiveActionRowItem.swift create mode 100644 Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/PickerRowItemView.swift rename Nynja/Modules/Flows/Search Flow/{View/InputView/EmailContentViewModel.swift => Entities/EmailTextValidator.swift} (51%) rename Nynja/Modules/Flows/Search Flow/{View/InputView/UsernameContentViewModel.swift => Entities/UsernameTextValidator.swift} (57%) create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Icons_General_ic_mail.pdf create mode 100644 Nynja/Resources/Assets.xcassets/ic_fb.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/ic_fb.imageset/ic_fb.pdf create mode 100644 Nynja/Resources/Assets.xcassets/ic_twitter.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/ic_twitter.imageset/ic_twitter.pdf create mode 100644 Nynja/SDK/Auth/Entities/PhoneNumberLabel.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index c09c9904b..17cfc8b0e 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -472,7 +472,10 @@ 35F2DA611F73CAD400777920 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F2DA601F73CAD400777920 /* NotificationManager.swift */; }; 38182BD2C2E0C783796C8AA1 /* QRCodeReaderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D247CBC45C1C1267BBBB289 /* QRCodeReaderInteractor.swift */; }; 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */; }; + 3A02381421C8D3A000A143FD /* SocialLinkValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A02381321C8D3A000A143FD /* SocialLinkValidator.swift */; }; 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0281F61F53794800206871 /* UIViewExtenstions.swift */; }; + 3A06B08E21CB99E400E7964B /* ContactInfoSectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A06B08D21CB99E400E7964B /* ContactInfoSectionViewController.swift */; }; + 3A06B09021CBA84500E7964B /* ContactInfoSectionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A06B08F21CBA84500E7964B /* ContactInfoSectionItem.swift */; }; 3A0A4C5B21B91D9000BA0D09 /* DeleteAccountErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A4C5A21B91D9000BA0D09 /* DeleteAccountErrors.swift */; }; 3A0A50CB21B7FEFE0052D334 /* MyGroupAliasPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50A821B7FEFE0052D334 /* MyGroupAliasPresenter.swift */; }; 3A0A50CC21B7FEFE0052D334 /* MyGroupAliasWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50AA21B7FEFE0052D334 /* MyGroupAliasWireframe.swift */; }; @@ -517,8 +520,8 @@ 3A14D83B21AC03A3009CD23A /* SearchAvailabilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */; }; 3A14D83D21AC136F009CD23A /* AuthProviderUIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */; }; 3A14D83F21AC1F07009CD23A /* NavigationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83E21AC1F07009CD23A /* NavigationError.swift */; }; - 3A184D1B21C0FD800083D367 /* UsernameContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1A21C0FD800083D367 /* UsernameContentViewModel.swift */; }; - 3A184D1D21C0FD8C0083D367 /* EmailContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1C21C0FD8C0083D367 /* EmailContentViewModel.swift */; }; + 3A184D1B21C0FD800083D367 /* UsernameTextValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1A21C0FD800083D367 /* UsernameTextValidator.swift */; }; + 3A184D1D21C0FD8C0083D367 /* EmailTextValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1C21C0FD8C0083D367 /* EmailTextValidator.swift */; }; 3A184D1F21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */; }; 3A184D2121C0FEBC0083D367 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */; }; 3A184D2421C100630083D367 /* SearchInputMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2321C100630083D367 /* SearchInputMode.swift */; }; @@ -549,9 +552,15 @@ 3A2843291EF9317100EFE21A /* Avenir.ttc in Resources */ = {isa = PBXBuildFile; fileRef = 3A2843261EF9314100EFE21A /* Avenir.ttc */; }; 3A2A99831EFAD2FB002749B3 /* PageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2A99821EFAD2FB002749B3 /* PageControl.swift */; }; 3A2C2DFC21C26708006A53BB /* SearchContactResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2C2DFB21C26708006A53BB /* SearchContactResponse.swift */; }; + 3A2CDABF21C9405E00B5E397 /* AvatarRowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2CDABE21C9405E00B5E397 /* AvatarRowItemView.swift */; }; + 3A2CDAC121C944CD00B5E397 /* FormHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2CDAC021C944CD00B5E397 /* FormHeaderView.swift */; }; + 3A2CDAC321C9648800B5E397 /* DestructiveActionRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2CDAC221C9648800B5E397 /* DestructiveActionRowItem.swift */; }; 3A37416121B58AAA00F212B9 /* ImageUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A37416021B58AAA00F212B9 /* ImageUploader.swift */; }; 3A3FD2831F39E0A000B6958F /* HistoryRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */; }; 3A62B7D81F4CB9D100F45B51 /* BaseMQTTModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */; }; + 3A6D7D3021CA7B4B00E1EF90 /* ContactInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7D2F21CA7B4B00E1EF90 /* ContactInfoViewModel.swift */; }; + 3A6D7D3221CA993300E1EF90 /* AccountSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7D3121CA993300E1EF90 /* AccountSettingsViewModel.swift */; }; + 3A6D7D3421CA996B00E1EF90 /* AccountTimeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7D3321CA996B00E1EF90 /* AccountTimeout.swift */; }; 3A771CAA1F191B38008D968A /* ProfileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A771CA91F191B38008D968A /* ProfileHandler.swift */; }; 3A771CB21F193945008D968A /* UpdateRosterModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A771CB11F193945008D968A /* UpdateRosterModel.swift */; }; 3A8045D21F60C8E200AED866 /* MQTTServiceFriend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045CE1F60C8E200AED866 /* MQTTServiceFriend.swift */; }; @@ -586,8 +595,6 @@ 3AE0A84B1F20321A008A04F3 /* Wheel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A83F1F20321A008A04F3 /* Wheel.swift */; }; 3AE0A84C1F20321A008A04F3 /* WheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A8401F20321A008A04F3 /* WheelItemModel.swift */; }; 3AE0A84D1F20321A008A04F3 /* WheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A8411F20321A008A04F3 /* WheelItemView.swift */; }; - 3AE2F98621B6A8D30068C3BC /* DestructiveActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98421B6A8D30068C3BC /* DestructiveActionTableViewCell.swift */; }; - 3AE2F98721B6A8D30068C3BC /* DestructiveActionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98521B6A8D30068C3BC /* DestructiveActionCellModel.swift */; }; 3AE2F98E21B6B5B30068C3BC /* DeleteAccountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98921B6B5B30068C3BC /* DeleteAccountPresenter.swift */; }; 3AE2F98F21B6B5B30068C3BC /* DeleteAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98A21B6B5B30068C3BC /* DeleteAccountViewController.swift */; }; 3AE2F99021B6B5B30068C3BC /* DeleteAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98B21B6B5B30068C3BC /* DeleteAccountProtocols.swift */; }; @@ -595,6 +602,7 @@ 3AE2F99221B6B5B30068C3BC /* DeleteAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE2F98D21B6B5B30068C3BC /* DeleteAccountWireframe.swift */; }; 3AF4A3DA1EFD7DFA0059B405 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF4A3D91EFD7DFA0059B405 /* StringExtensions.swift */; }; 3AF8E26F1F42E33300D81390 /* ReturnToCallContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF8E26E1F42E33300D81390 /* ReturnToCallContentView.swift */; }; + 3AFBC22A21C8D97C00D0248B /* LinkValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFBC22921C8D97C00D0248B /* LinkValidator.swift */; }; 3CDA490701EC3FEAAC2E9AFE /* TopUpAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498AA2E3A69072FEC336C1ED /* TopUpAccountInteractor.swift */; }; 3D7B572828F83EAFEDA78CEA /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4489153750EAC34408B967C0 /* MapViewController.swift */; }; 40C2631343E285717633ADFA /* LoginPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B051231EAD6BB435200B4C74 /* LoginPresenter.swift */; }; @@ -865,26 +873,7 @@ 5E278E14F45F56BACB71271C /* VideoPreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5541C91FE7845F3E5C7EB2 /* VideoPreviewWireframe.swift */; }; 5E7D5D38218C40B6009B5D8D /* AccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */; }; 5E7D5D3A218C42D0009B5D8D /* AccountSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D39218C42D0009B5D8D /* AccountSettingsInteractor.swift */; }; - 5E7D5D3D218C59F1009B5D8D /* UserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D3C218C59F1009B5D8D /* UserStatus.swift */; }; - 5E7D5D3F218C5A12009B5D8D /* StatusTimeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */; }; - 5E7D5D41218C5A36009B5D8D /* UserContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D40218C5A36009B5D8D /* UserContact.swift */; }; - 5E7D5D43218C5A4C009B5D8D /* UserContactAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */; }; - 5E7D5D45218C5A5D009B5D8D /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D44218C5A5D009B5D8D /* UserAccount.swift */; }; - 5E7D5D47218C5D0A009B5D8D /* SettingsSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */; }; - 5E7D5D4A218C5D42009B5D8D /* SettingsSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */; }; - 5E7D5D4C218C6239009B5D8D /* SettingsSetAvatarTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */; }; - 5E7D5D4E218C645A009B5D8D /* SettingsSetAvatarCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D4D218C645A009B5D8D /* SettingsSetAvatarCellModel.swift */; }; - 5E7D5D50218C6588009B5D8D /* SettingsSelectorTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D4F218C6588009B5D8D /* SettingsSelectorTVCell.swift */; }; - 5E7D5D52218C68BA009B5D8D /* SettingsSelectorCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D51218C68BA009B5D8D /* SettingsSelectorCellModel.swift */; }; - 5E7D5D54218FDD81009B5D8D /* Sizeble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D53218FDD81009B5D8D /* Sizeble.swift */; }; - 5E7D5D56218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D55218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift */; }; - 5E7D5D58218FF473009B5D8D /* MaterialTextFieldCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D57218FF473009B5D8D /* MaterialTextFieldCellModel.swift */; }; - 5E7D5D5A21901BC6009B5D8D /* DescriptionTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5921901BC6009B5D8D /* DescriptionTVCell.swift */; }; - 5E7D5D5C21901D18009B5D8D /* DescriptionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5B21901D18009B5D8D /* DescriptionCellModel.swift */; }; - 5E7D5D5E2190415F009B5D8D /* AddContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5D2190415F009B5D8D /* AddContactCell.swift */; }; - 5E7D5D60219044CB009B5D8D /* AddContactCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */; }; - 5E7D5D6221904E25009B5D8D /* ContactTVCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */; }; - 5E7D5D6421905390009B5D8D /* ContactTVCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */; }; + 5E7D5D3D218C59F1009B5D8D /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D3C218C59F1009B5D8D /* AccountStatus.swift */; }; 5EB13FDBA6153EE67366115F /* ScheduleMessageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */; }; 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD49CF323041B47A752603E /* MapSearchInteractor.swift */; }; 5EDD454F21885ED200C50BC8 /* AccountSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */; }; @@ -981,6 +970,11 @@ 850833DB2037171600587EEF /* FileExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850833DA2037171600587EEF /* FileExtensionView.swift */; }; 85086F4921C64D6D00194361 /* SearchContactResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F4821C64D6D00194361 /* SearchContactResult.swift */; }; 85086F4B21C672FD00194361 /* Alert+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F4A21C672FD00194361 /* Alert+Defaults.swift */; }; + 85086F5021C68B5600194361 /* PhoneNumberContactInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F4F21C68B5600194361 /* PhoneNumberContactInfoViewModel.swift */; }; + 85086F5221C68BD800194361 /* ContactInfoManagementViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F5121C68BD800194361 /* ContactInfoManagementViewModel.swift */; }; + 85086F5421C6AD3600194361 /* PickerRowItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F5321C6AD3600194361 /* PickerRowItemView.swift */; }; + 85086F5621C6B7E700194361 /* DestructiveNynjaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F5521C6B7E700194361 /* DestructiveNynjaButton.swift */; }; + 85086F5821C6CCD400194361 /* PhoneNumberLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85086F5721C6CCD400194361 /* PhoneNumberLabel.swift */; }; 8509452B206E684300B43C1C /* AddParticipantsContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509452A206E684300B43C1C /* AddParticipantsContactCell.swift */; }; 8509AC62206A54420089089B /* ResponseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509AC61206A54420089089B /* ResponseResult.swift */; }; 8509FC7B2158CCA800734D93 /* MessageInteractor+Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509FC7A2158CCA800734D93 /* MessageInteractor+Reply.swift */; }; @@ -1866,8 +1860,8 @@ A4679BAC20B2DD100021FE9C /* SubscribersSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BA120B2DD0F0021FE9C /* SubscribersSelectorViewController.swift */; }; A4679BAD20B2DD100021FE9C /* SubscribersSelectorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BA220B2DD0F0021FE9C /* SubscribersSelectorProtocols.swift */; }; A4679BAE20B2DD100021FE9C /* SubscribersSelectorInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BA420B2DD0F0021FE9C /* SubscribersSelectorInteractor.swift */; }; - A4679BBA20B305360021FE9C /* LinkValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BB720B305360021FE9C /* LinkValidator.swift */; }; - A4679BBB20B305360021FE9C /* LinkField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BB820B305360021FE9C /* LinkField.swift */; }; + A4679BBA20B305360021FE9C /* ChannelLinkValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BB720B305360021FE9C /* ChannelLinkValidator.swift */; }; + A4679BBB20B305360021FE9C /* NynjaLinkField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BB820B305360021FE9C /* NynjaLinkField.swift */; }; A4688DFA20650FF50013660D /* DBObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4688DF920650FF50013660D /* DBObserver.swift */; }; A4688DFC20652DE30013660D /* StorageChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4688DFB20652DE30013660D /* StorageChange.swift */; }; A46C362F2121995800172773 /* DebuggingDetectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46C362E2121995800172773 /* DebuggingDetectorProtocol.swift */; }; @@ -2941,7 +2935,10 @@ 35BAFB8584DC4B34650DE4EB /* Pods-Nynja.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.channels.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.channels.xcconfig"; sourceTree = ""; }; 35F2DA601F73CAD400777920 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerViewController.swift; sourceTree = ""; }; + 3A02381321C8D3A000A143FD /* SocialLinkValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialLinkValidator.swift; sourceTree = ""; }; 3A0281F61F53794800206871 /* UIViewExtenstions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtenstions.swift; sourceTree = ""; }; + 3A06B08D21CB99E400E7964B /* ContactInfoSectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoSectionViewController.swift; sourceTree = ""; }; + 3A06B08F21CBA84500E7964B /* ContactInfoSectionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoSectionItem.swift; sourceTree = ""; }; 3A0A4C5A21B91D9000BA0D09 /* DeleteAccountErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountErrors.swift; sourceTree = ""; }; 3A0A50A821B7FEFE0052D334 /* MyGroupAliasPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyGroupAliasPresenter.swift; sourceTree = ""; }; 3A0A50AA21B7FEFE0052D334 /* MyGroupAliasWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyGroupAliasWireframe.swift; sourceTree = ""; }; @@ -2985,8 +2982,8 @@ 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAvailabilityView.swift; sourceTree = ""; }; 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderUIConfiguration.swift; sourceTree = ""; }; 3A14D83E21AC1F07009CD23A /* NavigationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationError.swift; sourceTree = ""; }; - 3A184D1A21C0FD800083D367 /* UsernameContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameContentViewModel.swift; sourceTree = ""; }; - 3A184D1C21C0FD8C0083D367 /* EmailContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailContentViewModel.swift; sourceTree = ""; }; + 3A184D1A21C0FD800083D367 /* UsernameTextValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameTextValidator.swift; sourceTree = ""; }; + 3A184D1C21C0FD8C0083D367 /* EmailTextValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailTextValidator.swift; sourceTree = ""; }; 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberContentViewModel.swift; sourceTree = ""; }; 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; 3A184D2321C100630083D367 /* SearchInputMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchInputMode.swift; sourceTree = ""; }; @@ -3017,9 +3014,15 @@ 3A2843261EF9314100EFE21A /* Avenir.ttc */ = {isa = PBXFileReference; lastKnownFileType = file; path = Avenir.ttc; sourceTree = ""; }; 3A2A99821EFAD2FB002749B3 /* PageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PageControl.swift; path = PageControl/PageControl.swift; sourceTree = ""; }; 3A2C2DFB21C26708006A53BB /* SearchContactResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactResponse.swift; sourceTree = ""; }; + 3A2CDABE21C9405E00B5E397 /* AvatarRowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarRowItemView.swift; sourceTree = ""; }; + 3A2CDAC021C944CD00B5E397 /* FormHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormHeaderView.swift; sourceTree = ""; }; + 3A2CDAC221C9648800B5E397 /* DestructiveActionRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveActionRowItem.swift; sourceTree = ""; }; 3A37416021B58AAA00F212B9 /* ImageUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploader.swift; sourceTree = ""; }; 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HistoryRequestModel.swift; path = Services/Models/HistoryRequestModel.swift; sourceTree = ""; }; 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BaseMQTTModel.swift; path = Services/Models/BaseMQTTModel.swift; sourceTree = ""; }; + 3A6D7D2F21CA7B4B00E1EF90 /* ContactInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoViewModel.swift; sourceTree = ""; }; + 3A6D7D3121CA993300E1EF90 /* AccountSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewModel.swift; sourceTree = ""; }; + 3A6D7D3321CA996B00E1EF90 /* AccountTimeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTimeout.swift; sourceTree = ""; }; 3A771CA91F191B38008D968A /* ProfileHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProfileHandler.swift; path = Services/HandleServices/ProfileHandler.swift; sourceTree = ""; }; 3A771CB11F193945008D968A /* UpdateRosterModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UpdateRosterModel.swift; path = Services/Models/UpdateRosterModel.swift; sourceTree = ""; }; 3A8045CD1F60C8E200AED866 /* MQTTService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = ""; }; @@ -3058,8 +3061,6 @@ 3AE0A83F1F20321A008A04F3 /* Wheel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wheel.swift; sourceTree = ""; }; 3AE0A8401F20321A008A04F3 /* WheelItemModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WheelItemModel.swift; sourceTree = ""; }; 3AE0A8411F20321A008A04F3 /* WheelItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WheelItemView.swift; sourceTree = ""; }; - 3AE2F98421B6A8D30068C3BC /* DestructiveActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveActionTableViewCell.swift; sourceTree = ""; }; - 3AE2F98521B6A8D30068C3BC /* DestructiveActionCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveActionCellModel.swift; sourceTree = ""; }; 3AE2F98921B6B5B30068C3BC /* DeleteAccountPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountPresenter.swift; sourceTree = ""; }; 3AE2F98A21B6B5B30068C3BC /* DeleteAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountViewController.swift; sourceTree = ""; }; 3AE2F98B21B6B5B30068C3BC /* DeleteAccountProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountProtocols.swift; sourceTree = ""; }; @@ -3067,6 +3068,7 @@ 3AE2F98D21B6B5B30068C3BC /* DeleteAccountWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountWireframe.swift; sourceTree = ""; }; 3AF4A3D91EFD7DFA0059B405 /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 3AF8E26E1F42E33300D81390 /* ReturnToCallContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReturnToCallContentView.swift; sourceTree = ""; }; + 3AFBC22921C8D97C00D0248B /* LinkValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkValidator.swift; sourceTree = ""; }; 3CDE788B1BF51A83EA2F0056 /* QRCodeReaderPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeReaderPresenter.swift; sourceTree = ""; }; 3D6900257B1AB2CA0BC834EB /* HistoryViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = ""; }; 3E600F42D8040D91A16CE3D8 /* Pods-NynjaUnitTests.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.prerelease.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.prerelease.xcconfig"; sourceTree = ""; }; @@ -3304,26 +3306,7 @@ 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsPresenter.swift; sourceTree = ""; }; 5E7D5D39218C42D0009B5D8D /* AccountSettingsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsInteractor.swift; sourceTree = ""; }; - 5E7D5D3C218C59F1009B5D8D /* UserStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatus.swift; sourceTree = ""; }; - 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTimeout.swift; sourceTree = ""; }; - 5E7D5D40218C5A36009B5D8D /* UserContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContact.swift; sourceTree = ""; }; - 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContactAction.swift; sourceTree = ""; }; - 5E7D5D44218C5A5D009B5D8D /* UserAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = ""; }; - 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = ""; }; - 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeaderView.swift; sourceTree = ""; }; - 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSetAvatarTVCell.swift; sourceTree = ""; }; - 5E7D5D4D218C645A009B5D8D /* SettingsSetAvatarCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSetAvatarCellModel.swift; sourceTree = ""; }; - 5E7D5D4F218C6588009B5D8D /* SettingsSelectorTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSelectorTVCell.swift; sourceTree = ""; }; - 5E7D5D51218C68BA009B5D8D /* SettingsSelectorCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSelectorCellModel.swift; sourceTree = ""; }; - 5E7D5D53218FDD81009B5D8D /* Sizeble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizeble.swift; sourceTree = ""; }; - 5E7D5D55218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaterialTextFieldTVCell.swift; sourceTree = ""; }; - 5E7D5D57218FF473009B5D8D /* MaterialTextFieldCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaterialTextFieldCellModel.swift; sourceTree = ""; }; - 5E7D5D5921901BC6009B5D8D /* DescriptionTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionTVCell.swift; sourceTree = ""; }; - 5E7D5D5B21901D18009B5D8D /* DescriptionCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionCellModel.swift; sourceTree = ""; }; - 5E7D5D5D2190415F009B5D8D /* AddContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactCell.swift; sourceTree = ""; }; - 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactCellModel.swift; sourceTree = ""; }; - 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTVCell.swift; sourceTree = ""; }; - 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTVCellModel.swift; sourceTree = ""; }; + 5E7D5D3C218C59F1009B5D8D /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsCoordinator.swift; sourceTree = ""; }; 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsProtocols.swift; sourceTree = ""; }; 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsWireframe.swift; sourceTree = ""; }; @@ -3426,6 +3409,11 @@ 850833DA2037171600587EEF /* FileExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtensionView.swift; sourceTree = ""; }; 85086F4821C64D6D00194361 /* SearchContactResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchContactResult.swift; sourceTree = ""; }; 85086F4A21C672FD00194361 /* Alert+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Alert+Defaults.swift"; sourceTree = ""; }; + 85086F4F21C68B5600194361 /* PhoneNumberContactInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberContactInfoViewModel.swift; sourceTree = ""; }; + 85086F5121C68BD800194361 /* ContactInfoManagementViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoManagementViewModel.swift; sourceTree = ""; }; + 85086F5321C6AD3600194361 /* PickerRowItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerRowItemView.swift; sourceTree = ""; }; + 85086F5521C6B7E700194361 /* DestructiveNynjaButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveNynjaButton.swift; sourceTree = ""; }; + 85086F5721C6CCD400194361 /* PhoneNumberLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberLabel.swift; sourceTree = ""; }; 8509452A206E684300B43C1C /* AddParticipantsContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsContactCell.swift; sourceTree = ""; }; 8509AC61206A54420089089B /* ResponseResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseResult.swift; sourceTree = ""; }; 8509FC7A2158CCA800734D93 /* MessageInteractor+Reply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+Reply.swift"; sourceTree = ""; }; @@ -4140,8 +4128,8 @@ A4679BA120B2DD0F0021FE9C /* SubscribersSelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscribersSelectorViewController.swift; sourceTree = ""; }; A4679BA220B2DD0F0021FE9C /* SubscribersSelectorProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscribersSelectorProtocols.swift; sourceTree = ""; }; A4679BA420B2DD0F0021FE9C /* SubscribersSelectorInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscribersSelectorInteractor.swift; sourceTree = ""; }; - A4679BB720B305360021FE9C /* LinkValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkValidator.swift; sourceTree = ""; }; - A4679BB820B305360021FE9C /* LinkField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkField.swift; sourceTree = ""; }; + A4679BB720B305360021FE9C /* ChannelLinkValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLinkValidator.swift; sourceTree = ""; }; + A4679BB820B305360021FE9C /* NynjaLinkField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NynjaLinkField.swift; sourceTree = ""; }; A4688DF920650FF50013660D /* DBObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBObserver.swift; sourceTree = ""; }; A4688DFB20652DE30013660D /* StorageChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageChange.swift; sourceTree = ""; }; A46C362E2121995800172773 /* DebuggingDetectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingDetectorProtocol.swift; sourceTree = ""; }; @@ -6526,8 +6514,6 @@ 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */, 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */, 3A184D2721C128380083D367 /* TextFieldContentViewModel.swift */, - 3A184D1C21C0FD8C0083D367 /* EmailContentViewModel.swift */, - 3A184D1A21C0FD800083D367 /* UsernameContentViewModel.swift */, ); path = InputView; sourceTree = ""; @@ -6535,6 +6521,8 @@ 3A184D2221C100330083D367 /* Entities */ = { isa = PBXGroup; children = ( + 3A184D1C21C0FD8C0083D367 /* EmailTextValidator.swift */, + 3A184D1A21C0FD800083D367 /* UsernameTextValidator.swift */, 3A184D2321C100630083D367 /* SearchInputMode.swift */, 3A2C2DFB21C26708006A53BB /* SearchContactResponse.swift */, 85086F4821C64D6D00194361 /* SearchContactResult.swift */, @@ -6546,6 +6534,8 @@ isa = PBXGroup; children = ( 3A1A512F21BABE7A00369206 /* ContactInfoInputModel.swift */, + 3AFBC22921C8D97C00D0248B /* LinkValidator.swift */, + 3A02381321C8D3A000A143FD /* SocialLinkValidator.swift */, ); path = Entities; sourceTree = ""; @@ -6848,7 +6838,9 @@ 3AB7400921B9955900D1E967 /* View */ = { isa = PBXGroup; children = ( + 85086F5121C68BD800194361 /* ContactInfoManagementViewModel.swift */, 3AB73FFF21B9954100D1E967 /* ContactInfoManagementViewController.swift */, + 85086F4E21C68AFF00194361 /* InputView */, ); path = View; sourceTree = ""; @@ -8332,28 +8324,6 @@ path = Subviews; sourceTree = ""; }; - 5E7D5D3B218C44A7009B5D8D /* Cells */ = { - isa = PBXGroup; - children = ( - 5E7D5D4B218C6239009B5D8D /* SettingsSetAvatarTVCell.swift */, - 5E7D5D4F218C6588009B5D8D /* SettingsSelectorTVCell.swift */, - 5E7D5D55218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift */, - 5E7D5D5921901BC6009B5D8D /* DescriptionTVCell.swift */, - 5E7D5D5D2190415F009B5D8D /* AddContactCell.swift */, - 5E7D5D6121904E25009B5D8D /* ContactTVCell.swift */, - 3AE2F98421B6A8D30068C3BC /* DestructiveActionTableViewCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; - 5E7D5D48218C5D3A009B5D8D /* Header */ = { - isa = PBXGroup; - children = ( - 5E7D5D49218C5D42009B5D8D /* SettingsSectionHeaderView.swift */, - ); - path = Header; - sourceTree = ""; - }; 5EDD454621885EC400C50BC8 /* Account Flow */ = { isa = PBXGroup; children = ( @@ -8408,9 +8378,8 @@ 5EDD454B21885EC400C50BC8 /* View */ = { isa = PBXGroup; children = ( - 5E7D5D48218C5D3A009B5D8D /* Header */, - 5E7D5D3B218C44A7009B5D8D /* Cells */, 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */, + 3A06B08D21CB99E400E7964B /* ContactInfoSectionViewController.swift */, ); path = View; sourceTree = ""; @@ -8426,20 +8395,11 @@ 5EDD454D21885EC400C50BC8 /* Entities */ = { isa = PBXGroup; children = ( - 5E7D5D44218C5A5D009B5D8D /* UserAccount.swift */, - 5E7D5D3C218C59F1009B5D8D /* UserStatus.swift */, - 5E7D5D3E218C5A12009B5D8D /* StatusTimeout.swift */, - 5E7D5D40218C5A36009B5D8D /* UserContact.swift */, - 5E7D5D42218C5A4C009B5D8D /* UserContactAction.swift */, - 5E7D5D46218C5D0A009B5D8D /* SettingsSectionHeader.swift */, - 5E7D5D4D218C645A009B5D8D /* SettingsSetAvatarCellModel.swift */, - 5E7D5D51218C68BA009B5D8D /* SettingsSelectorCellModel.swift */, - 5E7D5D53218FDD81009B5D8D /* Sizeble.swift */, - 5E7D5D57218FF473009B5D8D /* MaterialTextFieldCellModel.swift */, - 5E7D5D5B21901D18009B5D8D /* DescriptionCellModel.swift */, - 5E7D5D5F219044CB009B5D8D /* AddContactCellModel.swift */, - 5E7D5D6321905390009B5D8D /* ContactTVCellModel.swift */, - 3AE2F98521B6A8D30068C3BC /* DestructiveActionCellModel.swift */, + 5E7D5D3C218C59F1009B5D8D /* AccountStatus.swift */, + 3A6D7D3321CA996B00E1EF90 /* AccountTimeout.swift */, + 3A6D7D3121CA993300E1EF90 /* AccountSettingsViewModel.swift */, + 3A06B08F21CBA84500E7964B /* ContactInfoSectionItem.swift */, + 3A6D7D2F21CA7B4B00E1EF90 /* ContactInfoViewModel.swift */, ); path = Entities; sourceTree = ""; @@ -9115,6 +9075,14 @@ path = UserSettings; sourceTree = ""; }; + 85086F4E21C68AFF00194361 /* InputView */ = { + isa = PBXGroup; + children = ( + 85086F4F21C68B5600194361 /* PhoneNumberContactInfoViewModel.swift */, + ); + path = InputView; + sourceTree = ""; + }; 8509547D20481AF900905B46 /* ThemePicker */ = { isa = PBXGroup; children = ( @@ -9584,6 +9552,7 @@ isa = PBXGroup; children = ( 853567BA21A6B00100AAEEF9 /* Form.swift */, + 3A2CDAC021C944CD00B5E397 /* FormHeaderView.swift */, 8542FBF421A6EDD400CC295B /* FieldRowItem */, 8542FBF521A6EDE800CC295B /* Items */, ); @@ -9911,9 +9880,12 @@ isa = PBXGroup; children = ( 851452B521A5A2E100DF10A6 /* ActionRowItemView.swift */, + 3A2CDAC221C9648800B5E397 /* DestructiveActionRowItem.swift */, + 3A2CDABE21C9405E00B5E397 /* AvatarRowItemView.swift */, 852037E521A5AD4A0085CF1F /* TextRowItemView.swift */, - 852037E721A5B1E00085CF1F /* SwitchRowItemView.swift */, 852037E921A5B4230085CF1F /* TextFieldRowItemView.swift */, + 85086F5321C6AD3600194361 /* PickerRowItemView.swift */, + 852037E721A5B1E00085CF1F /* SwitchRowItemView.swift */, ); path = Items; sourceTree = ""; @@ -10559,6 +10531,7 @@ 850B9D9E219C131E00EA0CF4 /* AuthResponse.swift */, 856A8EFB219C8D7A0004E11E /* AuthenticationType.swift */, 850B9DAA219C6EE800EA0CF4 /* PhoneNumberInfo.swift */, + 85086F5721C6CCD400194361 /* PhoneNumberLabel.swift */, ); path = Entities; sourceTree = ""; @@ -11170,6 +11143,7 @@ 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */, 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */, 8542FBF221A6ECC100CC295B /* NynjaSwitch.swift */, + 85086F5521C6B7E700194361 /* DestructiveNynjaButton.swift */, ); path = NynjaButton; sourceTree = ""; @@ -12255,8 +12229,8 @@ A4679BB620B305360021FE9C /* LinkField */ = { isa = PBXGroup; children = ( - A4679BB720B305360021FE9C /* LinkValidator.swift */, - A4679BB820B305360021FE9C /* LinkField.swift */, + A4679BB720B305360021FE9C /* ChannelLinkValidator.swift */, + A4679BB820B305360021FE9C /* NynjaLinkField.swift */, ); path = LinkField; sourceTree = ""; @@ -16484,6 +16458,7 @@ 859B862C204820DC003272B2 /* ThemePickerPresenter.swift in Sources */, 8509FC872158F7FC00734D93 /* DirectoryWatcher.swift in Sources */, 3AE2F99221B6B5B30068C3BC /* DeleteAccountWireframe.swift in Sources */, + 85086F5821C6CCD400194361 /* PhoneNumberLabel.swift in Sources */, 2603139B20A0A4BA009AC66D /* LanguageSelectorViewController.swift in Sources */, 2686D3201FC3E39C0079CB75 /* ContentNavigationVC.swift in Sources */, B7EF8EDB210C759400E0E981 /* InterpretationTypeCellModel.swift in Sources */, @@ -16558,7 +16533,6 @@ 3A3FD2831F39E0A000B6958F /* HistoryRequestModel.swift in Sources */, A42D51BC206A361400EEB952 /* error2.swift in Sources */, 8ED0F3BF1FBC5CB1004916AB /* DialogCellModel.swift in Sources */, - 5E7D5D6421905390009B5D8D /* ContactTVCellModel.swift in Sources */, 8502DB552061030100613C8C /* WheelPositionPickerInteractor.swift in Sources */, A4330A652109DFA00060BD93 /* UserInfo.swift in Sources */, F10B0E2120B4CF3800528E7A /* CameraCoordinator.swift in Sources */, @@ -16600,7 +16574,6 @@ FEA656002167777F00B44029 /* WalletBalancesWireFrame.swift in Sources */, FEA656062167777F00B44029 /* WalletBalancesViewModel.swift in Sources */, 4B3B35D2217111BC005A214A /* SharedServiceFactory.swift in Sources */, - 5E7D5D52218C68BA009B5D8D /* SettingsSelectorCellModel.swift in Sources */, 26131E02210399BA00BE94F9 /* TranscribeService.swift in Sources */, 853FB0752049B4FF000996C5 /* TextTableViewCell.swift in Sources */, A42D52DC206A53AB00EEB952 /* process_Spec.swift in Sources */, @@ -16687,6 +16660,7 @@ 26245F3E204EF58E00C8D3DD /* BasePresenterProtocol.swift in Sources */, 852037EA21A5B4230085CF1F /* TextFieldRowItemView.swift in Sources */, 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */, + 85086F5021C68B5600194361 /* PhoneNumberContactInfoViewModel.swift in Sources */, 5BC1D37B20D3B4A8002A44B3 /* GroupAddParticipantsCollectionViewCell.swift in Sources */, 269D9DF01FC3AF0D00324263 /* CGSizeExtension.swift in Sources */, 4BB0EFBD2151347900704136 /* AlertManager.swift in Sources */, @@ -16755,6 +16729,7 @@ 26142B1120472ECD004E5FE4 /* MessageLinkTable.swift in Sources */, A4679BAC20B2DD100021FE9C /* SubscribersSelectorViewController.swift in Sources */, 8514F17320EA219E00883513 /* ContextMenuConfiguration+Favorites.swift in Sources */, + 3A06B09021CBA84500E7964B /* ContactInfoSectionItem.swift in Sources */, A42D52BF206A53AA00EEB952 /* push_Spec.swift in Sources */, 3A1D03051F0BD93A005F5F18 /* LocationService.swift in Sources */, 26441A121F9FBF9300E724B5 /* MainViewController+NavigateProtocol.swift in Sources */, @@ -16778,6 +16753,7 @@ 26DCB2522064BA46001EF0AB /* ContactsInteractor.swift in Sources */, B767F48F215D1E0A00FA9B27 /* ComingSoonExtension.swift in Sources */, FBCE841120E525A6003B7558 /* URLSessionNetworkClient.swift in Sources */, + 85086F5221C68BD800194361 /* ContactInfoManagementViewModel.swift in Sources */, C4AE8B6EFD76A8C6ADF51422 /* TutorialProtocols.swift in Sources */, 3A213F7C1F0093F0006DBE91 /* LoginView.swift in Sources */, A43B259720AB1DFA00FF8107 /* RecordingInputContent.swift in Sources */, @@ -16789,7 +16765,6 @@ 35F2DA611F73CAD400777920 /* NotificationManager.swift in Sources */, 4B86C562219C12840006A192 /* DAO.swift in Sources */, 26C1A3ED2031D3030009F7F0 /* OtherUserContainerViewController.swift in Sources */, - 3AE2F98621B6A8D30068C3BC /* DestructiveActionTableViewCell.swift in Sources */, FB16E79920EFAFA8009FA203 /* Money.swift in Sources */, BA982E458F95A7A5AB4A8A73 /* TutorialViewController.swift in Sources */, B723C625204D86AF00884FFD /* SettingsDataAndStoragePresenter.swift in Sources */, @@ -16868,7 +16843,6 @@ 6D36F8E71F0BBFC300FA1AC8 /* ContactManager.swift in Sources */, 32868DD51F31CADF0028B260 /* ChatsListProtocols.swift in Sources */, 851FFA68219EAFBF0015F073 /* Validator.swift in Sources */, - 5E7D5D5E2190415F009B5D8D /* AddContactCell.swift in Sources */, A49CC1D220E4A9C000879D41 /* InputBar+DisplayMode.swift in Sources */, 851FFA6A219EB29A0015F073 /* PhoneNumberTextController.swift in Sources */, A42D51B3206A361400EEB952 /* Room.swift in Sources */, @@ -16888,16 +16862,17 @@ A408A0BC20C174040029F54B /* ChannelsListProtocols.swift in Sources */, 3A2374D91F262A1600701045 /* ContactHandler.swift in Sources */, 5EEB73AA215D406400D8ECE6 /* AuthCoordinator.swift in Sources */, - 5E7D5D41218C5A36009B5D8D /* UserContact.swift in Sources */, 6D5168A21F30430900DA3728 /* SpeakerView.swift in Sources */, 8503B529205046A6006F0593 /* NotificationSettingsWireFrame.swift in Sources */, FBD885782147F9640099B8C3 /* FontsConstants.swift in Sources */, 263D662D1FE8D03400A509F8 /* TypingModel.swift in Sources */, 853D0F9020C00806008C3684 /* StickerPreviewState.swift in Sources */, A42D51BE206A361400EEB952 /* Vox.swift in Sources */, + 3A02381421C8D3A000A143FD /* SocialLinkValidator.swift in Sources */, E77764C21FBDA9BD0042541D /* ImageWheelItemModel.swift in Sources */, 857A06612035E3360097C49B /* ForwardAvatarCollectionViewCell.swift in Sources */, E74FD69F1FC5DEAA00656611 /* DBContact.swift in Sources */, + 3A6D7D3221CA993300E1EF90 /* AccountSettingsViewModel.swift in Sources */, 0062D93C2062EC4100B915AC /* InviteFriendsPresenter.swift in Sources */, 0008E9052031E642003E316E /* UIEdgeInsets+Adjust.swift in Sources */, A4F3DAB5208494E300FF71C7 /* UIViewController+SafeArea.swift in Sources */, @@ -16912,7 +16887,6 @@ 857C070620DB8A3D00626EEB /* StickerInputState.swift in Sources */, 3AE2F98E21B6B5B30068C3BC /* DeleteAccountPresenter.swift in Sources */, A4B544EF20EFB4DF00EB7B0F /* BertTupleExtension.swift in Sources */, - 5E7D5D43218C5A4C009B5D8D /* UserContactAction.swift in Sources */, 8502DB512061030100613C8C /* WheelPositionPickerProtocols.swift in Sources */, 00F7B3402029DD6200E443E1 /* TextItemView.swift in Sources */, E77764BE1FBDA9B60042541D /* ImageWheelItemView.swift in Sources */, @@ -16960,7 +16934,6 @@ 4B5A714D204F069000A551F5 /* ChatService.swift in Sources */, 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */, 2603139420A0A4B9009AC66D /* LanguageSelectorPresenter.swift in Sources */, - 5E7D5D6221904E25009B5D8D /* ContactTVCell.swift in Sources */, 26E476591FFEE2D400C06C05 /* Modelka.swift in Sources */, FEA655DF2167777E00B44029 /* WalletDetailsViewInput.swift in Sources */, 4B749F07214FEE4F002F3A33 /* VerifyNumberProtocols.swift in Sources */, @@ -17017,7 +16990,6 @@ A42D51C9206A361400EEB952 /* writer.swift in Sources */, 2648C3E62069B49000863614 /* UITextField+Extension.swift in Sources */, A432CF1520B4347D00993AFB /* FloatingPlaceholderContainer.swift in Sources */, - 5E7D5D56218FEAC7009B5D8D /* MaterialTextFieldTVCell.swift in Sources */, E70F78BB1FD6CB5600385565 /* DBChatCheckpoint.swift in Sources */, 263C04E92132E2FF00B8F0BE /* WrappedTaskOperation.swift in Sources */, FEA655D92167777E00B44029 /* CreateWalletParams.swift in Sources */, @@ -17082,7 +17054,7 @@ A42D51A9206A361400EEB952 /* Service.swift in Sources */, 26245F41204EF58E00C8D3DD /* BaseInteractor.swift in Sources */, F11786CD20A8E4FD007A9A1B /* CameraVideoPreviewInteractor.swift in Sources */, - A4679BBA20B305360021FE9C /* LinkValidator.swift in Sources */, + A4679BBA20B305360021FE9C /* ChannelLinkValidator.swift in Sources */, 4BEE89D69CACB85ABEE9046F /* QRCodeGeneratorPresenter.swift in Sources */, 5EEB73B621604CF600D8ECE6 /* CodeConfirmationWireframe.swift in Sources */, 2605311B212740FD002E1CF1 /* LogOutputProtocols.swift in Sources */, @@ -17146,6 +17118,7 @@ B7EF8ED9210C71E800E0E981 /* InterpretationType.swift in Sources */, 6D5157D21F30B822002A27DB /* MicrophoneView.swift in Sources */, 5EEB73D22161CEA100D8ECE6 /* LoginFlow.swift in Sources */, + 3AFBC22A21C8D97C00D0248B /* LinkValidator.swift in Sources */, 260313AF20A0A50D009AC66D /* TranslationService.swift in Sources */, A42D51AD206A361400EEB952 /* cur.swift in Sources */, 8504DEAB206937A2006722AC /* MediaFullWheelItemView.swift in Sources */, @@ -17155,6 +17128,7 @@ 26541F722007B93400AAEACF /* DBMessageAction.swift in Sources */, 264638251FFFE78E002590E6 /* RepliesWireFrame.swift in Sources */, 005886CB2030F3F900FE2E89 /* NynjaTimeMinsDelegate.swift in Sources */, + 3A6D7D3021CA7B4B00E1EF90 /* ContactInfoViewModel.swift in Sources */, 0F409A888929B0CC8EDF6656 /* QRCodeReaderPresenter.swift in Sources */, 2646381E1FFFC5CB002590E6 /* RepliesProtocols.swift in Sources */, 3A0AEA6D21AFF3FE0066CBBA /* ProfileMockProvider.swift in Sources */, @@ -17240,7 +17214,6 @@ 267BE90C2069405200153FB8 /* StarMessageDAOProtocol.swift in Sources */, 8E55172E200D095B00C12B5D /* UserGroupRulesVC.swift in Sources */, A4B544EA20EFB1A800EB7B0F /* errors_Spec.swift in Sources */, - 5E7D5D45218C5A5D009B5D8D /* UserAccount.swift in Sources */, 4BDC7E63203494C000BCD381 /* ScheduleButton.swift in Sources */, FEA655E12167777E00B44029 /* SeedBackupWalletProtocols.swift in Sources */, 00E98250205C2668008BF03D /* SessionHeaderView.swift in Sources */, @@ -17400,7 +17373,6 @@ A42D51A2206A361400EEB952 /* Desc.swift in Sources */, 3A184D2421C100630083D367 /* SearchInputMode.swift in Sources */, C940514B204C7FAF00D72B04 /* DataAndStorageProtocols.swift in Sources */, - 5E7D5D5C21901D18009B5D8D /* DescriptionCellModel.swift in Sources */, F105C69C209F71BF0091786A /* CameraPresenter.swift in Sources */, 5BC1D37920D3B4A8002A44B3 /* GroupCollectionViewCell.swift in Sources */, 3A0A50D921B7FEFE0052D334 /* GroupInputProtocols.swift in Sources */, @@ -17418,10 +17390,12 @@ FE9E70D021175DDC0034067A /* ChatScreenAlertFactory.swift in Sources */, 4B7C73FB215A5522007924DB /* UILabel+Debug.swift in Sources */, A4B544FF20EFC1BA00EB7B0F /* StatusCode.swift in Sources */, + 3A2CDAC121C944CD00B5E397 /* FormHeaderView.swift in Sources */, 001169B5201A0B02001B435F /* MapSearchCell.swift in Sources */, 26342CB420ECFAB600D2196B /* MessageInteractor+Transcription.swift in Sources */, 8514DE892136A50100718DD8 /* DBStarAction.swift in Sources */, 00102F3E202C8E3A00A877A9 /* NynjaTimeControl.swift in Sources */, + 3A6D7D3421CA996B00E1EF90 /* AccountTimeout.swift in Sources */, 267BE2941FDEA24000C47E18 /* SettingsGroupDS.swift in Sources */, 260629712056EF2800CB8F65 /* LinksCell.swift in Sources */, F1EED41520C57C30001060C4 /* PhotoPreviewSource.swift in Sources */, @@ -17449,7 +17423,6 @@ 850B9DAD219C7ADA00EA0CF4 /* PlainLoginOption.swift in Sources */, A4679B8920B2DA550021FE9C /* Array+ChannelSubscriber.swift in Sources */, A4ED79AC20C7056C00A41F67 /* AllChannelsItemsFactory.swift in Sources */, - 5E7D5D4C218C6239009B5D8D /* SettingsSetAvatarTVCell.swift in Sources */, E707C4AF1FA0F6E700B86137 /* ProfileActionCell.swift in Sources */, 3A0A50D321B7FEFE0052D334 /* CellWithImageCellModel.swift in Sources */, 4BFED75A21A6BE49003CF1B3 /* CLLocation+GPSMetadata.swift in Sources */, @@ -17465,7 +17438,6 @@ FEA655D02167777E00B44029 /* SeedVerificationWalletInteractor.swift in Sources */, 3A0A50DC21B7FEFE0052D334 /* EditGroupNameWireframe.swift in Sources */, 8520040B20D4FB06007C0036 /* ReplyInfoView.swift in Sources */, - 5E7D5D47218C5D0A009B5D8D /* SettingsSectionHeader.swift in Sources */, E77D58991F98B94E00FBE926 /* ProfileTablewViewDS.swift in Sources */, E72906E72011156B007C5C5B /* UITableViewExtensions.swift in Sources */, 2603139320A0A4B9009AC66D /* LanguageSelectorProtocols.swift in Sources */, @@ -17503,7 +17475,7 @@ 4BF090CC21635FDC00DCCA5C /* Message+Type.swift in Sources */, 850C3025204DAC1000DB26C2 /* PrivacyListItemsFactory.swift in Sources */, 853D0F9520C0109F008C3684 /* StickerSearchResultView.swift in Sources */, - A4679BBB20B305360021FE9C /* LinkField.swift in Sources */, + A4679BBB20B305360021FE9C /* NynjaLinkField.swift in Sources */, 3A237BC91F30AB0F00C42B6E /* EditProfileVC.swift in Sources */, A406E39A210B457300435B3E /* DictionaryExtension.swift in Sources */, 2661D1331F373D5900F3E125 /* WheelConfiguration.swift in Sources */, @@ -17513,7 +17485,6 @@ A42CE5AD20692EDB000889CC /* StringAtom.swift in Sources */, A4CB1520210372DF00C3B68B /* JDMechanism.swift in Sources */, 8E23E0882006853000A59B8C /* GroupVideosListVC.swift in Sources */, - 5E7D5D60219044CB009B5D8D /* AddContactCellModel.swift in Sources */, A42D52C7206A53AA00EEB952 /* iter_Spec.swift in Sources */, E72AE64F1F8E3CCB006417D0 /* GradientButton.swift in Sources */, A43B25DE20AB1F5C00FF8107 /* RawRepresentable+Localized.swift in Sources */, @@ -17565,9 +17536,9 @@ 850B9DA6219C2B9500EA0CF4 /* AppConfigurationProvider.swift in Sources */, 267BE2AD1FE13AB600C47E18 /* ParticipantsProtocols.swift in Sources */, 268C341121067F1D00F1472A /* AudioLongTranscribeOperation.swift in Sources */, + 85086F5621C6B7E700194361 /* DestructiveNynjaButton.swift in Sources */, A4ED79B020C8041500A41F67 /* TableViewDataSourceProxy.swift in Sources */, 26E0C44721469E9800A58ECD /* ConnectionService.swift in Sources */, - 3AE2F98721B6A8D30068C3BC /* DestructiveActionCellModel.swift in Sources */, A45F113E20B4218D00F45004 /* MessageInteractor+Utils.swift in Sources */, A4CB153521038A7A00C3B68B /* UIDevice+Jailbreak.swift in Sources */, 3AE0A84C1F20321A008A04F3 /* WheelItemModel.swift in Sources */, @@ -17599,7 +17570,7 @@ B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */, A42D51CD206A361400EEB952 /* operation.swift in Sources */, 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, - 3A184D1D21C0FD8C0083D367 /* EmailContentViewModel.swift in Sources */, + 3A184D1D21C0FD8C0083D367 /* EmailTextValidator.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */, F10AFEBC20F7B1D200C7CE83 /* WheelPreviewFactory.swift in Sources */, @@ -17618,6 +17589,7 @@ 850A2E94219EF9B800C784D9 /* AlertDisplayable.swift in Sources */, B7B546B3210DD1EC002DCA55 /* AlertTextFieldViewController.swift in Sources */, FEA655EB2167777E00B44029 /* TransferHistoryTableDataSource.swift in Sources */, + 3A2CDAC321C9648800B5E397 /* DestructiveActionRowItem.swift in Sources */, F117871220ACF018007A9A1B /* CameraSettingsProtocols.swift in Sources */, 8503B526205046A6006F0593 /* NotificationSettingsViewController.swift in Sources */, 8580BAC820BD983400239D9D /* MentionCounterInteractive.swift in Sources */, @@ -17776,6 +17748,7 @@ A4679B8820B2DA550021FE9C /* ArrayExtension.swift in Sources */, 4B058F03204EA928004C7D9F /* DAOProtocol.swift in Sources */, A4B544E620EFAECE00EB7B0F /* LinkModel.swift in Sources */, + 3A06B08E21CB99E400E7964B /* ContactInfoSectionViewController.swift in Sources */, 26B32B961FE20BAB00888A0A /* DescExtension+BERT.swift in Sources */, 4B1D7E0B2029D8CD00703228 /* GroupOptionsItemsFactory.swift in Sources */, CCF8AA193F15D4191EC99051 /* SplashProtocols.swift in Sources */, @@ -17811,7 +17784,6 @@ 4B2C503921B573A100FBA9B1 /* RemoveP2pAndMucTables.swift in Sources */, F117871420ACF018007A9A1B /* CameraSettingsWireframe.swift in Sources */, E7E6E3DE1FB2F37900401D9E /* ParticipantsDelegate.swift in Sources */, - 5E7D5D54218FDD81009B5D8D /* Sizeble.swift in Sources */, A42D51AA206A361400EEB952 /* error.swift in Sources */, 628E2C26BE0854DB1DF64990 /* SplashWireframe.swift in Sources */, B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */, @@ -17868,7 +17840,6 @@ C940515B204C99C100D72B04 /* DataAndStorageTableDelegate.swift in Sources */, E09CECE79892CABEF8793389 /* ImagePreviewInteractor.swift in Sources */, 2603139820A0A4B9009AC66D /* LangCellViewModel.swift in Sources */, - 5E7D5D4E218C645A009B5D8D /* SettingsSetAvatarCellModel.swift in Sources */, A4BCEC6C20DBF2A40078B076 /* Link+DB.swift in Sources */, B79FA02B2107731400F286BF /* MarketplacePresenter.swift in Sources */, 0008E9282036F480003E316E /* ScheduledMessage.swift in Sources */, @@ -17890,7 +17861,6 @@ F105C6BC20A1347E0091786A /* PhotoPreviewWireframeProtocol.swift in Sources */, A43B25A620AB1DFA00FF8107 /* RecordingAudioWaveform.swift in Sources */, 26C1A3EB2031AAD20009F7F0 /* OtherUserInteractor.swift in Sources */, - 5E7D5D3F218C5A12009B5D8D /* StatusTimeout.swift in Sources */, 26D8317520EA65200067C5B4 /* TranslationInfo.swift in Sources */, 850EE2B021A75E270051F873 /* SelectCountryHeaderView.swift in Sources */, A4868F3A2121E22C001F624E /* AntiDebuggingService.swift in Sources */, @@ -17991,7 +17961,7 @@ 85D66A1020BD965300FBD803 /* MentionPanelView.swift in Sources */, 4B7C73F1215A5509007924DB /* SMSCodeProviding.swift in Sources */, 45F60C4B14438C65076457AB /* EditUsernameProtocols.swift in Sources */, - 3A184D1B21C0FD800083D367 /* UsernameContentViewModel.swift in Sources */, + 3A184D1B21C0FD800083D367 /* UsernameTextValidator.swift in Sources */, 00F7B33E2029DD4B00E443E1 /* AudioItemView.swift in Sources */, 4B7C73F9215A5522007924DB /* DebugLogs.swift in Sources */, 0062D9412062EC4100B915AC /* InviteFriendsSelectionCell.swift in Sources */, @@ -18027,7 +17997,6 @@ 260313A220A0A4BA009AC66D /* ActionCell.swift in Sources */, 26E7D04A1FCB8973001C69B7 /* Amazon+FileSync.swift in Sources */, E70938411FBEE488006CCDC6 /* TableDefinitionExtension.swift in Sources */, - 5E7D5D58218FF473009B5D8D /* MaterialTextFieldCellModel.swift in Sources */, 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, @@ -18083,6 +18052,7 @@ 4B06D30520287060003B275B /* WCDataManager.swift in Sources */, FB3FE46120AC1C4400F2B847 /* ImagePreviewTransitionProtocols.swift in Sources */, FEA655CC2167777E00B44029 /* SeedVerificationWalletWireFrame.swift in Sources */, + 3A2CDABF21C9405E00B5E397 /* AvatarRowItemView.swift in Sources */, DE89BF12597D1B7D5BB68AA3 /* TopUpAccountWireframe.swift in Sources */, E6BAC896CF1531C489B1549C /* AddParticipantsProtocols.swift in Sources */, 8596CEF42048A98E006FC65D /* Theme.swift in Sources */, @@ -18091,7 +18061,6 @@ 04EDA50C90C7EBD46AA1FCB2 /* AddParticipantsViewController.swift in Sources */, B750EF022046B24D00A99F9C /* TransferInfo.swift in Sources */, 859B862E204820DC003272B2 /* ThemePickerProtocols.swift in Sources */, - 5E7D5D4A218C5D42009B5D8D /* SettingsSectionHeaderView.swift in Sources */, 1D1D5634D125333796D14E10 /* AddParticipantsPresenter.swift in Sources */, 26EA201320BECDA600FBB9CA /* ConversationLanguageSettingServiceProtocol.swift in Sources */, A46C362F2121995800172773 /* DebuggingDetectorProtocol.swift in Sources */, @@ -18171,9 +18140,9 @@ 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */, 85D669E620BD956000FBD803 /* UIView+Shadow.swift in Sources */, 896D51F07E2F79C8B5502DBF /* EditGroupPhotoInteractor.swift in Sources */, - 5E7D5D50218C6588009B5D8D /* SettingsSelectorTVCell.swift in Sources */, A4ED79AA20C704F500A41F67 /* MyChannelsItemsFactory.swift in Sources */, 5BC1D38420D3B670002A44B3 /* CallCreatorMediator.swift in Sources */, + 85086F5421C6AD3600194361 /* PickerRowItemView.swift in Sources */, B7EF8ED2210C502D00E0E981 /* InterpretationTypeTableDelegate.swift in Sources */, 1325429A6216D23E2E67B6B7 /* EditGroupPhotoWireframe.swift in Sources */, 2603139720A0A4B9009AC66D /* LangCell.swift in Sources */, @@ -18241,7 +18210,7 @@ 8514F17A20EA219F00883513 /* ContextMenuArrowView.swift in Sources */, B3D0F59E1E7BDB7E485AE662 /* GroupStorageWireframe.swift in Sources */, A45F114120B4218D00F45004 /* MessageInteractor+StorageSubscriber.swift in Sources */, - 5E7D5D3D218C59F1009B5D8D /* UserStatus.swift in Sources */, + 5E7D5D3D218C59F1009B5D8D /* AccountStatus.swift in Sources */, 26ABCA3E21189DA400EA4782 /* Aps.swift in Sources */, 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */, 260313AB20A0A4BA009AC66D /* ChatLanguageSettingsInteractor.swift in Sources */, @@ -18249,7 +18218,6 @@ 8596CEF22048A763006FC65D /* ThemeCellModel.swift in Sources */, 4B030F3B2195CF8100F293B7 /* Host.swift in Sources */, 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */, - 5E7D5D5A21901BC6009B5D8D /* DescriptionTVCell.swift in Sources */, A42D52DB206A53AB00EEB952 /* messageEvent_Spec.swift in Sources */, 4B749F08214FEE4F002F3A33 /* VerifyNumberInteractor.swift in Sources */, A42D51CA206A361400EEB952 /* ExtendedStar.swift in Sources */, diff --git a/Nynja/DB/Models/DBAccount.swift b/Nynja/DB/Models/DBAccount.swift index 7009b717f..a9e816c28 100644 --- a/Nynja/DB/Models/DBAccount.swift +++ b/Nynja/DB/Models/DBAccount.swift @@ -149,21 +149,22 @@ final class DBAccount: Record, DBModel { // MARK: - Query + static func account(from db: Database, rowID: Int64) throws -> DBAccount? { + return try DBAccount.filter(Column.rowID == rowID).fetchOneConstructed(db) + } + static func account(from db: Database, accountId: String) throws -> DBAccount? { let accountIdColumn = Column(AccountTable.Column.accountId.title) - - let account = try DBAccount.filter(accountIdColumn == accountId).fetchOne(db) - try account?.construct(db) - - return account + return try DBAccount.filter(accountIdColumn == accountId).fetchOneConstructed(db) + } + + static func account(from db: Database, qrCode: String) throws -> DBAccount? { + let qrCodeColumn = Column(AccountTable.Column.qrCode.title) + return try DBAccount.filter(qrCodeColumn == qrCode).fetchOneConstructed(db) } static func accounts(from db: Database, profileId: String) throws -> [DBAccount] { let profileIdColumn = Column(AccountTable.Column.profileId.title) - - let accounts = try DBAccount.filter(profileIdColumn == profileId).fetchAll(db) - try accounts.forEach { try $0.construct(db) } - - return accounts + return try DBAccount.filter(profileIdColumn == profileId).fetchAllConstructed(db) } } diff --git a/Nynja/DBObserver.swift b/Nynja/DBObserver.swift index 995463616..4041d249a 100644 --- a/Nynja/DBObserver.swift +++ b/Nynja/DBObserver.swift @@ -21,6 +21,11 @@ final class DBObserver: StorageObserver, TransactionObserver { qos: .utility) + // MARK: - Dependencies + + private let accountDAO: AccountDAOProtocol = AccountDAO(dbManager: StorageService.sharedInstance) + + // MARK: - Properties var subscribers: [SubscribeType: [StorageSubscriberReference]] = [:] @@ -40,12 +45,16 @@ final class DBObserver: StorageObserver, TransactionObserver { StarTable.self, ProfileTable.self, RecentStickerTable.self, - StickerPackTable.self + StickerPackTable.self, + AccountTable.self, + ContactInfoTable.self ] return tables.map { $0.name } }() + // MARK: - TransactionObserver + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { switch eventKind.tableName { case ContactTable.name: @@ -95,7 +104,7 @@ final class DBObserver: StorageObserver, TransactionObserver { } private func handleDidCommit(_ db: Database) { - allChanges.forEach { (tableName, changes) in + allChanges.forEach { tableName, changes in switch tableName { case JobTable.name: handleJobDidCommit(changes: changes) @@ -117,6 +126,10 @@ final class DBObserver: StorageObserver, TransactionObserver { handleRecentStickerDidCommit(changes: changes) case StickerPackTable.name: handleStickerPackDidCommit(changes: changes) + case AccountTable.name: + handleAccountDidCommit(changes: changes) + case ContactInfoTable.name: + handleContactInfoDidCommit(changes: changes) default: break } @@ -271,6 +284,29 @@ final class DBObserver: StorageObserver, TransactionObserver { notify(with: storageChanges, type: .stickerPack(nil)) } + private func handleAccountDidCommit(changes: [ChangeInfo]) { + var storageChanges: [StorageChange] = [] + + changes.forEach { info in + let change = storageChange(from: info) + storageChanges.append(change) + + let account = changedValue(from: info) as? DBAccount + notify(with: info.event, entity: [change], type: .account(account?.accountId)) + } + + notify(with: storageChanges, type: .account(nil)) + } + + private func handleContactInfoDidCommit(changes: [ChangeInfo]) { + var storageChanges: [StorageChange] = [] + changes.forEach { info in + let change = storageChange(from: info) + storageChanges.append(change) + } + notify(with: storageChanges, type: .contactInfo) + } + func databaseDidRollback(_ db: Database) { processingQueue.async { self.clear() @@ -306,6 +342,10 @@ final class DBObserver: StorageObserver, TransactionObserver { value = StickerPackDAO.fetchRecentSticker(rowId: event.rowID) case StickerPackTable.name: value = StickerPackDAO.fetchStickerPack(rowId: event.rowID) + case AccountTable.name: + value = accountDAO.fetchAccount(by: event.rowID) + case ContactInfoTable.name: + value = accountDAO.fetchContactInfo(by: event.rowID) default: break } diff --git a/Nynja/Generated/AssetsConstants.swift b/Nynja/Generated/AssetsConstants.swift index ae213dc02..5585cf2b4 100644 --- a/Nynja/Generated/AssetsConstants.swift +++ b/Nynja/Generated/AssetsConstants.swift @@ -142,6 +142,8 @@ internal extension Image { static var iconsGeneralIcGoogle: ImageAsset { return ImageAsset(name: "Icons_General_ic_google") } /// "Icons_General_ic_grey_close" static var iconsGeneralIcGreyClose: ImageAsset { return ImageAsset(name: "Icons_General_ic_grey_close") } + /// "Icons_General_ic_mail" + static var iconsGeneralIcMail: ImageAsset { return ImageAsset(name: "Icons_General_ic_mail") } /// "Icons_General_ic_translate" static var iconsGeneralIcTranslate: ImageAsset { return ImageAsset(name: "Icons_General_ic_translate") } enum LastMessageType { @@ -714,6 +716,8 @@ internal extension Image { static var icEmptyAvatar: ImageAsset { return ImageAsset(name: "ic_empty_avatar") } /// "ic_facebook" static var icFacebook: ImageAsset { return ImageAsset(name: "ic_facebook") } + /// "ic_fb" + static var icFb: ImageAsset { return ImageAsset(name: "ic_fb") } /// "ic_flashlight_auto" static var icFlashlightAuto: ImageAsset { return ImageAsset(name: "ic_flashlight_auto") } /// "ic_flashlight_off" @@ -770,6 +774,8 @@ internal extension Image { static var icStar: ImageAsset { return ImageAsset(name: "ic_star") } /// "ic_style" static var icStyle: ImageAsset { return ImageAsset(name: "ic_style") } + /// "ic_twitter" + static var icTwitter: ImageAsset { return ImageAsset(name: "ic_twitter") } /// "ic_unmute" static var icUnmute: ImageAsset { return ImageAsset(name: "ic_unmute") } /// "ic_voice_call_outgoing_dark" diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 33c411b00..9abc56e92 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -434,6 +434,8 @@ internal extension String { static var enterInterpretationTime: String { return localizable.tr("Localizable", "enter_interpretation_time") } /// Enter passcode static var enterPasscode: String { return localizable.tr("Localizable", "enter_passcode") } + /// Facebook + static var facebook: String { return localizable.tr("Localizable", "facebook") } /// file static var file: String { return localizable.tr("Localizable", "file") } /// Files @@ -1180,6 +1182,8 @@ internal extension String { static var tutorialNeverGoBack: String { return localizable.tr("Localizable", "tutorial_never_go_back") } /// Welcome to static var tutorialWeclomeTo: String { return localizable.tr("Localizable", "tutorial_weclome_to") } + /// Twitter + static var twitter: String { return localizable.tr("Localizable", "twitter") } /// You are blocked static var uAreBlocked: String { return localizable.tr("Localizable", "u_are_blocked") } /// Unblock @@ -1510,10 +1514,46 @@ internal extension String { static var youWasRemoved: String { return localizable.tr("Localizable", "you_was_removed") } /// Your device appears to be rooted. The security of your app can be compromised. static var yourDeviceIsRooted: String { return localizable.tr("Localizable", "your_device_is_rooted") } + /// Add Contact Info + static var accountSettingsAddContactInfoAlertTitle: String { return localizable.tr("Localizable", "account_settings.add_contact_info_alert_title") } + /// Birthday + static var accountSettingsBirthdayFieldPlaceholder: String { return localizable.tr("Localizable", "account_settings.birthday_field_placeholder") } + /// Add Contact Info + static var accountSettingsContactInfoFieldTitle: String { return localizable.tr("Localizable", "account_settings.contact_info_field_title") } + /// Delete Account + static var accountSettingsDeleteAccountFieldTitle: String { return localizable.tr("Localizable", "account_settings.delete_account_field_title") } + /// Email + static var accountSettingsEmailInputAlertActionTitle: String { return localizable.tr("Localizable", "account_settings.email_input_alert_action_title") } + /// First Name + static var accountSettingsFirstNameFieldPlaceholder: String { return localizable.tr("Localizable", "account_settings.first_name_field_placeholder") } + /// Last Name + static var accountSettingsLastNameFieldPlaceholder: String { return localizable.tr("Localizable", "account_settings.last_name_field_placeholder") } + /// Phone Number + static var accountSettingsPhoneNumberInputAlertActionTitle: String { return localizable.tr("Localizable", "account_settings.phone_number_input_alert_action_title") } + /// Profile Message + static var accountSettingsProfileMessageFieldPlaceholder: String { return localizable.tr("Localizable", "account_settings.profile_message_field_placeholder") } /// SAVE static var accountSettingsSaveButton: String { return localizable.tr("Localizable", "account_settings.save_button") } /// ACCOUNT SETTINGS static var accountSettingsScreenTitle: String { return localizable.tr("Localizable", "account_settings.screen_title") } + /// Status + static var accountSettingsStatusAlertTitle: String { return localizable.tr("Localizable", "account_settings.status_alert_title") } + /// Status + static var accountSettingsStatusFieldTitle: String { return localizable.tr("Localizable", "account_settings.status_field_title") } + /// Inactive Timeout + static var accountSettingsTimeoutAlertTitle: String { return localizable.tr("Localizable", "account_settings.timeout_alert_title") } + /// Inactive Timeout + static var accountSettingsTimeoutFieldTitle: String { return localizable.tr("Localizable", "account_settings.timeout_field_title") } + /// Username + static var accountSettingsUsernameFieldPlaceholder: String { return localizable.tr("Localizable", "account_settings.username_field_placeholder") } + /// Contact Information + static var accountSettingsHeaderContactInformation: String { return localizable.tr("Localizable", "account_settings.header.contact_information") } + /// Perfomal Information + static var accountSettingsHeaderPersonalInformation: String { return localizable.tr("Localizable", "account_settings.header.personal_information") } + /// Username + static var accountSettingsHeaderUsername: String { return localizable.tr("Localizable", "account_settings.header.username") } + /// You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.\n\nYou can use a-z, 0-9 and underscores. Minimum length is 2 characters. + static var accountSettingsUsernameDescription: String { return localizable.tr("Localizable", "account_settings.username.description") } /// OR static var authAlternativeLabel: String { return localizable.tr("Localizable", "auth.alternative_label") } /// Email @@ -1574,6 +1614,28 @@ internal extension String { } /// Welcome to static var codeConfirmationWelcome: String { return localizable.tr("Localizable", "code_confirmation.welcome") } + /// Delete Email + static var contactInfoEmailDeleteButton: String { return localizable.tr("Localizable", "contact_info.email_delete_button") } + /// ADD EMAIL + static var contactInfoEmailScreenTitle: String { return localizable.tr("Localizable", "contact_info.email_screen_title") } + /// ADD FACEBOOK + static var contactInfoFacebookScreenTitle: String { return localizable.tr("Localizable", "contact_info.facebook_screen_title") } + /// Delete Phone Number + static var contactInfoPhoneNumberDeleteButton: String { return localizable.tr("Localizable", "contact_info.phone_number_delete_button") } + /// Phone number type + static var contactInfoPhoneNumberPickerAlertTitle: String { return localizable.tr("Localizable", "contact_info.phone_number_picker_alert_title") } + /// ADD NUMBER + static var contactInfoPhoneNumberScreenTitle: String { return localizable.tr("Localizable", "contact_info.phone_number_screen_title") } + /// Remove Account + static var contactInfoSocialDeleteButton: String { return localizable.tr("Localizable", "contact_info.social_delete_button") } + /// ADD TWITTER + static var contactInfoTwitterScreenTitle: String { return localizable.tr("Localizable", "contact_info.twitter_screen_title") } + /// Email + static var contactInfoEmailPlaceholder: String { return localizable.tr("Localizable", "contact_info.email.placeholder") } + /// Facebook Link + static var contactInfoFacebookPlaceholder: String { return localizable.tr("Localizable", "contact_info.facebook.placeholder") } + /// Twitter Link + static var contactInfoTwitterPlaceholder: String { return localizable.tr("Localizable", "contact_info.twitter.placeholder") } /// Account Name static var createProfileAccountNameFieldPlaceholder: String { return localizable.tr("Localizable", "create_profile.account_name_field_placeholder") } /// I agree at @@ -1620,6 +1682,16 @@ internal extension String { static var loginOptionsPhoneNumberOptionActionText: String { return localizable.tr("Localizable", "login_options.phone_number_option_action_text") } /// LOGIN OPTIONS static var loginOptionsScreenTitle: String { return localizable.tr("Localizable", "login_options.screen_title") } + /// Add Custom + static var phoneNumberLabelAddCustom: String { return localizable.tr("Localizable", "phone_number_label.add_custom") } + /// Home + static var phoneNumberLabelHome: String { return localizable.tr("Localizable", "phone_number_label.home") } + /// Mobile + static var phoneNumberLabelMobile: String { return localizable.tr("Localizable", "phone_number_label.mobile") } + /// Work + static var phoneNumberLabelWork: String { return localizable.tr("Localizable", "phone_number_label.work") } + /// Phone number type + static var phoneNumberPickerTitle: String { return localizable.tr("Localizable", "phone_number_picker.title") } /// Invite to NYNJA static var searchContactInviteButtonTitle: String { return localizable.tr("Localizable", "search_contact.invite_button_title") } /// Email diff --git a/Nynja/ImageSelector.swift b/Nynja/ImageSelector.swift index bfa583c1b..7f38f8141 100644 --- a/Nynja/ImageSelector.swift +++ b/Nynja/ImageSelector.swift @@ -66,7 +66,7 @@ final class ImageSelector { } } - private func startSelectAvatarFlow(source: SelectAvatarFlowCoordinatorSource, on viewController: UIViewController) { + private func startSelectAvatarFlow(source: ImageSource, on viewController: UIViewController) { let selectAvatarFlowCoordinator = SelectAvatarFlowCoordinator(dependencies: .init( source: source, rootViewController: viewController, diff --git a/Nynja/Improvements/StorageSubscriber.swift b/Nynja/Improvements/StorageSubscriber.swift index 7ef5878f3..fb245d072 100644 --- a/Nynja/Improvements/StorageSubscriber.swift +++ b/Nynja/Improvements/StorageSubscriber.swift @@ -17,6 +17,8 @@ enum SubscribeType: Hashable { case job(Int64?) case recentSticker(Int64?) case stickerPack(Int64?) + case account(String?) + case contactInfo case profile } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift index a9b4f2978..806c2caa3 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift @@ -57,6 +57,10 @@ class BaseNynjaButton: UIButton { self.titleLabel?.font = UIFont.makeFont(with: fontName, height: labelHeight) } + convenience init(frame: CGRect = .zero, font: FontConvertible, fontHeight: CGFloat) { + self.init(frame: frame, fontName: font.name, labelHeight: fontHeight) + } + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) baseSetup() diff --git a/Nynja/Library/UI/Buttons/NynjaButton/DestructiveNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/DestructiveNynjaButton.swift new file mode 100644 index 000000000..2ff95d8db --- /dev/null +++ b/Nynja/Library/UI/Buttons/NynjaButton/DestructiveNynjaButton.swift @@ -0,0 +1,65 @@ +// +// DestructiveNynjaButton.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +class DestructiveNynjaButton: UIButton { + + var isSeparatorVisible: Bool = true { + didSet { + separatorView.isHidden = !isSeparatorVisible + } + } + + private lazy var separatorView: SeparatorView = { + let separatorView = SeparatorView() + + addSubview(separatorView) + separatorView.snp.makeConstraints { maker in + maker.left.right.bottom.equalToSuperview() + } + + return separatorView + }() + + + // MARK: - Init + + override convenience init(frame: CGRect) { + self.init(frame: frame, fontName: FontFamily.NotoSans.medium.name, fontHeight: CGFloat(22.0.adjustedByWidth)) + } + + convenience init(height: CGFloat) { + self.init(frame: CGRect(origin: .zero, size: CGSize(width: 0, height: height))) + } + + init(frame: CGRect = .zero, fontName: String, fontHeight: CGFloat) { + super.init(frame: frame) + baseSetup() + self.titleLabel?.font = UIFont.makeFont(with: fontName, height: fontHeight) + } + + convenience init(frame: CGRect = .zero, font: FontConvertible, fontHeight: CGFloat) { + self.init(frame: frame, fontName: font.name, fontHeight: fontHeight) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + baseSetup() + } + + + // MARK: - Setup + + func baseSetup() { + separatorView.isHidden = !isSeparatorVisible + contentHorizontalAlignment = .left + setTitleColor(UIColor.nynja.mainRed, for: .normal) + } +} diff --git a/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift index f7859b08b..4124cc34d 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift @@ -15,5 +15,6 @@ class RoundNynjaButton: BaseNynjaButton { override func layoutSubviews() { super.layoutSubviews() layer.cornerRadius = bounds.height / 2 + layer.masksToBounds = true } } diff --git a/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift b/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift index 2122f4426..292ef9f43 100644 --- a/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift +++ b/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift @@ -42,6 +42,8 @@ final class NynjaControlContainerView: UIView { init(contentView: UIView, horizontalInset: CGFloat = Constraints.containerView.defaultHorizontalInset) { super.init(frame: .zero) setup(contentView: contentView, horizontalInset: horizontalInset) + containerView.backgroundColor = UIColor.nynja.backgroundColor + backgroundColor = UIColor.nynja.backgroundColor } required init?(coder aDecoder: NSCoder) { diff --git a/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift b/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift index 2a941388d..d646ff8a7 100644 --- a/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift +++ b/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift @@ -17,6 +17,18 @@ extension UIFont { private static var fontsCache: [FontInitialValues: UIFont] = [:] + static func makeFont(with font: FontConvertible, width: Int = 100, height: Int) -> UIFont? { + return makeFont(with: font.name, width: CGFloat(width), height: CGFloat(height)) + } + + static func makeFont(with font: FontConvertible, width: Double = 100, height: Double) -> UIFont? { + return makeFont(with: font.name, width: CGFloat(width), height: CGFloat(height)) + } + + static func makeFont(with font: FontConvertible, width: CGFloat = 100, height: CGFloat) -> UIFont? { + return makeFont(with: font.name, width: CGFloat(width), height: CGFloat(height)) + } + static func makeFont(with fontName: String, width: Int = 100, height: Int) -> UIFont? { return makeFont(with: fontName, width: CGFloat(width), height: CGFloat(height)) } diff --git a/Nynja/Library/UI/SeparatorView/SeparatorView.swift b/Nynja/Library/UI/SeparatorView/SeparatorView.swift index 282f1be80..7dc2ccff7 100644 --- a/Nynja/Library/UI/SeparatorView/SeparatorView.swift +++ b/Nynja/Library/UI/SeparatorView/SeparatorView.swift @@ -10,15 +10,21 @@ import UIKit import SnapKit final class SeparatorView: UIView { - - static let height: CGFloat = 1 - + var color: UIColor = UIColor.nynja.backgroundGray { didSet { backgroundColor = color } } + var height: CGFloat = 1 { + didSet { + snp.updateConstraints { maker in + maker.height.equalTo(height) + } + } + } + // MARK: - Init @@ -38,7 +44,7 @@ final class SeparatorView: UIView { private func setup() { backgroundColor = color snp.makeConstraints { maker in - maker.height.equalTo(SeparatorView.height) + maker.height.equalTo(height) } } } diff --git a/Nynja/Library/UI/TextInput/InputField/LinkField/LinkValidator.swift b/Nynja/Library/UI/TextInput/InputField/LinkField/ChannelLinkValidator.swift similarity index 95% rename from Nynja/Library/UI/TextInput/InputField/LinkField/LinkValidator.swift rename to Nynja/Library/UI/TextInput/InputField/LinkField/ChannelLinkValidator.swift index 0e2f89e52..63979defd 100644 --- a/Nynja/Library/UI/TextInput/InputField/LinkField/LinkValidator.swift +++ b/Nynja/Library/UI/TextInput/InputField/LinkField/ChannelLinkValidator.swift @@ -1,5 +1,5 @@ // -// LinkValidator.swift +// ChannelLinkValidator.swift // Nynja // // Created by Volodymyr Hryhoriev on 5/4/18. @@ -11,7 +11,7 @@ enum LengthWarning { case maxLength } -final class LinkValidator: NSObject, UITextViewDelegate { +final class ChannelLinkValidator: NSObject, UITextViewDelegate { typealias ValidationClosure = ((ValidationKind) -> Void) enum ValidationKind { diff --git a/Nynja/Library/UI/TextInput/InputField/LinkField/LinkField.swift b/Nynja/Library/UI/TextInput/InputField/LinkField/NynjaLinkField.swift similarity index 95% rename from Nynja/Library/UI/TextInput/InputField/LinkField/LinkField.swift rename to Nynja/Library/UI/TextInput/InputField/LinkField/NynjaLinkField.swift index 6a892dfd3..edc7daea1 100644 --- a/Nynja/Library/UI/TextInput/InputField/LinkField/LinkField.swift +++ b/Nynja/Library/UI/TextInput/InputField/LinkField/NynjaLinkField.swift @@ -1,12 +1,12 @@ // -// LinkField.swift +// NynjaLinkField.swift // Nynja // // Created by Volodymyr Hryhoriev on 5/4/18. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -final class LinkField: MaterialTextField { +final class NynjaLinkField: MaterialTextField { var domen: String = "" { willSet { setupDomen(newValue, oldDomen: domen) } @@ -17,7 +17,7 @@ final class LinkField: MaterialTextField { set { setupLink(newValue) } } - var linkValidator: LinkValidator? + var linkValidator: ChannelLinkValidator? // MARK: - Views diff --git a/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift b/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift index 81fe46802..d55535539 100644 --- a/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift +++ b/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift @@ -63,7 +63,7 @@ protocol NewChannelPresenterProtocol: BasePresenterProtocol, NavigationProtocol func nameEdited(with text: String) func descriptionEdited(with text: String) - func handleLink(validationKind: LinkValidator.ValidationKind) + func handleLink(validationKind: ChannelLinkValidator.ValidationKind) func createTapped() diff --git a/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift b/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift index a039f2a17..5ae72b4a8 100644 --- a/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift +++ b/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift @@ -135,7 +135,7 @@ final class NewChannelPresenter: BasePresenter, NewChannelPresenterProtocol, New // MARK: Handle Link - func handleLink(validationKind: LinkValidator.ValidationKind) { + func handleLink(validationKind: ChannelLinkValidator.ValidationKind) { updateLink(view.link) switch validationKind { diff --git a/Nynja/Modules/Channel/NewChannel/View/NewChannelViewController.swift b/Nynja/Modules/Channel/NewChannel/View/NewChannelViewController.swift index 3677cebb3..62217b091 100644 --- a/Nynja/Modules/Channel/NewChannel/View/NewChannelViewController.swift +++ b/Nynja/Modules/Channel/NewChannel/View/NewChannelViewController.swift @@ -79,8 +79,8 @@ final class NewChannelViewController: BaseVC, NewChannelViewProtocol, ALTextInpu return nameField }() - lazy var linkField: LinkField = { - let linkField = LinkField() + lazy var linkField: NynjaLinkField = { + let linkField = NynjaLinkField() linkField.placeholder = Strings.channelLink.localized @@ -182,7 +182,7 @@ final class NewChannelViewController: BaseVC, NewChannelViewProtocol, ALTextInpu let domen = "nynja.app/" linkField.domen = domen - linkField.linkValidator = LinkValidator(domen: domen, minLength: 2, maxLength: 48) { [weak self] kind in + linkField.linkValidator = ChannelLinkValidator(domen: domen, minLength: 2, maxLength: 48) { [weak self] kind in self?.presenter.handleLink(validationKind: kind) } diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift index 8e0325a40..80e004179 100644 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift @@ -10,15 +10,13 @@ import Foundation // MARK: - Wireframe -protocol AccountSettingsWireframeProtocol: class { - func back() +protocol AccountSettingsWireframeProtocol: AlertDisplayable { + func dismiss() - func chooseAvatar(completion: @escaping (UIImage?) -> Void) - func chooseStatus(completion: @escaping (UserStatus) -> Void) - func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) - - func addContact(completion: @escaping (Result) -> Void) - func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) + func chooseAvatar(from source: ImageSource, completion: @escaping (UIImage?) -> Void) + + func addContactInfo(ofType contactInfoType: ContactInfoInputModel.InputType, accountId: String) + func editContactInfo(_ contactInfo: ContactInfoInputModel.InputInfo, accountId: String) func deleteAccount(identityId: String, accountId: String) } @@ -26,61 +24,34 @@ protocol AccountSettingsWireframeProtocol: class { // MARK: - View protocol AccountSettingsViewInput: LoadingInteractive { - func reloadData() + func setup(form: Form) } // MARK: - Presenter protocol AccountSettingsPresenterProtocol: BasePresenterProtocol, NavigationProtocol { func save() - - func countOfSections() -> Int - func rowsInSection(section: Int) -> Int - func item(for section: Int) -> SettingsSectionHeader? - func item(for section: Int, row: Int) -> AnyObject? - - func chooseAvatar(completion: @escaping (UIImage?) -> Void) - func chooseStatus(completion: @escaping (UserStatus) -> Void) - func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) - func setProfileMessage(message: String) - - func setFirstName(value: String) - func setLastName(value: String) - func setBirthday(value: Date) - - func setUserName(value: String) - - func addContact(completion: @escaping (Result) -> Void) - func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) - func contacts() -> [UserContact] } // MARK: - Interactor // MARK: Input protocol AccountSettingsInteractorInput: BaseInteractorProtocol { - func save(completion: @escaping (Result) -> Void) var identityId: String { get } - var accountId: String { get } - var avatar: URL? { get } - var status: UserStatus { get set } - var statusTimeout: StatusTimeout { get set } - var profileMessage: String? { get set } + var accountId: String { get } - var firstName: String? { get set } - var lastName: String? { get set } - var birthday: Date? { get set } + var availableStatuses: [AccountStatus] { get } - var userName: String? { get set } + var availableTimeouts: [AccountTimeout] { get } - var contacts: [UserContact] { get } + var availableContactInfoTypes: [ContactInfoInputModel.InputType] { get } - func saveAvatar(_ avatar: UIImage) - func update(contact: UserContact, action: UserContactActions) + func save(_ settings: AccountSettingsViewModel, completion: @escaping (Result) -> Void) } // MARK: Output protocol AccountSettingsInteractorOutput: class { + func didUpdate(_ viewModel: AccountSettingsViewModel) } diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountSettingsViewModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountSettingsViewModel.swift new file mode 100644 index 000000000..7da7b0ea9 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountSettingsViewModel.swift @@ -0,0 +1,26 @@ +// +// AccountSettingsViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/19/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum ImageData { + case url(URL?) + case image(UIImage) +} + +struct AccountSettingsViewModel { + var avatar: ImageData + var status: AccountStatus + var timeout: AccountTimeout + var profileMessage: String? + var firstName: String + var lastName: String? + var birthday: Date? + var username: String? + var contactInfo: [ContactInfoViewModel]? +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountStatus.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountStatus.swift new file mode 100644 index 000000000..882f31661 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountStatus.swift @@ -0,0 +1,28 @@ +// +// AccountStatus.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum AccountStatus: String, CaseIterable { + case active + case inactive + case busy + case offline + + var displayName: String { + // TODO: localize + switch self { + case .active: + return "Active" + case .inactive: + return "Inactive" + case .busy: + return "Busy" + case .offline: + return "Offline" + } + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountTimeout.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountTimeout.swift new file mode 100644 index 000000000..138b79be4 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AccountTimeout.swift @@ -0,0 +1,25 @@ +// +// AccountTimeout.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/19/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum AccountTimeout { + /// Duration in seconds + case time(Int) + case never + + var displayName: String { + // TODO: localize + switch self { + case let .time(count): + return "\(count / 60) min" + case .never: + return "Never" + } + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AddContactCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AddContactCellModel.swift deleted file mode 100644 index 7aab2fe81..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/AddContactCellModel.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AddContactCellModel.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -final class AddContactCellModel: IdentityProtocol, VerticalSizeble { - static var identifier: String { - return "AddContactTVCell" - } - - var height: CGFloat { - return 44 - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoSectionItem.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoSectionItem.swift new file mode 100644 index 000000000..43107a40b --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoSectionItem.swift @@ -0,0 +1,29 @@ +// +// ContactInfoSectionItem.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/20/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class ContactInfoSectionItem: AnyFieldRowItem { + + private let data: [ContactInfoViewModel] + + private let viewController = ContactInfoSectionViewController() + + var height: CGFloat? { + return viewController.contentHeight + } + + init(data: [ContactInfoViewModel]) { + self.data = data + viewController.setup(data) + } + + func makeView() -> UIView { + return viewController.view + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoViewModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoViewModel.swift new file mode 100644 index 000000000..2033ec0dc --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoViewModel.swift @@ -0,0 +1,207 @@ +// +// ContactInfoViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/19/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import NynjaUIKit + +final class ContactInfoViewModel: CellViewModel, InteractiveCellViewModel { + enum InfoType { + case email + case phone + case facebook + case google + case twitter + + init(_ type: DBContactInfo.InfoType) { + switch type { + case .phone: + self = .phone + case .email: + self = .email + case .facebook: + self = .facebook + case .google: + self = .google + case .twitter: + self = .twitter + } + } + } + + var accessibilityIdentifier: String { + return "contact_info_cell" + } + + let type: InfoType + let value: String + let label: String? + var select: (() -> Void)? + + init(_ dbContactInfo: DBContactInfo) { + self.type = InfoType(dbContactInfo.type) + self.label = dbContactInfo.label + self.value = dbContactInfo.value + } + + func setup(cell: ContactInfoTableViewCell) { + switch type { + case .phone: + cell.iconImageView.image = UIImage.nynja.icPhone.image + case .email: + cell.iconImageView.image = UIImage.nynja.iconsGeneralIcMail.image + case .facebook: + cell.iconImageView.image = UIImage.nynja.icFb.image + case .google: + // FIXME: get icon for google + cell.iconImageView.image = UIImage.nynja.iconsGeneralIcMail.image + case .twitter: + cell.iconImageView.image = UIImage.nynja.icTwitter.image + } + cell.contentLabel.text = value + cell.label.text = label + } +} + +final class ContactInfoTableViewCell: UITableViewCell { + + // MARK: - Views + + private(set) lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + + contentView.addSubview(imageView) + imageView.snp.makeConstraints { maker in + let size = Constraints.iconImageView.size + let left = Constraints.iconImageView.left + + maker.left.equalToSuperview().offset(left) + maker.centerY.equalToSuperview() + maker.width.height.equalTo(size) + } + + return imageView + }() + + private(set) lazy var contentLabel: UILabel = { + let fontHeight = Constraints.contentLabel.fontHeight + let color = UIColor.nynja.white + let contentLabel = UILabel(height: fontHeight, color: color, font: FontFamily.NotoSans.regular) + + contentView.addSubview(contentLabel) + contentLabel.snp.makeConstraints { maker in + let left = Constraints.contentLabel.horizontal + maker.left.equalTo(iconImageView.snp.right).offset(left) + maker.centerY.equalToSuperview() + } + + return contentLabel + }() + + private(set) lazy var label: UILabel = { + let fontHeight = Constraints.label.fontHeight + let color = UIColor.nynja.white + let label = UILabel(height: fontHeight, color: color, font: FontFamily.NotoSans.regular) + + label.textAlignment = .right + label.setContentHuggingPriority(.required, for: .horizontal) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + + contentView.addSubview(label) + label.snp.makeConstraints { maker in + let left = Constraints.contentLabel.horizontal + maker.centerY.equalToSuperview() + maker.left.equalTo(contentLabel.snp.right).offset(left) + maker.right.equalTo(arrowImageView.snp.left) + } + + return label + }() + + private(set) lazy var arrowImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage.nynja.icArrowRight.image.withRenderingMode(.alwaysOriginal) + + contentView.addSubview(imageView) + imageView.snp.makeConstraints { maker in + let size = Constraints.arrowImageView.size + let right = Constraints.arrowImageView.right + + maker.right.equalToSuperview().inset(right) + maker.centerY.equalToSuperview() + maker.width.height.equalTo(size) + } + + return imageView + }() + + private lazy var separatorView: SeparatorView = { + let separatorView = SeparatorView() + + addSubview(separatorView) + separatorView.snp.makeConstraints { maker in + let horizontal = Constraints.arrowImageView.size + maker.bottom.equalToSuperview() + maker.left.right.equalToSuperview().inset(horizontal) + } + + return separatorView + }() + + + // MARK: - Init + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + backgroundColor = UIColor.nynja.clear + contentView.backgroundColor = UIColor.nynja.clear + selectionStyle = .none + separatorView.isHidden = false + } + + + // MARK: - Layout + + enum Constraints { + + static let height: CGFloat = CGFloat(44).adjustedByWidth + + fileprivate enum iconImageView { + static let left: CGFloat = CGFloat(16).adjustedByWidth + static let size: CGFloat = CGFloat(24).adjustedByWidth + } + + fileprivate enum contentLabel { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + } + + fileprivate enum label { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + } + + fileprivate enum arrowImageView { + static let right: CGFloat = CGFloat(8).adjustedByWidth + static let size: CGFloat = CGFloat(24).adjustedByWidth + } + + fileprivate enum separatorView { + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + } + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift deleted file mode 100644 index a7df9c868..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactTVCellModel.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ContactCellModel.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class ContactTVCellModel: IdentityProtocol, VerticalSizeble { - static var identifier: String { return "ContactTVCell" } - - var height: CGFloat { return 60 } - - let typeImage: UIImage - let title: String - let details: String - - init(typeImage: UIImage, title: String, details: String) { - self.typeImage = typeImage - self.title = title - self.details = details - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift deleted file mode 100644 index 96373e450..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DescriptionCellModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// DescriptionCellModel.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -struct DescriptionCellModel: IdentityProtocol{//}, VerticalSizeble { - static var identifier: String { - return "DescriptionTVCell" - } - -// var height: CGFloat { -// let text = -// "You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.".localized -// + "\n" -// + "You can use a-z, 0-9 and underscores. Minimum lenght is 2 characters.".localized -// -// let font = FontFamily.NotoSans.regular.font(size: 14) -// -// let size = (text as NSString).size(withAttributes: [NSAttributedStringKey.font: font]) -// -// return size.height -// } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift deleted file mode 100644 index a592f2ab7..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/DestructiveActionCellModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// DestructiveActionCellModel.swift -// Nynja -// -// Created by Anton Poltoratskyi on 12/4/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation -import NynjaUIKit - -final class DestructiveActionCellModel: CellViewModel, InteractiveCellViewModel { - - let title: String - let accessibilityIdentifier: String - var select: (() -> Void)? - - init(title: String, accessibilityIdentifier: String, selectionHandler: (() -> Void)?) { - self.title = title - self.accessibilityIdentifier = accessibilityIdentifier - self.select = selectionHandler - } - - func setup(cell: DestructiveActionTableViewCell) { - cell.titleLabel.text = title - } -} - diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift deleted file mode 100644 index 703e86b93..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/MaterialTextFieldCellModel.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MaterialTextFieldCellModel.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class MaterialTextFieldCellModel: IdentityProtocol, VerticalSizeble { - let profileField: ProfileField - let value: String - let action: (String) -> Void - - let height: CGFloat - - init(profileField: ProfileField, value: String, action: @escaping (String) -> Void, height: CGFloat = 65) { - self.profileField = profileField - self.value = value - self.action = action - self.height = height - } - - static var identifier: String { - return "MaterialTextFieldTVCell" - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift deleted file mode 100644 index 98bb0075f..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSectionHeader.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SettingsSectionHeader.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -struct SettingsSectionHeader { - let text: String - let height: CGFloat - - init(text: String, height: CGFloat = 40) { - self.text = text - self.height = height - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift deleted file mode 100644 index 4ebf4ff18..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSelectorCellModel.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SettingsSelectorCellModel.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -struct SettingsSelectorCellModel: IdentityProtocol, VerticalSizeble { - let title: String - let details: String - - let height: CGFloat - - init(title: String, details: String, height: CGFloat = 42) { - self.title = title - self.details = details - self.height = height - } - - static var identifier: String { - return "SettingsSelectorTVCell" - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift deleted file mode 100644 index 4628eb43b..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/SettingsSetAvatarCellModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SettingsSetAvatarCellModel.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -struct SettingsSetAvatarCellModel: IdentityProtocol, VerticalSizeble { - let imageURL: URL? - let height: CGFloat - - init(imageURL: URL?, height: CGFloat = 145) { - self.imageURL = imageURL - self.height = height - } - - static var identifier: String { - return "SettingsSetAvatarTVCell" - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/Sizeble.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/Sizeble.swift deleted file mode 100644 index f68783fe6..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/Sizeble.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Sizeble.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -protocol Sizeble: VerticalSizeble, HorizontalSizeble {} - -protocol VerticalSizeble { - var height: CGFloat { get } -} - -protocol HorizontalSizeble { - var width: CGFloat { get } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/StatusTimeout.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/StatusTimeout.swift deleted file mode 100644 index 0eb8545b9..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/StatusTimeout.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// StatusTimeout.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -enum StatusTimeout: String { - case fiveMin = "5 min" - case fifteenMin = "15 min" - case thirtyMin = "30 min" - case oneHour = "60 min" - case never = "Never" -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserAccount.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserAccount.swift deleted file mode 100644 index 385a05c70..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserAccount.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// UserAccount.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -struct UserAccount { - let avatar: UIImage? - let status: UserStatus - let statusTimeout: StatusTimeout - let profileMessage: String - - let firstName: String - let lastName: String - let birthday: Date? - - let userName: String - - let contacts: [UserContact] -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContact.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContact.swift deleted file mode 100644 index 70532c2ce..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContact.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// UserContact.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -struct UserContact { - enum ContactType { - case email - case phone - case facebook - case google - } - - enum ContactDetailType { - case mobile - case work - case home - case custom(String) - - - } - - let type: ContactType - let value: String - let detailType: ContactDetailType -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContactAction.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContactAction.swift deleted file mode 100644 index 2d2c1b58a..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserContactAction.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// UserContactAction.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -enum UserContactActions { - case create - case update - case delete -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserStatus.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserStatus.swift deleted file mode 100644 index 10a3f499c..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserStatus.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// UserStatus.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -enum UserStatus: String { - case active - case inactive - case busy - case offline -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift index d40579bf1..e96fa3339 100644 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift @@ -11,6 +11,10 @@ import Foundation final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractorInput, InitializeInjectable { private weak var presenter: AccountSettingsInteractorOutput? + + override var subscribes: [SubscribeType]? { + return [.account(accountId), .contactInfo] + } // MARK: - User Info @@ -18,22 +22,25 @@ final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractor let accountId: String - private var account: DBAccount? - - private var updatedAvatar: UIImage? - - var avatar: URL? - var status: UserStatus = .active - var statusTimeout: StatusTimeout = .fiveMin - var profileMessage: String? + var availableStatuses: [AccountStatus] { + return AccountStatus.allCases + } - var firstName: String? - var lastName: String? - var birthday: Date? + var availableTimeouts: [AccountTimeout] { + return [ + .time(5 * 60), + .time(15 * 60), + .time(30 * 60), + .time(60 * 60), + .never + ] + } - var userName: String? + var availableContactInfoTypes: [ContactInfoInputModel.InputType] { + return ContactInfoInputModel.InputType.allCases + } - var contacts: [UserContact] = [] + private var account: DBAccount? // MARK: - Services @@ -67,57 +74,67 @@ final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractor override func loadData() { super.loadData() - if let account = accountDAO.fetchAccount(by: accountId) { - setup(account) + fetchAccount() + } + + private func fetchAccount() { + guard let account = accountDAO.fetchAccount(byId: accountId) else { + return } + self.account = account + setup(account) } private func setup(_ account: DBAccount) { - avatar = account.avatar.flatMap { URL(string: $0) } - profileMessage = account.accountMark - firstName = account.firstName - lastName = account.lastName - birthday = account.birthday.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } - userName = account.username -// contacts.append(contentsOf: dependencies.userProfile.contacts) + let avatarURL = account.avatar.flatMap { URL(string: $0) } + let viewModel = AccountSettingsViewModel( + avatar: .url(avatarURL), + status: .active, + timeout: .never, + profileMessage: account.accountMark, + firstName: account.firstName, + lastName: account.lastName, + birthday: account.birthday.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) }, + username: account.username, + contactInfo: account.contactInfo?.compactMap { ContactInfoViewModel($0) } + ) + presenter?.didUpdate(viewModel) } // MARK: - Interactor Input - - func saveAvatar(_ image: UIImage) { - updatedAvatar = image - } - - func save(completion: @escaping (Result) -> Void) { - if let avatar = updatedAvatar { - imageUploader.uploadImageFile(avatar) { [weak self] result in + + func save(_ settings: AccountSettingsViewModel, completion: @escaping (Result) -> Void) { + switch settings.avatar { + case let .image(avatarImage): + imageUploader.uploadImageFile(avatarImage) { [weak self] result in guard let self = self else { return } switch result { case let .success(avatarURL): - self.updateAccount(avatarURL: avatarURL, completion: completion) + self.updateAccount(avatarURL: avatarURL, settings: settings, completion: completion) case let .failure(error): completion(.failure(error)) } } - } else { - updateAccount(avatarURL: nil, completion: completion) + case let .url(avatarURL): + updateAccount(avatarURL: avatarURL, settings: settings, completion: completion) } } - private func updateAccount(avatarURL: URL?, completion: @escaping (Result) -> Void) { + private func updateAccount(avatarURL: URL?, settings: AccountSettingsViewModel, completion: @escaping (Result) -> Void) { + // FIXME: save birthday let accountInfo = AccountInfo( accountId: accountId, avatar: avatarURL?.absoluteString, - accountMark: profileMessage, + accountMark: settings.profileMessage, accountName: nil, - firstName: firstName, - lastName: lastName, - username: userName, + firstName: settings.firstName, + lastName: settings.lastName, + username: settings.username, accountStatus: .enabled, roles: nil, - qrCode: nil, + qrCode: accountId, birthday: nil ) @@ -137,7 +154,23 @@ final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractor } } - func update(contact: UserContact, action: UserContactActions) { - + + // MARK: - Storage Observer + + override func update(with changes: [StorageChange], type: SubscribeType) { + switch type { + case .account: + guard let account = changes.first?.entity as? DBAccount else { + break + } + self.account = account + setup(account) + + case .contactInfo: + fetchAccount() + + default: + break + } } } diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift index 616423e68..6adae6a05 100644 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -7,174 +7,278 @@ // import Foundation +import NynjaUIKit final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterProtocol, SetInjectable, AccountSettingsInteractorOutput { - + private weak var view: AccountSettingsViewInput? - private var wireframe: AccountSettingsWireframeProtocol? - private var interactor: AccountSettingsInteractorInput? { + private var wireframe: AccountSettingsWireframeProtocol! + private var interactor: AccountSettingsInteractorInput! { didSet { _interactor = interactor } } - override func loadData() { - super.loadData() - view?.reloadData() - } + private var viewModel: AccountSettingsViewModel! - func save() { - view?.showLoading() - interactor?.save { [weak self] in - self?.view?.hideLoading() - $0.onSuccess { - self?.wireframe?.back() - } - } - } - func back() { - wireframe?.back() - } + // MARK: - View Model - func countOfSections() -> Int { - return 5 + private func setup(_ viewModel: AccountSettingsViewModel) { + let sections: [Form.Section] = [ + makeTopSection(for: viewModel), + makePersonalInfoSection(for: viewModel), + makeUsernameSection(for: viewModel), + makeContactInfoSection(for: viewModel), + makeDeleteAccountSection() + ] + sections.forEach { + $0.contentInset = UIEdgeInsets(top: CGFloat(8.0).adjustedByWidth, left: 0, bottom: CGFloat(8.0).adjustedByWidth, right: 0) + } + + let form = Form(sections: sections) + view?.setup(form: form) } - func rowsInSection(section: Int) -> Int { - switch section { - case 0: return 4 - case 1: return 2//3 - case 2: return 2 - case 3: return 0// 1 + (interactor?.contacts.count ?? 0) - case 4: return 1 // delete profile - default: return 0 + private func makeTopSection(for viewModel: AccountSettingsViewModel) -> Form.Section { + let avatarItem = AvatarRowItem(imageSource: viewModel.avatar, imageSize: CGSize(width: 95, height: 95), height: 144) + avatarItem.imageSelectionHandler = { [weak self, weak avatarItem] in + self?.chooseAvatar { image in + if let image = image { + self?.viewModel?.avatar = .image(image) + avatarItem?.imageSource = .image(image) + } + } + } + + let statusItem = PickerRowItem(title: String.localizable.accountSettingsStatusFieldTitle, label: viewModel.status.displayName, height: 44) + statusItem.labelSelectionHandler = { [weak self, weak statusItem] in + self?.chooseStatus { status in + self?.viewModel?.status = status + statusItem?.selectLabel(status.displayName) + } } + + let timeoutItem = PickerRowItem(title: String.localizable.accountSettingsTimeoutFieldTitle, label: viewModel.timeout.displayName, height: 44) + timeoutItem.labelSelectionHandler = { [weak self, weak timeoutItem] in + self?.chooseTimeout { timeout in + self?.viewModel?.timeout = timeout + timeoutItem?.selectLabel(timeout.displayName) + } + } + + let profileMessageItem = TextFieldRowItem(height: 64) + profileMessageItem.text = viewModel.profileMessage + profileMessageItem.placeholder = String.localizable.accountSettingsProfileMessageFieldPlaceholder + profileMessageItem.returnKeyType = .next + profileMessageItem.edges.top = 8 + profileMessageItem.textChangeAction = { [weak self] text in + self?.viewModel?.profileMessage = text + } + + return Form.Section(rows: [avatarItem, statusItem, timeoutItem, profileMessageItem]) + } + + private func makePersonalInfoSection(for viewModel: AccountSettingsViewModel) -> Form.Section { + let header = FormHeader(title: String.localizable.accountSettingsHeaderPersonalInformation, height: 32) + + let fistNameItem = TextFieldRowItem(height: 64) + fistNameItem.text = viewModel.firstName + fistNameItem.placeholder = "\(String.localizable.accountSettingsFirstNameFieldPlaceholder)*" + fistNameItem.returnKeyType = .next + fistNameItem.textChangeAction = { [weak self] text in + self?.viewModel?.firstName = text + } + + let lastNameItem = TextFieldRowItem(height: 64) + lastNameItem.text = viewModel.lastName + lastNameItem.placeholder = String.localizable.accountSettingsLastNameFieldPlaceholder + lastNameItem.returnKeyType = .next + lastNameItem.textChangeAction = { [weak self] text in + self?.viewModel?.lastName = text + } + + let birthdayItem = TextFieldRowItem(height: 64) + birthdayItem.placeholder = String.localizable.accountSettingsBirthdayFieldPlaceholder + birthdayItem.returnKeyType = .next + + return Form.Section(header: header, rows: [fistNameItem, lastNameItem, birthdayItem]) } - func item(for section: Int) -> SettingsSectionHeader? { - switch section { - case 1: return SettingsSectionHeader(text: "Personal Information".localized) - case 2: return SettingsSectionHeader(text: "Username".localized) - case 3: return SettingsSectionHeader(text: "Contact Information".localized) - case 4: return nil // delete profile - default: return nil + private func makeUsernameSection(for viewModel: AccountSettingsViewModel) -> Form.Section { + let header = FormHeader(title: String.localizable.accountSettingsHeaderUsername, height: 32) + + let usernameItem = TextFieldRowItem(validator: UsernameTextValidator(), height: 64) + usernameItem.text = viewModel.username + usernameItem.placeholder = String.localizable.accountSettingsUsernameFieldPlaceholder + usernameItem.returnKeyType = .done + usernameItem.textChangeAction = { [weak self] text in + self?.viewModel?.username = text } + + let textItem = TextRowItem(text: String.localizable.accountSettingsUsernameDescription) + + return Form.Section(header: header, rows: [usernameItem, textItem]) } - func item(for section: Int, row: Int) -> AnyObject? { - switch section { - case 0: - switch row { - case 0: return SettingsSetAvatarCellModel(imageURL: interactor?.avatar) as AnyObject - case 1: return SettingsSelectorCellModel(title: "Status".localized, details: (interactor?.status.rawValue.localized ?? "")) as AnyObject - case 2: return SettingsSelectorCellModel(title: "Idle Timeout".localized, details: (interactor?.statusTimeout.rawValue.localized ?? "")) as AnyObject - case 3: return MaterialTextFieldCellModel(profileField: .profileMessage, value: interactor?.profileMessage ?? "", action: setProfileMessage) - default: return nil - } - case 1: - switch row { - case 0: return MaterialTextFieldCellModel(profileField: .firstName, value: interactor?.firstName ?? "", action: setFirstName) - case 1: return MaterialTextFieldCellModel(profileField: .lastName, value: interactor?.lastName ?? "", action: setLastName) - default: return nil - } - case 2: - switch row { - case 0: return MaterialTextFieldCellModel(profileField: .userName, value: interactor?.userName ?? "", action: setUserName) - case 1: return DescriptionCellModel() as AnyObject - default: return nil - } - case 3: - switch row { - case 0: return AddContactCellModel() - default: - let contact = interactor?.contacts[row - 1] - return ContactTVCellModel(typeImage: UIImage(named: "arrow_up")!, title: contact?.value ?? "", details: "") + private func makeContactInfoSection(for viewModel: AccountSettingsViewModel) -> Form.Section { + let header = FormHeader(title: String.localizable.accountSettingsHeaderContactInformation, height: 32) + + var items: [AnyFieldRowItem] = [] + + let addContactInfoItem = ActionRowItem(text: String.localizable.accountSettingsContactInfoFieldTitle, height: 44) { [weak self] _ in + self?.chooseContactInfoType { [weak self] contactInfoType in + guard let accountId = self?.interactor?.accountId else { return } + self?.wireframe.addContactInfo(ofType: contactInfoType, accountId: accountId) } - case 4: - // FIXME: localization (in whole module) - return DestructiveActionCellModel(title: "Delete Account", accessibilityIdentifier: "delete_account_item") { [weak self] in - guard let self = self, let identityId = self.interactor?.identityId, let accountId = self.interactor?.accountId else { - return + } + items.append(addContactInfoItem) + + if let contactInfo = viewModel.contactInfo { + contactInfo.forEach { (info: ContactInfoViewModel) in + info.select = { [weak self, weak info] in + guard let self = self, let info = info, let accountId = self.interactor?.accountId else { + return + } + let editInfo: ContactInfoInputModel.InputInfo + switch info.type { + case .phone: + fatalError() + case .email: + editInfo = .email(info.value) + case .facebook: + editInfo = .social(.init(provider: .facebook, link: info.value)) + case .twitter: + editInfo = .social(.init(provider: .twitter, link: info.value)) + case .google: + fatalError() + } + + self.wireframe.editContactInfo(editInfo, accountId: accountId) } - self.wireframe?.deleteAccount(identityId: identityId, accountId: accountId) } - default: - return nil + let contactInfoSectionItem = ContactInfoSectionItem(data: contactInfo) + items.append(contactInfoSectionItem) } + + return Form.Section(header: header, rows: items) } - func chooseAvatar(completion: @escaping (UIImage?) -> Void) { - wireframe?.chooseAvatar { image in - if let image = image { - self.interactor?.saveAvatar(image) + private func makeDeleteAccountSection() -> Form.Section { + let deleteAccountItem = DestructiveActionRowItem(title: String.localizable.accountSettingsDeleteAccountFieldTitle, height: 44) { [weak self] _ in + guard let self = self, let identityId = self.interactor?.identityId, let accountId = self.interactor?.accountId else { + return } - completion(image) + self.wireframe?.deleteAccount(identityId: identityId, accountId: accountId) } + deleteAccountItem.additionalInset = UIEdgeInsets(top: CGFloat(16.0.adjustedByWidth), left: 0, bottom: 0, right: 0) + + return Form.Section(rows: [deleteAccountItem]) } - func chooseStatus(completion: @escaping (UserStatus) -> Void) { - wireframe?.chooseStatus { - self.interactor?.status = $0 - completion($0) - } - } - func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { - wireframe?.chooseTimeout { - self.interactor?.statusTimeout = $0 - completion($0) - } - } + // MARK: - Presenter - func setProfileMessage(message: String) { - interactor?.profileMessage = message + func save() { + guard let viewModel = viewModel else { + return + } + view?.showLoading() + interactor?.save(viewModel) { [weak self] in + self?.view?.hideLoading() + $0.onSuccess { + self?.wireframe?.dismiss() + } + } } - func setFirstName(value: String) { - interactor?.firstName = value + func back() { + wireframe?.dismiss() } - func setLastName(value: String) { - interactor?.lastName = value - } - func setBirthday(value: Date) { - interactor?.birthday = value + // MARK: - Alerts + + private func chooseAvatar(completion: @escaping (UIImage?) -> Void) { + var actions: [Alert.Action] = [ + .init(title: String.localizable.alertActionTakeFromCamera, style: .default) { [weak wireframe] _ in + wireframe?.chooseAvatar(from: .camera, completion: completion) + }, + .init(title: String.localizable.alertActionTakeFromGallery, style: .default) { [weak wireframe] _ in + wireframe?.chooseAvatar(from: .gallery, completion: completion) + } + ] + let cancelAction = Alert.Action(title: String.localizable.cancel, style: .cancel) + actions.append(cancelAction) + + let alert = Alert(style: .actionSheet, actions: actions) + wireframe?.present(alert) } - func setUserName(value: String) { - interactor?.userName = value + private func chooseStatus(completion: @escaping (AccountStatus) -> Void) { + let title = String.localizable.accountSettingsStatusAlertTitle + + var actions: [Alert.Action] = interactor.availableStatuses.map { status in + Alert.Action(title: status.displayName, style: .default) { _ in + completion(status) + } + } + let cancelAction = Alert.Action(title: String.localizable.cancel, style: .cancel) + actions.append(cancelAction) + + let alert = Alert(title: title, style: .actionSheet, actions: actions) + wireframe?.present(alert) } - func addContact(completion: @escaping (Result) -> Void) { - wireframe?.addContact { - $0 - .onSuccess { - self.interactor?.update(contact: $0, action: .create) - completion(.success(())) - } - .onFailure { completion(.failure($0)) } + private func chooseTimeout(completion: @escaping (AccountTimeout) -> Void) { + let title = String.localizable.accountSettingsTimeoutAlertTitle + + var actions: [Alert.Action] = interactor.availableTimeouts.map { timeout in + Alert.Action(title: timeout.displayName, style: .default) { _ in + completion(timeout) + } } + let cancelAction = Alert.Action(title: String.localizable.cancel, style: .cancel) + actions.append(cancelAction) + + let alert = Alert(title: title, style: .actionSheet, actions: actions) + wireframe?.present(alert) } - func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) { - wireframe?.contactDetails(contact: contact) { - $0 - .onSuccess { - self.interactor?.update(contact: $0, action: .update) - completion(.success(())) - } - .onFailure { completion(.failure($0)) } + private func chooseContactInfoType(completion: @escaping (ContactInfoInputModel.InputType) -> Void) { + let title = String.localizable.accountSettingsAddContactInfoAlertTitle + + var actions: [Alert.Action] = interactor.availableContactInfoTypes.map { inputType in + let title: String + switch inputType { + case .phoneNumber: + title = String.localizable.accountSettingsPhoneNumberInputAlertActionTitle + case .email: + title = String.localizable.accountSettingsEmailInputAlertActionTitle + case let .social(provider): + title = provider.displayName + } + return Alert.Action(title: title, style: .default) { _ in + completion(inputType) + } } + let cancelAction = Alert.Action(title: String.localizable.cancel, style: .cancel) + actions.append(cancelAction) + + let alert = Alert(title: title, style: .actionSheet, actions: actions) + wireframe?.present(alert) } - func contacts() -> [UserContact] { - return interactor?.contacts ?? [] + + // MARK: - Interactor Output + + func didUpdate(_ viewModel: AccountSettingsViewModel) { + self.viewModel = viewModel + setup(viewModel) } } -// MARK: - SetInjectable +// MARK: - Injection extension AccountSettingsPresenter { struct Dependencies { diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift index c4da46fc9..15f3f3240 100644 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift @@ -9,7 +9,7 @@ import Foundation import NynjaUIKit -final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, LoadingDisplayable, InitializeInjectable, UITableViewDelegate, UITableViewDataSource, KeyboardInteractive { +final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, LoadingDisplayable, InitializeInjectable { private let presenter: AccountSettingsPresenterProtocol @@ -18,78 +18,75 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa private(set) lazy var progressHUD = makeProgressHUD(on: view) - private lazy var tableView: UITableView = { - let table = UITableView(frame: CGRect.zero, style: .grouped) - view.addSubview(table) - - table.delegate = self - table.dataSource = self + // MARK: Container + + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.contentInset.bottom = CGFloat(28.0.adjustedByWidth) - table.register(SettingsSetAvatarTVCell.self, forCellReuseIdentifier: SettingsSetAvatarCellModel.identifier) - table.register(SettingsSelectorTVCell.self, forCellReuseIdentifier: SettingsSelectorCellModel.identifier) - table.register(MaterialTextFieldTVCell.self, forCellReuseIdentifier: MaterialTextFieldCellModel.identifier) - table.register(DescriptionTVCell.self, forCellReuseIdentifier: DescriptionCellModel.identifier) - table.register(AddContactTVCell.self, forCellReuseIdentifier: AddContactCellModel.identifier) - table.register(ContactTVCell.self, forCellReuseIdentifier: ContactTVCellModel.identifier) - table.register(viewModel: DestructiveActionCellModel.self) + view.addSubview(scrollView) + scrollView.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom) + maker.left.right.equalToSuperview() + } - table.separatorStyle = .none - table.backgroundColor = .clear + return scrollView + }() + + private lazy var contentView: UIView = { + let contentView = UIView() - table.contentInset.bottom = 28 - - table.snp.makeConstraints{ (make) in - make.top.equalTo(navigationView.snp.bottom) - make.left.right.equalToSuperview() - make.bottom.equalTo(saveButton.snp.top) + scrollView.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + maker.width.equalToSuperview() } - return table + return contentView }() - private lazy var gradientView: GradientView = { - let gradientHeight = 29.0.adjustedByWidth + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical - let backgroundColor = UIColor.nynja.backgroundColor - let colors = [backgroundColor.withAlphaComponent(0), backgroundColor] + contentView.addSubview(stackView) + stackView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } - let gradientView = GradientView(colors: colors) - gradientView.isUserInteractionEnabled = false + return stackView + }() + + // MARK: Control + + private lazy var controlContainerView: NynjaControlContainerView = { + let containerView = NynjaControlContainerView(contentView: saveButton) - view.addSubview(gradientView) - gradientView.snp.makeConstraints { maker in - maker.bottom.equalTo(tableView.snp.bottom) - maker.left.right.equalToSuperview() - maker.height.equalTo(gradientHeight) + view.addSubview(containerView) + containerView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(scrollView.snp.bottom) + make.bottom.equalTo(self.view.keyboardLayoutGuide.snp.top).offset(-28.0.adjustedByWidth) } - return gradientView + return containerView }() - private lazy var saveButton: UIButton = { - let button = UIButton() - view.addSubview(button) - - button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.mainRed), for: .normal) - button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.darkRed), for: .disabled) + private lazy var saveButton: RoundNynjaButton = { + let button = RoundNynjaButton(font: FontFamily.NotoSans.medium, fontHeight: CGFloat(22.0.adjustedByWidth)) button.setTitle(String.localizable.accountSettingsSaveButton, for: .normal) - button.titleLabel?.font = FontFamily.NotoSans.medium.font(size: 16) - button.addTarget(self, action: #selector(saveAction(sender:)), for: .touchUpInside) - - button.layer.cornerRadius = 22 - button.clipsToBounds = true - - button.snp.makeConstraints { (make) in - make.height.equalTo(44) - make.left.right.equalToSuperview().inset(16) - adjustVerticalInset(.bottom, make: make, offset: -28) + + button.snp.makeConstraints { maker in + maker.height.equalTo(44.0.adjustedByWidth) } return button }() + private var form: Form? + // MARK: - Init @@ -123,179 +120,84 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa backButtonImage: UIImage.nynja.icBackNavigation.image) ) - _ = [tableView, gradientView, saveButton] - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - registerForKeyboardNotifications() + controlContainerView.addGradientView() + scrollView.snp.makeConstraints { maker in + maker.bottom.equalTo(saveButton.snp.top) + } + + view.bringSubviewToFront(controlContainerView) } - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - unregisterForKeyboardNotifications() + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + view.endEditing(true) } // MARK: - View Input - func reloadData() { - tableView.reloadData() - } - - - // MARK: - Actions - - @objc private func saveAction(sender: UIButton) { - presenter.save() - } -} - -// MARK: - Table view - -extension AccountSettingsViewController { - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let item = presenter.item(for: section) else { - return nil + func setup(form: Form) { + self.form = form + stackView.arrangedSubviews.forEach { + stackView.removeArrangedSubview($0) + $0.removeFromSuperview() } - let header = SettingsSectionHeaderView() - header.configure(config: SettingsSectionHeaderView.Config(title: item.text)) - - return header - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - let item = presenter.item(for: section) - - return CGFloat(item?.height ?? 0) - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard let item = presenter.item(for: indexPath.section, row: indexPath.row) as? VerticalSizeble else { - return UITableView.automaticDimension + func addSpace(with height: CGFloat) { + let view = UIView() + view.snp.makeConstraints { maker in + maker.height.equalTo(height) + } + stackView.addArrangedSubview(view) } - return CGFloat(item.height) - } - - func numberOfSections(in tableView: UITableView) -> Int { - return presenter.countOfSections() - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return presenter.rowsInSection(section: section) - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // FIXME: temp workaround, will be removed soon - if let viewModel = presenter.item(for: indexPath.section, row: indexPath.row) as? InteractiveCellViewModel { - viewModel.select?() - return - } - switch (indexPath.section, indexPath.row) { - case (0, 1): presenter.chooseStatus { _ in - tableView.reloadRows(at: [indexPath], with: .automatic) + for section in form.sections { + if let header = section.header { + let headerView = header.makeView() ?? UIView() + headerView.snp.makeConstraints { maker in + maker.height.equalTo(header.height) + } + stackView.addArrangedSubview(headerView) } - case (0, 2): presenter.chooseTimeout { _ in - tableView.reloadRows(at: [indexPath], with: .automatic) + + if section.contentInset.top > 0 { + addSpace(with: section.contentInset.top) } - case (3, 0): presenter.addContact { _ in tableView.reloadData() } - case (3, _): presenter.contactDetails(contact: presenter.contacts()[indexPath.row - 1]) { _ in tableView.reloadData() } - default: break - } - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - // FIXME: temp workaround, will be removed soon - if let viewModel = presenter.item(for: indexPath.section, row: indexPath.row) as? AnyCellViewModel { - let cell = tableView.dequeueReusableCell(withModel: viewModel, for: indexPath) - cell.selectionStyle = .none - return cell - } - - guard let item = presenter.item(for: indexPath.section, row: indexPath.row) as? IdentityProtocol else { - return UITableViewCell() - } - - let cell = tableView.dequeueReusableCell(withIdentifier: type(of: item).identifier, for: indexPath) - switch cell { - case is SettingsSetAvatarTVCell: configureSetAvatarCell(cell: cell as? SettingsSetAvatarTVCell, item: item as? SettingsSetAvatarCellModel) - case is SettingsSelectorTVCell: configureSelector(cell: cell as? SettingsSelectorTVCell, item: item as? SettingsSelectorCellModel) - case is MaterialTextFieldTVCell: configureTextField(cell: cell as? MaterialTextFieldTVCell, item: item as? MaterialTextFieldCellModel) - case is DescriptionTVCell: break - case is ContactTVCell: configureContactCell(cell: cell as? ContactTVCell, item: item as? ContactTVCellModel) - default: break - } - - cell.selectionStyle = .none - - return cell - } - - private func configureContactCell(cell: ContactTVCell?, item: ContactTVCellModel?) { - guard let cell = cell, let item = item else { - return - } - - cell.configure(config: ContactTVCell.Config(typeImage: item.typeImage, title: item.title, details: item.details)) - } - - private func configureSetAvatarCell(cell: SettingsSetAvatarTVCell?, item: SettingsSetAvatarCellModel?) { - guard let cell = cell, let item = item else { - return - } - - cell.configure(config: SettingsSetAvatarTVCell.Config(imageURL: item.imageURL, chooseAvatarAction: { [weak self] in - self?.presenter.chooseAvatar { image in - if let image = image { - cell.avatarImageView.image = image + defer { + if section.contentInset.bottom > 0 { + addSpace(with: section.contentInset.bottom) + } + } + + for row in section.rows { + let itemView = row.makeView() + + if let height = row.height { + itemView.snp.makeConstraints { maker in + maker.height.equalTo(height) + } + } + + if let inset = row.additionalInset { + if inset.top > 0 { + addSpace(with: inset.top) + } + stackView.addArrangedSubview(itemView) + + if inset.bottom > 0{ + addSpace(with: inset.bottom) + } + } else { + stackView.addArrangedSubview(itemView) } } - })) - } - - private func configureSelector(cell: SettingsSelectorTVCell?, item: SettingsSelectorCellModel?) { - guard let cell = cell, let item = item else { - return } - - cell.configure(config: SettingsSelectorTVCell.Config(title: item.title, details: item.details)) } - private func configureTextField(cell: MaterialTextFieldTVCell?, item: MaterialTextFieldCellModel?) { - guard let cell = cell, let item = item else { - return - } - - cell.configure(config: MaterialTextFieldTVCell.Config( - value: item.value, - fieldType: item.profileField, - textChangedHandler: { [weak self] (field, value) in - switch field { - case .firstName: self?.presenter.setFirstName(value: value) - case .lastName: self?.presenter.setLastName(value: value) - case .accountName: break - case .userName: self?.presenter.setUserName(value: value) - case .profileMessage: self?.presenter.setProfileMessage(message: value) - } - }, - shouldChangeTextHandler: nil) - ) - } -} - -// MARK: - KeyboardInteractive - -extension AccountSettingsViewController { - func keyboardNotified(endFrame: CGRect) { - let bottomInset: CGFloat = 28 - - if endFrame.origin.y >= UIScreen.main.bounds.size.height { - updateToHide(view: saveButton, offset: -bottomInset) - } else { - updateToShow(view: saveButton, offset: -bottomInset - endFrame.height) - } + // MARK: - Actions + + @objc private func saveAction(sender: UIButton) { + presenter.save() } } diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/AddContactCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/AddContactCell.swift deleted file mode 100644 index 66d069e76..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/AddContactCell.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// AddContactCell.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class AddContactTVCell: UITableViewCell { - lazy var titleLabel: UILabel = { - let label = UILabel() - contentView.addSubview(label) - - textLabel?.text = "Add Contact Info".localized - textLabel?.font = FontFamily.NotoSans.regular.font(size: 16) - textLabel?.textColor = UIColor.nynja.white - - label.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - make.left.equalToSuperview().inset(16) - } - - return label - }() - - lazy var plusImageView: UIImageView = { - let imageView = UIImageView() - contentView.addSubview(imageView) - - imageView.image = UIImage(named: "ic_add") - imageView.contentMode = .scaleAspectFill - - imageView.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - make.right.equalToSuperview().inset(16) - make.height.width.equalTo(24) - } - - return imageView - }() - - lazy var separatorView: UIView = { - let view = UIView() - contentView.addSubview(view) - view.backgroundColor = UIColor.nynja.gray - view.snp.makeConstraints { (make) in - make.height.equalTo(1) - make.bottom.equalToSuperview() - make.left.right.equalToSuperview().inset(16) - } - - return view - }() - - override func prepareForReuse() { - super.prepareForReuse() - backgroundColor = UIColor.nynja.clear - contentView.backgroundColor = UIColor.nynja.clear - _ = [titleLabel, plusImageView, separatorView] - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift deleted file mode 100644 index 11a777ad6..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/ContactTVCell.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// ContactTVCell.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class ContactTVCell: UITableViewCell, Configurable { - lazy var typeImageView: UIImageView = { - let imageView = UIImageView() - contentView.addSubview(imageView) - - imageView.contentMode = .scaleAspectFill - - imageView.snp.makeConstraints { (make) in - make.width.height.equalTo(24) - make.left.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - } - - return imageView - }() - - lazy var titleLabel: UILabel = { - let label = UILabel() - contentView.addSubview(label) - - label.font = FontFamily.NotoSans.regular.font(size: 16) - label.textColor = UIColor.nynja.white - - label.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - make.left.equalTo(typeImageView.snp.right).offset(16) - } - - return label - }() - - lazy var detailedImageView: UIImageView = { - let imageView = UIImageView() - contentView.addSubview(imageView) - - imageView.image = UIImage(named: "disclosure_indicator") - - imageView.snp.makeConstraints { (make) in - make.right.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - - return imageView - }() - - lazy var detailsLabel: UILabel = { - let label = UILabel() - contentView.addSubview(label) - - label.font = FontFamily.NotoSans.regular.font(size: 16) - label.textColor = UIColor.nynja.dustyGray - - label.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - make.right.equalTo(detailedImageView.snp.left) - } - - return label - }() - - lazy var separatorView: UIView = { - let view = UIView() - contentView.addSubview(view) - view.backgroundColor = UIColor.nynja.gray - view.snp.makeConstraints { (make) in - make.height.equalTo(1) - make.bottom.equalToSuperview() - make.left.right.equalToSuperview().inset(16) - } - - return view - }() - - struct Config { - let typeImage: UIImage - let title: String - let details: String - } - - func configure(config: ContactTVCell.Config) { - _ = [typeImageView, titleLabel, detailsLabel, detailedImageView, separatorView] - - typeImageView.image = config.typeImage - titleLabel.text = config.title - detailsLabel.text = config.details - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift deleted file mode 100644 index ef65e51f0..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DescriptionTVCell.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// DescriptionTVCell.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class DescriptionTVCell: UITableViewCell { - lazy var descriptionLabel: UILabel = { - let label = UILabel() - contentView.addSubview(label) - - label.text = - "You can choose username on NYNJA. If you do, other people will be able to find you by this username and contact you without your phone number.".localized - + "\n" - + "You can use a-z, 0-9 and underscores. Minimum lenght is 2 characters.".localized - - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - - label.numberOfLines = 0 - - label.snp.makeConstraints { (make) in - make.left.right.equalToSuperview().inset(16) - make.top.bottom.equalToSuperview() - } - - return label - }() - - override func prepareForReuse() { - super.prepareForReuse() - contentView.backgroundColor = UIColor.nynja.clear - backgroundColor = UIColor.nynja.clear - _ = [descriptionLabel] - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift deleted file mode 100644 index d62c12fbf..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/DestructiveActionTableViewCell.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// DestructiveActionTableViewCell.swift -// Nynja -// -// Created by Anton Poltoratskyi on 12/4/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -final class DestructiveActionTableViewCell: UITableViewCell { - - // MARK: - Views - - private(set) lazy var titleLabel: UILabel = { - let textLabel = UILabel(height: Constraints.titleLabel.fontHeight, - color: UIColor.nynja.mainRed, - font: FontFamily.NotoSans.medium) - - contentView.addSubview(textLabel) - textLabel.snp.makeConstraints { maker in - maker.left.equalToSuperview().offset(Constraints.titleLabel.left) - maker.right.equalToSuperview().inset(Constraints.titleLabel.right) - maker.centerY.equalToSuperview() - } - - return textLabel - }() - - - // MARK: - Init - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setup() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - - // MARK: - Setup - - private func setup() { - backgroundColor = .clear - contentView.backgroundColor = .clear - titleLabel.isHidden = false - } - - - // MARK: - Layout - - private enum Constraints { - - enum titleLabel { - static let fontHeight: CGFloat = 20 - static let left: CGFloat = 16 - static let right: CGFloat = 16 - } - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift deleted file mode 100644 index 6a231dff0..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/MaterialTextFieldTVCell.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// MaterialTextFieldTVCell.swift -// Nynja -// -// Created by Ash on 11/5/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class MaterialTextFieldTVCell: UITableViewCell, Configurable { - lazy var mainTextField: MaterialTextField = { - let textField = MaterialTextField() - contentView.addSubview(textField) - - textField.snp.makeConstraints { (make) in - make.left.right.equalToSuperview().inset(16) - make.centerY.equalToSuperview() - make.height.equalTo(65) - } - - return textField - }() - - struct Config { - let value: String - let fieldType: ProfileField - let textChangedHandler: ((ProfileField, String) -> Void)? - let shouldChangeTextHandler: ((ProfileField, String) -> Bool)? - } - - func configure(config: MaterialTextFieldTVCell.Config) { - mainTextField.text = config.value - contentView.backgroundColor = UIColor.nynja.clear - backgroundColor = UIColor.nynja.clear - - mainTextField.placeholder = config.fieldType.placeholder.localized + (config.fieldType.isRequired ? "*" : "") - mainTextField.textChanged = { (input) in - config.textChangedHandler?(config.fieldType, input.text) - } - - mainTextField.shouldTextChanged = { (input, range, string) in - return config.shouldChangeTextHandler?(config.fieldType, input.text + string) ?? true - } - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift deleted file mode 100644 index abfdfcb39..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSelectorTVCell.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// SettingsSelectorTVCell.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class SettingsSelectorTVCell: UITableViewCell, Configurable { - lazy var titleLabel: UILabel = { - let label = UILabel() - contentView.addSubview(label) - - label.textColor = UIColor.nynja.white - label.font = FontFamily.NotoSans.medium.font(size: 16) - - label.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - make.left.equalToSuperview().offset(16) - } - - return label - }() - - lazy var detailsLabel: UILabel = { - let label = UILabel() - contentView.addSubview(label) - - label.textColor = UIColor.nynja.dustyGray - label.font = FontFamily.NotoSans.medium.font(size: 16) - - label.snp.makeConstraints { (make) in - make.centerY.equalToSuperview() - - make.right.equalToSuperview().offset(-16) - } - - return label - }() - - lazy var separatorView: UIView = { - let view = UIView() - contentView.addSubview(view) - view.backgroundColor = UIColor.nynja.gray - view.snp.makeConstraints { (make) in - make.height.equalTo(1) - make.bottom.equalToSuperview() - make.left.right.equalToSuperview().inset(16) - } - - return view - }() - - struct Config { - let title: String - let details: String - } - - func configure(config: SettingsSelectorTVCell.Config) { - backgroundColor = UIColor.nynja.clear - contentView.backgroundColor = UIColor.nynja.clear - _ = [separatorView] - titleLabel.text = config.title - detailsLabel.text = config.details - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift deleted file mode 100644 index aab5fd586..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Cells/SettingsSetAvatarTVCell.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SettingsSetAvatarTVCell.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class SettingsSetAvatarTVCell: UITableViewCell, Configurable { - private var chooseAvatarAction: (() -> Void)? - - private lazy var avatarButton: UIButton = { - let button = UIButton() - button.addTarget(self, action: #selector(chooseAvatarAction(sender:)), for: .touchUpInside) - - contentView.addSubview(button) - button.snp.makeConstraints { maker in - maker.edges.equalTo(avatarImageView) - } - - return button - }() - - private(set) lazy var avatarImageView: UIImageView = { - let imageView = UIImageView() - contentView.addSubview(imageView) - - imageView.layer.cornerRadius = 47 - imageView.clipsToBounds = true - - imageView.snp.makeConstraints { (make) in - make.center.equalToSuperview() - make.height.width.equalTo(95) - } - - return imageView - }() - - struct Config { - let imageURL: URL? - let chooseAvatarAction: () -> Void - } - - func configure(config: Config) { - avatarImageView.setImage(url: config.imageURL, placeHolder: UIImage.nynja.icPhotoPlaceholder.image) - backgroundColor = UIColor.nynja.clear - contentView.backgroundColor = UIColor.nynja.clear - chooseAvatarAction = config.chooseAvatarAction - avatarImageView.isHidden = false - avatarButton.isHidden = false - } - - @objc func chooseAvatarAction(sender: UIImageView) { - chooseAvatarAction?() - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/ContactInfoSectionViewController.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/ContactInfoSectionViewController.swift new file mode 100644 index 000000000..b4cf9771d --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/ContactInfoSectionViewController.swift @@ -0,0 +1,67 @@ +// +// ContactInfoSectionViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/20/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +final class ContactInfoSectionViewController: UIViewController, UITableViewDelegate { + + private lazy var datasource = TableViewDataSource(tableView: self.tableView) + + // MARK: - Views + + var contentHeight: CGFloat { + return tableView.contentSize.height + } + + private lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.isScrollEnabled = false + tableView.backgroundColor = UIColor.nynja.clear + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + tableView.showsVerticalScrollIndicator = false + tableView.rowHeight = ContactInfoViewModel.Cell.Constraints.height + tableView.estimatedRowHeight = tableView.rowHeight + + tableView.register(viewModel: ContactInfoViewModel.self) + + view.addSubview(tableView) + tableView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return tableView + }() + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + } + + + // MARK: - View Input + + func setup(_ data: [ContactInfoViewModel]) { + tableView.dataSource = datasource + tableView.delegate = self + datasource.data = data + } + + + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let selectedModel = datasource.itemModel(at: indexPath) as? InteractiveCellViewModel else { + return + } + selectedModel.select?() + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift deleted file mode 100644 index 5249d716b..000000000 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/Header/SettingsSectionHeaderView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// SettingsSectionHeaderView.swift -// Nynja -// -// Created by Ash on 11/2/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation -import SnapKit - - -final class SettingsSectionHeaderView: UIView, Configurable { - lazy var label: UILabel = { - let label = UILabel() - addSubview(label) - - label.textColor = UIColor.nynja.dustyGray - label.textAlignment = .left - label.font = FontFamily.NotoSans.regular.font(size: 14) - - label.snp.makeConstraints { (make) in - make.left.right.equalToSuperview().inset(16) - make.centerY.equalToSuperview() - } - - return label - }() - - struct Config { - let title: String - } - - func configure(config: SettingsSectionHeaderView.Config) { - backgroundColor = UIColor.nynja.lightTransparentBlack - label.text = config.title - } -} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift index 983cc5aef..2622a8767 100644 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -7,8 +7,9 @@ // import Foundation +import NynjaUIKit -protocol AccountSettingsCoordinatorProtocol: class { +protocol AccountSettingsCoordinatorProtocol: AlertDisplayable { func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) } @@ -32,12 +33,10 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco } enum State { - case back - case chooseAvatar(completion: (UIImage?) -> Void) - case chooseStatus(completion: (UserStatus) -> Void) - case chooseTimeout(completion: (StatusTimeout) -> Void) - case addContact(completion: (Result) -> Void) - case contactDetails(contact: UserContact, completion: (Result) -> Void) + case dismiss + case chooseAvatar(source: ImageSource, completion: (UIImage?) -> Void) + case addContactInfo(type: ContactInfoInputModel.InputType, accountId: String) + case editContactInfo(ContactInfoInputModel.InputInfo, accountId: String) case deleteAccount(identityId: String, accountId: String) } @@ -60,28 +59,24 @@ final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtoco return view } - func back() { - coordinator.wireframe(self, didEndWithState: .back) + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) } - func chooseAvatar(completion: @escaping (UIImage?) -> Void) { - coordinator.wireframe(self, didEndWithState: .chooseAvatar(completion: completion)) + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) } - func chooseStatus(completion: @escaping (UserStatus) -> Void) { - coordinator.wireframe(self, didEndWithState: .chooseStatus(completion: completion)) + func chooseAvatar(from source: ImageSource, completion: @escaping (UIImage?) -> Void) { + coordinator.wireframe(self, didEndWithState: .chooseAvatar(source: source, completion: completion)) } - func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { - coordinator.wireframe(self, didEndWithState: .chooseTimeout(completion: completion)) + func addContactInfo(ofType contactInfoType: ContactInfoInputModel.InputType, accountId: String) { + coordinator.wireframe(self, didEndWithState: .addContactInfo(type: contactInfoType, accountId: accountId)) } - func addContact(completion: @escaping (Result) -> Void) { - coordinator.wireframe(self, didEndWithState: .addContact(completion: completion)) - } - - func contactDetails(contact: UserContact, completion: @escaping (Result) -> Void) { - coordinator.wireframe(self, didEndWithState: .contactDetails(contact: contact, completion: completion) ) + func editContactInfo(_ contactInfo: ContactInfoInputModel.InputInfo, accountId: String) { + coordinator.wireframe(self, didEndWithState: .editContactInfo(contactInfo, accountId: accountId)) } func deleteAccount(identityId: String, accountId: String) { diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift index 33f86a9e6..b1f2ba9e5 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift @@ -11,17 +11,22 @@ import UIKit // MARK: - Wireframe protocol ContactInfoManagementWireframeProtocol: AlertDisplayable { + func selectCountry(completion: @escaping (Result) -> Void) + func finish() func dismiss() } // MARK: - View protocol ContactInfoManagementViewInput: LoadingInteractive { + func setupContent(_ viewModel: ContactInfoManagementViewModel) + func setActionsEnabled(_ isEnabled: Bool) } // MARK: - Presenter protocol ContactInfoManagementPresenterProtocol: BasePresenterProtocol, NavigationProtocol { + func viewDidLoad() func delete() func save() } @@ -30,12 +35,15 @@ protocol ContactInfoManagementPresenterProtocol: BasePresenterProtocol, Navigati // MARK: Input protocol ContactInfoManagementInteractorInput: class { - func save(_ contactInfo: ContactInfoInputModel.ContactInfo) + func save(_ contactInfo: ContactInfoInputModel.InputInfo) + func edit(_ oldContactInfo: ContactInfoInputModel.InputInfo, by newContactInfo: ContactInfoInputModel.InputInfo) + func delete(_ contactInfo: ContactInfoInputModel.InputInfo) } // MARK: Output protocol ContactInfoManagementInteractorOutput: class { - func didDeleteContactInfo() func didSaveContactInfo() - func didReceiveFailure(_ error: Error?) + func didEditContactInfo() + func didDeleteContactInfo() + func didReceiveFailure(_ error: Error) } diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift index 591fde60f..df7824e3b 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift @@ -8,46 +8,52 @@ import Foundation -struct ContactInfoInputModel { - enum InfoType { +enum ContactInfoInputModel { + case empty(InputType) + case data(InputInfo) + + enum InputType: CaseIterable { case phoneNumber case email - case social - } - - enum Data { - case empty - case data(ContactInfo) + case social(SocialProvider) + + static var allCases: [InputType] { + var allCases: [InputType] = [.phoneNumber, .email] + allCases.append(contentsOf: SocialProvider.allCases.map { .social($0) }) + return allCases + } } - enum ContactInfo { + enum InputInfo { case phoneNumber(PhoneNumber) case email(String) - case social(SocialProfile) - - struct PhoneNumber { - enum Label { - case mobile - case home - case work - case custom(String) - } - - let numberInfo: PhoneNumberInfo - let label: Label - } + case social(SocialInfo) + } +} + +extension ContactInfoInputModel { + + struct PhoneNumber { + let number: PhoneNumberInfo + let label: PhoneNumberLabel + } + + struct SocialInfo { + let provider: SocialProvider + let link: String + } + + enum SocialProvider: CaseIterable { + case facebook + case twitter - struct SocialProfile { - enum Provider { - case facebook - case google - case twitter + var displayName: String { + switch self { + case .facebook: + return String.localizable.facebook + case .twitter: + return String.localizable.twitter } - let provider: Provider - let link: String } } - - let type: InfoType - let data: Data } diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/LinkValidator.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/LinkValidator.swift new file mode 100644 index 000000000..91ea4efb6 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/LinkValidator.swift @@ -0,0 +1,13 @@ +// +// LinkValidator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/18/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol LinkValidator: MTIValidator { + var domain: String { get } +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift new file mode 100644 index 000000000..3d4323d15 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift @@ -0,0 +1,34 @@ +// +// SocialLinkValidator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/18/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct SocialLinkValidator: LinkValidator { + + let domain: String + + var validationHandler: ((Bool) -> Void)? + + private var regexp: String { + return "\(domain)/[a-zA-Z0-9_/.]+" + } + + init(domain: String) { + self.domain = domain + } + + func validate(text: String) -> InputInfo? { + let predicate = NSPredicate(format: "SELF MATCHES %@", regexp) + let isValid = predicate.evaluate(with: text) + + validationHandler?(isValid) + + // FIXME: return errors + return nil + } +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift index 25413beac..a8ae2de0f 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift @@ -20,6 +20,8 @@ final class ContactInfoManagementInteractor: BaseInteractor, ContactInfoManageme private let accountService: AccountService + private let accountDAO: AccountDAOProtocol + // MARK: - Init @@ -27,19 +29,114 @@ final class ContactInfoManagementInteractor: BaseInteractor, ContactInfoManageme let presenter: ContactInfoManagementInteractorOutput let accountId: String let accountService: AccountService + let accountDAO: AccountDAOProtocol } init(dependencies: Dependencies) { presenter = dependencies.presenter accountId = dependencies.accountId accountService = dependencies.accountService + accountDAO = dependencies.accountDAO } // MARK: - Interactor Input - func save(_ contactInfo: ContactInfoInputModel.ContactInfo) { -// let contactDetails = NYNContactDetails -// accountService.addContactInfo(to: "", contactDetails: ) + func save(_ contactInfo: ContactInfoInputModel.InputInfo) { + let contactDetails = self.contactDetails(from: contactInfo) + + accountService.addContactInfo(contactDetails, to: accountId) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + do { + guard let dbContactInfo = DBContactInfo(accountId: self.accountId, contactInfo: contactDetails) else { + return + } + try self.accountDAO.saveContactInfo(dbContactInfo) + self.presenter?.didSaveContactInfo() + } catch { + self.presenter?.didReceiveFailure(error) + } + case let .failure(error): + self.presenter?.didReceiveFailure(error) + } + } + } + + func edit(_ oldContactInfo: ContactInfoInputModel.InputInfo, by newContactInfo: ContactInfoInputModel.InputInfo) { + let oldContactDetails = self.contactDetails(from: oldContactInfo) + let newContactDetails = self.contactDetails(from: newContactInfo) + + let updateInfo = UpdateInfo(oldValue: oldContactDetails, newValue: newContactDetails) + + accountService.editContactInfo(updateInfo, in: accountId) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + do { + guard let dbOldContactInfo = DBContactInfo(accountId: self.accountId, contactInfo: oldContactDetails) else { return } + guard let dbNewContactInfo = DBContactInfo(accountId: self.accountId, contactInfo: oldContactDetails) else { return } + + try self.accountDAO.editContactInfo(dbOldContactInfo, by: dbNewContactInfo) + self.presenter?.didSaveContactInfo() + } catch { + self.presenter?.didReceiveFailure(error) + } + case let .failure(error): + self.presenter?.didReceiveFailure(error) + } + } + } + + func delete(_ contactInfo: ContactInfoInputModel.InputInfo) { + let contactDetails = self.contactDetails(from: contactInfo) + + accountService.deleteContactInfo(contactDetails, from: accountId) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + do { + guard let dbContactInfo = DBContactInfo(accountId: self.accountId, contactInfo: contactDetails) else { + return + } + try self.accountDAO.deleteContactInfo(dbContactInfo) + self.presenter?.didDeleteContactInfo() + } catch { + self.presenter?.didReceiveFailure(error) + } + case let .failure(error): + self.presenter?.didReceiveFailure(error) + } + } + } + + private func contactDetails(from contactInfo: ContactInfoInputModel.InputInfo) -> NYNContactDetails { + let contactDetails = NYNContactDetails() + + switch contactInfo { + case let .phoneNumber(inputNumber): + contactDetails.type = .phone + contactDetails.value = inputNumber.number.fullNumber + contactDetails.label = inputNumber.label.title + + case let .email(inputEmail): + contactDetails.type = .email + contactDetails.value = inputEmail + + case let .social(inputSocialInfo): + switch inputSocialInfo.provider { + case .twitter: + contactDetails.type = .twitter + case .facebook: + contactDetails.type = .facebook + } + contactDetails.value = inputSocialInfo.link + } + + return contactDetails } } diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift index f5571c264..b91fdcf32 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift @@ -15,33 +15,309 @@ final class ContactInfoManagementPresenter: BasePresenter, ContactInfoManagement private var interactor: ContactInfoManagementInteractorInput! private var wireframe: ContactInfoManagementWireframeProtocol! + private let inputData: ContactInfoInputModel + + private var phoneTextController: PhoneNumberTextController! + + private lazy var contentViewModel: ContactInfoManagementViewModel = makeContentViewModel() + + + // MARK: - Init + + init(inputData: ContactInfoInputModel) { + self.inputData = inputData + super.init() + } + + + // MARK: - View Model + + private func makeContentViewModel() -> ContactInfoManagementViewModel { + let title: String + let viewModel: ContentViewModel + let deleteButtonAppearance: ContactInfoManagementViewModel.DeleteButtonAppearance + + switch inputData { + case let .empty(type): + switch type { + case .phoneNumber: + title = String.localizable.contactInfoPhoneNumberScreenTitle + viewModel = makePhoneNumberViewModel(for: nil) + deleteButtonAppearance = .hidden + + case .email: + title = String.localizable.contactInfoEmailScreenTitle + viewModel = makeEmailViewModel(for: nil) + deleteButtonAppearance = .hidden + + case let .social(socialProvider): + title = self.title(for: socialProvider) + viewModel = makeSocialViewModel(for: socialProvider, inputLink: nil) + deleteButtonAppearance = .hidden + } + case let .data(type): + switch type { + case let .phoneNumber(inputNumber): + title = inputNumber.number.displayString + viewModel = makePhoneNumberViewModel(for: inputNumber) + deleteButtonAppearance = .visible(title: String.localizable.contactInfoPhoneNumberDeleteButton) + + case let .email(inputEmail): + title = inputEmail + viewModel = makeEmailViewModel(for: inputEmail) + deleteButtonAppearance = .visible(title: String.localizable.contactInfoEmailDeleteButton) + + case let .social(inputSocialInfo): + title = inputSocialInfo.link + viewModel = makeSocialViewModel(for: inputSocialInfo.provider, inputLink: inputSocialInfo.link) + deleteButtonAppearance = .visible(title: String.localizable.contactInfoSocialDeleteButton) + } + } + + return ContactInfoManagementViewModel(title: title, content: viewModel, deleteButtonAppearance: deleteButtonAppearance) + } + + private func makePhoneNumberViewModel(for inputNumber: ContactInfoInputModel.PhoneNumber?) -> ContentViewModel { + let inputLabel = inputNumber?.label ?? .mobile + + let viewModel = PhoneNumberContactInfoViewModel( + controller: phoneTextController, + inputNumber: inputNumber?.number, + inputLabel: inputLabel + ) + + viewModel.countrySelectionHandler = { [weak wireframe, weak viewModel] in + wireframe?.selectCountry { [weak viewModel] result in + guard case let .success(country) = result else { + return + } + viewModel?.selectCountry(country) + } + } + + viewModel.labelSelectionHandler = { [weak wireframe, weak viewModel] in + let title = String.localizable.contactInfoPhoneNumberPickerAlertTitle + + let actions: [Alert.Action] = [ + .init(title: PhoneNumberLabel.mobile.title, style: .default) { _ in + viewModel?.selectLabel(.mobile) + }, + .init(title: PhoneNumberLabel.home.title, style: .default) { _ in + viewModel?.selectLabel(.home) + }, + .init(title: PhoneNumberLabel.work.title, style: .default) { _ in + viewModel?.selectLabel(.work) + }, + .init(title: String.localizable.phoneNumberLabelAddCustom, style: .default) { _ in + // TODO: present alert with text field + }, + .init(title: String.localizable.cancel, style: .cancel, handler: nil) + ] + + let alert = Alert(title: title, style: .actionSheet, actions: actions) + wireframe?.present(alert) + } + + viewModel.validationHandler = { [weak view] isEnabled in + view?.setActionsEnabled(isEnabled) + } + + return viewModel + } + + private func makeEmailViewModel(for inputEmail: String?) -> ContentViewModel { + let viewModel = TextFieldContentViewModel(validator: EmailTextValidator(), initialText: inputEmail) + + viewModel.contentInset = textFieldContentInset() + viewModel.placeholder = String.localizable.contactInfoEmailPlaceholder + viewModel.textContentType = .emailAddress + viewModel.keyboardType = .emailAddress + viewModel.returnKeyType = .done + + setupHandlers(for: viewModel) + + return viewModel + } + + private func makeSocialViewModel(for provider: ContactInfoInputModel.SocialProvider, inputLink: String?) -> ContentViewModel { + let validator = self.validator(for: provider) + + let viewModel = TextFieldContentViewModel(validator: validator, initialText: "\(validator.domain)/") + + viewModel.contentInset = textFieldContentInset() + viewModel.placeholder = self.placeholder(for: provider) + viewModel.keyboardType = .URL + viewModel.returnKeyType = .done + + setupHandlers(for: viewModel) + + return viewModel + } + + private func setupHandlers(for viewModel: TextFieldContentViewModel) { + var isValid = false + + viewModel.returnHandler = { [weak self] in + if isValid { + self?.save() + } + } + + viewModel.validationHandler = { [weak view] isEnabled in + isValid = isEnabled + view?.setActionsEnabled(isEnabled) + } + } + + private func textFieldContentInset() -> UIEdgeInsets { + let top = CGFloat(32.0.adjustedByWidth) + let horizontal = CGFloat(16.0.adjustedByWidth) + return UIEdgeInsets(top: top, left: horizontal, bottom: 0, right: horizontal) + } + + private func validator(for socialProvider: ContactInfoInputModel.SocialProvider) -> LinkValidator { + switch socialProvider { + case .facebook: + return SocialLinkValidator(domain: "facebook.com") + case .twitter: + return SocialLinkValidator(domain: "twitter.com") + } + } + + private func placeholder(for socialProvider: ContactInfoInputModel.SocialProvider) -> String { + switch socialProvider { + case .facebook: + return String.localizable.contactInfoFacebookPlaceholder + case .twitter: + return String.localizable.contactInfoTwitterPlaceholder + } + } + + private func title(for socialProvider: ContactInfoInputModel.SocialProvider) -> String { + switch socialProvider { + case .facebook: + return String.localizable.contactInfoFacebookScreenTitle + case .twitter: + return String.localizable.contactInfoTwitterScreenTitle + } + } + // MARK: - Presenter + func viewDidLoad() { + view?.setupContent(contentViewModel) + view?.setActionsEnabled(false) + } + func back() { wireframe.dismiss() } - func delete() { + func save() { + view?.showLoading() + switch contentViewModel.content { + case let viewModel as PhoneNumberContactInfoViewModel: + guard let inputNumber = viewModel.inputNumber else { + break + } + save(inputNumber, inputLabel: viewModel.inputLabel) + + case let viewModel as TextFieldContentViewModel: + save(viewModel.inputText) + + default: + break + } } - func save() { + private func save(_ inputNumber: PhoneNumberInfo, inputLabel: PhoneNumberLabel) { + let number = ContactInfoInputModel.PhoneNumber(number: inputNumber, label: inputLabel) + let contactInfo = ContactInfoInputModel.InputInfo.phoneNumber(number) + switch inputData { + case let .data(oldContactInfo): + interactor.edit(oldContactInfo, by: contactInfo) + case .empty: + interactor.save(contactInfo) + } } - // MARK: - Interactor Output + private func save(_ inputText: String) { + switch inputData { + case let .empty(type): + switch type { + case .email: + interactor.save(.email(inputText)) + + case let .social(socialProvider): + let inputSocialInfo = ContactInfoInputModel.SocialInfo(provider: socialProvider, link: inputText) + interactor.save(.social(inputSocialInfo)) + + case .phoneNumber: + // should never happen + break + } + + case let .data(oldContactInfo): + switch oldContactInfo { + case .email: + interactor.edit(oldContactInfo, by: .email(inputText)) + + case let .social(socialInfo): + let inputSocialInfo = ContactInfoInputModel.SocialInfo(provider: socialInfo.provider, link: inputText) + interactor.edit(oldContactInfo, by: .social(inputSocialInfo)) + + case .phoneNumber: + // should never happen + break + } + } + } - func didDeleteContactInfo() { + func delete() { + view?.showLoading() + switch inputData { + case let .data(contactInfo): + interactor.delete(contactInfo) + + case .empty: + // should never happen + break + } } + + // MARK: - Interactor Output + func didSaveContactInfo() { - + view?.hideLoading() + wireframe.finish() + } + + func didEditContactInfo() { + view?.hideLoading() + wireframe.finish() } - func didReceiveFailure(_ error: Error?) { + func didDeleteContactInfo() { + view?.hideLoading() + wireframe.finish() + } + + func didReceiveFailure(_ error: Error) { + view?.hideLoading() + + let title = "Failure" + let message = "Something went wrong" + let actions: [Alert.Action] = [ + .init(title: String.localizable.ok, style: .default, handler: nil) + ] + let alert = Alert(title: title, message: message, actions: actions) + wireframe.present(alert) } } @@ -53,11 +329,13 @@ extension ContactInfoManagementPresenter: SetInjectable { let view: ContactInfoManagementViewInput let interactor: ContactInfoManagementInteractorInput let wireframe: ContactInfoManagementWireframeProtocol + let phoneTextController: PhoneNumberTextController } func inject(dependencies: Dependencies) { view = dependencies.view interactor = dependencies.interactor wireframe = dependencies.wireframe + phoneTextController = dependencies.phoneTextController } } diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift index 3bfe23f67..1300e3b0f 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift @@ -20,27 +20,28 @@ final class ContactInfoManagementContainerViewController: BaseVC, ContactInfoMan private(set) lazy var progressHUD = makeProgressHUD(on: view) - private(set) lazy var contentView: UIView = { - let contentView = UIView() + private(set) lazy var inputContainerView: UIView = { + let containerView = UIView() - view.addSubview(contentView) - contentView.snp.makeConstraints { maker in + view.addSubview(containerView) + containerView.snp.makeConstraints { maker in maker.top.equalTo(navigationView.snp.bottom) maker.left.right.equalToSuperview() } - return contentView + return containerView }() - private(set) lazy var deleteButton: UIButton = { + private(set) lazy var deleteButton: DestructiveNynjaButton = { let height = Constraints.deleteButton.height + let fontHeight = Constraints.deleteButton.fontHeight let horizontal = Constraints.deleteButton.horizontal - let button = UIButton() + let button = DestructiveNynjaButton(font: FontFamily.NotoSans.medium, fontHeight: fontHeight) view.addSubview(button) button.snp.makeConstraints { maker in - maker.top.equalTo(contentView.snp.bottom) + maker.top.equalTo(inputContainerView.snp.bottom) maker.left.right.equalToSuperview().inset(horizontal) maker.height.equalTo(height) } @@ -50,9 +51,11 @@ final class ContactInfoManagementContainerViewController: BaseVC, ContactInfoMan private(set) lazy var saveButton: BaseNynjaButton = { let height = Constraints.saveButton.height + let fontHeight = Constraints.deleteButton.fontHeight let horizontal = Constraints.saveButton.horizontal - let button = BaseNynjaButton() + let button = RoundNynjaButton(font: FontFamily.NotoSans.medium, fontHeight: fontHeight) + button.setTitle(String.localizable.save, for: .normal) view.addSubview(button) button.snp.makeConstraints { maker in @@ -89,6 +92,11 @@ final class ContactInfoManagementContainerViewController: BaseVC, ContactInfoMan setupUI() } + override func viewDidLoad() { + super.viewDidLoad() + presenter.viewDidLoad() + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) registerForKeyboardNotifications() @@ -99,6 +107,11 @@ final class ContactInfoManagementContainerViewController: BaseVC, ContactInfoMan unregisterForKeyboardNotifications() } + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + view.endEditing(true) + } + // MARK: - UI Setup @@ -127,16 +140,44 @@ final class ContactInfoManagementContainerViewController: BaseVC, ContactInfoMan } + // MARK: - View Input + + func setActionsEnabled(_ isEnabled: Bool) { + saveButton.isEnabled = isEnabled + } + + func setupContent(_ viewModel: ContactInfoManagementViewModel) { + screenTitle = viewModel.title.uppercased() + + let contentView = viewModel.content.makeContentView() + + inputContainerView.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + switch viewModel.deleteButtonAppearance { + case let .visible(buttonTitle): + deleteButton.setTitle(buttonTitle, for: .normal) + deleteButton.isHidden = false + case .hidden: + deleteButton.isHidden = true + } + } + + // MARK: - Layout private enum Constraints { enum deleteButton { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth static let height: CGFloat = CGFloat(44).adjustedByWidth static let horizontal: CGFloat = CGFloat(16).adjustedByWidth } enum saveButton { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth static let height: CGFloat = CGFloat(44).adjustedByWidth static let horizontal: CGFloat = CGFloat(16).adjustedByWidth static let bottom: CGFloat = CGFloat(28).adjustedByWidth diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewModel.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewModel.swift new file mode 100644 index 000000000..5a5184dea --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewModel.swift @@ -0,0 +1,18 @@ +// +// ContactInfoManagementViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct ContactInfoManagementViewModel { + let title: String + let content: ContentViewModel + + enum DeleteButtonAppearance { + case hidden + case visible(title: String) + } + let deleteButtonAppearance: DeleteButtonAppearance +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/InputView/PhoneNumberContactInfoViewModel.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/InputView/PhoneNumberContactInfoViewModel.swift new file mode 100644 index 000000000..7706ba1a4 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/InputView/PhoneNumberContactInfoViewModel.swift @@ -0,0 +1,126 @@ +// +// PhoneNumberContactInfoViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class PhoneNumberContactInfoViewModel: ContentViewModel { + + private let controller: PhoneNumberTextController + + var countrySelectionHandler: (() -> Void)? + + var labelSelectionHandler: (() -> Void)? + + var validationHandler: ((Bool) -> Void)? { + didSet { + controller.validationAction = validationHandler + } + } + + private(set) var inputLabel: PhoneNumberLabel + + private let _inputNumber: PhoneNumberInfo? + + var inputNumber: PhoneNumberInfo? { + guard let view = numberInputView else { + return nil + } + let country = controller.country + + let text = view.phoneNumberTextField.text ?? "" + let phoneNumber = text.replacingOccurrences(of: " ", with: "") + + return PhoneNumberInfo(country: country, number: phoneNumber) + } + + private weak var numberPickerView: PickerRowItemView? + private weak var numberInputView: PhoneNumberLoginView? + + + // MARK: - Init + + init(controller: PhoneNumberTextController, inputNumber: PhoneNumberInfo?, inputLabel: PhoneNumberLabel) { + self.controller = controller + self._inputNumber = inputNumber + self.inputLabel = inputLabel + } + + + // MARK: - Actions + + func selectCountry(_ country: Country) { + numberInputView?.selectCountry(country) + } + + func selectLabel(_ label: PhoneNumberLabel) { + inputLabel = label + updateLabel(label) + } + + private func updateLabel(_ label: PhoneNumberLabel) { + numberPickerView?.accessoryButton.setTitle(label.title, for: .normal) + } + + @objc private func actionLabelButtonTapped(sender: Any) { + labelSelectionHandler?() + } + + + // MARK: - Content View + + func makeContentView() -> UIView { + let container = UIView() + + let typePickerView = makeTypePickerView() + + container.addSubview(typePickerView) + typePickerView.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + maker.height.equalTo(64.0.adjustedByWidth) + } + + let phoneNumberInputView = makePhoneInputView() + + container.addSubview(phoneNumberInputView) + phoneNumberInputView.snp.makeConstraints { maker in + maker.top.equalTo(typePickerView.snp.bottom).offset(16.0.adjustedByWidth) + maker.left.right.equalToSuperview().inset(16.0.adjustedByWidth) + maker.bottom.equalToSuperview() + } + + self.numberPickerView = typePickerView + self.numberInputView = phoneNumberInputView + + if let inputNumber = _inputNumber { + numberInputView?.updatePhoneNumber(inputNumber) + } + updateLabel(inputLabel) + + return container + } + + private func makeTypePickerView() -> PickerRowItemView { + let typePickerView = PickerRowItemView() + + typePickerView.titleLabel.text = String.localizable.phoneNumberPickerTitle + typePickerView.accessoryButton.addTarget(self, action: #selector(actionLabelButtonTapped(sender:)), for: .touchUpInside) + + return typePickerView + } + + private func makePhoneInputView() -> PhoneNumberLoginView { + let phoneNumberInputView = PhoneNumberLoginView(textController: controller) + phoneNumberInputView.configure(config: + .init( + country: controller.country, + countrySelectorAction: countrySelectionHandler + ) + ) + return phoneNumberInputView + } +} diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift index 519bb6d0b..9200c57e8 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift @@ -29,24 +29,34 @@ final class ContactInfoManagementWireframe: Wireframe, ContactInfoManagementWire struct Dependencies { let accountService: AccountService + let accountDAO: AccountDAOProtocol + let phoneTextController: PhoneNumberTextController } enum State { + case selectCountry(callback: (Result) -> Void) + case finish case dismiss } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { - let presenter = ContactInfoManagementPresenter() + let presenter = ContactInfoManagementPresenter(inputData: parameters.inputData) let view = ContactInfoManagementContainerViewController(dependencies: .init(presenter: presenter)) let interactor = ContactInfoManagementInteractor(dependencies: .init( presenter: presenter, accountId: parameters.accountId, - accountService: dependencies.accountService) + accountService: dependencies.accountService, + accountDAO: dependencies.accountDAO) ) - presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + presenter.inject(dependencies: .init( + view: view, + interactor: interactor, + wireframe: self, + phoneTextController: dependencies.phoneTextController) + ) return view } @@ -55,6 +65,14 @@ final class ContactInfoManagementWireframe: Wireframe, ContactInfoManagementWire coordinator.present(alert, completion: completion) } + func selectCountry(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .selectCountry(callback: completion)) + } + + func finish() { + coordinator.wireframe(self, didEndWithState: .finish) + } + func dismiss() { coordinator.wireframe(self, didEndWithState: .dismiss) } diff --git a/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift b/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift index 68c59ffbb..d6a47162d 100644 --- a/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift +++ b/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift @@ -25,11 +25,19 @@ final class AccountSettingsCoordinator: Coordinator, NavigationContainer { private let serviceFactory: ServiceFactoryProtocol + private var selectCountryCallback: ((Result) -> Void)? + + + // MARK: - Init + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { self.navigation = navigation self.serviceFactory = serviceFactory } + + // MARK: - Coordinator + func start() { let storageService = serviceFactory.makeStorageService() @@ -74,61 +82,90 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) { switch state { - case .back: + case .dismiss: end(with: .dismissed) - case let .addContact(completion): - showAddContactPopup(completion: completion) + case let .addContactInfo(contactInfoType, accountId): + showContactInfoManagment(with: .empty(contactInfoType), for: accountId) - case let .chooseAvatar(completion): - showAvatarSourceOptionPopup { [weak self] result in - guard case let .success(imageSource) = result else { - return - } - self?.getAvatar(source: imageSource, completion: completion) - } + case let .editContactInfo(contactInfo, accountId): + showContactInfoManagment(with: .data(contactInfo), for: accountId) - case let .chooseStatus(completion): - chooseStatus(completion: completion) + case let .chooseAvatar(imageSource, completion): + showAvatarSelector(source: imageSource, completion: completion) - case let .chooseTimeout(completion): - chooseTimeout(completion: completion) - - case let .contactDetails(contact, completion): - break - case let .deleteAccount(identityId, accountId): showDeleteAccount(identityId: identityId, accountId: accountId) } } +} + +// MARK: - Contact Info Management + +extension AccountSettingsCoordinator: ContactInfoManagementCoordinatorProtocol { - private func showAddContactPopup(completion: (Result) -> Void) { - let view = UIAlertController.init(title: "Add Contact Info".localized, message: nil, preferredStyle: .actionSheet) - - let phoneNumberAction = UIAlertAction(title: "Phone Number".localized, style: .default) { _ in -// completion(.fiveMin) - } - - let emailAction = UIAlertAction(title: "Email".localized, style: .default) { _ in -// completion(.fifteenMin) + func wireframe(_ wireframe: ContactInfoManagementWireframe, didEndWithState state: ContactInfoManagementWireframe.State) { + switch state { + case .dismiss, .finish: + navigation.popViewController(animated: true) + case let .selectCountry(callback): + selectCountryCallback = callback + showCountrySelector() } - - let facebookAction = UIAlertAction(title: "Facebook".localized, style: .default) { _ in -// completion(.thirtyMin) + } +} + +// MARK: - Country Selector + +extension AccountSettingsCoordinator: CountrySelectorCoordinatorProtocol { + + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { + switch state { + case let .selected(country): + selectCountryCallback?(.success(country)) + case .dismiss: + selectCountryCallback?(.failure(NavigationError.dismissed)) } - - let twitterAction = UIAlertAction(title: "Twitter".localized, style: .default) { _ in -// completion(.oneHour) + selectCountryCallback = nil + navigation.popViewController(animated: true) + } +} + +// MARK: - Delete Account + +extension AccountSettingsCoordinator: DeleteAccountCoordinatorProtocol { + + func wireframe(_ wireframe: DeleteAccountWireframe, didEndWithState state: DeleteAccountWireframe.State) { + switch state { + case .dismiss: + navigation.popViewController(animated: true) + case .accountDeleted: + end(with: .accountDeleted) } - - let cancelAction = UIAlertAction.init(title: "Cancel".localized, style: .cancel, handler: nil) - - [phoneNumberAction, emailAction, facebookAction, twitterAction, cancelAction].forEach { view.addAction($0) } - - navigation.present(view, animated: true, completion: nil) } +} + +// MARK: - Presentation + +private extension AccountSettingsCoordinator { - private func getAvatar(source: SelectAvatarFlowCoordinatorSource, completion: @escaping (UIImage?) -> Void) { + func showContactInfoManagment(with inputData: ContactInfoInputModel, for accountId: String) { + let wireframe = ContactInfoManagementWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init( + accountId: accountId, + inputData: inputData + ), + dependencies: .init( + accountService: serviceFactory.makeAccountService(), + accountDAO: serviceFactory.makeAccountDAO(), + phoneTextController: serviceFactory.makePhoneNumberTextController() + ) + ) + navigation.pushViewController(view, animated: true) + } + + func showAvatarSelector(source: ImageSource, completion: @escaping (UIImage?) -> Void) { let coordinatorDependencies = SelectAvatarFlowCoordinator.Dependencies( source: source, rootViewController: navigation.viewControllers.last!, @@ -140,82 +177,7 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { coordinator.start() } - private func showAvatarSourceOptionPopup(completion: @escaping (Result) -> Void) { - enum AvatarSourceError: Error { - case cancelled - } - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - - let camera = UIAlertAction(title: String.localizable.alertActionTakeFromCamera, style: .default) { _ in - completion(.success(.camera)) - } - - let gallery = UIAlertAction(title: String.localizable.alertActionTakeFromGallery, style: .default) { _ in - completion(.success(.gallery)) - } - - let cancel = UIAlertAction(title: String.localizable.cancel, style: .cancel) { _ in - completion(.failure(AvatarSourceError.cancelled)) - } - - [camera, gallery, cancel].forEach { alert.addAction($0) } - - navigation.present(alert, animated: true, completion: nil) - } - - private func chooseStatus(completion: @escaping (UserStatus) -> Void) { - let view = UIAlertController(title: "Status".localized, message: nil, preferredStyle: .actionSheet) - - let activeAction = UIAlertAction(title: "Active".localized, style: .default) { _ in - completion(.active) - } - - let inactiveAction = UIAlertAction(title: "Inactive".localized, style: .default) { _ in - completion(.inactive) - } - - let busyAction = UIAlertAction(title: "Busy".localized, style: .default) { _ in - completion(.busy) - } - - let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil) - - [activeAction, inactiveAction, busyAction, cancelAction].forEach { view.addAction($0) } - - navigation.present(view, animated: true, completion: nil) - } - - private func chooseTimeout(completion: @escaping (StatusTimeout) -> Void) { - let view = UIAlertController.init(title: "Idle Timeout".localized, message: nil, preferredStyle: .actionSheet) - - let timeout5MinAction = UIAlertAction(title: "5 min".localized, style: .default) { _ in - completion(.fiveMin) - } - - let timeout15MinAction = UIAlertAction(title: "15 min".localized, style: .default) { _ in - completion(.fifteenMin) - } - - let timeout30MinAction = UIAlertAction(title: "30 min".localized, style: .default) { _ in - completion(.thirtyMin) - } - - let timeout60MinAction = UIAlertAction(title: "60 min".localized, style: .default) { _ in - completion(.oneHour) - } - - let timeoutNeverAction = UIAlertAction(title: "Never".localized, style: .default) { _ in - completion(.never) - } - - let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil) - - [timeout5MinAction, timeout15MinAction, timeout30MinAction, timeout60MinAction, timeoutNeverAction, cancelAction].forEach { view.addAction($0) } - - navigation.present(view, animated: true, completion: nil) - } - - private func showDeleteAccount(identityId: String, accountId: String) { + func showDeleteAccount(identityId: String, accountId: String) { let wireframe = DeleteAccountWireframe(coordinator: self) let view = wireframe.prepareModule( parameters: .init( @@ -229,30 +191,14 @@ extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { navigation.pushViewController(view, animated: true) } -} - -// MARK: - Contact Info Management - -extension AccountSettingsCoordinator: ContactInfoManagementCoordinatorProtocol { - func wireframe(_ wireframe: ContactInfoManagementWireframe, didEndWithState state: ContactInfoManagementWireframe.State) { - switch state { - case .dismiss: - navigation.popViewController(animated: true) - } - } -} - -// MARK: - Delete Account - -extension AccountSettingsCoordinator: DeleteAccountCoordinatorProtocol { - - func wireframe(_ wireframe: DeleteAccountWireframe, didEndWithState state: DeleteAccountWireframe.State) { - switch state { - case .dismiss: - navigation.popViewController(animated: true) - case .accountDeleted: - end(with: .accountDeleted) - } + func showCountrySelector() { + let wireframe = SelectCountryWireFrame(coordinator: self) + let view = wireframe.prepareModule( + dependencies: .init( + countriesProvider: serviceFactory.makeCountriesProvider() + ) + ) + navigation.pushViewController(view, animated: true) } } diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift index 6a09e753a..49fc21e3c 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift @@ -12,15 +12,12 @@ protocol AnyFieldRowItem: class { typealias ResponderAction = (Bool) -> Void var height: CGFloat? { get } + var additionalInset: UIEdgeInsets? { get } - var nextResponder: AnyFieldRowItem? { get } - var responderAction: ResponderAction? { get } - - func becomeFirstResponder() - func resignFirstResponder() + func becomeFirstResponder() -> Bool + func resignFirstResponder() -> Bool func makeView() -> UIView - func configure(_ view: UIView) } extension AnyFieldRowItem { @@ -29,23 +26,15 @@ extension AnyFieldRowItem { return nil } - var nextResponder: AnyFieldRowItem? { - return nil - } - - var responderAction: ResponderAction? { + var additionalInset: UIEdgeInsets? { return nil } - var hasNextResponder: Bool { - return nextResponder != nil - } - - func becomeFirstResponder() { - responderAction?(true) + func becomeFirstResponder() -> Bool { + return false } - func resignFirstResponder() { - responderAction?(false) + func resignFirstResponder() -> Bool { + return false } } diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift index 94fcfeab6..893d78d63 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift @@ -11,17 +11,6 @@ import Foundation protocol FieldRowItem: AnyFieldRowItem { associatedtype View: UIView - func makeView() -> View + func makeView() -> UIView func configure(_ view: View) } - -extension FieldRowItem { - - func configure(_ view: UIView) { - configure(view as! View) - } - - func makeView() -> UIView { - return makeView() as View - } -} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Form.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Form.swift index e0af5c120..c26f6eae4 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Form.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Form.swift @@ -11,11 +11,14 @@ import Foundation final class Form { final class Section { - var header: AnyFieldRowItem? - var rows: [AnyFieldRowItem] = [] + var header: FormHeader? + var rows: [AnyFieldRowItem] + var contentInset: UIEdgeInsets - init(header: AnyFieldRowItem? = nil, rows: [AnyFieldRowItem]) { + init(header: FormHeader? = nil, rows: [AnyFieldRowItem], contentInset: UIEdgeInsets = .zero) { + self.header = header self.rows = rows + self.contentInset = contentInset } } diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormHeaderView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormHeaderView.swift new file mode 100644 index 000000000..0f601030d --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormHeaderView.swift @@ -0,0 +1,59 @@ +// +// FormHeaderView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/18/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +final class FormHeader { + let title: String + let height: CGFloat + + init(title: String, height: CGFloat) { + self.title = title + self.height = height + } + + func makeView() -> UIView? { + let view = FormHeaderView() + + view.titleLabel.text = title + view.snp.makeConstraints { maker in + maker.height.equalTo(height) + } + + return view + } +} + +final class FormHeaderView: BaseView { + + // MARK: - Views + + private(set) lazy var titleLabel: UILabel = { + let label = UILabel(height: CGFloat(20.0.adjustedByWidth), color: UIColor.nynja.dustyGray, font: FontFamily.NotoSans.regular) + label.textAlignment = .left + + addSubview(label) + label.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(16.0.adjustedByWidth) + make.centerY.equalToSuperview() + } + + return label + }() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + backgroundColor = UIColor.nynja.lightTransparentBlack + titleLabel.isHidden = false + } +} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift index f2fd87954..1f283778f 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift @@ -23,6 +23,14 @@ final class ActionRowItem: FieldRowItem { let icon: UIImage let height: CGFloat? + var backgroundColor: UIColor = .clear + var edges: UIEdgeInsets = UIEdgeInsets( + top: 0, + left: CGFloat(16.0.adjustedByWidth), + bottom: 0, + right: CGFloat(16.0.adjustedByWidth) + ) + var isEnabled: Bool = true { didSet { view?.isEnabled = isEnabled @@ -32,7 +40,12 @@ final class ActionRowItem: FieldRowItem { private weak var view: View? - init(text: String, font: UIFont, textColor: UIColor, icon: UIImage, height: CGFloat, action: Action?) { + init(text: String, + font: UIFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 22.0.adjustedByWidth)!, + textColor: UIColor = UIColor.nynja.white, + icon: UIImage = UIImage.nynja.icAdd.image, + height: CGFloat, + action: Action?) { self.text = text self.font = font self.textColor = textColor @@ -41,7 +54,7 @@ final class ActionRowItem: FieldRowItem { self.action = action } - func makeView() -> View { + func makeView() -> UIView { let view = View() self.view = view diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/AvatarRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/AvatarRowItemView.swift new file mode 100644 index 000000000..d24667d39 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/AvatarRowItemView.swift @@ -0,0 +1,113 @@ +// +// AvatarRowItemView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/18/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +// MARK: - Model + +final class AvatarRowItem: FieldRowItem { + + typealias View = AvatarRowItemView + + var imageSource: ImageData { + didSet { + setupImageFromSource() + } + } + + let imagePlaceholder: UIImage? + + let imageSize: CGSize + + let height: CGFloat? + + var additionalInset: UIEdgeInsets? + + var imageSelectionHandler: (() -> Void)? + + private weak var view: View? + + init(imageSource: ImageData, + imagePlaceholder: UIImage? = UIImage.nynja.icPhotoPlaceholder.image, + imageSize: CGSize, + height: CGFloat) { + + self.imageSource = imageSource + self.imagePlaceholder = imagePlaceholder + self.imageSize = imageSize + self.height = height + } + + private func setupImageFromSource() { + switch imageSource { + case let .image(image): + view?.avatarImageView.image = image + case let .url(url): + view?.avatarImageView.setImage(url: url, placeHolder: imagePlaceholder) + } + } + + func makeView() -> UIView { + let view = View() + + self.view = view + configure(view) + + return view + } + + func configure(_ view: View) { + setupImageFromSource() + view.chooseAvatarAction = { [weak self] in + self?.imageSelectionHandler?() + } + view.avatarImageView.snp.makeConstraints { maker in + maker.size.equalTo(imageSize) + } + } +} + +final class AvatarRowItemView: BaseView { + + fileprivate var chooseAvatarAction: (() -> Void)? + + private(set) lazy var avatarButton: UIButton = { + let button = UIButton() + button.addTarget(self, action: #selector(chooseAvatarAction(sender:)), for: .touchUpInside) + + addSubview(button) + button.snp.makeConstraints { maker in + maker.edges.equalTo(avatarImageView) + } + + return button + }() + + private(set) lazy var avatarImageView: RoundImageView = { + let imageView = RoundImageView() + addSubview(imageView) + + imageView.snp.makeConstraints { (make) in + make.center.equalToSuperview() + } + + return imageView + }() + + override func baseSetup() { + super.baseSetup() + avatarButton.isHidden = false + avatarImageView.isHidden = false + } + + @objc func chooseAvatarAction(sender: UIButton) { + chooseAvatarAction?() + } +} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/DestructiveActionRowItem.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/DestructiveActionRowItem.swift new file mode 100644 index 000000000..b7e0b24a6 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/DestructiveActionRowItem.swift @@ -0,0 +1,77 @@ +// +// DestructiveActionRowItem.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/18/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +final class DestructiveActionRowItem: FieldRowItem { + + typealias View = DestructiveNynjaButton + typealias Action = (DestructiveNynjaButton) -> Void + + let title: String + + let height: CGFloat? + + var additionalInset: UIEdgeInsets? + + var edges: UIEdgeInsets = UIEdgeInsets( + top: 0, + left: CGFloat(16.0.adjustedByWidth), + bottom: 0, + right: CGFloat(16.0.adjustedByWidth) + ) { + didSet { + view?.snp.updateConstraints { maker in + maker.edges.equalTo(edges) + } + } + } + + var isEnabled: Bool = true { + didSet { + view?.isEnabled = isEnabled + } + } + let action: Action? + + private weak var view: DestructiveNynjaButton? + + init(title: String, height: CGFloat, action: Action?) { + self.title = title + self.height = height + self.action = action + } + + func makeView() -> UIView { + let view = UIView() + + let button = View() + + view.addSubview(button) + button.snp.makeConstraints { maker in + maker.edges.equalTo(edges) + } + + self.view = button + configure(button) + + return view + } + + func configure(_ view: View) { + view.isEnabled = isEnabled + view.setTitle(title, for: .normal) + view.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) + } + + @objc private func actionButtonTapped(sender: View) { + action?(sender) + } +} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/PickerRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/PickerRowItemView.swift new file mode 100644 index 000000000..f21b63142 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/PickerRowItemView.swift @@ -0,0 +1,140 @@ +// +// PickerRowItemView.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +// MARK: - Model + +final class PickerRowItem: FieldRowItem { + + typealias View = PickerRowItemView + + private let title: String + private(set) var inputLabel: String + + let height: CGFloat? + var labelSelectionHandler: (() -> Void)? + + private weak var view: View? + + init(title: String, label: String, height: CGFloat? = nil) { + self.title = title + self.inputLabel = label + self.height = height + } + + func makeView() -> UIView { + let view = View() + + self.view = view + configure(view) + + return view + } + + func selectLabel(_ label: String) { + inputLabel = label + view?.accessoryButton.setTitle(label, for: .normal) + } + + func configure(_ view: View) { + view.titleLabel.text = title + view.accessoryButton.setTitle(inputLabel, for: .normal) + view.accessoryButton.addTarget(self, action: #selector(accessoryButtonTapped(sender:)), for: .touchUpInside) + } + + @objc private func accessoryButtonTapped(sender: Any) { + labelSelectionHandler?() + } +} + + +// MARK: - View - + +final class PickerRowItemView: BaseView { + + // MARK: - Views + + private(set) lazy var titleLabel: UILabel = { + let height = Constraints.titleLabel.fontHeight + let horizontalInset = Constraints.horizontal + + let label = UILabel(height: height, color: UIColor.nynja.white, font: FontFamily.NotoSans.medium) + + addSubview(label) + label.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalTo(accessoryButton.snp.left) + } + + return label + }() + + private(set) lazy var accessoryButton: UIButton = { + let fontHeight = Constraints.accessoryButton.fontHeight + let width = Constraints.accessoryButton.width + let horizontalInset = Constraints.horizontal + + let button = UIButton() + button.titleLabel?.font = UIFont.makeFont(with: FontFamily.NotoSans.regular, height: fontHeight) + button.setTitleColor(UIColor.nynja.manatee, for: .normal) + button.contentEdgeInsets.left = horizontalInset + button.contentEdgeInsets.right = horizontalInset + + addSubview(button) + button.snp.makeConstraints { maker in + maker.top.bottom.right.equalToSuperview() + maker.width.equalTo(width) + } + + return button + }() + + private(set) lazy var separatorView: SeparatorView = { + let separatorView = SeparatorView() + + addSubview(separatorView) + separatorView.snp.makeConstraints { maker in + maker.bottom.equalToSuperview() + } + + return separatorView + }() + + + // MARK: - Setup + + override func baseSetup() { + super.baseSetup() + titleLabel.isHidden = false + accessoryButton.isHidden = false + accessoryButton.contentHorizontalAlignment = .right + + separatorView.snp.makeConstraints { maker in + maker.left.right.equalToSuperview().inset(Constraints.horizontal) + } + } + + + // MARK: - Layout + + private enum Constraints { + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + + enum titleLabel { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + } + enum accessoryButton { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + static let width: CGFloat = CGFloat(156).adjustedByWidth + } + } +} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift index 634439e2d..740ef2640 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift @@ -15,74 +15,109 @@ import NynjaUIKit final class TextFieldRowItem: FieldRowItem { typealias View = TextFieldRowItemView - typealias TextChangeAction = (View) -> Void - - struct FieldConfig { - var edges: UIEdgeInsets - var font: UIFont - var text: String? - var placeholder: String? = nil - var textColor: UIColor? - var placeholderColor: UIColor? = .black - var keyboardType: UIKeyboardType = .default - var keyboardAppearance: UIKeyboardAppearance = .default - var returnKeyType: UIReturnKeyType = .default - var textContentType: UITextContentType? = nil - var autocapitalizationType: UITextAutocapitalizationType = .sentences - var isSecureTextEntry: Bool = false - } + typealias TextChangeAction = (String) -> Void - let validator: Validator? - let config: FieldConfig - let height: CGFloat? - let textChangeAction: TextChangeAction? + let validator: MTIValidator? + + var font: UIFont = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 22.0.adjustedByWidth)! + + var text: String? + var placeholder: String? + + var textColor = UIColor.nynja.white + var placeholderColor = UIColor.nynja.dustyGray + var separatorColor = UIColor.nynja.dustyGray + + var keyboardType: UIKeyboardType = .default + var keyboardAppearance: UIKeyboardAppearance = .default + var returnKeyType: UIReturnKeyType = .default + var textContentType: UITextContentType? + var autocapitalizationType: UITextAutocapitalizationType = .sentences + var isSecureTextEntry: Bool = false - weak var nextResponder: AnyFieldRowItem? + var edges: UIEdgeInsets = UIEdgeInsets( + top: 0, + left: CGFloat(16.0.adjustedByWidth), + bottom: 0, + right: CGFloat(16.0.adjustedByWidth) + ) - private(set) var responderAction: ResponderAction? + let height: CGFloat? + let additionalInset: UIEdgeInsets? + + var textChangeAction: TextChangeAction? + var returnHandler: (() -> Void)? + var inputText: String? { + get { return view?.textField.text } + set { view?.textField.text = newValue ?? "" } + } + private weak var view: View? - init(validator: Validator?, config: FieldConfig, height: CGFloat, textChangeAction: TextChangeAction?) { + init(validator: MTIValidator? = nil, height: CGFloat? = nil, additionalInset: UIEdgeInsets? = nil) { self.validator = validator - self.config = config self.height = height - self.textChangeAction = textChangeAction + self.additionalInset = additionalInset } - func makeView() -> View { + func makeView() -> UIView { let view = View() self.view = view configure(view) + view.textField.snp.makeConstraints { maker in + maker.edges.equalToSuperview().inset(edges) + } + return view } func configure(_ view: View) { - responderAction = { [weak view] isFirstResponder in - if isFirstResponder { - _ = view?.textField.becomeFirstResponder() - } else { - _ = view?.textField.resignFirstResponder() - } + let textField = view.textField + + textField.returnHandler = { [weak self] _ in + self?.returnHandler?() + return false } - view.textField.snp.updateConstraints { maker in - maker.edges.equalToSuperview().inset(config.edges) + textField.textChanged = { [weak self] field in + self?.textChangeAction?(field.text) } - view.textField.returnKeyType = hasNextResponder ? .next : .done + textField.font = font - view.textField.returnHandler = { [weak self] _ in - if let responder = self?.nextResponder { - responder.becomeFirstResponder() - } else { - _ = self?.resignFirstResponder() - } - return false + textField.text = text ?? "" + textField.placeholder = placeholder + + textField.textColor = textColor + textField.placeholderColor = placeholderColor + textField.separatorColor = separatorColor + + textField.keyboardType = keyboardType + textField.keyboardAppearance = keyboardAppearance + textField.returnKeyType = returnKeyType + textField.textContentType = textContentType + textField.autocapitalizationType = autocapitalizationType + textField.isSecureTextEntry = isSecureTextEntry + + validator.flatMap { textField.validators = [$0] } + + textField.snp.updateConstraints { maker in + maker.edges.equalToSuperview().inset(edges) } } + + func becomeFirstResponder() -> Bool { + _ = view?.textField.becomeFirstResponder() + return true + } + + func resignFirstResponder() -> Bool { + _ = view?.textField.resignFirstResponder() + return true + } } // MARK: - View - diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift index 30ed44013..43f927ce4 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift @@ -17,14 +17,24 @@ final class TextRowItem: FieldRowItem { typealias View = TextRowItemView let text: String - let font: UIFont - let textColor: UIColor - let backgroundColor: UIColor - let edges: UIEdgeInsets + var font: UIFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: 20.0.adjustedByWidth)! + var textColor: UIColor = UIColor.nynja.manatee + var backgroundColor: UIColor = .clear + var edges: UIEdgeInsets = UIEdgeInsets( + top: 0, + left: CGFloat(16.0.adjustedByWidth), + bottom: 0, + right: CGFloat(16.0.adjustedByWidth) + ) + var additionalInset: UIEdgeInsets? private weak var view: View? - init(text: String, font: UIFont, textColor: UIColor, backgroundColor: UIColor, edges: UIEdgeInsets) { + init(text: String) { + self.text = text + } + + init(text: String, font: UIFont, textColor: UIColor, backgroundColor: UIColor = .clear, edges: UIEdgeInsets = .zero) { self.text = text self.font = font self.textColor = textColor @@ -32,7 +42,7 @@ final class TextRowItem: FieldRowItem { self.edges = edges } - func makeView() -> View { + func makeView() -> UIView { let view = View() self.view = view diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift index 5100623da..dd2d7ab4b 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift @@ -47,7 +47,7 @@ final class LoginOptionSwitchRowItem: SwitchRowItem, FieldRowItem { super.init(isEnabled: option.isAvailableForSearch, height: height, switchHandler: switchHandler) } - func makeView() -> View { + func makeView() -> UIView { let view = View() self.view = view diff --git a/Nynja/Modules/Flows/AppCoordinator.swift b/Nynja/Modules/Flows/AppCoordinator.swift index aefdd82ad..c1845c7fc 100644 --- a/Nynja/Modules/Flows/AppCoordinator.swift +++ b/Nynja/Modules/Flows/AppCoordinator.swift @@ -7,6 +7,7 @@ // import UIKit +import NynjaUIKit protocol AppCoordinatorInput: Coordinator { func logout() diff --git a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift index 8da21093e..2773df302 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift @@ -81,11 +81,8 @@ extension AuthCoordinator: SplashCoordinatorProtocol { func wireframe(_ wireframe: SplashWireframe, didEndWithState state: SplashWireframe.State) { switch state { - case .showTutorial: - showTutorial() - - case .showAuth, .showCreateProfile: - // TODO: show create profile if needed in new UI flow. + case .showTutorial, .showAuth, .showCreateProfile: + // TODO: show (tutorial / create profile) if needed in new UI flow. showAuth() case let .showMain(isRegistered, checkSession): @@ -94,6 +91,7 @@ extension AuthCoordinator: SplashCoordinatorProtocol { } } + @available(*, deprecated, message: "Won't exists in new auth flow. Remove this method when Tutorial module will be removed") private func showTutorial() { let wireframe = TutorialWireFrame(coordinator: self, navigation: navigation) let view = wireframe.prepareModule() @@ -212,7 +210,7 @@ extension AuthCoordinator: FacebookAuthCoordinatorProtocol { extension AuthCoordinator: CountrySelectorCoordinatorProtocol { func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) { switch state { - case .selected(let country): + case let .selected(country): selectCountryCallback?(.success(country)) case .dismiss: selectCountryCallback?(.failure(NavigationError.dismissed)) @@ -292,7 +290,7 @@ extension AuthCoordinator: CreateProfileCoordinatorProtocol { } // FIXME: should be in presenter - private func showAvatarSourceOptionPopup(completion: @escaping (Result) -> Void) { + private func showAvatarSourceOptionPopup(completion: @escaping (Result) -> Void) { enum AvatarSourceError: Error { case cancelled } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift index b6490367c..59e389ee2 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -84,13 +84,18 @@ extension PhoneNumberLoginView { ) } + private func updatePhoneCountry(_ country: Country) { + updateCountryInfo(for: country) + } + func selectCountry(_ country: Country) { updatePhoneCountry(country) phoneNumberTextField.text = "" } - private func updatePhoneCountry(_ country: Country) { - updateCountryInfo(for: country) + func updatePhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) { + updatePhoneCountry(phoneNumberInfo.country) + phoneNumberTextField.text = phoneNumberInfo.number } } diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/EmailContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/Entities/EmailTextValidator.swift similarity index 51% rename from Nynja/Modules/Flows/Search Flow/View/InputView/EmailContentViewModel.swift rename to Nynja/Modules/Flows/Search Flow/Entities/EmailTextValidator.swift index fa8df777f..4720b29ff 100644 --- a/Nynja/Modules/Flows/Search Flow/View/InputView/EmailContentViewModel.swift +++ b/Nynja/Modules/Flows/Search Flow/Entities/EmailTextValidator.swift @@ -1,5 +1,5 @@ // -// EmailContentViewModel.swift +// EmailTextValidator.swift // Nynja // // Created by Anton Poltoratskyi on 12/12/18. @@ -23,17 +23,3 @@ struct EmailTextValidator: MTIValidator { return nil } } - -final class EmailContentViewModel: TextFieldContentViewModel, ContentViewModel { - - func makeContentView() -> UIView { - let textField = makeInputField() - - textField.placeholder = String.localizable.searchContactEmailPlaceholder - textField.textContentType = .emailAddress - textField.keyboardType = .emailAddress - textField.returnKeyType = .search - - return textField - } -} diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/UsernameContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/Entities/UsernameTextValidator.swift similarity index 57% rename from Nynja/Modules/Flows/Search Flow/View/InputView/UsernameContentViewModel.swift rename to Nynja/Modules/Flows/Search Flow/Entities/UsernameTextValidator.swift index 4b2f85ec1..d3699ef4f 100644 --- a/Nynja/Modules/Flows/Search Flow/View/InputView/UsernameContentViewModel.swift +++ b/Nynja/Modules/Flows/Search Flow/Entities/UsernameTextValidator.swift @@ -1,5 +1,5 @@ // -// UsernameContentViewModel.swift +// UsernameTextValidator.swift // Nynja // // Created by Anton Poltoratskyi on 12/12/18. @@ -24,16 +24,3 @@ struct UsernameTextValidator: MTIValidator { return nil } } - -final class UsernameContentViewModel: TextFieldContentViewModel, ContentViewModel { - - func makeContentView() -> UIView { - let textField = makeInputField() - - textField.placeholder = String.localizable.searchContactUsernamePlaceholder - textField.keyboardType = .default - textField.returnKeyType = .search - - return textField - } -} diff --git a/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift b/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift index cb7a7a19b..197d3dafe 100644 --- a/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift +++ b/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift @@ -86,7 +86,11 @@ final class SearchContactPresenter: BasePresenter, SearchContactPresenterProtoco private func makeEmailViewModel() -> ContentViewModel { var isValid = false - let viewModel = EmailContentViewModel(validator: EmailTextValidator()) + let viewModel = TextFieldContentViewModel(validator: EmailTextValidator()) + viewModel.placeholder = String.localizable.searchContactEmailPlaceholder + viewModel.textContentType = .emailAddress + viewModel.keyboardType = .emailAddress + viewModel.returnKeyType = .search viewModel.returnHandler = { [weak self] in if isValid { @@ -105,7 +109,10 @@ final class SearchContactPresenter: BasePresenter, SearchContactPresenterProtoco private func makeUsernameViewModel() -> ContentViewModel { var isValid = false - let viewModel = UsernameContentViewModel(validator: UsernameTextValidator()) + let viewModel = TextFieldContentViewModel(validator: UsernameTextValidator()) + viewModel.placeholder = String.localizable.searchContactUsernamePlaceholder + viewModel.keyboardType = .default + viewModel.returnKeyType = .search viewModel.returnHandler = { [weak self] in if isValid { @@ -135,10 +142,10 @@ final class SearchContactPresenter: BasePresenter, SearchContactPresenterProtoco case let viewModel as PhoneNumberContentViewModel: interactor.searchByPhoneNumber(viewModel.inputNumber) - case let viewModel as UsernameContentViewModel: + case let viewModel as TextFieldContentViewModel where inputMode == .username: interactor.searchByUsername(viewModel.inputText) - case let viewModel as EmailContentViewModel: + case let viewModel as TextFieldContentViewModel where inputMode == .email: interactor.searchByEmail(viewModel.inputText) default: @@ -196,7 +203,15 @@ final class SearchContactPresenter: BasePresenter, SearchContactPresenterProtoco } private func openAccountDetails(for searchResponse: SearchContactResponse) { - wireframe.showAccountDetails(for: searchResponse) +// wireframe.showAccountDetails(for: searchResponse) + let title = "Search Result" + let message = searchResponse.fullName + let actions: [Alert.Action] = [ + .init(title: String.localizable.ok, style: .default, handler: nil) + ] + + let alert = Alert(title: title, message: message, actions: actions) + wireframe.present(alert) } private func handlePhonebookResponse(_ phonebookContacts: [PhoneContact]) { diff --git a/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift index 7e4851b55..7273f8055 100644 --- a/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift +++ b/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift @@ -6,35 +6,78 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +import UIKit -class TextFieldContentViewModel { +class TextFieldContentViewModel: ContentViewModel { private var validator: MTIValidator var returnHandler: (() -> Void)? + private let initialText: String? + var inputText: String { - guard let view = view else { fatalError("view = nil") } - return view.text.trimmed() + return view?.text.trimmed() ?? "" + } + + var placeholder: String? { + didSet { + view?.placeholder = placeholder + } + } + + var textContentType: UITextContentType? { + didSet { + view?.textContentType = textContentType + } + } + + var keyboardType: UIKeyboardType = .default { + didSet { + view?.keyboardType = keyboardType + } + } + + var returnKeyType: UIReturnKeyType = .default { + didSet { + view?.textContentType = textContentType + } } var validationHandler: ((Bool) -> Void)? { didSet { - validator.validationHandler = validationHandler + view?.validationHandler = validationHandler + } + } + + var contentInset: UIEdgeInsets = .zero { + didSet { + view?.snp.updateConstraints { maker in + maker.top.equalToSuperview().offset(contentInset.top) + maker.left.equalToSuperview().offset(contentInset.left) + maker.right.equalToSuperview().inset(contentInset.right) + maker.bottom.equalToSuperview().inset(contentInset.bottom) + } } } private weak var view: MaterialTextField? - init(validator: MTIValidator) { + init(validator: MTIValidator, initialText: String? = nil) { self.validator = validator + self.initialText = initialText } func makeInputField() -> MaterialTextField { let textField = MaterialTextField() self.view = textField + textField.text = initialText ?? "" + textField.placeholder = placeholder + textField.textContentType = textContentType + textField.keyboardType = keyboardType + textField.returnKeyType = returnKeyType + textField.textColor = UIColor.nynja.white textField.placeholderColor = UIColor.nynja.dustyGray textField.separatorColor = UIColor.nynja.dustyGray @@ -47,6 +90,8 @@ class TextFieldContentViewModel { textField.validators = [validator] + textField.validationHandler = validationHandler + textField.returnHandler = { [weak self, weak textField] textInput in self?.returnHandler?() textField?.endEditing(true) @@ -55,4 +100,20 @@ class TextFieldContentViewModel { return textField } + + func makeContentView() -> UIView { + let inputField = makeInputField() + + let containerView = UIView() + + containerView.addSubview(inputField) + inputField.snp.makeConstraints { maker in + maker.top.equalToSuperview().offset(contentInset.top) + maker.left.equalToSuperview().offset(contentInset.left) + maker.right.equalToSuperview().inset(contentInset.right) + maker.bottom.equalToSuperview().inset(contentInset.bottom) + } + + return containerView + } } diff --git a/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift b/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift index 5454ec502..58336cbca 100644 --- a/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift +++ b/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift @@ -9,7 +9,7 @@ import Foundation import UIKit -enum SelectAvatarFlowCoordinatorSource { +enum ImageSource { case camera case gallery } @@ -20,13 +20,13 @@ PhotoPreviewCoordinatorProtocol {} final class SelectAvatarFlowCoordinator: SelectAvatarFlowCoordinatorProtocol, InitializeInjectable { private var rootViewController: UIViewController private var serviceFactory: ServiceFactoryProtocol - private var source: SelectAvatarFlowCoordinatorSource + private var source: ImageSource private var completion: (_ imageURL: URL) -> Void private var mainFlowVC: UIViewController? struct Dependencies { - let source: SelectAvatarFlowCoordinatorSource + let source: ImageSource let rootViewController: UIViewController let serviceFactory: ServiceFactoryProtocol let completion: (_ imageURL: URL) -> Void diff --git a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift index d518992e7..8592e1981 100644 --- a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift +++ b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift @@ -12,7 +12,7 @@ final class QRCodeReaderInteractor: QRCodeReaderInteractorInput, IoHandlerDelega // MARK: - Services - private let accountId: String + private let ownQRCode: String private let accountService: AccountService @@ -23,14 +23,14 @@ final class QRCodeReaderInteractor: QRCodeReaderInteractorInput, IoHandlerDelega struct Dependencies { let presenter: QRCodeReaderInteractorOutput - let accountId: String + let ownQRCode: String let accountService: AccountService let accountDAO: AccountDAOProtocol } init(dependencies: Dependencies) { presenter = dependencies.presenter - accountId = dependencies.accountId + ownQRCode = dependencies.ownQRCode accountService = dependencies.accountService accountDAO = dependencies.accountDAO } @@ -39,12 +39,12 @@ final class QRCodeReaderInteractor: QRCodeReaderInteractorInput, IoHandlerDelega // MARK: - Interactor Input func search(by qrCode: String) { - guard qrCode != accountId else { + guard qrCode != ownQRCode else { presenter?.didReceiveOwnAccount() return } - if let account = accountDAO.fetchAccount(by: qrCode) { + if let account = accountDAO.fetchAccount(byQRCode: qrCode) { let result = SearchContactResponse(account: account, inputField: .qrCode) presenter?.didReceiveSearchResult(result) return diff --git a/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift b/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift index 984bf9626..bbfd66f2a 100644 --- a/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift +++ b/Nynja/Modules/QRCodeReader/Presenter/QRCodeReaderPresenter.swift @@ -15,8 +15,8 @@ final class QRCodeReaderPresenter: QRCodeReaderPresenterProtocol, QRCodeReaderIn // MARK: - Presenter - func didScan(qrcode: String) { - interactor.search(by: qrcode) + func didScan(qrСode: String) { + interactor.search(by: qrСode) } func showOwnQRCode() { diff --git a/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift b/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift index 83a811cab..18344bad2 100644 --- a/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift +++ b/Nynja/Modules/QRCodeReader/QRCodeReaderProtocols.swift @@ -31,7 +31,7 @@ protocol QRCodeReaderViewInput: class { // MARK: - Presenter protocol QRCodeReaderPresenterProtocol: class { - func didScan(qrcode: String) + func didScan(qrСode: String) func showOwnQRCode() } diff --git a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift index e35716733..cdf84d965 100644 --- a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift +++ b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift @@ -350,7 +350,7 @@ extension QRCodeReaderViewController: AVCaptureMetadataOutputObjectsDelegate { guard metadata.type == .qr, let qrCode = metadata.stringValue else { return } captureSession?.stopRunning() - presenter.didScan(qrcode: qrCode) + presenter.didScan(qrСode: qrCode) } } diff --git a/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift b/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift index 369480d15..ac9cdaff3 100644 --- a/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift +++ b/Nynja/Modules/QRCodeReader/WireFrame/QRCodeReaderWireframe.swift @@ -20,7 +20,7 @@ final class QRCodeReaderWireFrame: Wireframe, QRCodeReaderWireFrameProtocol { var mode: CodeReaderPresentationMode = .fromMainScreen struct Parameters { - let accountId: String + let ownQRCode: String } struct Dependencies { @@ -39,7 +39,7 @@ final class QRCodeReaderWireFrame: Wireframe, QRCodeReaderWireFrameProtocol { let interactor = QRCodeReaderInteractor( dependencies: .init( presenter: presenter, - accountId: parameters.accountId, + ownQRCode: parameters.ownQRCode, accountService: dependencies.accountService, accountDAO: dependencies.accountDAO ) @@ -65,7 +65,7 @@ final class QRCodeReaderWireFrame: Wireframe, QRCodeReaderWireFrameProtocol { let view = prepareModule( parameters: .init( - accountId: accountId + ownQRCode: accountId ), dependencies: .init( accountService: serviceFactory.makeAccountService(), diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Contents.json new file mode 100644 index 000000000..a08e78b82 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Icons_General_ic_mail.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Icons_General_ic_mail.pdf b/Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Icons_General_ic_mail.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b0a3f8d15b32b1beb952640cf4c1994888438782 GIT binary patch literal 4895 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 Q1PT=v0kE+tYAON#2O&g)n*aa+ literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/ic_fb.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_fb.imageset/Contents.json new file mode 100644 index 000000000..7b50dd81f --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/ic_fb.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_fb.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_fb.imageset/ic_fb.pdf b/Nynja/Resources/Assets.xcassets/ic_fb.imageset/ic_fb.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e3ce027c01a1bad2b8ea49f10109ed60770809cb GIT binary patch literal 4064 zcmai%c|27A_s1=hWk`yMR5u?>$}(dHN%mzZWtW}V>`aXbk*zFYWX~3|Z&?ZvjeU); zNw!o-wrp9m`}obY{62j?pYP-Gy^s62_kGX%+;h)8=k>?)2{+Q4uC0^m@N;LDc*32m&a9l;)uqL}mn2@MBl0v1oXJ0l5LH7pv3!2${jU^fCD zi*x~d(z@uIUvGi1?k$Pj4G7*!+DhSAZlyiXuF3ND`DQ|y!Fw0lO8tq@#T8GlAmM7d zV}@Ha3$6KD3<6JrPCCq7Ml?y*nYX3YSjA{}6+Ahe5;5mhKPHDTjuBgCATKk+c)R;A zF)`%Wq|{lpsf#fzEiZejnz-t+K&PE=p;p3NZ|PUPuW4`zFkQoT%3OMt+s0hUzQOPy z=b4cf$)ZDS%Y6ago}G9B#4cdT=iAzs(H41_Kgy=?e8}emKC4cLS;hmdUKtdO3_x*~;x)K1`j~?k` z-Eg<@XsjC`{llP&b0tvr-2iG|^{M^(8IQXEtIxW49NGX&0L&>t)ieN0KtdJgjKdq; zM53_(RS2qXP(bEqfZt+V{4K^$t;l}&lDJMuNJ5#a7gvf{00}j$y8{}lk5KvFGD>~q zVQ4VS6+$M-klq~Q2L#u7y_kWwa+=3MdZAkWx^%&TLhfpZG&qln7-I|@Xb~a&Xmw7O z`}a^o%&Y9WZH;Qf^`C+4MMskPDM{dUu_;hCzTVY?-tp&A<9y?dZ0f(flZJi~wKfdF^;a5O}Z28v}`7{4db@9AjFm$UH=|QJ}D_*v#osNtGGX=_Je4>u8`Z{fnPMg zjH&D;CL1hlwpx5_{lEoE+dv?sFh4JwRx_uZ|ku(4TLkvx_d?_!#Pw^ zos;Kl=qeozQ(TMeD?D5Hwyb>`jc2aiWEdz^3zd6xDA8f+ONZ^wO1Z%{3VI=~`P%pc zPi^vg1?xiyw|^<_kvgrQsY{Z5^kOmJ;-cx`wEcGd@YrczgMIl9uU(@Rp#^f9GBM3` z%z5Fd1c{4gL%=NMRC!&z5lw{)gVNOaQ=KJ%X7vzF#pvU;)4|SGF)g22g;~NE7M$H^ z)1AaGe398rzlVEWA1u3>)b(}OST2`aG2joa<1noHRBm4GRkiMJR<;B(W`C3YV4^|R z;Xj?tqL0%XxL1O{80me9V-5_puMN5!U=k&c0SEdGjQgFC7G~t}O?Be5zimz96C|r! zt;N`Zlc!PrgI>T@(TmP!-kco@?x=fZUmRWP7AfiV=giI-7K<>~acyOOgXs;{NDN*u zQe^54%Y}3eW`lYqHZnq8Hbsb5H_3S5|mO16GE< zqjkUUc$20-!0Qd@`aS8J3>Kgp4To>Cu=*pEz`>1IA+)0b4ovr!*^g%`7e;YqA4yUf zWIt)fbVpTL_aY~Kt7>QzdyLZd8=?FY=kFJ0sOrkE|3TobVY0lx^MvsY0B6{z^$9$h zNY{rv{Ep^@|8SWc9}}Xf>yw)2@yCqZ4Ijp@RC6h?RW)&spIoL94YF>EWm4*3qVLr( zVLG{@rV?c$KzsD8Q#5}zU4eRX6kGPmml2xLwpCPGY!<0xa~AnFQa zAQ7Ettj#?wZOaY|*uIy88hDm#BK%a*omu*5N`zR8u}!3nd0E0U`gh0$IX<@b2A_3=wPn=bI0Y3$1v*IzP&}bni%`+cN6z**dTvpWo@R$TMyA!coj- zAg6mr&OQPJoCOTod6{OI_5+VL!Sw?2bqM^y>~~r$P$JJBzEggs#V2$h&7)&H^n!_J5~N#pP>n2 zvjx-V$sxuPYl)F-@adQ23GxT&QU|MpS0eG-=Uva=jpvWIji-%|EYvo_4R8*m7P=Jb z^g|ql9FdO0j=KZmMG@pYvPT*@)f#>N{w+UBHY0S?E z%cw`DA+yHOe-uU+^`6f%%aUumh<t)<FCEBMw*l_|~-jh2rU0GZpHs)Tx=p&bGSvJB|d%c#t!M4G_E3^w`ln?O? zA+c?~Be{^t?_C; ze6%=5oM`QNGo#<=L;Qr<{7(0^p@stTs;zv2yc(ei=E*mvsgVy zD7_JVqqHh!6Sd^Hl&5e>Aygqkfuqj7uE<6J7O$Afh zCF|BNJ*GfIv9%$)F*9u!W+GNBx3GKBpoGBZfxT)8Eta8ZtrD%{S`&0S%9WpPKet{D zm@J5kipvs#74#PT$04v{tD;Xu5E)i>!4f7fOp<7ZXF*hJUONnYHR8xr>W$Vu=VoeK zoA~fg?QfbyffT{}rYz--&qwVL*!0(W-AAX_CzB^#yH0hkb?*8~*-ClbwySghGkC92 z>Z=o5anN9i#fO*IJuDk*#=q6gN4fwmn#0Ro8eI|ClVh%7qy2hM^vddN?8}#?+f-hv z=#7)add%6JTb++PFOJxj7dw7P;J51ihU)qp_E|^pW6b=Rsn3n=k?xJaMN-a2&aTHN zcP5XIBeOMYCP^q$tKl~#pDc@~x>qw-t7>m7)EOhqZ`mAih{QxzNajB$T`6@$4CKva z&js*CCQp;*ysH*d-?x7lsvmRl>!XdJZ)H?C&B^!5uVYi!saV;i75Q=eX)}2~bFQY| z&d=IUZBcV2nv4iS|ceregAKYDrniXjec&snyC$e8C6O z?w(OXkzI+brJPTy4{0%YKVtB7ZQ3L21z(5R$|V=l?9KB1M-#;l>kj$M>{#wfjBeEU zOg%hEJxL$Vu=7>C&ilaXqx`JT=zcF_!4WIPZm)~`_WS6CiA-Uz&iS-L#p3;ets&b2 z()z-aAsce}1G~uJYE_AY`0Y}i=0oHH@&%0wjX6Xyf=t{dj(9y=Z(Z&pCiG1N)re&D;?Pj3GU%c0PpZ=Ab^L<2eofC&JFh(lrG(tzG=B%a^} zPze1y)O!*{e&r4QH5{B%yCeyLUW7_Q;7}M84wW&3K!hphU!K3^L!o}mZ8T+~`~FSJ zfTv0s&RG2SvDv`Ei&8(z0Hlw@QS?F80Hu&xuC_ReSbnV4C~~qO^BnkaKqcXf!mV>(=9Y3d z9{07jGa>*8fU|R9R8Rn<^f2y@o=$)?C1eChX*y#)F?h-qi}J*%W6(Hz44|aM=;4XS zpxhXJsFO`}@0uW4-!~fXfZV1>birkQU_5)*oBy3dx@BWMWBUUiG)?-H;gkx?|k{GeoQ zDGXgtKLXRdI8zt!-CNSBW3NVu5HR=E$GYO{_YWM-m#yW#;+DTYv$@savY`=rklPXM z|7`7JPn~5Y{!0@xy(kD$&=i==d}85B?wBqpGB!q_Tlz*PpG{?Dx9<(?3pPGw*1~pR zR7WPDl0o5g&vPTu1OX$PGB#yxrVW?cGm!c;~TMcb?;{(0p+{LGq?CwC!k|R)A**wxwt$_ zW#8hroUfJ6(MINE4Jn)At7wM9w$)6-bNbSkr-}^|BeXLw$K~S9kS7GbUQ>NytkTIY z5PCq9dG}2gIhXUc#}!T5)#)>Ac_zF@x`UCsAUcEVyss~1q|eOy_Is|-!R@;$$__Gu!KSW9?&Hr2t=?i|ZCy8JV%Fmq2o>~u8QoeC6NFW| zD*28@U0#ug%=z?AZiU!e;rW!Lb2N0BA({|&3~h`;u&;tgWs+=X{NQ66gqRPqXS^(g zL0kRdG?*P7bK7_SJw^px1Jd9L7Hc~GE^Ldbrw72&XO}u#@Dwh|Le}Wo9P|5zAf=A+ zaz-*RQ0pU$vKL+59yf!D;Fj6PT02Cf7;-yZj$#Gol zioMAzYOM%Cv<3&$?GU>WhA*rJ?M;U28gt`BSBAA>>5Yjra)s1w?O71X#74q2ICxegzksOdlUk8=c$Vm0q|+lRpB%^8C~%~XUC!~5`_6M8J014c z%Z+#Kpy!iXE=}F>(IahEFyDo62bJRPX;2HByQMnCuN3pGte6kaJMA`(PM!`hCM$OO z?VGNPER!Co5FeROVwa1g>ba=4ge)GMDsM9-ex_Rd4+WTv#Uk?Va6-&^{|hoh98p{LL;4O!xVze;w0@ELj#AV0#C?@((weO zyK*?)w51}1Aq}c@=sIzVRBC5vgxoLqfe1^MtWd_zhB~L>_)?EpnBVt>J$@#uDEBEn z6+z?qE#_Ezyzmooo~+4GY!iqrj?VKjBO+K50usM=lrIQ=on``{3Jua`rMX8VsSR>g z20vF(dP~Kq0)0#Eb=&1S)nKq+ANX>J%=IHy;H$5WUT0zs(o$v&Z_B;IC?f4tBG)E#bQTI=9we!qG zm=mY$uAqkPE<%RxqwinQvAanddj6~~|Yq8fX%Tk`v zyhSa`^RaZi;+f{V9#D0{3g#|d0)BaHgjgH2)N*u6WZkFk+yuv3$gw7m&vv&28W;ol z0#8rI^0zmN#xYocT|%D&3JYS~6d^V|o`3hvjWTjlOTRs8V|^A2)KOZMshK;mNCCjZC!O$LlG#&YiL z9N!%8T={FGBh8R&J8&bOY+@p@mKeK%n6D4#FBT_J<@3V@SCq??OnNo05aiRW%f`8CFV~W` zShfWBMfRa|iV;2$^(@h7yL3FMXV<>Rl(!(24qsJ{!=vZl4$F5J#)}>EP2LaX|LzfNX&v)t6S^%Tg6dw)&BWoy2s+R zqC1ju6Bl?%Og48?O0ULWEq$4=ZMWvKmaFuKQlwIh5_^MJLm@$G2So;vz4pd8Mm{g> zjqK}#<-kP@9~qy5HNjWtl$c-)Y_vDQElt!-wITdcZ)}CE2Dw10-_*Rq6d0tLtvUBR zPDqL)ibR%ut4w_Ye7wODb{gtxt+m>*oo_m|qGPypxx6*IwAwU=v@U7*YouuCrkIJF zm(OYvEr>vlA{YNjPJbaUeE#;EaovcS-=Kiqt7`z3fbKPfILOAMahpZ}+GXvI#&fT}Pms_eWqTv4=Mss)h+QLB0FJhW=c z{zSPyUhk}jxkK&!yWjP`Y7>PX2;VkmDtCE4;i!elcx~8oe13B_ZPvZ}RM$q=et@)t zwD(QN2Cwho-kTCHk0RWH$N$%1Xea$;A;9m`r`oG$BPe)Db3%^fTnlvX|-5u}Q z3SFts-pbzh{@_LL{eFD0X2UGi&fI3SujGSu@m$ZBCtqIHUR`dug0j4E?U-|{eQX6R z?|J>jQWveE+)qzG1@p$H&DVeOf4P$Wu48PZandbtfI5bzjZWz_2VY%a=eB`sv5H$8 z>itwv3u)=er% z*$Wp9;91o>U^xNg`gF$*|JoyFg;C!qLcyXaIb-WZpGA~K=J5OB$QMs#oc;CuCl`YT z(oglClDP2T;MOK_D>beBwWBU6zL)di17wDGz!%l_u=VIj?ub9GdMeda%)Uu4DJz-i z4o)exS+Ba(%BX+t zQNe{`^3cwRLw^0{^5c?H?Te3yo5TF%cId|bzxnx)LVrNB3>5YYcn|sY zFh+q~9W^yoln2HhI0RTD!1|Zu5TbuE@!yQ?0Z3iK*gKiCYqOdr}zwq7TC%6BNV1xvL){`;VNI;3b^>@8ADksSh6Gzz6}La7M`g9e|9q zG+Y{R0Di{cNCc(tI6MINUonUrgtFX!#^AD0%2xa{27$o-rGv=IQTFJ+baHb4(!pi^ zA01MfvK#-r7cL|7uY7P>B&B@*ODBi;cMSRa|M0}4oUs`Ek5?7O&VH2VM`?hJa5zeR z9M%BkLF%|W;3&oN*I1oWE(-Pt3={){*&$?LXc!tJZD;QwD~*Jq, in accountId: String, completion: @escaping StatusCompletion) - func deleteContactInfo(from accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) + func deleteContactInfo(_ contactDetails: NYNContactDetails, from accountId: String, completion: @escaping StatusCompletion) // MARK: - Search diff --git a/Nynja/SDK/Account/Service/AccountServiceImpl.swift b/Nynja/SDK/Account/Service/AccountServiceImpl.swift index bccc4e582..8b9576b86 100644 --- a/Nynja/SDK/Account/Service/AccountServiceImpl.swift +++ b/Nynja/SDK/Account/Service/AccountServiceImpl.swift @@ -211,7 +211,7 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, // MARK: - Account's Contact Info - public func addContactInfo(to accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { + public func addContactInfo(_ contactDetails: NYNContactDetails, to accountId: String, completion: @escaping StatusCompletion) { guard let token = token else { return } @@ -232,7 +232,7 @@ final class AccountServiceImpl: NSObject, InitializeInjectable, AccountService, withEditedContactDetails: editInfo.newValue) } - public func deleteContactInfo(from accountId: String, contactDetails: NYNContactDetails, completion: @escaping StatusCompletion) { + public func deleteContactInfo(_ contactDetails: NYNContactDetails, from accountId: String, completion: @escaping StatusCompletion) { guard let token = token else { return } diff --git a/Nynja/SDK/Auth/Entities/PhoneNumberLabel.swift b/Nynja/SDK/Auth/Entities/PhoneNumberLabel.swift new file mode 100644 index 000000000..7bb2a3f10 --- /dev/null +++ b/Nynja/SDK/Auth/Entities/PhoneNumberLabel.swift @@ -0,0 +1,27 @@ +// +// PhoneNumberLabel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum PhoneNumberLabel { + case mobile + case home + case work + case custom(String) + + var title: String { + switch self { + case .mobile: + return String.localizable.phoneNumberLabelMobile + case .home: + return String.localizable.phoneNumberLabelHome + case .work: + return String.localizable.phoneNumberLabelWork + case let .custom(text): + return text + } + } +} diff --git a/Nynja/SDK/Auth/Service/AuthService.swift b/Nynja/SDK/Auth/Service/AuthService.swift index b5fae6c84..359df2835 100644 --- a/Nynja/SDK/Auth/Service/AuthService.swift +++ b/Nynja/SDK/Auth/Service/AuthService.swift @@ -12,6 +12,7 @@ import NynjaSDK protocol AuthService: class { typealias LoginCompletion = (Result) -> Void typealias CodeConfirmationCompletion = (Result) -> Void + typealias StatusCompletion = (Result) -> Void typealias RefreshTokenCompletion = (Result) -> Void func login(by email: String, completion: @escaping LoginCompletion) diff --git a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift index 4b493ef70..a32aa96fb 100644 --- a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -33,6 +33,7 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog private var loginByFacebookCompletion: LoginCompletion? private var loginByGoogleCompletion: LoginCompletion? private var confirmCodeCompletion: CodeConfirmationCompletion? + private var verifyAuthProviderCompletion: StatusCompletion? private var refreshTokenCompletion: RefreshTokenCompletion? // MARK: - Properties @@ -132,6 +133,11 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog loginManager.confirmCode(code, withCredential: socialToken) } + func verifyAuthProvider(completion: @escaping StatusCompletion) { + bind(completion, to: \AuthServiceImpl.verifyAuthProviderCompletion) + // loginManager.verify + } + func refresh(accessToken: String, completion: @escaping RefreshTokenCompletion) { bind(completion, to: \AuthServiceImpl.refreshTokenCompletion) loginManager.refreshAccessToken(accessToken) @@ -210,6 +216,12 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } } + func verifyAuthProviderDidFinish(withStatus status: String?, withError error: Error?) { + handleResponse(nil, to: \AuthServiceImpl.verifyAuthProviderCompletion) { completion in + self.processResponseBody(status, error: error, completion: completion) + } + } + func refreshTokenDidFinish(withAccessToken accessToken: String, withRefreshToken refreshToken: String, withExpiration expiration: NSNumber?, diff --git a/Nynja/Services/Storage/DAO/Account/AccountDAO.swift b/Nynja/Services/Storage/DAO/Account/AccountDAO.swift index 8390a7141..9695a8210 100644 --- a/Nynja/Services/Storage/DAO/Account/AccountDAO.swift +++ b/Nynja/Services/Storage/DAO/Account/AccountDAO.swift @@ -17,13 +17,66 @@ final class AccountDAO: AccountDAOProtocol { self.dbManager = dbManager } - func fetchAccount(by accountId: String) -> DBAccount? { + + // MARK: - Account + + func fetchAccount(byId accountId: String) -> DBAccount? { return dbManager.fetch { db in return try DBAccount.account(from: db, accountId: accountId) } } + func fetchAccount(byQRCode qrCode: String) -> DBAccount? { + return dbManager.fetch { db in + return try DBAccount.account(from: db, qrCode: qrCode) + } + } + + func fetchAccount(by rowID: Int64) -> DBAccount? { + return dbManager.fetch { db in + return try DBAccount.account(from: db, rowID: rowID) + } + } + func save(_ account: Account) throws { try dbManager.perform(action: .save, with: account) } + + + // MARK: - Contact Info + + func fetchContactInfo(by rowID: Int64) -> DBContactInfo? { + return dbManager.fetch { db in + return try DBContactInfo.filter(Column.rowID == rowID).fetchOne(db) + } + } + + func saveContactInfo(_ contactInfo: DBContactInfo) throws { + try dbManager.perform(action: .save, with: contactInfo) + } + + func editContactInfo(_ contactInfo: DBContactInfo, by newContactInfo: DBContactInfo) throws { + try dbManager.write { db in + try deleteContactInfo(contactInfo, from: db) + try newContactInfo.saveAggregate(db) + } + } + + func deleteContactInfo(_ contactInfo: DBContactInfo) throws { + try dbManager.write { db in + try deleteContactInfo(contactInfo, from: db) + } + } + + private func deleteContactInfo(_ contactInfo: DBContactInfo, from db: Database) throws { + let accountIdColumn = Column(ContactInfoTable.Column.accountId.title) + let typeColumn = Column(ContactInfoTable.Column.type.title) + let valueColumn = Column(ContactInfoTable.Column.value.title) + + try DBContactInfo + .filter(accountIdColumn == contactInfo.accountId + && typeColumn == contactInfo.type.rawValue + && valueColumn == contactInfo.value) + .deleteAll(db) + } } diff --git a/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift b/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift index d38643db0..28f53cfc9 100644 --- a/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift +++ b/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift @@ -9,6 +9,13 @@ import Foundation protocol AccountDAOProtocol: class { - func fetchAccount(by accountId: String) -> DBAccount? + func fetchAccount(byId accountId: String) -> DBAccount? + func fetchAccount(byQRCode qrCode: String) -> DBAccount? + func fetchAccount(by rowID: Int64) -> DBAccount? func save(_ account: Account) throws + + func fetchContactInfo(by rowID: Int64) -> DBContactInfo? + func saveContactInfo(_ contactInfo: DBContactInfo) throws + func editContactInfo(_ contactInfo: DBContactInfo, by newContactInfo: DBContactInfo) throws + func deleteContactInfo(_ contactInfo: DBContactInfo) throws } diff --git a/Podfile b/Podfile index c01f520e6..a6be6478c 100644 --- a/Podfile +++ b/Podfile @@ -40,7 +40,7 @@ def commonPodsForNynja pod 'JTAppleCalendar', '= 7.1.6' # pod 'NynjaSDK', '= 1.8' - pod 'NynjaSDK-MultiAcc', '= 0.5.6.4' + pod 'NynjaSDK-MultiAcc', '= 0.5.6.5' pod 'CryptoSwift', '= 0.13.0' diff --git a/Podfile.lock b/Podfile.lock index c6571b3df..d04904a4e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -91,7 +91,7 @@ PODS: - MQTTClient/Min - SocketRocket - MulticastDelegateSwift (2.1.1) - - NynjaSDK-MultiAcc (0.5.6.4) + - NynjaSDK-MultiAcc (0.5.6.5) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -126,7 +126,7 @@ DEPENDENCIES: - MaterialComponents/FlexibleHeader (= 55.3.0) - MQTTClient/Websocket (= 0.15.2) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK-MultiAcc (= 0.5.6.4) + - NynjaSDK-MultiAcc (= 0.5.6.5) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.2.0) @@ -215,7 +215,7 @@ SPEC CHECKSUMS: MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MQTTClient: 902c7bcac1501595f3d0b15178c7205b40331fb0 MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK-MultiAcc: ead0f0eb472eccddf71cc40cd155184bd9925dbf + NynjaSDK-MultiAcc: 67e8020651c12becf5ad35292537c173eaee38a3 QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a @@ -224,6 +224,6 @@ SPEC CHECKSUMS: SwiftyJSON: c4bcba26dd9ec7a027fc8eade48e2c911f229e96 TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: 1d41f8e1e45db54475f314e6bddc09e3048fd03a +PODFILE CHECKSUM: 89a0867b7eed221c97f9eeb9ab0b2e1e00b8276b COCOAPODS: 1.5.3 -- GitLab From 3228d0fd571a1df6def88c0adc99998c4a057410 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 21 Dec 2018 16:52:34 +0200 Subject: [PATCH 136/138] Minor fix after merge. --- .../Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift index 63e692193..a054829e4 100644 --- a/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift @@ -118,10 +118,7 @@ final class SplashInteractor: SplashInteractorInput, InitializeInjectable { private func showAuth() { LogService.log(topic: .db) { "Clear storage: Splash" } - storageService.clearStorage() - mqttService.reconnect() - presenter?.showAuth() } } -- GitLab From 09a38839347523916d32bd32b42e4c01da18c779 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 4 Jan 2019 12:35:08 +0200 Subject: [PATCH 137/138] [NY-6418] Input fields validation (#1578) --- Nynja.xcodeproj/project.pbxproj | 73 ++--- Nynja/Generated/LocalizableConstants.swift | 22 +- .../Buttons/NynjaButton/NynjaCheckBox.swift | 88 ++++++ .../NynjaControlContainerView.swift | 4 +- .../Material/Base/MaterialTextInput.swift | 24 +- .../InputInfoContainer/InputInfo.swift | 10 + .../Material/Validator/ClosureValidator.swift | 1 + .../Material/Validator/EmailValidator.swift | 43 +++ .../Validator/FirstNameValidator.swift | 21 ++ .../Validator/LastNameValidator.swift | 16 ++ .../Material/Validator/LengthValidator.swift | 2 +- .../Material/Validator/MTIValidator.swift | 9 - .../Validator/UsernameValidator.swift | 36 +++ .../Presenter/AccountSettingsPresenter.swift | 43 +-- .../View/AccountSettingsViewController.swift | 67 +---- .../AuthProviderUIConfiguration.swift | 2 +- .../Presenter/AuthProviderPresenter.swift | 14 +- .../View/AuthProviderViewController.swift | 4 +- .../Entities/SocialLinkValidator.swift | 20 +- .../ContactInfoManagementPresenter.swift | 2 +- .../View/Forms/FormContainer.swift | 80 ++++++ .../Forms/Items/TextFieldRowItemView.swift | 30 +- .../Entities/EmailTextController.swift | 32 --- .../AuthModule/Entities/Validator.swift | 22 -- .../AuthModule/View/AuthViewController.swift | 12 +- .../View/Subviews/EmailLoginView.swift | 16 +- .../AuthModule/Wireframe/AuthWireframe.swift | 3 +- .../CreateProfileProtocols.swift | 26 +- .../Entities/CreateProfileViewModel.swift | 17 ++ .../CreateProfile/Entities/ProfileField.swift | 38 --- .../Interactor/CreateProfileInteractor.swift | 108 +------ .../Presenter/CreateProfilePresenter.swift | 128 +++++++-- .../View/CreateProfileViewController.swift | 252 ++++++++++++++--- .../Subviews/CreateProfileContentView.swift | 149 ---------- .../CreateProfileViewsFactory.swift | 264 ------------------ .../Wireframe/CreateProfileWireframe.swift | 3 +- .../Entities/EmailTextValidator.swift | 25 -- .../Entities/UsernameTextValidator.swift | 26 -- .../Presenter/SearchContactPresenter.swift | 4 +- Nynja/Resources/en.lproj/Localizable.strings | 15 +- .../ValidatorFactoryImpl.swift | 2 +- 41 files changed, 821 insertions(+), 932 deletions(-) create mode 100644 Nynja/Library/UI/Buttons/NynjaButton/NynjaCheckBox.swift create mode 100644 Nynja/Library/UI/TextInput/Material/Validator/EmailValidator.swift create mode 100644 Nynja/Library/UI/TextInput/Material/Validator/FirstNameValidator.swift create mode 100644 Nynja/Library/UI/TextInput/Material/Validator/LastNameValidator.swift create mode 100644 Nynja/Library/UI/TextInput/Material/Validator/UsernameValidator.swift create mode 100644 Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormContainer.swift delete mode 100644 Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift delete mode 100644 Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/Validator.swift create mode 100644 Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/CreateProfileViewModel.swift delete mode 100644 Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/ProfileField.swift delete mode 100644 Nynja/Modules/Flows/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift delete mode 100644 Nynja/Modules/Flows/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift delete mode 100644 Nynja/Modules/Flows/Search Flow/Entities/EmailTextValidator.swift delete mode 100644 Nynja/Modules/Flows/Search Flow/Entities/UsernameTextValidator.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 479182d2a..a7624d70d 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -520,8 +520,8 @@ 3A14D83B21AC03A3009CD23A /* SearchAvailabilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */; }; 3A14D83D21AC136F009CD23A /* AuthProviderUIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */; }; 3A14D83F21AC1F07009CD23A /* NavigationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A14D83E21AC1F07009CD23A /* NavigationError.swift */; }; - 3A184D1B21C0FD800083D367 /* UsernameTextValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1A21C0FD800083D367 /* UsernameTextValidator.swift */; }; - 3A184D1D21C0FD8C0083D367 /* EmailTextValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1C21C0FD8C0083D367 /* EmailTextValidator.swift */; }; + 3A184D1B21C0FD800083D367 /* UsernameValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1A21C0FD800083D367 /* UsernameValidator.swift */; }; + 3A184D1D21C0FD8C0083D367 /* EmailValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1C21C0FD8C0083D367 /* EmailValidator.swift */; }; 3A184D1F21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */; }; 3A184D2121C0FEBC0083D367 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */; }; 3A184D2421C100630083D367 /* SearchInputMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A184D2321C100630083D367 /* SearchInputMode.swift */; }; @@ -557,6 +557,9 @@ 3A2CDAC321C9648800B5E397 /* DestructiveActionRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2CDAC221C9648800B5E397 /* DestructiveActionRowItem.swift */; }; 3A37416121B58AAA00F212B9 /* ImageUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A37416021B58AAA00F212B9 /* ImageUploader.swift */; }; 3A3FD2831F39E0A000B6958F /* HistoryRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */; }; + 3A4D098221DCB9A700103E95 /* NynjaCheckBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4D098121DCB9A700103E95 /* NynjaCheckBox.swift */; }; + 3A4D098421DCCA7400103E95 /* FormContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4D098321DCCA7400103E95 /* FormContainer.swift */; }; + 3A4D098621DCCCDA00103E95 /* CreateProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4D098521DCCCDA00103E95 /* CreateProfileViewModel.swift */; }; 3A62B7D81F4CB9D100F45B51 /* BaseMQTTModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */; }; 3A6D7D3021CA7B4B00E1EF90 /* ContactInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7D2F21CA7B4B00E1EF90 /* ContactInfoViewModel.swift */; }; 3A6D7D3221CA993300E1EF90 /* AccountSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D7D3121CA993300E1EF90 /* AccountSettingsViewModel.swift */; }; @@ -893,9 +896,7 @@ 5E07BC4F216F659E000E4558 /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */; }; 5E07BC51216F6617000E4558 /* CreateProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */; }; 5E07BC53216F6661000E4558 /* CreateProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC52216F6661000E4558 /* CreateProfilePresenter.swift */; }; - 5E07BC55216F66F3000E4558 /* CreateProfileViewsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC54216F66F3000E4558 /* CreateProfileViewsFactory.swift */; }; 5E07BC57216F6722000E4558 /* CreateProfileWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */; }; - 5E0B9FF22170BCE600A95467 /* CreateProfileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */; }; 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */; }; 5E278E14F45F56BACB71271C /* VideoPreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5541C91FE7845F3E5C7EB2 /* VideoPreviewWireframe.swift */; }; 5E7D5D38218C40B6009B5D8D /* AccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */; }; @@ -1085,13 +1086,13 @@ 8514F17B20EA219F00883513 /* ContextMenuConfiguration+GroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514F17020EA219E00883513 /* ContextMenuConfiguration+GroupStorage.swift */; }; 8514F17C20EA219F00883513 /* ContextMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514F17120EA219E00883513 /* ContextMenuConfiguration.swift */; }; 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */; }; + 8516219B21D9453100EB7F58 /* FirstNameValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8516219A21D9453100EB7F58 /* FirstNameValidator.swift */; }; + 8516219D21D9455900EB7F58 /* LastNameValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8516219C21D9455900EB7F58 /* LastNameValidator.swift */; }; 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872BE20CD457F007CD6CA /* StickersProviding.swift */; }; 851872C120CD45B3007CD6CA /* StickersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872C020CD45B3007CD6CA /* StickersProvider.swift */; }; 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */; }; 851C6A54218B560B0062B148 /* MQTTFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */; }; 851EBD7F20B418890065C644 /* StickersInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EBD7E20B418890065C644 /* StickersInputView.swift */; }; - 851FFA66219EAF980015F073 /* EmailTextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851FFA65219EAF980015F073 /* EmailTextController.swift */; }; - 851FFA68219EAFBF0015F073 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851FFA67219EAFBF0015F073 /* Validator.swift */; }; 851FFA6A219EB29A0015F073 /* PhoneNumberTextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */; }; 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F520D4194A007C0036 /* DBRecentSticker.swift */; }; 852003F820D419E9007C0036 /* RecentStickerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F720D419E9007C0036 /* RecentStickerTable.swift */; }; @@ -1268,7 +1269,6 @@ 8572C3B92092364C00E4840C /* StickerPackageDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */; }; 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */; }; 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BD2092368600E4840C /* StickerDataSource.swift */; }; - 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */; }; 85739FBD2190AAC3001C4EC8 /* ConfirmationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85739FBC2190AAC3001C4EC8 /* ConfirmationData.swift */; }; 85788C3C204422FB003600C9 /* BuildNumberProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */; }; 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C412044237B003600C9 /* BuildNumberViewController.swift */; }; @@ -3005,8 +3005,8 @@ 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAvailabilityView.swift; sourceTree = ""; }; 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthProviderUIConfiguration.swift; sourceTree = ""; }; 3A14D83E21AC1F07009CD23A /* NavigationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationError.swift; sourceTree = ""; }; - 3A184D1A21C0FD800083D367 /* UsernameTextValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameTextValidator.swift; sourceTree = ""; }; - 3A184D1C21C0FD8C0083D367 /* EmailTextValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailTextValidator.swift; sourceTree = ""; }; + 3A184D1A21C0FD800083D367 /* UsernameValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameValidator.swift; sourceTree = ""; }; + 3A184D1C21C0FD8C0083D367 /* EmailValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailValidator.swift; sourceTree = ""; }; 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberContentViewModel.swift; sourceTree = ""; }; 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; 3A184D2321C100630083D367 /* SearchInputMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchInputMode.swift; sourceTree = ""; }; @@ -3042,6 +3042,9 @@ 3A2CDAC221C9648800B5E397 /* DestructiveActionRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveActionRowItem.swift; sourceTree = ""; }; 3A37416021B58AAA00F212B9 /* ImageUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploader.swift; sourceTree = ""; }; 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HistoryRequestModel.swift; path = Services/Models/HistoryRequestModel.swift; sourceTree = ""; }; + 3A4D098121DCB9A700103E95 /* NynjaCheckBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaCheckBox.swift; sourceTree = ""; }; + 3A4D098321DCCA7400103E95 /* FormContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormContainer.swift; sourceTree = ""; }; + 3A4D098521DCCCDA00103E95 /* CreateProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewModel.swift; sourceTree = ""; }; 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BaseMQTTModel.swift; path = Services/Models/BaseMQTTModel.swift; sourceTree = ""; }; 3A6D7D2F21CA7B4B00E1EF90 /* ContactInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoViewModel.swift; sourceTree = ""; }; 3A6D7D3121CA993300E1EF90 /* AccountSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewModel.swift; sourceTree = ""; }; @@ -3337,9 +3340,7 @@ 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = ""; }; 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileInteractor.swift; sourceTree = ""; }; 5E07BC52216F6661000E4558 /* CreateProfilePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfilePresenter.swift; sourceTree = ""; }; - 5E07BC54216F66F3000E4558 /* CreateProfileViewsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileViewsFactory.swift; sourceTree = ""; }; 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileWireframe.swift; sourceTree = ""; }; - 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileContentView.swift; sourceTree = ""; }; 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsPresenter.swift; sourceTree = ""; }; 5E7D5D39218C42D0009B5D8D /* AccountSettingsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsInteractor.swift; sourceTree = ""; }; @@ -3528,13 +3529,13 @@ 8514F17020EA219E00883513 /* ContextMenuConfiguration+GroupStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ContextMenuConfiguration+GroupStorage.swift"; sourceTree = ""; }; 8514F17120EA219E00883513 /* ContextMenuConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuConfiguration.swift; sourceTree = ""; }; 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDetailsPreviewView.swift; sourceTree = ""; }; + 8516219A21D9453100EB7F58 /* FirstNameValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstNameValidator.swift; sourceTree = ""; }; + 8516219C21D9455900EB7F58 /* LastNameValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastNameValidator.swift; sourceTree = ""; }; 851872BE20CD457F007CD6CA /* StickersProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProviding.swift; path = Services/StickersProvider/StickersProviding.swift; sourceTree = ""; }; 851872C020CD45B3007CD6CA /* StickersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProvider.swift; path = Services/StickersProvider/StickersProvider.swift; sourceTree = ""; }; 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceFactoryProtocol.swift; sourceTree = ""; }; 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTFactoryProtocol.swift; sourceTree = ""; }; 851EBD7E20B418890065C644 /* StickersInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputView.swift; sourceTree = ""; }; - 851FFA65219EAF980015F073 /* EmailTextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailTextController.swift; sourceTree = ""; }; - 851FFA67219EAFBF0015F073 /* Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberTextController.swift; sourceTree = ""; }; 852003F520D4194A007C0036 /* DBRecentSticker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRecentSticker.swift; sourceTree = ""; }; 852003F720D419E9007C0036 /* RecentStickerTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentStickerTable.swift; sourceTree = ""; }; @@ -3676,7 +3677,6 @@ 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackageDataSource.swift; sourceTree = ""; }; 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionDataSource.swift; sourceTree = ""; }; 8572C3BD2092368600E4840C /* StickerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDataSource.swift; sourceTree = ""; }; - 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileField.swift; sourceTree = ""; }; 85739FBC2190AAC3001C4EC8 /* ConfirmationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationData.swift; sourceTree = ""; }; 85788C3B204422FB003600C9 /* BuildNumberProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberProtocols.swift; sourceTree = ""; }; 85788C412044237B003600C9 /* BuildNumberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildNumberViewController.swift; sourceTree = ""; }; @@ -6558,8 +6558,6 @@ 3A184D2221C100330083D367 /* Entities */ = { isa = PBXGroup; children = ( - 3A184D1C21C0FD8C0083D367 /* EmailTextValidator.swift */, - 3A184D1A21C0FD800083D367 /* UsernameTextValidator.swift */, 3A184D2321C100630083D367 /* SearchInputMode.swift */, 3A2C2DFB21C26708006A53BB /* SearchContactResponse.swift */, 85086F4821C64D6D00194361 /* SearchContactResult.swift */, @@ -8057,6 +8055,10 @@ 4BBAEBBC21AC68FD0089B703 /* MTIValidator.swift */, 4BBAEBB921AC62740089B703 /* LengthValidator.swift */, 4BBAEBBE21AC6DF10089B703 /* ClosureValidator.swift */, + 3A184D1C21C0FD8C0083D367 /* EmailValidator.swift */, + 3A184D1A21C0FD800083D367 /* UsernameValidator.swift */, + 8516219A21D9453100EB7F58 /* FirstNameValidator.swift */, + 8516219C21D9455900EB7F58 /* LastNameValidator.swift */, ); path = Validator; sourceTree = ""; @@ -8371,21 +8373,11 @@ 5E07BC48216F64DB000E4558 /* View */ = { isa = PBXGroup; children = ( - 5E0B9FF02170BCD400A95467 /* Subviews */, - 5E07BC49216F64DB000E4558 /* ViewsFactory */, 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */, ); path = View; sourceTree = ""; }; - 5E07BC49216F64DB000E4558 /* ViewsFactory */ = { - isa = PBXGroup; - children = ( - 5E07BC54216F66F3000E4558 /* CreateProfileViewsFactory.swift */, - ); - path = ViewsFactory; - sourceTree = ""; - }; 5E07BC4A216F64DB000E4558 /* Interactor */ = { isa = PBXGroup; children = ( @@ -8394,14 +8386,6 @@ path = Interactor; sourceTree = ""; }; - 5E0B9FF02170BCD400A95467 /* Subviews */ = { - isa = PBXGroup; - children = ( - 5E0B9FF12170BCE600A95467 /* CreateProfileContentView.swift */, - ); - path = Subviews; - sourceTree = ""; - }; 5EDD454621885EC400C50BC8 /* Account Flow */ = { isa = PBXGroup; children = ( @@ -8595,8 +8579,6 @@ children = ( 5EEB73D12161CEA100D8ECE6 /* LoginFlow.swift */, 850B9DAC219C7ADA00EA0CF4 /* PlainLoginOption.swift */, - 851FFA67219EAFBF0015F073 /* Validator.swift */, - 851FFA65219EAF980015F073 /* EmailTextController.swift */, 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */, 8541995121A2B003004009F7 /* PhoneNumberFormatter.swift */, ); @@ -9631,6 +9613,7 @@ children = ( 853567BA21A6B00100AAEEF9 /* Form.swift */, 3A2CDAC021C944CD00B5E397 /* FormHeaderView.swift */, + 3A4D098321DCCA7400103E95 /* FormContainer.swift */, 8542FBF421A6EDD400CC295B /* FieldRowItem */, 8542FBF521A6EDE800CC295B /* Items */, ); @@ -10255,7 +10238,7 @@ 85739FB92190A3A5001C4EC8 /* Entities */ = { isa = PBXGroup; children = ( - 85739FBA2190A3E0001C4EC8 /* ProfileField.swift */, + 3A4D098521DCCCDA00103E95 /* CreateProfileViewModel.swift */, ); path = Entities; sourceTree = ""; @@ -11222,6 +11205,7 @@ 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */, 8542FBF221A6ECC100CC295B /* NynjaSwitch.swift */, 85086F5521C6B7E700194361 /* DestructiveNynjaButton.swift */, + 3A4D098121DCB9A700103E95 /* NynjaCheckBox.swift */, ); path = NynjaButton; sourceTree = ""; @@ -16472,8 +16456,6 @@ F11DF05F20BD93FB00F3E005 /* UIViewExtensions.swift in Sources */, 4B1D7E112029FF5000703228 /* Array+WheelItemModel.swift in Sources */, A46C36342121999100172773 /* DDMechanism.swift in Sources */, - 851FFA66219EAF980015F073 /* EmailTextController.swift in Sources */, - 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */, 4B483AB621BEC0C100FCF879 /* UserIdentifiers.swift in Sources */, 3A8045D81F60C98200AED866 /* MQTTService+Helper.swift in Sources */, 8E9601971FF2EC8100E0C21D /* GroupFilesListVC.swift in Sources */, @@ -16655,6 +16637,7 @@ 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */, A42D51B5206A361400EEB952 /* chain.swift in Sources */, 3A1A513021BABE7A00369206 /* ContactInfoInputModel.swift in Sources */, + 8516219D21D9455900EB7F58 /* LastNameValidator.swift in Sources */, 3A0A50CB21B7FEFE0052D334 /* MyGroupAliasPresenter.swift in Sources */, 8509452B206E684300B43C1C /* AddParticipantsContactCell.swift in Sources */, 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */, @@ -16903,6 +16886,7 @@ FEA655F42167777E00B44029 /* PaymentViewController.swift in Sources */, 4B4266C3204D923400194BC1 /* Array+UIView.swift in Sources */, A4CB15232103735200C3B68B /* JDFileBasedMechanism.swift in Sources */, + 8516219B21D9453100EB7F58 /* FirstNameValidator.swift in Sources */, 855A4EA0219B35B700B6E90B /* AuthConfirmationType.swift in Sources */, 3A771CAA1F191B38008D968A /* ProfileHandler.swift in Sources */, 3A0E865C21B14ECB00BAF80B /* AccountExtension.swift in Sources */, @@ -16926,7 +16910,6 @@ FBCE841320E525A6003B7558 /* NetworkRouter.swift in Sources */, A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, - 5E0B9FF22170BCE600A95467 /* CreateProfileContentView.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 85EB37F321831094003A2D6F /* ChatListMessageDetailsView.swift in Sources */, 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */, @@ -16937,7 +16920,6 @@ 268C34152107479600F1472A /* TranscribeLongResponseData.swift in Sources */, 6D36F8E71F0BBFC300FA1AC8 /* ContactManager.swift in Sources */, 32868DD51F31CADF0028B260 /* ChatsListProtocols.swift in Sources */, - 851FFA68219EAFBF0015F073 /* Validator.swift in Sources */, A49CC1D220E4A9C000879D41 /* InputBar+DisplayMode.swift in Sources */, 851FFA6A219EB29A0015F073 /* PhoneNumberTextController.swift in Sources */, A42D51B3206A361400EEB952 /* Room.swift in Sources */, @@ -17081,7 +17063,6 @@ 4B8996E4204EEC5A00DCB183 /* MessageDAOProtocol.swift in Sources */, E76491961F7A529D001E741C /* WheelContainer.swift in Sources */, 2648C4122069B52100863614 /* ChangeNumberStep3ViewController.swift in Sources */, - 5E07BC55216F66F3000E4558 /* CreateProfileViewsFactory.swift in Sources */, 8E9601931FF295DF00E0C21D /* ItemsSelector.swift in Sources */, A42D51C9206A361400EEB952 /* writer.swift in Sources */, 2648C3E62069B49000863614 /* UITextField+Extension.swift in Sources */, @@ -17619,7 +17600,6 @@ E79117921F97A48900462D68 /* ProfileDetailsViewLayout.swift in Sources */, 3A0A94D721B53491007421AA /* AccountDAO.swift in Sources */, 8595E0DC204863DB00178171 /* CarouselPickerCellModel.swift in Sources */, - 85739FBB2190A3E0001C4EC8 /* ProfileField.swift in Sources */, 2648C41C2069B52100863614 /* ChangeNumberStep2Interactor.swift in Sources */, 0DE4B40440737CF42D3E0204 /* HistoryWireframe.swift in Sources */, F117871820ACF018007A9A1B /* LabeledTableCell.swift in Sources */, @@ -17667,7 +17647,7 @@ B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */, A42D51CD206A361400EEB952 /* operation.swift in Sources */, 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, - 3A184D1D21C0FD8C0083D367 /* EmailTextValidator.swift in Sources */, + 3A184D1D21C0FD8C0083D367 /* EmailValidator.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */, F10AFEBC20F7B1D200C7CE83 /* WheelPreviewFactory.swift in Sources */, @@ -17717,6 +17697,7 @@ 2686D3231FC63B6A0079CB75 /* SyncFileManager.swift in Sources */, 269848D0200FB82A00590D6F /* DBStar.swift in Sources */, 853FB0672049B193000996C5 /* SupportViewController.swift in Sources */, + 3A4D098221DCB9A700103E95 /* NynjaCheckBox.swift in Sources */, 3A1DFD7E1F5370A600F3A3D8 /* UIImageExtensions.swift in Sources */, 85057962206D0C8400565C60 /* MediaPlaceholderWheelItemModel.swift in Sources */, E78EFB8B1FC8876F00C44975 /* DBMember.swift in Sources */, @@ -17785,6 +17766,7 @@ 260313A420A0A4BA009AC66D /* DirectableActionCellViewModel.swift in Sources */, 859773232087965700B03B4A /* NynjaControlContainerView.swift in Sources */, 3A0AEA7321B01EC50066CBBA /* DBContactInfo.swift in Sources */, + 3A4D098421DCCA7400103E95 /* FormContainer.swift in Sources */, E743B58A1FB0911200F72F92 /* ParticipantsContactCell.swift in Sources */, A46679F120F10B5900DBC6B4 /* LinkModelFactory.swift in Sources */, F119E66C20D24BAF0043A532 /* MultiplePreviewViewController.swift in Sources */, @@ -18061,7 +18043,7 @@ 85D66A1020BD965300FBD803 /* MentionPanelView.swift in Sources */, 4B7C73F1215A5509007924DB /* SMSCodeProviding.swift in Sources */, 45F60C4B14438C65076457AB /* EditUsernameProtocols.swift in Sources */, - 3A184D1B21C0FD800083D367 /* UsernameTextValidator.swift in Sources */, + 3A184D1B21C0FD800083D367 /* UsernameValidator.swift in Sources */, 00F7B33E2029DD4B00E443E1 /* AudioItemView.swift in Sources */, 4B7C73F9215A5522007924DB /* DebugLogs.swift in Sources */, 0062D9412062EC4100B915AC /* InviteFriendsSelectionCell.swift in Sources */, @@ -18321,6 +18303,7 @@ 8596CEF22048A763006FC65D /* ThemeCellModel.swift in Sources */, 4B030F3B2195CF8100F293B7 /* Host.swift in Sources */, 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */, + 3A4D098621DCCCDA00103E95 /* CreateProfileViewModel.swift in Sources */, A42D52DB206A53AB00EEB952 /* messageEvent_Spec.swift in Sources */, 4B749F08214FEE4F002F3A33 /* VerifyNumberInteractor.swift in Sources */, A42D51CA206A361400EEB952 /* ExtendedStar.swift in Sources */, diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 53213cfca..2bddfd44f 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -288,10 +288,6 @@ internal extension String { static var channelListCreateChannel: String { return localizable.tr("Localizable", "channel_list_create_channel") } /// List of your channels is empty. You can create your own channel. static var channelListIsEmpty: String { return localizable.tr("Localizable", "channel_list_is_empty") } - /// Max %d symbols - static func channelMaxLengthWarning(_ p1: Int) -> String { - return localizable.tr("Localizable", "channel_max_length_warning", p1) - } /// At least %d symbols static func channelMinLengthWarning(_ p1: Int) -> String { return localizable.tr("Localizable", "channel_min_length_warning", p1) @@ -1712,6 +1708,24 @@ internal extension String { static var searchContactUsernameScreenTitle: String { return localizable.tr("Localizable", "search_contact.username.screen_title") } /// Enter Username static var searchContactUsernameSubtitle: String { return localizable.tr("Localizable", "search_contact.username.subtitle") } + /// Max %d symbols + static func validationMaxLengthWarning(_ p1: Int) -> String { + return localizable.tr("Localizable", "validation.max_length_warning", p1) + } + /// Min %d symbols + static func validationMinLengthWarning(_ p1: Int) -> String { + return localizable.tr("Localizable", "validation.min_length_warning", p1) + } + /// Please, fill in account link + static var validationAccountInfoLinkEmpty: String { return localizable.tr("Localizable", "validation.account_info_link.empty") } + /// Sorry, seems link is not correct + static var validationAccountInfoLinkInvalid: String { return localizable.tr("Localizable", "validation.account_info_link.invalid") } + /// Please, fill in email + static var validationEmailEmpty: String { return localizable.tr("Localizable", "validation.email.empty") } + /// Sorry, seems email is not correct + static var validationEmailInvalid: String { return localizable.tr("Localizable", "validation.email.invalid") } + /// Sorry, seems username is not correct + static var validationUsernameInvalid: String { return localizable.tr("Localizable", "validation.username.invalid") } } } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/NynjaCheckBox.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCheckBox.swift new file mode 100644 index 000000000..3b5b5f754 --- /dev/null +++ b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCheckBox.swift @@ -0,0 +1,88 @@ +// +// NynjaCheckBox.swift +// Nynja +// +// Created by Anton Poltoratskyi on 1/2/19. +// Copyright © 2019 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class NynjaCheckBox: UIButton { + + var borderColor: UIColor = UIColor.nynja.dustyGray { + didSet { + layer.borderColor = borderColor.cgColor + } + } + + var borderWidth: CGFloat = 1 { + didSet { + layer.borderWidth = borderWidth + } + } + + var isChecked: Bool = false { + didSet { + setChecked(isChecked) + selectionChangeHandler?(isChecked) + } + } + + var shouldChangeSelectionHandler: ((NynjaCheckBox) -> Bool)? + + var selectionChangeHandler: ((Bool) -> Void)? + + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + private func setup() { + layer.borderColor = borderColor.cgColor + layer.borderWidth = borderWidth + + setChecked(isChecked) + addTarget(self, action: #selector(actionCheckBoxSelected(sender:)), for: .touchUpInside) + } + + private func setChecked(_ isChecked: Bool) { + let image: UIImage? = isChecked + ? UIImage.nynja.tableOverridesRightOverridesCheckboxIcUnchecked.image + : nil + + setImage(image, for: .normal) + } + + + // MARK: - Layout + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = min(bounds.width, bounds.height) / 2 + layer.masksToBounds = true + } + + + // MARK: - Actions + + @objc private func actionCheckBoxSelected(sender: Any) { + let shouldChange = shouldChangeSelectionHandler?(self) ?? true + + if shouldChange { + isChecked.toggle() + } + } +} diff --git a/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift b/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift index 292ef9f43..df9ffcd71 100644 --- a/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift +++ b/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift @@ -16,6 +16,8 @@ final class NynjaControlContainerView: UIView { private var leftContainerConstraint: Constraint? private var rightContainerConstraint: Constraint? + let gradientHeight: CGFloat = Constraints.gradientView.defaultHeight + // MARK: - Views @@ -93,7 +95,7 @@ final class NynjaControlContainerView: UIView { gradientView.snp.makeConstraints { maker in maker.left.right.equalToSuperview() maker.bottom.equalTo(containerView.snp.top) - maker.height.equalTo(Constraints.gradientView.defaultHeight) + maker.height.equalTo(gradientHeight) } } diff --git a/Nynja/Library/UI/TextInput/Material/Base/MaterialTextInput.swift b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextInput.swift index 2e1b381fb..14d7eb344 100644 --- a/Nynja/Library/UI/TextInput/Material/Base/MaterialTextInput.swift +++ b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextInput.swift @@ -147,9 +147,25 @@ extension MaterialTextInput { } } - func validate(text: String) { + func isValid(text: String) -> Bool { guard !validators.isEmpty else { - return + return true + } + let result = validators + .map { $0.validate(text: text) } + .compactMap { $0 } + + return isValid(result) + } + + private func isValid(_ inputInfos: [InputInfo]) -> Bool { + return inputInfos.isEmpty || !inputInfos.contains { $0.kind != .success } + } + + @discardableResult + func validate(text: String) -> Bool { + guard !validators.isEmpty else { + return true } let infos = validators @@ -157,7 +173,9 @@ extension MaterialTextInput { .compactMap { $0 } info = infos.first - let isValid = info == nil + let isValid = self.isValid(infos) validationHandler?(isValid) + + return isValid } } diff --git a/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfo.swift b/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfo.swift index 9b27debe9..aa320b832 100644 --- a/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfo.swift +++ b/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfo.swift @@ -16,3 +16,13 @@ struct InputInfo { let text: String let kind: Kind } + +extension Optional where Wrapped == InputInfo { + + var isValid: Bool { + if let wrapped = self { + return wrapped.kind == .success + } + return true + } +} diff --git a/Nynja/Library/UI/TextInput/Material/Validator/ClosureValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/ClosureValidator.swift index 9d6ca91b3..85a3589c1 100644 --- a/Nynja/Library/UI/TextInput/Material/Validator/ClosureValidator.swift +++ b/Nynja/Library/UI/TextInput/Material/Validator/ClosureValidator.swift @@ -7,6 +7,7 @@ // final class ClosureValidator: MTIValidator { + private let closure: (String) -> InputInfo? init(closure: @escaping (String) -> InputInfo?) { diff --git a/Nynja/Library/UI/TextInput/Material/Validator/EmailValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/EmailValidator.swift new file mode 100644 index 000000000..a03cec1f3 --- /dev/null +++ b/Nynja/Library/UI/TextInput/Material/Validator/EmailValidator.swift @@ -0,0 +1,43 @@ +// +// EmailValidator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class EmailValidator: MTIValidator { + + var validationHandler: ((Bool) -> Void)? + + private(set) var isValid: Bool = false + + func validate(text: String) -> InputInfo? { + let isValid = self.isValid(text: text) + + var inputInfo: InputInfo? + if text.isEmpty { + inputInfo = InputInfo(text: String.localizable.validationEmailEmpty, kind: .warning) + } else if !isValid { + inputInfo = InputInfo(text: String.localizable.validationEmailInvalid, kind: .warning) + } + + notify(isValid) + + return inputInfo + } + + private func notify(_ isValid: Bool) { + self.isValid = isValid + validationHandler?(isValid) + } + + private func isValid(text: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx) + + return emailTest.evaluate(with: text) + } +} diff --git a/Nynja/Library/UI/TextInput/Material/Validator/FirstNameValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/FirstNameValidator.swift new file mode 100644 index 000000000..bc16a91e1 --- /dev/null +++ b/Nynja/Library/UI/TextInput/Material/Validator/FirstNameValidator.swift @@ -0,0 +1,21 @@ +// +// FirstNameValidator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class FirstNameValidator: LengthValidator { + + init() { + super.init(length: + .both( + max: .max(32, String.localizable.validationMaxLengthWarning), + min: .max(2, String.localizable.validationMinLengthWarning) + ) + ) + } +} diff --git a/Nynja/Library/UI/TextInput/Material/Validator/LastNameValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/LastNameValidator.swift new file mode 100644 index 000000000..fa59da831 --- /dev/null +++ b/Nynja/Library/UI/TextInput/Material/Validator/LastNameValidator.swift @@ -0,0 +1,16 @@ +// +// LastNameValidator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.12.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class LastNameValidator: LengthValidator { + + init() { + super.init(length: .max(32, String.localizable.validationMaxLengthWarning)) + } +} diff --git a/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift index da07fbe74..e2a49c139 100644 --- a/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift +++ b/Nynja/Library/UI/TextInput/Material/Validator/LengthValidator.swift @@ -6,8 +6,8 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // - class LengthValidator: MTIValidator { + private let length: Length private let shouldTrim: Bool diff --git a/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift index d5e030177..de8b02eb1 100644 --- a/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift +++ b/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift @@ -7,14 +7,5 @@ // protocol MTIValidator { - var validationHandler: ((Bool) -> Void)? { get set } func validate(text: String) -> InputInfo? } - -extension MTIValidator { - - var validationHandler: ((Bool) -> Void)? { - get { return nil } - set { } - } -} diff --git a/Nynja/Library/UI/TextInput/Material/Validator/UsernameValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/UsernameValidator.swift new file mode 100644 index 000000000..77768fe57 --- /dev/null +++ b/Nynja/Library/UI/TextInput/Material/Validator/UsernameValidator.swift @@ -0,0 +1,36 @@ +// +// UsernameValidator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class UsernameValidator: LengthValidator { + + private let regexp = "^([a-zA-Z]|[0-9]|_){2,}$" + + init() { + super.init(length: + .both( + max: .max(32, String.localizable.validationMaxLengthWarning), + min: .max(2, String.localizable.validationMinLengthWarning) + ) + ) + } + + override func validate(text: String) -> InputInfo? { + var result = super.validate(text: text) + + if result.isValid { + let predicate = NSPredicate(format: "SELF MATCHES %@", regexp) + if !predicate.evaluate(with: text) { + result = InputInfo(text: String.localizable.validationUsernameInvalid, kind: .warning) + } + } + + return result + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift index 6adae6a05..786bf9f04 100644 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -41,7 +41,9 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } private func makeTopSection(for viewModel: AccountSettingsViewModel) -> Form.Section { - let avatarItem = AvatarRowItem(imageSource: viewModel.avatar, imageSize: CGSize(width: 95, height: 95), height: 144) + let avatarItem = AvatarRowItem(imageSource: viewModel.avatar, + imageSize: CGSize(width: 95, height: 95).adjustedByWidth, + height: CGFloat(144).adjustedByWidth) avatarItem.imageSelectionHandler = { [weak self, weak avatarItem] in self?.chooseAvatar { image in if let image = image { @@ -51,7 +53,9 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } } - let statusItem = PickerRowItem(title: String.localizable.accountSettingsStatusFieldTitle, label: viewModel.status.displayName, height: 44) + let statusItem = PickerRowItem(title: String.localizable.accountSettingsStatusFieldTitle, + label: viewModel.status.displayName, + height: 44) statusItem.labelSelectionHandler = { [weak self, weak statusItem] in self?.chooseStatus { status in self?.viewModel?.status = status @@ -59,7 +63,9 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } } - let timeoutItem = PickerRowItem(title: String.localizable.accountSettingsTimeoutFieldTitle, label: viewModel.timeout.displayName, height: 44) + let timeoutItem = PickerRowItem(title: String.localizable.accountSettingsTimeoutFieldTitle, + label: viewModel.timeout.displayName, + height: 44) timeoutItem.labelSelectionHandler = { [weak self, weak timeoutItem] in self?.chooseTimeout { timeout in self?.viewModel?.timeout = timeout @@ -67,12 +73,13 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } } - let profileMessageItem = TextFieldRowItem(height: 64) + let profileMessageValidator = LengthValidator(length: .max(64, String.localizable.validationMaxLengthWarning)) + let profileMessageItem = TextFieldRowItem(validator: profileMessageValidator, height: CGFloat(64).adjustedByWidth) profileMessageItem.text = viewModel.profileMessage profileMessageItem.placeholder = String.localizable.accountSettingsProfileMessageFieldPlaceholder profileMessageItem.returnKeyType = .next profileMessageItem.edges.top = 8 - profileMessageItem.textChangeAction = { [weak self] text in + profileMessageItem.textChangeAction = { [weak self] text, _ in self?.viewModel?.profileMessage = text } @@ -80,25 +87,25 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } private func makePersonalInfoSection(for viewModel: AccountSettingsViewModel) -> Form.Section { - let header = FormHeader(title: String.localizable.accountSettingsHeaderPersonalInformation, height: 32) + let header = FormHeader(title: String.localizable.accountSettingsHeaderPersonalInformation, height: CGFloat(32).adjustedByWidth) - let fistNameItem = TextFieldRowItem(height: 64) + let fistNameItem = TextFieldRowItem(validator: FirstNameValidator(), height: CGFloat(64).adjustedByWidth) fistNameItem.text = viewModel.firstName fistNameItem.placeholder = "\(String.localizable.accountSettingsFirstNameFieldPlaceholder)*" fistNameItem.returnKeyType = .next - fistNameItem.textChangeAction = { [weak self] text in + fistNameItem.textChangeAction = { [weak self] text, _ in self?.viewModel?.firstName = text } - let lastNameItem = TextFieldRowItem(height: 64) + let lastNameItem = TextFieldRowItem(validator: LastNameValidator(), height: CGFloat(64).adjustedByWidth) lastNameItem.text = viewModel.lastName lastNameItem.placeholder = String.localizable.accountSettingsLastNameFieldPlaceholder lastNameItem.returnKeyType = .next - lastNameItem.textChangeAction = { [weak self] text in + lastNameItem.textChangeAction = { [weak self] text, _ in self?.viewModel?.lastName = text } - let birthdayItem = TextFieldRowItem(height: 64) + let birthdayItem = TextFieldRowItem(height: CGFloat(64).adjustedByWidth) birthdayItem.placeholder = String.localizable.accountSettingsBirthdayFieldPlaceholder birthdayItem.returnKeyType = .next @@ -106,13 +113,13 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } private func makeUsernameSection(for viewModel: AccountSettingsViewModel) -> Form.Section { - let header = FormHeader(title: String.localizable.accountSettingsHeaderUsername, height: 32) + let header = FormHeader(title: String.localizable.accountSettingsHeaderUsername, height: CGFloat(32).adjustedByWidth) - let usernameItem = TextFieldRowItem(validator: UsernameTextValidator(), height: 64) + let usernameItem = TextFieldRowItem(validator: UsernameValidator(), height: CGFloat(64).adjustedByWidth) usernameItem.text = viewModel.username usernameItem.placeholder = String.localizable.accountSettingsUsernameFieldPlaceholder usernameItem.returnKeyType = .done - usernameItem.textChangeAction = { [weak self] text in + usernameItem.textChangeAction = { [weak self] text, _ in self?.viewModel?.username = text } @@ -122,11 +129,12 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } private func makeContactInfoSection(for viewModel: AccountSettingsViewModel) -> Form.Section { - let header = FormHeader(title: String.localizable.accountSettingsHeaderContactInformation, height: 32) + let header = FormHeader(title: String.localizable.accountSettingsHeaderContactInformation, height: CGFloat(32).adjustedByWidth) var items: [AnyFieldRowItem] = [] - let addContactInfoItem = ActionRowItem(text: String.localizable.accountSettingsContactInfoFieldTitle, height: 44) { [weak self] _ in + let addContactInfoItem = ActionRowItem(text: String.localizable.accountSettingsContactInfoFieldTitle, + height: CGFloat(44).adjustedByWidth) { [weak self] _ in self?.chooseContactInfoType { [weak self] contactInfoType in guard let accountId = self?.interactor?.accountId else { return } self?.wireframe.addContactInfo(ofType: contactInfoType, accountId: accountId) @@ -165,7 +173,8 @@ final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterPro } private func makeDeleteAccountSection() -> Form.Section { - let deleteAccountItem = DestructiveActionRowItem(title: String.localizable.accountSettingsDeleteAccountFieldTitle, height: 44) { [weak self] _ in + let deleteAccountItem = DestructiveActionRowItem(title: String.localizable.accountSettingsDeleteAccountFieldTitle, + height: CGFloat(44).adjustedByWidth) { [weak self] _ in guard let self = self, let identityId = self.interactor?.identityId, let accountId = self.interactor?.accountId else { return } diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift index 15f3f3240..b445a9390 100644 --- a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift @@ -9,7 +9,7 @@ import Foundation import NynjaUIKit -final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, LoadingDisplayable, InitializeInjectable { +final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, FormContainer, LoadingDisplayable, InitializeInjectable { private let presenter: AccountSettingsPresenterProtocol @@ -45,7 +45,7 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa return contentView }() - private lazy var stackView: UIStackView = { + private(set) lazy var stackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical @@ -85,7 +85,7 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa return button }() - private var form: Form? + var form: Form? // MARK: - Init @@ -134,67 +134,6 @@ final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, Loa } - // MARK: - View Input - - func setup(form: Form) { - self.form = form - stackView.arrangedSubviews.forEach { - stackView.removeArrangedSubview($0) - $0.removeFromSuperview() - } - - func addSpace(with height: CGFloat) { - let view = UIView() - view.snp.makeConstraints { maker in - maker.height.equalTo(height) - } - stackView.addArrangedSubview(view) - } - - for section in form.sections { - if let header = section.header { - let headerView = header.makeView() ?? UIView() - headerView.snp.makeConstraints { maker in - maker.height.equalTo(header.height) - } - stackView.addArrangedSubview(headerView) - } - - if section.contentInset.top > 0 { - addSpace(with: section.contentInset.top) - } - defer { - if section.contentInset.bottom > 0 { - addSpace(with: section.contentInset.bottom) - } - } - - for row in section.rows { - let itemView = row.makeView() - - if let height = row.height { - itemView.snp.makeConstraints { maker in - maker.height.equalTo(height) - } - } - - if let inset = row.additionalInset { - if inset.top > 0 { - addSpace(with: inset.top) - } - stackView.addArrangedSubview(itemView) - - if inset.bottom > 0{ - addSpace(with: inset.bottom) - } - } else { - stackView.addArrangedSubview(itemView) - } - } - } - } - - // MARK: - Actions @objc private func saveAction(sender: UIButton) { diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift index f1fe853fc..7c533e8c6 100644 --- a/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift @@ -9,7 +9,7 @@ struct AuthProviderUIConfiguration { enum Content { - case email(controller: EmailTextController) + case email(validator: EmailValidator) case phoneNumber(controller: PhoneNumberTextController, country: Country, selectionHandler: () -> Void) } diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift index d14e8dccc..19a211570 100644 --- a/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift @@ -46,9 +46,9 @@ final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, case .email: screenTitle = String.localizable.authProviderEmailScreenTitle - let controller = makeEmailController() + let controller = makeEmailValidator() - content = .email(controller: controller) + content = .email(validator: controller) case .phoneNumber: screenTitle = String.localizable.authProviderPhoneScreenTitle @@ -80,14 +80,14 @@ final class AuthProviderPresenter: BasePresenter, AuthProviderPresenterProtocol, return controller } - private func makeEmailController() -> EmailTextController { - let controller = EmailTextController(validator: EmailValidator()) + private func makeEmailValidator() -> EmailValidator { + let validator = EmailValidator() - controller.validationAction = { [weak view] result in - view?.setNextActionEnabled(result) + validator.validationHandler = { [weak view] isValid in + view?.setNextActionEnabled(isValid) } - return controller + return validator } func setAvailableForSearch(_ isAvailable: Bool) { diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift index 0b2eddae7..e2e7b478a 100644 --- a/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift @@ -163,8 +163,8 @@ final class AuthProviderViewController: BaseVC, AuthProviderViewInput, LoadingDi let contentView: UIView switch configuration.content { - case let .email(controller): - let emailView = EmailLoginView(textController: controller) + case let .email(validator): + let emailView = EmailLoginView(validator: validator) self.emailView = emailView contentView = emailView diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift index 3d4323d15..2811d19a7 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift @@ -8,7 +8,7 @@ import Foundation -struct SocialLinkValidator: LinkValidator { +final class SocialLinkValidator: LinkValidator { let domain: String @@ -23,12 +23,22 @@ struct SocialLinkValidator: LinkValidator { } func validate(text: String) -> InputInfo? { - let predicate = NSPredicate(format: "SELF MATCHES %@", regexp) - let isValid = predicate.evaluate(with: text) + let isValid = self.isValid(text: text) + + var inputInfo: InputInfo? + if text.isEmpty { + inputInfo = InputInfo(text: String.localizable.validationAccountInfoLinkEmpty, kind: .warning) + } else if !isValid { + inputInfo = InputInfo(text: String.localizable.validationAccountInfoLinkInvalid, kind: .warning) + } validationHandler?(isValid) - // FIXME: return errors - return nil + return inputInfo + } + + private func isValid(text: String) -> Bool { + let predicate = NSPredicate(format: "SELF MATCHES %@", regexp) + return predicate.evaluate(with: text) } } diff --git a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift index b91fdcf32..b72cb720e 100644 --- a/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift @@ -126,7 +126,7 @@ final class ContactInfoManagementPresenter: BasePresenter, ContactInfoManagement } private func makeEmailViewModel(for inputEmail: String?) -> ContentViewModel { - let viewModel = TextFieldContentViewModel(validator: EmailTextValidator(), initialText: inputEmail) + let viewModel = TextFieldContentViewModel(validator: EmailValidator(), initialText: inputEmail) viewModel.contentInset = textFieldContentInset() viewModel.placeholder = String.localizable.contactInfoEmailPlaceholder diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormContainer.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormContainer.swift new file mode 100644 index 000000000..cd97539e8 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormContainer.swift @@ -0,0 +1,80 @@ +// +// FormContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 1/2/19. +// Copyright © 2019 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +protocol FormContainer: class { + + var stackView: UIStackView { get } + + var form: Form? { get set } + + func setup(form: Form) +} + +extension FormContainer { + + func setup(form: Form) { + self.form = form + stackView.arrangedSubviews.forEach { + stackView.removeArrangedSubview($0) + $0.removeFromSuperview() + } + + func addSpace(with height: CGFloat) { + let view = UIView() + view.snp.makeConstraints { maker in + maker.height.equalTo(height) + } + stackView.addArrangedSubview(view) + } + + for section in form.sections { + if let header = section.header { + let headerView = header.makeView() ?? UIView() + headerView.snp.makeConstraints { maker in + maker.height.equalTo(header.height) + } + stackView.addArrangedSubview(headerView) + } + + if section.contentInset.top > 0 { + addSpace(with: section.contentInset.top) + } + defer { + if section.contentInset.bottom > 0 { + addSpace(with: section.contentInset.bottom) + } + } + + for row in section.rows { + let itemView = row.makeView() + + if let height = row.height { + itemView.snp.makeConstraints { maker in + maker.height.equalTo(height) + } + } + + if let inset = row.additionalInset { + if inset.top > 0 { + addSpace(with: inset.top) + } + stackView.addArrangedSubview(itemView) + + if inset.bottom > 0{ + addSpace(with: inset.bottom) + } + } else { + stackView.addArrangedSubview(itemView) + } + } + } + } +} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift index 740ef2640..ba29e5b76 100644 --- a/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift @@ -15,13 +15,23 @@ import NynjaUIKit final class TextFieldRowItem: FieldRowItem { typealias View = TextFieldRowItemView - typealias TextChangeAction = (String) -> Void + typealias TextChangeAction = (_ newText: String, _ oldText: String) -> Void let validator: MTIValidator? var font: UIFont = UIFont.makeFont(with: FontFamily.NotoSans.medium.name, height: 22.0.adjustedByWidth)! - var text: String? + var text: String? { + didSet { + let newText = text ?? "" + if view?.textField.text != newText { + view?.textField.text = newText + } + + let oldText = oldValue ?? "" + textChangeAction?(newText, oldText) + } + } var placeholder: String? var textColor = UIColor.nynja.white @@ -83,7 +93,7 @@ final class TextFieldRowItem: FieldRowItem { } textField.textChanged = { [weak self] field in - self?.textChangeAction?(field.text) + self?.text = field.text } textField.font = font @@ -118,6 +128,20 @@ final class TextFieldRowItem: FieldRowItem { _ = view?.textField.resignFirstResponder() return true } + + func validate() -> Bool { + guard let textField = view?.textField else { + return false + } + return textField.validate(text: textField.text) + } + + func isValid() -> Bool { + guard let textField = view?.textField else { + return false + } + return textField.isValid(text: textField.text) + } } // MARK: - View - diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift deleted file mode 100644 index fea133f14..000000000 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/EmailTextController.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// EmailTextController.swift -// Nynja -// -// Created by Anton Poltoratskyi on 16.11.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -final class EmailTextController { - - private let validator: Validator - - var validationAction: ((Bool) -> Void)? - - private(set) var isValid: Bool = false - - init(validator: Validator) { - self.validator = validator - } - - func textDidChange(_ textInput: MaterialTextInput) { - isValid = validator.isValid(text: textInput.text.trimmed()) - validationAction?(isValid) - } - - func textInputShouldReturn(_ textInput: MaterialTextField) -> Bool { - _ = textInput.resignFirstResponder() - return false - } -} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/Validator.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/Validator.swift deleted file mode 100644 index c3ce48625..000000000 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/Validator.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Validator.swift -// Nynja -// -// Created by Anton Poltoratskyi on 16.11.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol Validator { - func isValid(text: String) -> Bool -} - -struct EmailValidator: Validator { - func isValid(text: String) -> Bool { - let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx) - - return emailTest.evaluate(with: text) - } -} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift index 865dc2fcc..1560ffe4a 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift @@ -14,8 +14,8 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec private let presenter: AuthPresenterProtocol private let phoneNumberTextController: PhoneNumberTextController - - private let emailTextController: EmailTextController + + private let emailValidator = EmailValidator() // MARK: - Views @@ -183,13 +183,11 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec struct Dependencies { let presenter: AuthPresenterProtocol let phoneNumberController: PhoneNumberTextController - let emailController: EmailTextController } init(dependencies: Dependencies) { presenter = dependencies.presenter phoneNumberTextController = dependencies.phoneNumberController - emailTextController = dependencies.emailController super.init(nibName: nil, bundle: nil) } @@ -248,7 +246,7 @@ final class AuthViewController: UIViewController, AuthViewInput, InitializeInjec } private func setupEmailController() { - emailTextController.validationAction = { [weak self] result in + emailValidator.validationHandler = { [weak self] result in self?.nextButton.isEnabled = result } } @@ -320,7 +318,7 @@ private extension AuthViewController { func showEmailLogin(animated: Bool) { if animated { - let nextButtonEnabled = emailTextController.isValid + let nextButtonEnabled = emailValidator.isValid animateChangingViews(first: phoneContainerView, second: emailContainerView, isNextActionEnabled: nextButtonEnabled) } else { emailContainerView.isHidden = false @@ -329,7 +327,7 @@ private extension AuthViewController { } func makeEmailLoginView(on view: UIView) -> DetailContainerView { - let loginView = EmailLoginView(textController: emailTextController) + let loginView = EmailLoginView(validator: emailValidator) let container = DetailContainerView(contentView: loginView) container.detailsLabel.text = String.localizable.authEnterEmailAddressComment diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift index 209128992..a27cdf834 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift @@ -14,13 +14,13 @@ final class EmailLoginView: UIView { private(set) lazy var inputField = makeInputField() - private let textController: EmailTextController + private let validator: EmailValidator - // MARK: - Init + // MARK: - Initvalidator - init(textController: EmailTextController) { - self.textController = textController + init(validator: EmailValidator) { + self.validator = validator super.init(frame: .zero) @@ -37,13 +37,7 @@ final class EmailLoginView: UIView { // MARK: - Setup private func setup() { - inputField.textChanged = { [weak textController] textInput in - textController?.textDidChange(textInput) - } - - inputField.returnHandler = { [weak textController] textInput in - return textController?.textInputShouldReturn(textInput) ?? false - } + inputField.validators = [validator] } } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift index fea9e977e..1145eca49 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift @@ -43,8 +43,7 @@ final class AuthWireframe: Wireframe, AuthWireframeProtocol { let view = AuthViewController( dependencies: .init( presenter: presenter, - phoneNumberController: PhoneNumberTextController(countryProvider: dependencies.countriesProvider), - emailController: EmailTextController(validator: EmailValidator()) + phoneNumberController: PhoneNumberTextController(countryProvider: dependencies.countriesProvider) ) ) diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift index 3af5676f1..65c3227fa 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift @@ -19,39 +19,31 @@ protocol CreateProfileWireframeProtocol: class { // MARK: - View -protocol CreateProfileViewInput: LoadingInteractive where Self: UIViewController { - func updateProfileField(_ field: ProfileField, value: String) - func setCreateEnabled(_ enabled: Bool) +protocol CreateProfileViewInput: LoadingInteractive { + func setup(form: Form) + func setTermsAccepted(_ isAccepted: Bool) + func setActionEnabled(_ isEnabled: Bool) } // MARK: - Presenter protocol CreateProfilePresenterProtocol: NavigationProtocol { - func createAccount() - func isValidValue(_ value: String, for field: ProfileField) -> Result - func setProfileField(value: String, field: ProfileField) - func chooseAvatar(completion: @escaping (UIImage?) -> Void) - func checkTermsOfUse() -> Bool + func viewDidLoad() + func open(url: URL) + func acceptTerms(_ isAccepted: Bool) + func createAccount() } // MARK: - Interactor // MARK: Input protocol CreateProfileInteractorInput: class { - func setProfileField(_ field: ProfileField, value: String) - func checkTermsOfUse() -> Bool - func setAvatar(image: UIImage?) - func isValidValue(_ value: String, for field: ProfileField) -> Result - func createAccount() + func createAccount(from viewModel: CreateProfileViewModel) } // MARK: Output protocol CreateProfileInteractorOutput: class { - - func minimalRequirementsAreSatisfied(_ satisfied: Bool) - func profileFieldUpdated(_ profileField: ProfileField, value: String) - func didReceiveCreatedAccount() func didReceiveFailure(_ error: Error?) } diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/CreateProfileViewModel.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/CreateProfileViewModel.swift new file mode 100644 index 000000000..0f59891f2 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/CreateProfileViewModel.swift @@ -0,0 +1,17 @@ +// +// CreateProfileViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 1/2/19. +// Copyright © 2019 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct CreateProfileViewModel { + var avatar: ImageData = .url(nil) + var firstName: String = "" + var lastName: String? + var username: String = "" + var isTermsAccepted: Bool = false +} diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/ProfileField.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/ProfileField.swift deleted file mode 100644 index ef3cce061..000000000 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Entities/ProfileField.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// ProfileField.swift -// Nynja -// -// Created by Anton Poltoratskyi on 05.11.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -enum ProfileField { - case firstName - case lastName - case accountName - case userName - case profileMessage - - var isRequired: Bool { - return self == .firstName - } - - var validationRule: String { - return "^([a-zA-Z]|[0-9]|_){2,}$" - } - - var placeholder: String { - switch self { - case .firstName: - return String.localizable.createProfileFirstNameFieldPlaceholder - case .lastName: - return String.localizable.createProfileLastNameFieldPlaceholder - case .accountName: - return String.localizable.createProfileAccountNameFieldPlaceholder - case .userName: - return String.localizable.createProfileUsernameFieldPlaceholder - case .profileMessage: - return String.localizable.createProfileProfileMessageFieldPlaceholder - } - } -} diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift index d735568ba..b17033e9f 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -16,44 +16,6 @@ final class CreateProfileInteractor: CreateProfileInteractorInput, InitializeInj private let accountId: String - private var avatar: UIImage? = nil - - private var firstName: String = "" { - didSet { - if oldValue == userName { - userName = firstName - } - - presenter?.profileFieldUpdated(.firstName, value: firstName) - presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) - } - } - private var lastName: String = "" { - didSet { - presenter?.profileFieldUpdated(.lastName, value: lastName) - presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) - } - } - - private var accountName: String = "" { - didSet { - presenter?.profileFieldUpdated(.accountName, value: accountName) - presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) - } - } - private var userName: String = "" { - didSet { - presenter?.profileFieldUpdated(.userName, value: userName) - presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) - } - } - - private var checkTermsOfUsage: Bool = false { - didSet { - presenter?.minimalRequirementsAreSatisfied(isAllRequirementsAreSatisfied()) - } - } - // MARK: - Services @@ -89,29 +51,30 @@ final class CreateProfileInteractor: CreateProfileInteractorInput, InitializeInj // MARK: - Interactor Input - func createAccount() { - if let avatar = avatar { + func createAccount(from viewModel: CreateProfileViewModel) { + switch viewModel.avatar { + case let .image(avatar): imageUploader.uploadImageFile(avatar) { [weak self] result in switch result { case let .success(avatarURL): - self?.createAccount(withAvatar: avatarURL) + self?.createAccount(withAvatar: avatarURL, viewModel: viewModel) case let .failure(error): self?.presenter?.didReceiveFailure(error) } } - } else { - createAccount(withAvatar: nil) + case let .url(avatarURL): + createAccount(withAvatar: avatarURL, viewModel: viewModel) } } - private func createAccount(withAvatar avatarURL: URL?) { + private func createAccount(withAvatar avatarURL: URL?, viewModel: CreateProfileViewModel) { let accountInfo = AccountInfo(accountId: accountId, avatar: avatarURL?.absoluteString, accountMark: nil, - accountName: accountName, - firstName: firstName, - lastName: lastName, - username: userName, + accountName: nil, + firstName: viewModel.firstName, + lastName: viewModel.lastName, + username: viewModel.username, accountStatus: .enabled, roles: nil, qrCode: accountId, @@ -133,53 +96,4 @@ final class CreateProfileInteractor: CreateProfileInteractorInput, InitializeInj } } } - - func checkTermsOfUse() -> Bool { - checkTermsOfUsage = !checkTermsOfUsage - - return checkTermsOfUsage - } - - func setAvatar(image: UIImage?) { - avatar = image - } - - func setProfileField(_ field: ProfileField, value: String) { - switch field { - case .firstName: - firstName = value - case .lastName: - lastName = value - case .accountName: - accountName = value - case .userName: - userName = value - case .profileMessage: - break - } - } - - func isValidValue(_ value: String, for field: ProfileField) -> Result { - enum ValidationError: Error { - case somethingWentWrong - } - - let predicate = NSPredicate(format: "SELF MATCHES %@", field.validationRule) - - return predicate.evaluate(with: value) - ? .success(()) - : .failure(ValidationError.somethingWentWrong) - } -} - -// MARK: - Private - -private extension CreateProfileInteractor { - func isAllRequirementsAreSatisfied() -> Bool { - return checkTermsOfUsage - && isValidValue(firstName, for: .firstName).isSuccess - && firstName.count >= 2 - && isValidValue(userName, for: .userName).isSuccess - && userName.count >= 2 - } } diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift index ad8dd2e48..0e2852b16 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -13,53 +13,121 @@ final class CreateProfilePresenter: CreateProfileInteractorOutput, CreateProfile private weak var view: CreateProfileViewInput? private var interactor: CreateProfileInteractorInput! private var wireframe: CreateProfileWireframeProtocol! - - // MARK: - Presenter - - func createAccount() { - view?.showLoading() - interactor.createAccount() - } + private var viewModel = CreateProfileViewModel() - func isValidValue(_ value: String, for field: ProfileField) -> Result { - return interactor.isValidValue(value, for: field) - } - func setProfileField(value: String, field: ProfileField) { - interactor.setProfileField(field, value: value) - } + // MARK: - Fields - func chooseAvatar(completion: @escaping (UIImage?) -> Void) { - wireframe.chooseAvatar { image in - completion(image) - self.interactor?.setAvatar(image: image) + private lazy var avatarField: AvatarRowItem = { + let avatarItem = AvatarRowItem(imageSource: viewModel.avatar, + imageSize: CGSize(width: 94, height: 94).adjustedByWidth, + height: CGFloat(142).adjustedByWidth) + + avatarItem.imageSelectionHandler = { [weak self, weak avatarItem] in + self?.chooseAvatar { image in + if let image = image { + self?.viewModel.avatar = .image(image) + avatarItem?.imageSource = .image(image) + } + } } - } + + return avatarItem + }() + + private lazy var firstNameField: TextFieldRowItem = { + let fistNameItem = TextFieldRowItem(validator: FirstNameValidator(), height: CGFloat(64).adjustedByWidth) + fistNameItem.text = viewModel.firstName + fistNameItem.placeholder = "\(String.localizable.createProfileFirstNameFieldPlaceholder)*" + fistNameItem.returnKeyType = .next + fistNameItem.textChangeAction = { [weak self] newText, oldText in + self?.validate { + self?.viewModel.firstName = newText + if oldText == self?.usernameField.text { + self?.usernameField.text = newText + } + } + } + return fistNameItem + }() + + private lazy var lastNameField: TextFieldRowItem = { + let lastNameItem = TextFieldRowItem(validator: LastNameValidator(), height: CGFloat(64).adjustedByWidth) + lastNameItem.text = viewModel.lastName + lastNameItem.placeholder = String.localizable.createProfileLastNameFieldPlaceholder + lastNameItem.returnKeyType = .next + lastNameItem.textChangeAction = { [weak self] text, _ in + self?.validate { + self?.viewModel.lastName = text + } + } + return lastNameItem + }() + + private lazy var usernameField: TextFieldRowItem = { + let usernameItem = TextFieldRowItem(validator: UsernameValidator(), height: CGFloat(64).adjustedByWidth) + usernameItem.text = viewModel.username + usernameItem.placeholder = String.localizable.createProfileUsernameFieldPlaceholder + usernameItem.returnKeyType = .done + usernameItem.textChangeAction = { [weak self] text, _ in + self?.validate { + self?.viewModel.username = text + } + } + return usernameItem + }() + + private lazy var descriptionField: TextRowItem = TextRowItem(text: String.localizable.createProfileTermsHint) + + + // MARK: - Presenter - func profileFieldUpdated(_ profileField: ProfileField, value: String) { - view?.updateProfileField(profileField, value: value) + func viewDidLoad() { + let section = Form.Section(rows: [avatarField, firstNameField, lastNameField, usernameField, descriptionField]) + section.contentInset = UIEdgeInsets(top: CGFloat(8.0).adjustedByWidth, left: 0, bottom: 0, right: 0) + + let form = Form(sections: [section]) + view?.setup(form: form) + view?.setTermsAccepted(viewModel.isTermsAccepted) } - func checkTermsOfUse() -> Bool { - return interactor.checkTermsOfUse() + func back() { + wireframe.back() } func open(url: URL) { wireframe.open(url: url) } - - // MARK: - Interactor Output - - func minimalRequirementsAreSatisfied(_ satisfied: Bool) { - view?.setCreateEnabled(satisfied) + func acceptTerms(_ isAccepted: Bool) { + validate { + viewModel.isTermsAccepted = isAccepted + } + } + + func createAccount() { + view?.showLoading() + interactor.createAccount(from: viewModel) + } + + private func chooseAvatar(completion: @escaping (UIImage?) -> Void) { + wireframe.chooseAvatar(completion: completion) } - func back() { - wireframe.back() + private func validate(_ block: () -> Void) { + block() + let isRequirementSatisfied = viewModel.isTermsAccepted + && firstNameField.isValid() + && lastNameField.isValid() + && usernameField.isValid() + + view?.setActionEnabled(isRequirementSatisfied) } + + // MARK: - Interactor Output + func didReceiveCreatedAccount() { view?.hideLoading() wireframe?.end() @@ -70,7 +138,7 @@ final class CreateProfilePresenter: CreateProfileInteractorOutput, CreateProfile } } -// MARK: - SetInjectable +// MARK: - Injection extension CreateProfilePresenter { struct Dependencies { diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift index 2fa1fdc6e..500f6eed9 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift @@ -6,39 +6,137 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +import UIKit +import NynjaUIKit -final class CreateProfileViewController: BaseVC, CreateProfileViewInput, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { +final class CreateProfileViewController: BaseVC, CreateProfileViewInput, FormContainer, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { + private let presenter: CreateProfilePresenterProtocol - private let viewsFactory: CreateProfileViewsFactoryProtocol // MARK: - Views private(set) lazy var progressHUD = makeProgressHUD(on: view) - private lazy var container = viewsFactory.makeContainer(on: view, headerView: navigationView, footerView: createButton) - private lazy var scrollView = viewsFactory.makeScrollView(on: container) - private lazy var contentContainer = viewsFactory.makeContentContainer(on: scrollView, widthView: view, presenter: presenter) + // MARK: Scroll Container + + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + + view.addSubview(scrollView) + scrollView.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom) + maker.left.right.equalToSuperview() + } + + return scrollView + }() + + private lazy var contentView: UIView = { + let contentView = UIView() + + scrollView.addSubview(contentView) + contentView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + maker.width.equalToSuperview() + } + + return contentView + }() + + // MARK: Stack + + var form: Form? + + private(set) lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + + contentView.addSubview(stackView) + stackView.snp.makeConstraints { maker in + maker.top.left.right.equalToSuperview() + } + + return stackView + }() + + // MARK: Terms - private lazy var createButton = viewsFactory.makeCreateButton(on: view, target: self, selector: #selector(createAccount(sender:))) + private lazy var termsContainerView: UIView = { + let containerView = UIView() + + contentView.addSubview(containerView) + containerView.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + let top = Constraints.termsContainer.top + let height = Constraints.termsContainer.height + + maker.top.equalTo(stackView.snp.bottom).offset(top) + maker.left.right.equalToSuperview().inset(horizontal) + maker.bottom.equalToSuperview() + maker.height.equalTo(height) + } + + return containerView + }() + + private lazy var checkBox: NynjaCheckBox = { + let checkBox = NynjaCheckBox() + + termsContainerView.addSubview(checkBox) + checkBox.snp.makeConstraints { maker in + let size = Constraints.checkBox.size + + maker.centerY.equalToSuperview() + maker.left.equalToSuperview() + maker.width.height.equalTo(size) + } + + return checkBox + }() - private lazy var gradientView: GradientView = { - let gradientHeight = 29.0.adjustedByWidth + private lazy var termsTextView: UITextView = { + let textView = makeTermsOfUseTextView(on: termsContainerView) - let backgroundColor = UIColor.nynja.backgroundColor - let colors = [backgroundColor.withAlphaComponent(0), backgroundColor] + textView.snp.makeConstraints { maker in + let left = Constraints.termsTextView.left + maker.centerY.equalToSuperview() + maker.left.equalTo(checkBox.snp.right).offset(left) + maker.right.equalToSuperview() + } - let gradientView = GradientView(colors: colors) - gradientView.isUserInteractionEnabled = false + return textView + }() + + // MARK: Button + + private let bottomInset: CGFloat = Constraints.createButton.bottom + + private lazy var controlContainerView: NynjaControlContainerView = { + let containerView = NynjaControlContainerView(contentView: createButton) - view.addSubview(gradientView) - gradientView.snp.makeConstraints { maker in - maker.bottom.equalTo(createButton.snp.top) + view.addSubview(containerView) + containerView.snp.makeConstraints { maker in + maker.top.equalTo(scrollView.snp.bottom) maker.left.right.equalToSuperview() - maker.height.equalTo(gradientHeight) + adjustVerticalInset(.bottom, make: maker, offset: -bottomInset) } - return gradientView + containerView.addGradientView() + + return containerView + }() + + private lazy var createButton: RoundNynjaButton = { + let button = RoundNynjaButton(font: FontFamily.NotoSans.medium, fontHeight: Constraints.createButton.fontHeight) + + button.setTitle(String.localizable.createProfileCreateButton, for: .normal) + + button.snp.makeConstraints { maker in + let height = Constraints.createButton.height + maker.height.equalTo(height) + } + + return button }() @@ -46,12 +144,10 @@ final class CreateProfileViewController: BaseVC, CreateProfileViewInput, Keyboar struct Dependencies { let presenter: CreateProfilePresenterProtocol - let viewsFactory: CreateProfileViewsFactoryProtocol } init(dependencies: Dependencies) { presenter = dependencies.presenter - viewsFactory = dependencies.viewsFactory super.init(nibName: nil, bundle: nil) } @@ -62,14 +158,14 @@ final class CreateProfileViewController: BaseVC, CreateProfileViewInput, Keyboar // MARK: - Life Cycle - override func viewDidLoad() { - super.viewDidLoad() + override func initialize() { + super.initialize() setupUI() - enableKeyboardHidingWhenTappedAround() + presenter.viewDidLoad() } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) registerForKeyboardNotifications() } @@ -78,6 +174,11 @@ final class CreateProfileViewController: BaseVC, CreateProfileViewInput, Keyboar unregisterForKeyboardNotifications() } + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + view.endEditing(true) + } + // MARK: - UI Setup @@ -92,38 +193,49 @@ final class CreateProfileViewController: BaseVC, CreateProfileViewInput, Keyboar backButtonImage: UIImage.nynja.icBackNavigation.image) ) - _ = [container, scrollView, contentContainer, createButton, gradientView] + _ = [scrollView, contentView, stackView, termsContainerView, checkBox, termsTextView, controlContainerView, createButton] + + scrollView.contentInset.bottom = controlContainerView.gradientHeight + + checkBox.selectionChangeHandler = { [weak presenter] isChecked in + presenter?.acceptTerms(isChecked) + } + + createButton.addTarget(self, action: #selector(createAccount(sender:)), for: .touchUpInside) - contentContainer.termsOfUseTextView.delegate = self + termsTextView.delegate = self + } + + + // MARK: - Actions + + @objc func createAccount(sender: UIButton) { + presenter.createAccount() } } // MARK: - View Input extension CreateProfileViewController { - func updateProfileField(_ field: ProfileField, value: String) { - contentContainer.textFieldValueDidChange(value: value, field: field) + + func setTermsAccepted(_ isAccepted: Bool) { + checkBox.isChecked = isAccepted } - func setCreateEnabled(_ enabled: Bool) { - createButton.isEnabled = enabled + func setActionEnabled(_ isEnabled: Bool) { + createButton.isEnabled = isEnabled } } // MARK: - KeyboardInteractive extension CreateProfileViewController { + func keyboardNotified(endFrame: CGRect) { - var bottomInset: CGFloat = 28 - - if endFrame.origin.y < UIScreen.main.bounds.size.height { - bottomInset += endFrame.height + if endFrame.origin.y >= UIScreen.main.bounds.size.height { + updateToHide(view: controlContainerView, offset: -bottomInset) } else { - bottomInset += UIWindow.safeAreaBottomPadding() - } - - createButton.snp.updateConstraints { (make) in - make.bottom.equalToSuperview().inset(bottomInset) + updateToShow(view: controlContainerView, offset: -bottomInset - endFrame.height) } } } @@ -138,11 +250,65 @@ extension CreateProfileViewController: UITextViewDelegate { } } -// MARK: - Actions +// MARK: - Layout private extension CreateProfileViewController { - @objc func createAccount(sender: UIButton) { - presenter.createAccount() + func makeTermsOfUseTextView(on view: UIView) -> UITextView { + let textView = UITextView() + view.addSubview(textView) + + textView.backgroundColor = UIColor.nynja.clear + textView.isScrollEnabled = false + + textView.dataDetectorTypes = .link + textView.isEditable = false + + let text = NSMutableAttributedString(string: String.localizable.createProfileAgreeAtTerms) + text.addAttributes([.foregroundColor : UIColor.nynja.dustyGray, + .font: FontFamily.NotoSans.regular.font(size: 14)], + range: NSMakeRange(0, text.length)) + + let attributes: [NSAttributedString.Key : Any] = [ + .link : "https://landing.nynja.io/terms-of-use", + .underlineStyle: NSUnderlineStyle.single.rawValue, + .underlineColor: UIColor.nynja.blue, + .foregroundColor: UIColor.nynja.blue, + .font: FontFamily.NotoSans.regular.font(size: 14) + ] + + let termsOfUseStr = NSMutableAttributedString(string: String.localizable.createProfileTermsOfUse) + termsOfUseStr.addAttributes(attributes, range: NSMakeRange(0, termsOfUseStr.length)) + + text.append(NSAttributedString(string: " ")) + text.append(termsOfUseStr) + + textView.attributedText = text + + return textView + } + + enum Constraints { + + static let horizontal: CGFloat = CGFloat(16).adjustedByWidth + + enum termsContainer { + static let top: CGFloat = CGFloat(8).adjustedByWidth + static let height: CGFloat = CGFloat(44).adjustedByWidth + } + + enum checkBox { + static let size: CGFloat = CGFloat(24).adjustedByWidth + } + + enum termsTextView { + static let left: CGFloat = CGFloat(8).adjustedByWidth + } + + enum createButton { + static let fontHeight: CGFloat = CGFloat(22).adjustedByWidth + static let bottom: CGFloat = CGFloat(28).adjustedByWidth + static let height: CGFloat = CGFloat(44).adjustedByWidth + } } } diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift deleted file mode 100644 index f303d4cd2..000000000 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/Subviews/CreateProfileContentView.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// CreateProfileContentView.swift -// Nynja -// -// Created by Ash on 10/12/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -final class CreateProfileContentView: UIView, Configurable { - private let viewsFactory: CreateProfileViewsFactoryProtocol - - private var chooseAvatarAction: (() -> Void)? - private var markCheckAction: (() -> Bool)? - private var textChangedAction: ((String, ProfileField) -> Void)? - private var isValidAction: ((String, ProfileField) -> Result)? - - init(viewsFactory: CreateProfileViewsFactoryProtocol) { - self.viewsFactory = viewsFactory - - super.init(frame: CGRect.zero) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private(set) lazy var avatarbutton = viewsFactory.makeAvatarButton(on: self, target: self, selector: #selector(chooseAvatar(sender:))) - - private(set) lazy var firstNameTextField = viewsFactory.makeFirstNameTextField( - on: self, - top: avatarbutton, - textChangedHandler: textChangedHandler, - shouldChangeTextHandler: shouldChangeTextHandler) - - private(set) lazy var lastNameTextField = viewsFactory.makeLastNameTextField( - on: self, - top: firstNameTextField, - textChangedHandler: textChangedHandler, - shouldChangeTextHandler: shouldChangeTextHandler) - - private(set) lazy var usernameTextField = viewsFactory.makeUsernameTextField( - on: self, - top: lastNameTextField, - textChangedHandler: textChangedHandler, - shouldChangeTextHandler: shouldChangeTextHandler) - - private(set) lazy var descriptionLabel = viewsFactory.makeDescriptionLabel(on: self, top: usernameTextField) - - private(set) lazy var checkBoxContainerView = viewsFactory.makeCheckBoxContainerView(on: self, top: descriptionLabel) - - private(set) lazy var checkButton = viewsFactory.makeCheckButton(on: checkBoxContainerView, - target: self, - selector: #selector(markCheck(sender:))) - - private(set) lazy var termsOfUseTextView = viewsFactory.makeTermsOfUseTextView(on: checkBoxContainerView, - left: checkButton) -} - -// MARK: - Configurable - -extension CreateProfileContentView { - struct Config { - let chooseAvatarAction: () -> Void - let markCheckAction: () -> Bool - let textChangedAction: (String, ProfileField) -> Void - let isValidAction: (String, ProfileField) -> Result - } - - func configure(config: CreateProfileContentView.Config) { - chooseAvatarAction = config.chooseAvatarAction - markCheckAction = config.markCheckAction - isValidAction = config.isValidAction - textChangedAction = config.textChangedAction - - _ = [avatarbutton, firstNameTextField, lastNameTextField, usernameTextField, descriptionLabel, checkBoxContainerView, checkButton, termsOfUseTextView] - } -} - -// MARK: - Public - -extension CreateProfileContentView { - func textFieldValueDidChange(value: String, field: ProfileField) { - switch field { - case .firstName: - firstNameTextField.text = value - case .lastName: - lastNameTextField.text = value - case .accountName: -// accountNameTextField.text = value - break - case .userName: - usernameTextField.text = value - case .profileMessage: - break - } - } - - func updateAvatar(with image: UIImage?) { - let avatar = image ?? UIImage.nynja.icEmptyAvatar.image - avatarbutton.setImage(avatar, for: .normal) - } -} - -// MARK: - Actions - -extension CreateProfileContentView { - @objc func chooseAvatar(sender: UIButton) { - chooseAvatarAction?() - } - - @objc func markCheck(sender: UIButton) { - guard let result = markCheckAction?() else { - return - } - - let imageForSender = result - ? UIImage.nynja.tableOverridesRightOverridesCheckboxIcUnchecked.image - : nil - - sender.setImage(imageForSender, for: .normal) - } -} - -// MARK: - Private - -private extension CreateProfileContentView { - func textChangedHandler(field: ProfileField) -> MTITextChangedHandler { - return { [weak self] input in - self?.textChangedAction?(input.text, field) - } - } - - func shouldChangeTextHandler(field: ProfileField) -> MTIShouldChangeTextHandler { - return { [weak self] input, range, string in - guard let isValid = self?.isValidAction?((input.text as NSString).replacingCharacters(in: range, with: string), field) else { - return true - } - - isValid - .onSuccess { input.info = nil } - .onFailure { input.info = InputInfo(text: $0.localizedDescription, kind: .warning) } - - return true - } - } -} diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift deleted file mode 100644 index 6c4070bf1..000000000 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/ViewsFactory/CreateProfileViewsFactory.swift +++ /dev/null @@ -1,264 +0,0 @@ -// -// CreateProfileViewsFactory.swift -// Nynja -// -// Created by Ash on 10/11/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol CreateProfileViewsFactoryProtocol { - typealias ChangeTextHandler = (ProfileField) -> MTITextChangedHandler - typealias ShouldChangeTextHandler = (ProfileField) -> MTIShouldChangeTextHandler - - // MARK: - CreateProfileViewController - - func makeCreateButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeContainer(on view: UIView, headerView: UIView, footerView: UIView) -> UIView - func makeScrollView(on view: UIView) -> UIScrollView - func makeContentContainer(on view: UIView, widthView: UIView, presenter: CreateProfilePresenterProtocol) -> CreateProfileContentView - - // MARK: - CreateProfileContentView - - func makeAvatarButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeFirstNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField - func makeLastNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField - func makeUsernameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField - func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel - func makeCheckBoxContainerView(on view: UIView, top: UIView) -> UIView - func makeCheckButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton - func makeTermsOfUseTextView(on view: UIView, left: UIView) -> UITextView -} - -final class CreateProfileViewsFactory: CreateProfileViewsFactoryProtocol { - - // MARK: - CreateProfileViewController - - func makeCreateButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.mainRed), for: .normal) - button.setBackgroundImage(UIImage.makeImageFromColor(UIColor.nynja.darkRed), for: .disabled) - - button.setTitle(String.localizable.createProfileCreateButton, for: .normal) - - button.setTitleColor(UIColor.nynja.white, for: .normal) - button.setTitleColor(UIColor.nynja.gray, for: .disabled) - - button.layer.cornerRadius = 22 - button.clipsToBounds = true - - button.isEnabled = false - - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.left.right.equalToSuperview().inset(16) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottomPadding() - 28) - make.height.equalTo(44) - } - - return button - } - - func makeContainer(on view: UIView, headerView: UIView, footerView: UIView) -> UIView { - let container = UIView() - view.addSubview(container) - - container.backgroundColor = UIColor.nynja.clear - container.clipsToBounds = true - - container.snp.makeConstraints { (make) in - make.top.equalTo(headerView.snp.bottom) - make.left.right.equalToSuperview() - make.bottom.equalTo(footerView.snp.top) - } - - return container - } - - func makeScrollView(on view: UIView) -> UIScrollView { - let scrollView = UIScrollView() - view.addSubview(scrollView) - - scrollView.backgroundColor = UIColor.nynja.clear - - scrollView.snp.makeConstraints { (make) in - make.edges.equalToSuperview() - } - - return scrollView - } - - func makeContentContainer(on view: UIView, widthView: UIView, presenter: CreateProfilePresenterProtocol) -> CreateProfileContentView { - let contentContainer = CreateProfileContentView(viewsFactory: self) - view.addSubview(contentContainer) - - let config = CreateProfileContentView.Config( - chooseAvatarAction: { - presenter.chooseAvatar(completion: contentContainer.updateAvatar) - }, - markCheckAction: presenter.checkTermsOfUse, - textChangedAction: presenter.setProfileField, - isValidAction: presenter.isValidValue) - - contentContainer.configure(config: config) - - contentContainer.snp.makeConstraints { (make) in - make.top.equalToSuperview() - make.bottom.equalToSuperview().offset(-16) - make.left.right.equalTo(widthView) - } - - return contentContainer - } - - // MARK: - CreateProfileContentView - - func makeAvatarButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.setImage(UIImage.nynja.icEmptyAvatar.image, for: .normal) - button.addTarget(target, action: selector, for: .touchUpInside) - - button.layer.cornerRadius = 47 - button.clipsToBounds = true - - button.snp.makeConstraints { (make) in - make.centerX.equalToSuperview() - make.top.equalTo(32) - make.height.width.equalTo(94) - } - - return button - } - - func makeFirstNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { - return makeTextField(fieldType: .firstName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - } - - func makeLastNameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { - return makeTextField(fieldType: .lastName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - } - - func makeUsernameTextField(on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { - return makeTextField(fieldType: .userName, on: view, top: top, textChangedHandler: textChangedHandler, shouldChangeTextHandler: shouldChangeTextHandler) - } - - func makeDescriptionLabel(on view: UIView, top: UIView) -> UILabel { - let label = UILabel() - view.addSubview(label) - - label.text = String.localizable.createProfileTermsHint - - label.font = FontFamily.NotoSans.regular.font(size: 14) - label.textColor = UIColor.nynja.dustyGray - - label.numberOfLines = 0 - - label.snp.makeConstraints { (make) in - make.left.right.equalToSuperview().inset(16) - make.top.equalTo(top.snp.bottom) - } - - return label - } - - func makeCheckBoxContainerView(on view: UIView, top: UIView) -> UIView { - let containerView = UIView() - view.addSubview(containerView) - containerView.backgroundColor = UIColor.nynja.clear - - containerView.snp.makeConstraints { (make) in - make.left.right.equalToSuperview().inset(16) - make.top.equalTo(top.snp.bottom) - make.height.equalTo(56) - make.bottom.equalToSuperview().offset(16) - } - - return containerView - } - - func makeCheckButton(on view: UIView, target: AnyObject, selector: Selector) -> UIButton { - let button = UIButton() - view.addSubview(button) - - button.layer.cornerRadius = 12 - button.layer.borderColor = UIColor.nynja.dustyGray.cgColor - button.layer.borderWidth = 1 - - button.addTarget(target, action: selector, for: .touchUpInside) - - button.snp.makeConstraints { (make) in - make.height.width.equalTo(24) - make.left.equalToSuperview() - make.centerY.equalToSuperview() - } - - return button - } - - func makeTermsOfUseTextView(on view: UIView, left: UIView) -> UITextView { - let textView = UITextView() - view.addSubview(textView) - - textView.backgroundColor = UIColor.nynja.clear - textView.isScrollEnabled = false - - textView.dataDetectorTypes = .link - textView.isEditable = false - - let beginOfStr = NSMutableAttributedString(string: String.localizable.createProfileAgreeAtTerms) - beginOfStr.addAttributes([.foregroundColor : UIColor.nynja.dustyGray, - .font: FontFamily.NotoSans.regular.font(size: 14)], range: NSMakeRange(0, beginOfStr.length)) - - let attributes: [NSAttributedString.Key : Any] = [ - .link : "https://landing.nynja.io/terms-of-use", - .underlineStyle: NSUnderlineStyle.single.rawValue, - .underlineColor: UIColor.nynja.blue, - .foregroundColor: UIColor.nynja.blue, - .font: FontFamily.NotoSans.regular.font(size: 14) - ] - - let termsOfUseStr = NSMutableAttributedString(string: String.localizable.createProfileTermsOfUse) - termsOfUseStr.addAttributes(attributes, range: NSMakeRange(0, termsOfUseStr.length)) - - beginOfStr.append(NSAttributedString(string: " ")) - beginOfStr.append(termsOfUseStr) - - textView.attributedText = beginOfStr - - textView.snp.makeConstraints { (make) in - make.left.equalTo(left.snp.right) - make.centerY.equalTo(left.snp.centerY) - make.right.equalToSuperview() - } - - textView.sizeToFit() - - return textView - } -} - -private extension CreateProfileViewsFactory { - func makeTextField(fieldType: ProfileField, on view: UIView, top: UIView, textChangedHandler: ChangeTextHandler, shouldChangeTextHandler: ShouldChangeTextHandler) -> MaterialTextField { - let textField = MaterialTextField() - view.addSubview(textField) - - textField.placeholder = fieldType.placeholder + (fieldType.isRequired ? "*" : "") - - textField.snp.makeConstraints { (make) in - make.left.right.equalToSuperview().inset(16) - make.top.equalTo(top.snp.bottom).offset(22) - make.height.equalTo(65) - } - - textField.textChanged = textChangedHandler(fieldType) - textField.shouldTextChanged = shouldChangeTextHandler(fieldType) - - return textField - } -} diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift index fccba6de6..e497b46dd 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -42,8 +42,7 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { let presenter = CreateProfilePresenter() let view = CreateProfileViewController(dependencies: .init( - presenter: presenter, - viewsFactory: CreateProfileViewsFactory()) + presenter: presenter) ) let interactor = CreateProfileInteractor(dependencies: .init( diff --git a/Nynja/Modules/Flows/Search Flow/Entities/EmailTextValidator.swift b/Nynja/Modules/Flows/Search Flow/Entities/EmailTextValidator.swift deleted file mode 100644 index 4720b29ff..000000000 --- a/Nynja/Modules/Flows/Search Flow/Entities/EmailTextValidator.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// EmailTextValidator.swift -// Nynja -// -// Created by Anton Poltoratskyi on 12/12/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -struct EmailTextValidator: MTIValidator { - - private let validator = EmailValidator() - - var validationHandler: ((Bool) -> Void)? - - func validate(text: String) -> InputInfo? { - let isValid = validator.isValid(text: text) - - validationHandler?(isValid) - - // FIXME: return errors - return nil - } -} diff --git a/Nynja/Modules/Flows/Search Flow/Entities/UsernameTextValidator.swift b/Nynja/Modules/Flows/Search Flow/Entities/UsernameTextValidator.swift deleted file mode 100644 index d3699ef4f..000000000 --- a/Nynja/Modules/Flows/Search Flow/Entities/UsernameTextValidator.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// UsernameTextValidator.swift -// Nynja -// -// Created by Anton Poltoratskyi on 12/12/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -struct UsernameTextValidator: MTIValidator { - - private let regexp = "^([a-zA-Z]|[0-9]|_){2,}$" - - var validationHandler: ((Bool) -> Void)? - - func validate(text: String) -> InputInfo? { - let predicate = NSPredicate(format: "SELF MATCHES %@", regexp) - let isValid = predicate.evaluate(with: text) - - validationHandler?(isValid) - - // FIXME: return errors - return nil - } -} diff --git a/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift b/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift index 197d3dafe..0cc414a06 100644 --- a/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift +++ b/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift @@ -86,7 +86,7 @@ final class SearchContactPresenter: BasePresenter, SearchContactPresenterProtoco private func makeEmailViewModel() -> ContentViewModel { var isValid = false - let viewModel = TextFieldContentViewModel(validator: EmailTextValidator()) + let viewModel = TextFieldContentViewModel(validator: EmailValidator()) viewModel.placeholder = String.localizable.searchContactEmailPlaceholder viewModel.textContentType = .emailAddress viewModel.keyboardType = .emailAddress @@ -109,7 +109,7 @@ final class SearchContactPresenter: BasePresenter, SearchContactPresenterProtoco private func makeUsernameViewModel() -> ContentViewModel { var isValid = false - let viewModel = TextFieldContentViewModel(validator: UsernameTextValidator()) + let viewModel = TextFieldContentViewModel(validator: UsernameValidator()) viewModel.placeholder = String.localizable.searchContactUsernamePlaceholder viewModel.keyboardType = .default viewModel.returnKeyType = .search diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 18956e359..c93c0a950 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -826,7 +826,6 @@ "channel_name" = "Channel Name*"; "channel_link" = "Channel Link*"; "channel_description" = "Description"; -"channel_max_length_warning" = "Max %d symbols"; "channel_min_length_warning" = "At least %d symbols"; "channel_link_is_already_in_use" = "This link is already in use"; "channel_link_is_available" = "This link is available"; @@ -1157,3 +1156,17 @@ "alert_action_take_from_camera" = "Take from Camera"; "alert_action_take_from_gallery" = "Take from Gallery"; + + +// MARK: - Validation + +"validation.email.empty" = "Please, fill in email"; +"validation.email.invalid" = "Sorry, seems email is not correct"; + +"validation.username.invalid" = "Sorry, seems username is not correct"; + +"validation.account_info_link.empty" = "Please, fill in account link"; +"validation.account_info_link.invalid" = "Sorry, seems link is not correct"; + +"validation.min_length_warning" = "Min %d symbols"; +"validation.max_length_warning" = "Max %d symbols"; diff --git a/Nynja/ValidatorFactory/ValidatorFactoryImpl.swift b/Nynja/ValidatorFactory/ValidatorFactoryImpl.swift index 2a22b9b40..8816b27b9 100644 --- a/Nynja/ValidatorFactory/ValidatorFactoryImpl.swift +++ b/Nynja/ValidatorFactory/ValidatorFactoryImpl.swift @@ -11,7 +11,7 @@ class ValidatorFactoryImpl: ValidatorFactory { func makeGroupInputValidators(maxLength: Int, emptyWarningMessage message: String) -> [MTIValidator] { return [ - LengthValidator(length: .max(maxLength, String.localizable.channelMaxLengthWarning)), + LengthValidator(length: .max(maxLength, String.localizable.validationMaxLengthWarning)), ClosureValidator { text in let text = text.trimmed() if text == "" { -- GitLab From 43a7bf7c6eab2112463039d5de773f56a5ef4553 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 9 Jan 2019 15:08:26 +0200 Subject: [PATCH 138/138] [NY-6452] [NY-6453] Implemented social data prefill on Create Profile screen. (#1589) --- Nynja.xcodeproj/project.pbxproj | 4 ++++ .../Flows/Auth Flow/AuthCoordinator.swift | 10 +++++----- .../AuthModule/Entities/AuthFlowDetails.swift | 15 +++++++++++++++ .../AuthModule/Entities/LoginFlow.swift | 4 ++-- .../AuthModule/Interactor/AuthInteractor.swift | 18 ++++++++++-------- .../Presenter/CreateProfilePresenter.swift | 14 ++++++++++++++ .../Wireframe/CreateProfileWireframe.swift | 3 ++- Nynja/SDK/Auth/Entities/AuthResponse.swift | 7 +++++++ Nynja/SDK/Auth/Service/AuthServiceImpl.swift | 9 ++++++++- Podfile.lock | 2 +- 10 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/AuthFlowDetails.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index a7624d70d..554e5348d 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -592,6 +592,7 @@ 3ABA189E21C005880026B96B /* SearchResultCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABA189C21C005880026B96B /* SearchResultCellModel.swift */; }; 3ABCE8F11EC9330D00A80B15 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */; }; 3ABCE9061EC9357900A80B15 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */; }; + 3ABD5BFD21E4C11A00DAE935 /* AuthFlowDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABD5BFC21E4C11A00DAE935 /* AuthFlowDetails.swift */; }; 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC07E3B1F055B3F00ADBE26 /* DoubleExtensions.swift */; }; 3AC321781EEAC4C10068F3C8 /* AuthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC321771EEAC4C10068F3C8 /* AuthModel.swift */; }; 3AE0A8431F20321A008A04F3 /* CountryWheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A8351F20321A008A04F3 /* CountryWheelItemView.swift */; }; @@ -3080,6 +3081,7 @@ 3ABCE8F01EC9330D00A80B15 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3ABCE9041EC9357900A80B15 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 3ABCE9091EC93D4800A80B15 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3ABD5BFC21E4C11A00DAE935 /* AuthFlowDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFlowDetails.swift; sourceTree = ""; }; 3AC07E2E1F05572400ADBE26 /* Nynja-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Nynja-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; 3AC07E3B1F055B3F00ADBE26 /* DoubleExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoubleExtensions.swift; sourceTree = ""; }; 3AC321771EEAC4C10068F3C8 /* AuthModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthModel.swift; path = Services/Models/AuthModel.swift; sourceTree = ""; }; @@ -8578,6 +8580,7 @@ isa = PBXGroup; children = ( 5EEB73D12161CEA100D8ECE6 /* LoginFlow.swift */, + 3ABD5BFC21E4C11A00DAE935 /* AuthFlowDetails.swift */, 850B9DAC219C7ADA00EA0CF4 /* PlainLoginOption.swift */, 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */, 8541995121A2B003004009F7 /* PhoneNumberFormatter.swift */, @@ -17819,6 +17822,7 @@ 8512349221221B9E000129A2 /* Collection.swift in Sources */, 853D55B220CE66180080659F /* StickersInputData.swift in Sources */, 260313A320A0A4BA009AC66D /* ActionCellViewModel.swift in Sources */, + 3ABD5BFD21E4C11A00DAE935 /* AuthFlowDetails.swift in Sources */, E764919C1F7A5485001E741C /* MainWheelContainerDataSource.swift in Sources */, A4A242482060373000B0A804 /* BaseHandler.swift in Sources */, 2600CCBF216D447200EDC9C3 /* OptionallyActionCell.swift in Sources */, diff --git a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift index 161375a62..e578e2d4e 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift @@ -149,10 +149,10 @@ extension AuthCoordinator: AuthCoordinatorProtocol { case let .phoneNumber(numberInfo): showCodeConfirmation(with: .phoneNumber(numberInfo)) - case let .google(accountId, authenticationType), let .facebook(accountId, authenticationType): - switch authenticationType { + case let .google(authFlowDetails), let .facebook(authFlowDetails): + switch authFlowDetails.authenticationType { case .register: - showCreateProfile(with: accountId) + showCreateProfile(with: authFlowDetails.accountId, prefillInfo: authFlowDetails.prefillInfo) case .login: end() } @@ -235,10 +235,10 @@ extension AuthCoordinator: CodeConfirmationCoordinatorProtocol { } } - private func showCreateProfile(with accountId: String) { + private func showCreateProfile(with accountId: String, prefillInfo: AuthPrefillInfo? = nil) { let wireframe = CreateProfileWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: .init(accountId: accountId), + parameters: .init(accountId: accountId, prefillInfo: prefillInfo), dependencies: .init( storageService: serviceFactory.makeStorageService(), imageUploader: serviceFactory.makeImageUploader(), diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/AuthFlowDetails.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/AuthFlowDetails.swift new file mode 100644 index 000000000..81830cac6 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/AuthFlowDetails.swift @@ -0,0 +1,15 @@ +// +// AuthFlowDetails.swift +// Nynja +// +// Created by Anton Poltoratskyi on 1/8/19. +// Copyright © 2019 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct AuthFlowDetails { + let accountId: String + let authenticationType: AuthenticationType + let prefillInfo: AuthPrefillInfo? +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift index b1ae231fb..b393172e1 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/LoginFlow.swift @@ -9,6 +9,6 @@ enum LoginFlow { case phoneNumber(PhoneNumberInfo) case email(String) - case facebook(accountId: String, authenticationType: AuthenticationType) - case google(accountId: String, authenticationType: AuthenticationType) + case facebook(AuthFlowDetails) + case google(AuthFlowDetails) } diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift index 29110b89b..6ecc4dfe2 100644 --- a/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift @@ -76,8 +76,8 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { self.handleSocialAuthResponse(result: result) { authResult in switch authResult { - case let .success(accountId, authenticationType): - self.presenter?.didAuthenticated(with: .facebook(accountId: accountId, authenticationType: authenticationType)) + case let .success(authDetails): + self.presenter?.didAuthenticated(with: .facebook(authDetails)) case let .failure(error): self.presenter?.didReceiveAuthenticationFailure(error) } @@ -102,8 +102,8 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { self.handleSocialAuthResponse(result: result) { authResult in switch authResult { - case let .success(accountId, authenticationType): - self.presenter?.didAuthenticated(with: .google(accountId: accountId, authenticationType: authenticationType)) + case let .success(authDetails): + self.presenter?.didAuthenticated(with: .google(authDetails)) case let .failure(error): self.presenter?.didReceiveAuthenticationFailure(error) } @@ -114,12 +114,14 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { /// Generic method for social auth processing /// /// - Parameters: - /// - completion: accountId + authenticationType for success case, otherwise - error. - private func handleSocialAuthResponse(result: Result, completion: @escaping (Result<(String, AuthenticationType)>) -> Void) { + /// - completion: `AuthFlowDetails` for success case, otherwise - error. + private func handleSocialAuthResponse(result: Result, completion: @escaping (Result) -> Void) { switch result { case let .success(authResponse): let accountId = authResponse.accountId let authType = authResponse.authenticationType + let prefillInfo = authResponse.socialPrefillInfo + let details = AuthFlowDetails(accountId: accountId, authenticationType: authType, prefillInfo: prefillInfo) switch authType { case .login: @@ -130,7 +132,7 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { case let .success(account): do { try self.authService.processAuthenticatedAccount(account) - completion(.success((accountId, authType))) + completion(.success(details)) } catch { completion(.failure(error)) } @@ -139,7 +141,7 @@ final class AuthInteractor: AuthInteractorInput, InitializeInjectable { } } case .register: - completion(.success((accountId, authType))) + completion(.success(details)) } case let .failure(error): completion(.failure(error)) diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift index 0e2852b16..4498f2854 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -16,6 +16,20 @@ final class CreateProfilePresenter: CreateProfileInteractorOutput, CreateProfile private var viewModel = CreateProfileViewModel() + init(prefillInfo: AuthPrefillInfo?) { + guard let prefillInfo = prefillInfo else { + return + } + if let avatarURL = prefillInfo.avatarURL { + viewModel.avatar = .url(avatarURL) + } + if let firstName = prefillInfo.firstName { + viewModel.firstName = firstName + } + if let lastName = prefillInfo.lastName { + viewModel.lastName = lastName + } + } // MARK: - Fields diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift index e497b46dd..9f73b8fc0 100644 --- a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -23,6 +23,7 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { struct Parameters { let accountId: String + let prefillInfo: AuthPrefillInfo? } struct Dependencies { @@ -39,7 +40,7 @@ final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { } func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { - let presenter = CreateProfilePresenter() + let presenter = CreateProfilePresenter(prefillInfo: parameters.prefillInfo) let view = CreateProfileViewController(dependencies: .init( presenter: presenter) diff --git a/Nynja/SDK/Auth/Entities/AuthResponse.swift b/Nynja/SDK/Auth/Entities/AuthResponse.swift index b14b8a90d..11e4b5f12 100644 --- a/Nynja/SDK/Auth/Entities/AuthResponse.swift +++ b/Nynja/SDK/Auth/Entities/AuthResponse.swift @@ -6,7 +6,14 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +struct AuthPrefillInfo { + let avatarURL: URL? + let firstName: String? + let lastName: String? +} + struct AuthResponse { let accountId: String let authenticationType: AuthenticationType + let socialPrefillInfo: AuthPrefillInfo? } diff --git a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift index a32aa96fb..8b7ec0397 100644 --- a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -210,7 +210,14 @@ final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLog } self.storage.save(accessToken: accessToken, refreshToken: refreshToken) - let response = AuthResponse(accountId: accountId, authenticationType: pending ? .register : .login) + let socialInfo: AuthPrefillInfo? = socialLoginDetails.flatMap { details in + let avatar = URL(string: details.pictureUrl) + let fistName = details.firstName.isEmpty ? nil : details.firstName + let lastName = details.lastName.isEmpty ? nil : details.lastName + + return AuthPrefillInfo(avatarURL: avatar, firstName: fistName, lastName: lastName) + } + let response = AuthResponse(accountId: accountId, authenticationType: pending ? .register : .login, socialPrefillInfo: socialInfo) completion?(.success(response)) } diff --git a/Podfile.lock b/Podfile.lock index ed9404e4a..a8b790f1c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -203,7 +203,7 @@ SPEC CHECKSUMS: GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f GoogleUtilities: abb092d2c12e817fa3e0e7b274987dd72fb86ec3 GRDBCipher: eef21d242c727a21e0f87ad44f8ea2df03edd252 - GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2 + GTMOAuth2: e8b6512c896235149df975c41d9a36c868ab7fba GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e Intercom: 083a05bf222811b0b5e0a0b24c863544123397f0 JTAppleCalendar: abb30678f42a4ef8a340a932b1dcb8c85a33dac2 -- GitLab