diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index e054b515fdaefc9e1f1f7f3a8e52a1a8dcd9bfaa..c40d9a8cabd7d27295e238f1e03920ef52f4f2fb 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -8,6 +8,13 @@ /* 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 */; }; + 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, ); }; }; 8514D4DF20EE2D970002378A /* InteractiveCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D4CF20EE2D970002378A /* InteractiveCellViewModel.swift */; }; 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D4D020EE2D970002378A /* LayoutRepresentableCellViewModel.swift */; }; @@ -32,14 +39,34 @@ 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 */; }; + 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 */; }; + 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 */ /* 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 = ""; }; 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -66,6 +93,19 @@ 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 = ""; }; + 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 = ""; }; + 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; }; 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 = ""; }; @@ -86,6 +126,57 @@ /* 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 = ""; + }; + 3ABA189421C000EF0026B96B /* DataSource */ = { + isa = PBXGroup; + children = ( + 3ABA189521C001210026B96B /* TableViewDataSource.swift */, + ); + path = DataSource; + sourceTree = ""; + }; + 3AE2F99721B6D9B00068C3BC /* Alerts */ = { + isa = PBXGroup; + children = ( + 3AE2F99C21B6E31C0068C3BC /* Alert.swift */, + ); + path = Alerts; + sourceTree = ""; + }; 703C0F257F98BCEAD4D25D95 /* Pods */ = { isa = PBXGroup; children = ( @@ -138,7 +229,7 @@ 8514D4D820EE2D970002378A /* Reusable.swift */, 8514D4D920EE2D970002378A /* XibInitializable.swift */, 8514D4CD20EE2D970002378A /* ViewModels */, - 8514D4DA20EE2D970002378A /* Extensions */, + 3ABA189421C000EF0026B96B /* DataSource */, ); path = Collection; sourceTree = ""; @@ -149,6 +240,8 @@ 8514D4CE20EE2D970002378A /* Cell */, 8514D4D320EE2D970002378A /* SupplementaryView */, 8514D4D520EE2D970002378A /* Accessibility */, + 8514D4DB20EE2D970002378A /* UITableView+ViewModels.swift */, + 8514D4DC20EE2D970002378A /* UICollectionView+ViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -181,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 = ( @@ -273,7 +357,9 @@ 8514D51920EE41AC0002378A /* Extensions */ = { isa = PBXGroup; children = ( + 8540A00D2181ED10003A010F /* CoreAnimation */, 8514D51F20EE47350002378A /* UIWindow */, + 3A07C5BA21AF00C900AE3429 /* TextInput */, ); path = Extensions; sourceTree = ""; @@ -281,6 +367,12 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( + 8540A00B2181EBD2003A010F /* BaseView.swift */, + 8540A01A218213E8003A010F /* Utils */, + 855A4E84219AFA6C00B6E90B /* Controls */, + 8541995821A2C272004009F7 /* LoadingIndicator */, + 85409FFD2181C8AF003A010F /* Avatar */, + 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, ); path = Views; @@ -295,12 +387,76 @@ 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 */, + 3A2C2DFF21C26B95006A53BB /* RoundImageView.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 8541995821A2C272004009F7 /* LoadingIndicator */ = { + isa = PBXGroup; + children = ( + 8541995B21A2C2B7004009F7 /* ProgressHUD.swift */, + ); + path = LoadingIndicator; + sourceTree = ""; + }; + 855A4E84219AFA6C00B6E90B /* Controls */ = { + isa = PBXGroup; + children = ( + 3A07C5B121AED3FC00AE3429 /* SecureCode */, + 3A07C5AE21AED30300AE3429 /* TextInput */, + 3A07C5AD21AED2F800AE3429 /* Button */, + ); + path = Controls; + sourceTree = ""; + }; + 855A4E8B219AFEE000B6E90B /* TextInput */ = { + isa = PBXGroup; + children = ( + 855A4E89219AFB9C00B6E90B /* InputsCachePolicy.swift */, + 855A4E8C219AFF0300B6E90B /* ProhibitedOptions.swift */, + ); + path = TextInput; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( 8514D51920EE41AC0002378A /* Extensions */, 8514D4DD20EE2D970002378A /* Layout */, 8514D4CC20EE2D970002378A /* Collection */, + 855A4E8B219AFEE000B6E90B /* TextInput */, + 3AE2F99721B6D9B00068C3BC /* Alerts */, ); path = Core; sourceTree = ""; @@ -414,27 +570,47 @@ buildActionMask = 2147483647; 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 */, + 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 */, + 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 */, 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 */, + 855A4E88219AFB0F00B6E90B /* TextField.swift in Sources */, 8514D4E320EE2D970002378A /* SupplementaryViewModel.swift in Sources */, 8514D51C20EE41E90002378A /* UIWindowExtensions.swift in Sources */, 8514D4E520EE2D970002378A /* AccessibilityConfigurable.swift in Sources */, 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 */, + 3AE2F99D21B6E31C0068C3BC /* Alert.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/Alerts/Alert.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Alerts/Alert.swift new file mode 100644 index 0000000000000000000000000000000000000000..3dfd5f5e41ccced7ac3ed6b2bce68d496f16f94f --- /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/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/DataSource/TableViewDataSource.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/DataSource/TableViewDataSource.swift new file mode 100644 index 0000000000000000000000000000000000000000..943784489ad00c4a5e245a7b7922056c3d1ec203 --- /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/Core/Extensions/CoreAnimation/CALayer+Animation.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift new file mode 100644 index 0000000000000000000000000000000000000000..df70d9566c817f5aafe48a81d6a54d51c3bd887a --- /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/Core/Extensions/TextInput/TextInputUtils.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/TextInput/TextInputUtils.swift new file mode 100644 index 0000000000000000000000000000000000000000..93b1ff63bd21cca227a2b439ccb02f665d7d8e62 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/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/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 89d0c94a71da754b903e0f50dbbe468c11bba5cd..98f1591bc8720af3374db1a8e58fe14c03c1eefd 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/Core/Extensions/UIWindow/UIWindowExtensions.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/UIWindow/UIWindowExtensions.swift index 4dc57d015c575dae6660d66183462cb03d91e743..e4616b6eedc61a693cc884d8ce0fbc4e92603a97 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/UIWindow/UIWindowExtensions.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/UIWindow/UIWindowExtensions.swift @@ -20,13 +20,22 @@ extension UIWindow { return vc } - 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/TextInput/InputsCachePolicy.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/InputsCachePolicy.swift similarity index 87% rename from Nynja/Library/UI/TextInput/InputsCachePolicy.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/InputsCachePolicy.swift index a29237124fa9265582e398b09c54d9b50fac17e9..10293290ec3ca3314502f8e5ee5aa1e53df92a27 100644 --- a/Nynja/Library/UI/TextInput/InputsCachePolicy.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/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/TextInput/ProhibitedOptions.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/ProhibitedOptions.swift new file mode 100644 index 0000000000000000000000000000000000000000..4495fecaa33c15c599754350b4d7b0d6ce7affeb --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/TextInput/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 new file mode 100644 index 0000000000000000000000000000000000000000..8a6f570c79dbad7fed01f6394703cbe983736848 --- /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 baseSetup() { + super.baseSetup() + + 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 0000000000000000000000000000000000000000..35ef92e8f920a7e16ef21e23a5ea829638f5d956 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift @@ -0,0 +1,41 @@ +// +// BaseView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +open class BaseView: UIView { + + open var activatedViews: [UIView] { + return [] + } + + + // MARK: - Init + + public override init(frame: CGRect) { + super.init(frame: frame) + baseSetup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + baseSetup() + } + + + // MARK: - 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 ab503c85567eb9e0eb949bcae783f3b4b3edf0c1..68a3dcfaf726c0f79145900430f8839e74bc30d0 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 baseSetup() { + super.baseSetup() clipsToBounds = true contentView.layer.cornerRadius = cornerRadius contentView.clipsToBounds = true diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/Button/UnderlinedButton.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/Button/UnderlinedButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..93c2c0269851771f548e3e02e2ce02f087bd697b --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/Button/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/SecureCode/SecureCodeContainerView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/SecureCode/SecureCodeContainerView.swift new file mode 100644 index 0000000000000000000000000000000000000000..159e0987350e732900f8526c4e423ae8843d9662 --- /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 0000000000000000000000000000000000000000..5205967ccb738f85c9d87e2bf71bc5f5e52cffeb --- /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/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/TextField.swift similarity index 84% rename from Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/TextField.swift index 5a613b91e2af90b29a098cce47bfa199d4943297..466a3a05e5fad344eb0f6c8ccd6ef0965d836479 100644 --- a/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/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 @@ -119,5 +105,4 @@ open class TextField: UITextField { selectedTextRange = textRange(from: beginningOfDocument, to: beginningOfDocument) didResetHandler?() } - } diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/UnderlinedTextField.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/UnderlinedTextField.swift new file mode 100644 index 0000000000000000000000000000000000000000..038be962ef8dbb9e1af312ca95e7d3463fbf05c2 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Controls/TextInput/UnderlinedTextField.swift @@ -0,0 +1,155 @@ +// +// UnderlinedTextField.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +open class UnderlinedTextField: BaseView, InputsCachePolicy { + + 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) + } + } + } + + 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 } + } + + 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 textAlignment: NSTextAlignment { + get { return textField.textAlignment } + set { textField.textAlignment = 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 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 { + 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 } + } + + public var shouldResetAfterBackground: Bool { + get { return textField.shouldResetAfterBackground } + set { textField.shouldResetAfterBackground = newValue } + } + + + // MARK: - Views + + public private(set) lazy var textField: T = { + let textField = T() + 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/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/LoadingIndicator/ProgressHUD.swift new file mode 100644 index 0000000000000000000000000000000000000000..6d17edd106e0419b23f3d8448a9385aac20fe52b --- /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?.bringSubviewToFront(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?.sendSubviewToBack(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/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/RecordingIndicatorView.swift new file mode 100644 index 0000000000000000000000000000000000000000..96f8d786ea96d891a64cecb375e6a733d35cd8f5 --- /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 baseSetup() { + super.baseSetup() + + 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/TypingBoldIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift new file mode 100644 index 0000000000000000000000000000000000000000..7316394e6061e8428a9246fbd05cc7da9df62c63 --- /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/TypingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift new file mode 100644 index 0000000000000000000000000000000000000000..e0a8a55037e1e9a6b3cb51f82aee82cbc82c6f3c --- /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 baseSetup() { + super.baseSetup() + 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 0000000000000000000000000000000000000000..2bb7e2080b27408feca7fbcb5fd5e0ba5a0c788a --- /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 baseSetup() { + super.baseSetup() + + 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/RoundImageView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundImageView.swift new file mode 100644 index 0000000000000000000000000000000000000000..c4bae62a7cf750f95eb47a4b6d18b8680d178d1c --- /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/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift new file mode 100644 index 0000000000000000000000000000000000000000..1ea96fd8ed0f770212713d84d1aad5a503f01f82 --- /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 + +public class RoundView: BaseView { + + public override func baseSetup() { + super.baseSetup() + layer.masksToBounds = true + } + + public override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = min(bounds.width, bounds.height) / 2 + } +} diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index 9b29ae8376528913d70ef75e796e7d6f04cce52d..1c629917492daa6667a6d989ecf2b1dca0e879f9 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 0.8.0 CFBundleVersion - 0.8.5 + 0.8.5.multi-acc Config $(Config) ModelsVersion diff --git a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift index 200518760cc06c4cefaee8a3f04e2fc412a3440e..f4a7d5195a5884ce8f8ff34bb63264ecfed1c6d0 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 a2041fabf5a966ec2a9b7060bf89021a504b9b97..bdf7650af20bdbd611ef38f081938f85fe08aba6 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 b0daa836e26b469d689d1165b99f1c503d7a9884..3c2b31d72a086b865cad146c16a6ddac16e75055 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 faceba4a8ff24979b1aa15ce1ad34473eec87fe5..132e274b434849d17fcd0d15c549b779a5b1163b 100644 --- a/Nynja-Share/Services/Handlers/ProfileHandler.swift +++ b/Nynja-Share/Services/Handlers/ProfileHandler.swift @@ -12,22 +12,21 @@ protocol ProfileHandlerDelegate: class { func getProfileSuccess(model: Profile) } -extension ProfileHandlerDelegate { - func getProfileSuccess(model: Profile) {} -} - -class ProfileHandler:BaseHandler { - static weak var delegate :ProfileHandlerDelegate? +final class ProfileHandler: BaseHandler { + + static let shared = ProfileHandler() + + private init() {} + + weak var delegate: ProfileHandlerDelegate? - static func executeHandle(data: BertTuple) { - guard let profile = get_Profile().parse(bert: data) as? Profile, - let status = profile.status?.string else { - return + func executeHandle(data: BertTuple) { + guard let profile = get_Profile().parse(bert: data) as? Profile, let status = profile.status?.string else { + return } - switch status { case "get", "init": - self.delegate?.getProfileSuccess(model: profile) + delegate?.getProfileSuccess(model: profile) default: break } diff --git a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift index cc538af190cdc8206bbb4a717b39875721704842..0392785d3d60e241ca7c8332eccfb0d3ba37fc73 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 a8231969325b181d1993cbec0e6565d3ec2d6ac9..554e5348dab0f9e94afbc736026d163d321e9a9f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -29,7 +29,6 @@ 005886C92030F13100FE2E89 /* NynjaTimeHoursDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005886C82030F13100FE2E89 /* NynjaTimeHoursDelegate.swift */; }; 005886CB2030F3F900FE2E89 /* NynjaTimeMinsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005886CA2030F3F900FE2E89 /* NynjaTimeMinsDelegate.swift */; }; 005886CD2030F41700FE2E89 /* NynjaTimeAmPmDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005886CC2030F41700FE2E89 /* NynjaTimeAmPmDelegate.swift */; }; - 005A877F2034C22200372B03 /* JobExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005A877E2034C22200372B03 /* JobExtension.swift */; }; 005B0B1E2029AB9F000D6416 /* MessageToView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005B0B1D2029AB9F000D6416 /* MessageToView.swift */; }; 005B0B202029ABC2000D6416 /* TimeZoneItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005B0B1F2029ABC2000D6416 /* TimeZoneItemView.swift */; }; 005B0B222029ABDA000D6416 /* DateTimeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005B0B212029ABDA000D6416 /* DateTimeItemView.swift */; }; @@ -84,7 +83,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 */; }; @@ -174,8 +172,6 @@ 26342CB220ECDDC400D2196B /* Encodable+Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CB120ECDDC400D2196B /* Encodable+Dictionary.swift */; }; 26342CB420ECFAB600D2196B /* MessageInteractor+Transcription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CB320ECFAB600D2196B /* MessageInteractor+Transcription.swift */; }; 263529132075725200DC6FBD /* SendJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0008E9182032F4C8003E316E /* SendJob.swift */; }; - 263529152075729400DC6FBD /* Job+DB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263529142075729400DC6FBD /* Job+DB.swift */; }; - 26352916207572AA00DC6FBD /* JobExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005A877E2034C22200372B03 /* JobExtension.swift */; }; 26352917207572DC00DC6FBD /* JobExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0008E9142032D6B7003E316E /* JobExtension+BERT.swift */; }; 263529182075730500DC6FBD /* actExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F75D203F36140003181A /* actExtension+BERT.swift */; }; 263A60AC1FB4F8F7006F9D52 /* ParticipantsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263A60AB1FB4F8F7006F9D52 /* ParticipantsDataSource.swift */; }; @@ -450,8 +446,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 */; }; 359E343F1F55FA0F002F5F3E /* 1-second-of-silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 359E343E1F55FA0F002F5F3E /* 1-second-of-silence.mp3 */; }; @@ -478,9 +472,64 @@ 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 */; }; + 3A0A50CD21B7FEFE0052D334 /* MyGroupAliasProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50AB21B7FEFE0052D334 /* MyGroupAliasProtocols.swift */; }; + 3A0A50CE21B7FEFE0052D334 /* CreateGroupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50AE21B7FEFE0052D334 /* CreateGroupPresenter.swift */; }; + 3A0A50CF21B7FEFE0052D334 /* CreateGroupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50B021B7FEFE0052D334 /* CreateGroupWireframe.swift */; }; + 3A0A50D021B7FEFE0052D334 /* CreateGroupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50B121B7FEFE0052D334 /* CreateGroupProtocols.swift */; }; + 3A0A50D121B7FEFE0052D334 /* CellWithArrowCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50B521B7FEFE0052D334 /* CellWithArrowCellModel.swift */; }; + 3A0A50D221B7FEFE0052D334 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50B621B7FEFE0052D334 /* CellWithArrowTableViewCell.swift */; }; + 3A0A50D321B7FEFE0052D334 /* CellWithImageCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50B821B7FEFE0052D334 /* CellWithImageCellModel.swift */; }; + 3A0A50D421B7FEFE0052D334 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50B921B7FEFE0052D334 /* CellWithImageTableViewCell.swift */; }; + 3A0A50D521B7FEFE0052D334 /* CellWithImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50BA21B7FEFE0052D334 /* CellWithImage.swift */; }; + 3A0A50D621B7FEFE0052D334 /* CreateGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50BC21B7FEFE0052D334 /* CreateGroupViewController.swift */; }; + 3A0A50D721B7FEFE0052D334 /* CreateGroupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50BE21B7FEFE0052D334 /* CreateGroupInteractor.swift */; }; + 3A0A50D821B7FEFE0052D334 /* GroupInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50C121B7FEFE0052D334 /* GroupInputViewModel.swift */; }; + 3A0A50D921B7FEFE0052D334 /* GroupInputProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50C321B7FEFE0052D334 /* GroupInputProtocols.swift */; }; + 3A0A50DA21B7FEFE0052D334 /* GroupInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50C421B7FEFE0052D334 /* GroupInputViewController.swift */; }; + 3A0A50DB21B7FEFE0052D334 /* EditGroupNamePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50C721B7FEFE0052D334 /* EditGroupNamePresenter.swift */; }; + 3A0A50DC21B7FEFE0052D334 /* EditGroupNameWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50C921B7FEFE0052D334 /* EditGroupNameWireframe.swift */; }; + 3A0A50DD21B7FEFE0052D334 /* EditGroupNameProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50CA21B7FEFE0052D334 /* EditGroupNameProtocols.swift */; }; + 3A0A50E221B8198E0052D334 /* Job+DB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50E021B8198E0052D334 /* Job+DB.swift */; }; + 3A0A50E321B8198E0052D334 /* JobExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50E121B8198E0052D334 /* JobExtension.swift */; }; + 3A0A50E521B819A60052D334 /* Contact+Desc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50E421B819A60052D334 /* Contact+Desc.swift */; }; + 3A0A50E621B81A100052D334 /* JobExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0A50E121B8198E0052D334 /* JobExtension.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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; @@ -502,8 +551,19 @@ 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; @@ -512,24 +572,46 @@ 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 /* 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 */; }; + 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 */; }; 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 */; }; + 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 */; }; + 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 */; }; 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22A35AB9BCD04336504B01A9 /* AddContactByUsernameInteractor.swift */; }; 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047FFB74EBFF53E57AB3EB3E /* EditPhotoViewController.swift */; }; - 43F333D298934DCBAC8D8192 /* EditGroupNamePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA78C91DFDF5884E382D38FA /* EditGroupNamePresenter.swift */; }; 45F60C4B14438C65076457AB /* EditUsernameProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83894D517BFF22637F2878B7 /* EditUsernameProtocols.swift */; }; 4B02130220372C5700650298 /* OtherItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02130120372C5700650298 /* OtherItemView.swift */; }; 4B0213042037331100650298 /* ScheduleMessageViewControllerConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0213032037331100650298 /* ScheduleMessageViewControllerConstants.swift */; }; @@ -733,11 +815,8 @@ 4BB35E23219AF46E0007C18E /* RosterRelatedQueryArgs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB35E22219AF46E0007C18E /* RosterRelatedQueryArgs.swift */; }; 4BB9588221D12148007E76CC /* OtherUserInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB9588121D12148007E76CC /* OtherUserInput.swift */; }; 4BBAEBBA21AC62740089B703 /* LengthValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBB921AC62740089B703 /* LengthValidator.swift */; }; - 4BBAEBBD21AC68FD0089B703 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBBC21AC68FD0089B703 /* Validator.swift */; }; + 4BBAEBBD21AC68FD0089B703 /* MTIValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBBC21AC68FD0089B703 /* MTIValidator.swift */; }; 4BBAEBBF21AC6DF10089B703 /* ClosureValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBBE21AC6DF10089B703 /* ClosureValidator.swift */; }; - 4BBAEBC521ADAA1A0089B703 /* GroupInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBC421ADAA190089B703 /* GroupInputViewController.swift */; }; - 4BBAEBC721ADAA470089B703 /* GroupInputProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBC621ADAA470089B703 /* GroupInputProtocols.swift */; }; - 4BBAEBCB21ADAB900089B703 /* GroupInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBCA21ADAB900089B703 /* GroupInputViewModel.swift */; }; 4BBAEBCE21AE9F790089B703 /* ValidatorFactoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBCD21AE9F790089B703 /* ValidatorFactoryImpl.swift */; }; 4BBAEBD021AE9F9D0089B703 /* ValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBAEBCF21AE9F9D0089B703 /* ValidatorFactory.swift */; }; 4BC8B38D2191AC360086DC6C /* ContactsProvidingFetchingArgs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC8B38C2191AC360086DC6C /* ContactsProvidingFetchingArgs.swift */; }; @@ -805,8 +884,6 @@ 5894F4C605B66B55F21D406E /* DateTimePickerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA2BE900351F21464CE687 /* DateTimePickerInteractor.swift */; }; 5A48445F21178E33000657ED /* AlertTextFieldViewControllerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A48445E21178E33000657ED /* AlertTextFieldViewControllerLayout.swift */; }; 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B772E08B9E40EB48DD87082 /* EditUsernameViewController.swift */; }; - 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBACEAABEE65D7EC5572C4E /* CreateGroupPresenter.swift */; }; - 5B5EE777EF301CFC1FDCF307 /* CreateGroupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC76E2B3DD0BCA0A622A5CD /* CreateGroupInteractor.swift */; }; 5BBEF53C212DE09F00F10768 /* ringback.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBEF53B212DE09F00F10768 /* ringback.m4a */; }; 5BC1D37920D3B4A8002A44B3 /* GroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37420D3B4A6002A44B3 /* GroupCollectionViewCell.swift */; }; 5BC1D37B20D3B4A8002A44B3 /* GroupAddParticipantsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37620D3B4A7002A44B3 /* GroupAddParticipantsCollectionViewCell.swift */; }; @@ -816,10 +893,40 @@ 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 */; }; + 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 */; }; + 5E07BC57216F6722000E4558 /* CreateProfileWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07BC56216F6722000E4558 /* CreateProfileWireframe.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 /* 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 */; }; + 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 */; }; + 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 /* 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 */; }; + 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 */; }; @@ -845,13 +952,11 @@ 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D67310F1F29E1F4003E8F8F /* BottomCallView.swift */; }; 6DD72F601F1547AC008CFF83 /* GCD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD72F5F1F1547AC008CFF83 /* GCD.swift */; }; 6DEEE1931F1F9CF6000FAF09 /* UIViewController+Child.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEEE1921F1F9CF6000FAF09 /* UIViewController+Child.swift */; }; - 6E7CD38810BC3B896070C819 /* EditGroupNameWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273EABBCA8570D21A8683273 /* EditGroupNameWireframe.swift */; }; 6F3F21025258D8071BCF95EF /* LoginWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB2678C74CCC58C8AAFADD6 /* LoginWireframe.swift */; }; 705B483A1FCDEA2273CEFE2C /* EditPhotoWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F56141F2CF85255940EA304F /* EditPhotoWireframe.swift */; }; 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 */; }; @@ -892,6 +997,13 @@ 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 */; }; + 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 */; }; @@ -906,6 +1018,16 @@ 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 */; }; + 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 /* 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 */; }; @@ -916,6 +1038,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 */; }; @@ -930,6 +1062,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 */; }; @@ -947,9 +1087,14 @@ 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 */; }; + 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 */; }; @@ -964,6 +1109,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 */; }; @@ -975,6 +1124,14 @@ 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 */; }; + 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 */; }; @@ -982,6 +1139,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 */; }; @@ -1012,14 +1171,19 @@ 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 */; }; + 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 */; }; 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 */; }; + 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 */; }; @@ -1052,10 +1216,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 */; }; - 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 */; }; - 854A4B312080D6C400759152 /* CellWithImageCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2F2080D6C400759152 /* CellWithImageCellModel.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 */; }; 854CFB08210704AE00FBC133 /* CGRectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */; }; 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */; }; 854FC1CB204468FC00B12BE5 /* CarouselFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854FC1CA204468FC00B12BE5 /* CarouselFlowLayout.swift */; }; @@ -1072,6 +1235,13 @@ 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 */; }; + 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 */; }; 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 */; }; @@ -1083,6 +1253,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 */; }; @@ -1093,10 +1265,12 @@ 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566771F20C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift */; }; 8566BB11215BC39500320E15 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566BB10215BC39500320E15 /* Feed.swift */; }; 8566BB12215BC39D00320E15 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566BB10215BC39500320E15 /* Feed.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 */; }; 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BD2092368600E4840C /* StickerDataSource.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 */; }; @@ -1117,7 +1291,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 */; }; @@ -1125,7 +1298,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 */; }; @@ -1157,6 +1329,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 */; }; @@ -1176,6 +1352,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 */; }; @@ -1206,11 +1384,13 @@ 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 */; }; 87A3D03524B9258B33726A57 /* HistoryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */; }; - 87DE79674FF430A52D2A0BB7 /* MyGroupAliasProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8606C1D61AA46EB77821B1B0 /* MyGroupAliasProtocols.swift */; }; 896D51F07E2F79C8B5502DBF /* EditGroupPhotoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC691A6DB133F6319B4FCC4F /* EditGroupPhotoInteractor.swift */; }; 8E23E086200614AB00A59B8C /* GroupVideosCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E23E085200614AB00A59B8C /* GroupVideosCell.swift */; }; 8E23E0882006853000A59B8C /* GroupVideosListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E23E0872006852F00A59B8C /* GroupVideosListVC.swift */; }; @@ -1255,8 +1435,6 @@ 95FE45E089AF69B08815EB9E /* ProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AE7296B9A53355289740D1 /* ProfilePresenter.swift */; }; 9763CCDFE5AF7B58C21CDED9 /* GroupStoragePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A789D358750ACE1089447D31 /* GroupStoragePresenter.swift */; }; 986BE2204D6D0813B13618B1 /* AddContactViaPhonePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 929DD72B2F10916CF6C383F6 /* AddContactViaPhonePresenter.swift */; }; - 990A25B2C84CE09B4CE64533 /* MyGroupAliasPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9D0CBC2BAD6DC6C7047A26 /* MyGroupAliasPresenter.swift */; }; - 99B9D27D2F0EFE051E6581ED /* CreateGroupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE9DC6ADA0E71241C49A328 /* CreateGroupProtocols.swift */; }; 9B0C32F12153CF1600094ECF /* HintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0C32F02153CF1600094ECF /* HintView.swift */; }; 9B2C6693216F82AB00116486 /* NynjaJoinByLinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2C6692216F82AB00116486 /* NynjaJoinByLinkService.swift */; }; 9B81AD92215A5EEA00993A8C /* ActiveSpeakerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B81AD91215A5EEA00993A8C /* ActiveSpeakerView.swift */; }; @@ -1276,7 +1454,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 */; }; @@ -1551,12 +1728,9 @@ 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 */; }; A433D9A120A5C18C00C946F9 /* ContactsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A433D9A020A5C18C00C946F9 /* ContactsProvider.swift */; }; A433D9A320A5C19600C946F9 /* ContactsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A433D9A220A5C19600C946F9 /* ContactsProviding.swift */; }; - A438DB9220763AFB00AA86A2 /* Contact+Desc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A438DB9120763AFB00AA86A2 /* Contact+Desc.swift */; }; A43B259420AB1DFA00FF8107 /* InputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B257320AB1DFA00FF8107 /* InputBar.swift */; }; A43B259520AB1DFA00FF8107 /* InputContentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B257520AB1DFA00FF8107 /* InputContentProtocol.swift */; }; A43B259620AB1DFA00FF8107 /* RecordDisplayInputContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B257620AB1DFA00FF8107 /* RecordDisplayInputContent.swift */; }; @@ -1571,8 +1745,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 /* CountryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43B258620AB1DFA00FF8107 /* CountryModel.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 */; }; @@ -1582,13 +1754,10 @@ 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 */; }; - 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 */; }; @@ -1697,8 +1866,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 */; }; @@ -1721,8 +1888,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 */; }; @@ -1730,7 +1897,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 */; }; @@ -1771,9 +1937,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 */; }; @@ -1905,19 +2069,15 @@ B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */; }; B83D907D324553FB792968EE /* TimeZoneSelectorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28416F302A40E1E56041080 /* TimeZoneSelectorProtocols.swift */; }; BA982E458F95A7A5AB4A8A73 /* TutorialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DD73BCBB9741C19646F0E9D /* TutorialViewController.swift */; }; - BBF46945EB64E07C58817ACA /* EditGroupNameProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA60D8A9D5F677A9AAAF57 /* EditGroupNameProtocols.swift */; }; BC1BA70218B40F3F64841848 /* LanguageSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C9355E1AA4B373567F765 /* LanguageSettingsInteractor.swift */; }; BDC42BA204F86F13E9FE24FA /* ScheduleMessageProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4948B03AEE34116DB6A7A06D /* ScheduleMessageProtocols.swift */; }; BF20ED73252DE6954B6CDCA8 /* QRCodeReaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23CE9A2F01B3FFE156ED77E2 /* QRCodeReaderViewController.swift */; }; C018D77E539F6831CFE89216 /* EditUsernameWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 905153A519BFB29C2F3AE149 /* EditUsernameWireframe.swift */; }; - C02DD71CA3832908D422B83C /* CreateGroupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA27F453DD5811D59708B747 /* CreateGroupWireframe.swift */; }; - C493782D4488E45CB1D67DE4 /* CreateGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2CBB5F32D209160D00F744 /* CreateGroupViewController.swift */; }; C4AE8B6EFD76A8C6ADF51422 /* TutorialProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95853EBE3A525E3069F4637 /* TutorialProtocols.swift */; }; C52453F5FF7E703BE0E7561C /* AddContactByUsernameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549BC324D11F1DDF87DAAEB7 /* AddContactByUsernameViewController.swift */; }; C6B308C6734EFB77892832A0 /* ActiveSessionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762BA232B5D027BD943DFA18 /* ActiveSessionsPresenter.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 */; }; @@ -1935,14 +2095,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 */; }; - 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 */; }; CCF8AA193F15D4191EC99051 /* SplashProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00EBF72D0964E3EC64F5B966 /* SplashProtocols.swift */; }; @@ -1969,7 +2121,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 */; }; @@ -2131,7 +2282,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 */; }; @@ -2190,7 +2341,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 */; }; @@ -2270,7 +2421,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 */; }; @@ -2462,7 +2612,6 @@ 005886C82030F13100FE2E89 /* NynjaTimeHoursDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaTimeHoursDelegate.swift; sourceTree = ""; }; 005886CA2030F3F900FE2E89 /* NynjaTimeMinsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaTimeMinsDelegate.swift; sourceTree = ""; }; 005886CC2030F41700FE2E89 /* NynjaTimeAmPmDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaTimeAmPmDelegate.swift; sourceTree = ""; }; - 005A877E2034C22200372B03 /* JobExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobExtension.swift; sourceTree = ""; }; 005B0B1D2029AB9F000D6416 /* MessageToView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageToView.swift; sourceTree = ""; }; 005B0B1F2029ABC2000D6416 /* TimeZoneItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneItemView.swift; sourceTree = ""; }; 005B0B212029ABDA000D6416 /* DateTimeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeItemView.swift; sourceTree = ""; }; @@ -2599,7 +2748,6 @@ 26342CAE20ECD16A00D2196B /* TranscribeResponseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeResponseData.swift; sourceTree = ""; }; 26342CB120ECDDC400D2196B /* Encodable+Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Dictionary.swift"; sourceTree = ""; }; 26342CB320ECFAB600D2196B /* MessageInteractor+Transcription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+Transcription.swift"; sourceTree = ""; }; - 263529142075729400DC6FBD /* Job+DB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Job+DB.swift"; sourceTree = ""; }; 263A60AB1FB4F8F7006F9D52 /* ParticipantsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsDataSource.swift; sourceTree = ""; }; 263A60AD1FB51C22006F9D52 /* MemberExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberExtension.swift; sourceTree = ""; }; 263C04E82132E2FF00B8F0BE /* WrappedTaskOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedTaskOperation.swift; sourceTree = ""; }; @@ -2786,7 +2934,6 @@ 26FA420B2017AE3300E6F6EC /* StarMessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StarMessageCellLayout.swift; path = StarCell/StarMessageCellLayout.swift; sourceTree = ""; }; 26FA420D201812D600E6F6EC /* StarTableDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarTableDS.swift; sourceTree = ""; }; 26FA420F201821B400E6F6EC /* StarHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StarHandler.swift; path = Services/HandleServices/StarHandler.swift; sourceTree = ""; }; - 273EABBCA8570D21A8683273 /* EditGroupNameWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupNameWireframe.swift; sourceTree = ""; }; 28FFFCFDD6E86C9E608CD1F3 /* VideoPreviewProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VideoPreviewProtocols.swift; sourceTree = ""; }; 293EBD42735B5838AC2EE46E /* Pods_Nynja_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Nynja_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 294E9FA97D8C23C864C04C0E /* MainPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainPresenter.swift; sourceTree = ""; }; @@ -2812,9 +2959,63 @@ 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 = ""; }; + 3A0A50AB21B7FEFE0052D334 /* MyGroupAliasProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyGroupAliasProtocols.swift; sourceTree = ""; }; + 3A0A50AE21B7FEFE0052D334 /* CreateGroupPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupPresenter.swift; sourceTree = ""; }; + 3A0A50B021B7FEFE0052D334 /* CreateGroupWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupWireframe.swift; sourceTree = ""; }; + 3A0A50B121B7FEFE0052D334 /* CreateGroupProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupProtocols.swift; sourceTree = ""; }; + 3A0A50B521B7FEFE0052D334 /* CellWithArrowCellModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellWithArrowCellModel.swift; sourceTree = ""; }; + 3A0A50B621B7FEFE0052D334 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; + 3A0A50B821B7FEFE0052D334 /* CellWithImageCellModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellWithImageCellModel.swift; sourceTree = ""; }; + 3A0A50B921B7FEFE0052D334 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; + 3A0A50BA21B7FEFE0052D334 /* CellWithImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellWithImage.swift; sourceTree = ""; }; + 3A0A50BC21B7FEFE0052D334 /* CreateGroupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupViewController.swift; sourceTree = ""; }; + 3A0A50BE21B7FEFE0052D334 /* CreateGroupInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupInteractor.swift; sourceTree = ""; }; + 3A0A50C121B7FEFE0052D334 /* GroupInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInputViewModel.swift; sourceTree = ""; }; + 3A0A50C321B7FEFE0052D334 /* GroupInputProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInputProtocols.swift; sourceTree = ""; }; + 3A0A50C421B7FEFE0052D334 /* GroupInputViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInputViewController.swift; sourceTree = ""; }; + 3A0A50C721B7FEFE0052D334 /* EditGroupNamePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditGroupNamePresenter.swift; sourceTree = ""; }; + 3A0A50C921B7FEFE0052D334 /* EditGroupNameWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditGroupNameWireframe.swift; sourceTree = ""; }; + 3A0A50CA21B7FEFE0052D334 /* EditGroupNameProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditGroupNameProtocols.swift; sourceTree = ""; }; + 3A0A50E021B8198E0052D334 /* Job+DB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Job+DB.swift"; sourceTree = ""; }; + 3A0A50E121B8198E0052D334 /* JobExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JobExtension.swift; sourceTree = ""; }; + 3A0A50E421B819A60052D334 /* Contact+Desc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Contact+Desc.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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -2836,8 +3037,19 @@ 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -2847,12 +3059,29 @@ 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 /* 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 = ""; }; 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 = ""; }; @@ -2860,8 +3089,14 @@ 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -3041,11 +3276,8 @@ 4BB35E22219AF46E0007C18E /* RosterRelatedQueryArgs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RosterRelatedQueryArgs.swift; sourceTree = ""; }; 4BB9588121D12148007E76CC /* OtherUserInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserInput.swift; sourceTree = ""; }; 4BBAEBB921AC62740089B703 /* LengthValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LengthValidator.swift; sourceTree = ""; }; - 4BBAEBBC21AC68FD0089B703 /* Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; + 4BBAEBBC21AC68FD0089B703 /* MTIValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MTIValidator.swift; sourceTree = ""; }; 4BBAEBBE21AC6DF10089B703 /* ClosureValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureValidator.swift; sourceTree = ""; }; - 4BBAEBC421ADAA190089B703 /* GroupInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInputViewController.swift; sourceTree = ""; }; - 4BBAEBC621ADAA470089B703 /* GroupInputProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInputProtocols.swift; sourceTree = ""; }; - 4BBAEBCA21ADAB900089B703 /* GroupInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInputViewModel.swift; sourceTree = ""; }; 4BBAEBCD21AE9F790089B703 /* ValidatorFactoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorFactoryImpl.swift; sourceTree = ""; }; 4BBAEBCF21AE9F9D0089B703 /* ValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorFactory.swift; sourceTree = ""; }; 4BC8B38C2191AC360086DC6C /* ContactsProvidingFetchingArgs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsProvidingFetchingArgs.swift; sourceTree = ""; }; @@ -3082,7 +3314,6 @@ 4BFF6C8121C84B6A00BB9432 /* DBUnreadInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBUnreadInfo.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 = ""; }; @@ -3091,7 +3322,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 = ""; }; @@ -3108,8 +3338,38 @@ 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 = ""; }; + 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 = ""; }; + 5E07BC56216F6722000E4558 /* CreateProfileWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfileWireframe.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 /* 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 = ""; }; + 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewController.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 = ""; }; + 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 /* 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 = ""; }; + 5EEB73DD21623FF900D8ECE6 /* UIViewControllerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtensions.swift; sourceTree = ""; }; 5F509C0C8B9C738DBC7ABE07 /* ActiveSessionsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewController.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 = ""; }; @@ -3138,7 +3398,6 @@ 6EC0DFB96051C50F0FC5B9CA /* FavoritesWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FavoritesWireframe.swift; sourceTree = ""; }; 6F6115584B396A194F86AE04 /* DateTimePickerWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerWireframe.swift; sourceTree = ""; }; 6F9DAA21CA2D2DCC02DB186E /* VideoPreviewViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VideoPreviewViewController.swift; sourceTree = ""; }; - 6FFA60D8A9D5F677A9AAAF57 /* EditGroupNameProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupNameProtocols.swift; sourceTree = ""; }; 705B62097A99515B3C778F35 /* GroupRulesPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupRulesPresenter.swift; sourceTree = ""; }; 70CEB31C47B0FEB0040FD0DA /* MainInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainInteractor.swift; sourceTree = ""; }; 7154170549A4A686815BA4F0 /* SplashPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SplashPresenter.swift; sourceTree = ""; }; @@ -3147,8 +3406,6 @@ 762BA232B5D027BD943DFA18 /* ActiveSessionsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActiveSessionsPresenter.swift; sourceTree = ""; }; 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 = ""; }; @@ -3190,6 +3447,13 @@ 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 = ""; }; + 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 = ""; }; @@ -3202,6 +3466,14 @@ 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 = ""; }; + 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 /* 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 = ""; }; @@ -3212,6 +3484,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 = ""; }; @@ -3224,6 +3506,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 = ""; }; @@ -3241,9 +3531,14 @@ 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 = ""; }; + 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 = ""; }; @@ -3254,6 +3549,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 = ""; }; @@ -3262,12 +3561,22 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -3295,14 +3604,19 @@ 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -3316,10 +3630,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 = ""; }; - 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 = ""; }; - 854A4B2F2080D6C400759152 /* CellWithImageCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageCellModel.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 = ""; }; 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectExtensions.swift; sourceTree = ""; }; 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewLayout.swift; sourceTree = ""; }; 854FC1CA204468FC00B12BE5 /* CarouselFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselFlowLayout.swift; sourceTree = ""; }; @@ -3333,6 +3646,12 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -3344,6 +3663,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 = ""; }; @@ -3353,10 +3674,12 @@ 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 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.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 = ""; }; 8572C3BD2092368600E4840C /* StickerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDataSource.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 = ""; }; @@ -3377,7 +3700,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 = ""; }; @@ -3385,7 +3707,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 = ""; }; @@ -3416,6 +3737,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 = ""; }; 85AC1342219F16CF002ADE57 /* PrereleaseDebugConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PrereleaseDebugConfig.xcconfig; sourceTree = ""; }; @@ -3434,6 +3759,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 = ""; }; @@ -3461,14 +3788,15 @@ 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 = ""; }; - 8606C1D61AA46EB77821B1B0 /* MyGroupAliasProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MyGroupAliasProtocols.swift; sourceTree = ""; }; 8B2389EFD3432F86296722BE /* QRCodeGeneratorProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeGeneratorProtocols.swift; sourceTree = ""; }; 8B772E08B9E40EB48DD87082 /* EditUsernameViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditUsernameViewController.swift; sourceTree = ""; }; 8C986781EE944D55A2B7374C /* GroupStorageInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupStorageInteractor.swift; sourceTree = ""; }; - 8CBACEAABEE65D7EC5572C4E /* CreateGroupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupPresenter.swift; sourceTree = ""; }; 8D4ACB985C2F0674717F1045 /* MainProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainProtocols.swift; sourceTree = ""; }; 8DD73BCBB9741C19646F0E9D /* TutorialViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TutorialViewController.swift; sourceTree = ""; }; 8E23E085200614AB00A59B8C /* GroupVideosCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupVideosCell.swift; sourceTree = ""; }; @@ -3698,7 +4026,6 @@ A4330A702109EBB30060BD93 /* CountriesProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountriesProviding.swift; sourceTree = ""; }; A433D9A020A5C18C00C946F9 /* ContactsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsProvider.swift; sourceTree = ""; }; A433D9A220A5C19600C946F9 /* ContactsProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsProviding.swift; sourceTree = ""; }; - A438DB9120763AFB00AA86A2 /* Contact+Desc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contact+Desc.swift"; sourceTree = ""; }; A43B257320AB1DFA00FF8107 /* InputBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputBar.swift; sourceTree = ""; }; A43B257520AB1DFA00FF8107 /* InputContentProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputContentProtocol.swift; sourceTree = ""; }; A43B257620AB1DFA00FF8107 /* RecordDisplayInputContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordDisplayInputContent.swift; sourceTree = ""; }; @@ -3713,8 +4040,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 /* CountryModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryModel.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 = ""; }; @@ -3724,7 +4049,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 = ""; }; @@ -3823,7 +4147,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 = ""; }; @@ -3844,8 +4167,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 = ""; }; @@ -3930,9 +4253,7 @@ A789D358750ACE1089447D31 /* GroupStoragePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupStoragePresenter.swift; sourceTree = ""; }; A9537952568A7532147DE548 /* GroupStorageProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupStorageProtocols.swift; sourceTree = ""; }; A95853EBE3A525E3069F4637 /* TutorialProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TutorialProtocols.swift; sourceTree = ""; }; - A977DE2CFBD3AB55AB05CF71 /* MyGroupAliasWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MyGroupAliasWireframe.swift; sourceTree = ""; }; A9898C7E717C5DA85654181E /* TopUpAccountWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TopUpAccountWireframe.swift; sourceTree = ""; }; - AA27F453DD5811D59708B747 /* CreateGroupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupWireframe.swift; sourceTree = ""; }; AA65B365E335DD42254D1CF4 /* AddContactWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactWireframe.swift; sourceTree = ""; }; ABAB01C8A746E1E3C525A2B4 /* EditProfilePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditProfilePresenter.swift; sourceTree = ""; }; AC691A6DB133F6319B4FCC4F /* EditGroupPhotoInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupPhotoInteractor.swift; sourceTree = ""; }; @@ -3941,9 +4262,7 @@ AE19B8A785E7FA17723F4D85 /* EditProfileWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditProfileWireframe.swift; sourceTree = ""; }; AE929B30A3869179E76E59A9 /* FavoritesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FavoritesInteractor.swift; sourceTree = ""; }; 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 = ""; }; @@ -4022,10 +4341,8 @@ 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 = ""; }; - 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 = ""; }; @@ -4043,15 +4360,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 = ""; }; - 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 = ""; }; 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 = ""; }; CBE3BAC9B7EA418FB463EF04 /* EditUsernameInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditUsernameInteractor.swift; sourceTree = ""; }; @@ -4066,7 +4374,6 @@ D7956526150F4211DE78173E /* AddContactViaPhoneInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactViaPhoneInteractor.swift; sourceTree = ""; }; D8AC83D4F29DA35FEFDFBC65 /* QRCodeGeneratorViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeGeneratorViewController.swift; sourceTree = ""; }; DA319AD39D8A6999732AF4DE /* Pods-NynjaUnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.release.xcconfig"; sourceTree = ""; }; - DC9D0CBC2BAD6DC6C7047A26 /* MyGroupAliasPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MyGroupAliasPresenter.swift; sourceTree = ""; }; DDA3E9BFB878BBDEE7C8A85F /* Pods-Nynja-Nynja-Share.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Nynja-Share.release.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Nynja-Share/Pods-Nynja-Nynja-Share.release.xcconfig"; sourceTree = ""; }; DFAB7D8D9024C26FA51BF783 /* QRCodeGeneratorPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeGeneratorPresenter.swift; sourceTree = ""; }; DFBBE8C9FC347038AB74CF43 /* LanguageSettingsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsProtocols.swift; sourceTree = ""; }; @@ -4076,7 +4383,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 = ""; }; @@ -4237,7 +4543,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 = ""; }; @@ -4296,7 +4602,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 = ""; }; @@ -4352,7 +4658,6 @@ FD011693E63204766A30F18C /* TutorialPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TutorialPresenter.swift; sourceTree = ""; }; FD21B8AD8A89427D88B48BC6 /* AddContactProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactProtocols.swift; sourceTree = ""; }; FD33E34E2376166F3A132271 /* ImagePreviewProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImagePreviewProtocols.swift; sourceTree = ""; }; - FDE9DC6ADA0E71241C49A328 /* CreateGroupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupProtocols.swift; sourceTree = ""; }; FE21ACA72113AA7F006010A0 /* NynjaIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NynjaIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; FE21ACAB2113AA7F006010A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FE21ACB82113AB3B006010A0 /* KeychainServiceTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainServiceTest.swift; sourceTree = ""; }; @@ -4653,14 +4958,6 @@ path = Interactor; sourceTree = ""; }; - 0AD119947B4A6FA309A1060E /* Interactor */ = { - isa = PBXGroup; - children = ( - C68A1D12FEF0CE24D6B3F6F5 /* SelectCountryInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; 0CAFBBC1CE7BB9EBD7BDAABB /* WireFrame */ = { isa = PBXGroup; children = ( @@ -4669,14 +4966,6 @@ path = WireFrame; sourceTree = ""; }; - 0E2F86AFA19D5CA844D6AC27 /* Presenter */ = { - isa = PBXGroup; - children = ( - 8CBACEAABEE65D7EC5572C4E /* CreateGroupPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; 0F7B35B2E89F718690B39759 /* View */ = { isa = PBXGroup; children = ( @@ -4693,27 +4982,6 @@ path = View; sourceTree = ""; }; - 115A968821FB24FA3C58A6D5 /* SelectCountry */ = { - isa = PBXGroup; - children = ( - 4F7C039B61A0663D43BE5AE5 /* SelectCountryProtocols.swift */, - C90EE13D20246E2700FDB873 /* SelctCountryDelegate.swift */, - 43D5323E27F49A5C95BBB6D6 /* View */, - 337A8E299DCF438AD28A7043 /* Presenter */, - 0AD119947B4A6FA309A1060E /* Interactor */, - CADE0A8BB5BE972F51CE1E2F /* WireFrame */, - ); - path = SelectCountry; - sourceTree = ""; - }; - 12396B05D93D1CA3A8410766 /* Interactor */ = { - isa = PBXGroup; - children = ( - AFC76E2B3DD0BCA0A622A5CD /* CreateGroupInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; 141E3D82AC55AEFA2F91A213 /* Presenter */ = { isa = PBXGroup; children = ( @@ -5947,14 +6215,6 @@ name = WireFrame; sourceTree = ""; }; - 337A8E299DCF438AD28A7043 /* Presenter */ = { - isa = PBXGroup; - children = ( - 5522F1F73FC8C564BF0254BF /* SelectCountryPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; 351B7F3065DD333AFEA34D24 /* View */ = { isa = PBXGroup; children = ( @@ -6025,6 +6285,298 @@ path = Model; sourceTree = ""; }; + 3A0A4C5921B91D7600BA0D09 /* Entities */ = { + isa = PBXGroup; + children = ( + 3A0A4C5A21B91D9000BA0D09 /* DeleteAccountErrors.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 3A0A50A521B7FEFE0052D334 /* CreateGroupFlow */ = { + isa = PBXGroup; + children = ( + 3A0A50A621B7FEFE0052D334 /* MyGroupAlias */, + 3A0A50AC21B7FEFE0052D334 /* CreateGroup */, + 3A0A50BF21B7FEFE0052D334 /* GroupInput */, + 3A0A50C521B7FEFE0052D334 /* EditGroupName */, + ); + path = CreateGroupFlow; + sourceTree = ""; + }; + 3A0A50A621B7FEFE0052D334 /* MyGroupAlias */ = { + isa = PBXGroup; + children = ( + 3A0A50A721B7FEFE0052D334 /* Presenter */, + 3A0A50A921B7FEFE0052D334 /* WireFrame */, + 3A0A50AB21B7FEFE0052D334 /* MyGroupAliasProtocols.swift */, + ); + path = MyGroupAlias; + sourceTree = ""; + }; + 3A0A50A721B7FEFE0052D334 /* Presenter */ = { + isa = PBXGroup; + children = ( + 3A0A50A821B7FEFE0052D334 /* MyGroupAliasPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 3A0A50A921B7FEFE0052D334 /* WireFrame */ = { + isa = PBXGroup; + children = ( + 3A0A50AA21B7FEFE0052D334 /* MyGroupAliasWireframe.swift */, + ); + path = WireFrame; + sourceTree = ""; + }; + 3A0A50AC21B7FEFE0052D334 /* CreateGroup */ = { + isa = PBXGroup; + children = ( + 3A0A50AD21B7FEFE0052D334 /* Presenter */, + 3A0A50AF21B7FEFE0052D334 /* WireFrame */, + 3A0A50B121B7FEFE0052D334 /* CreateGroupProtocols.swift */, + 3A0A50B221B7FEFE0052D334 /* View */, + 3A0A50BD21B7FEFE0052D334 /* Interactor */, + ); + path = CreateGroup; + sourceTree = ""; + }; + 3A0A50AD21B7FEFE0052D334 /* Presenter */ = { + isa = PBXGroup; + children = ( + 3A0A50AE21B7FEFE0052D334 /* CreateGroupPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 3A0A50AF21B7FEFE0052D334 /* WireFrame */ = { + isa = PBXGroup; + children = ( + 3A0A50B021B7FEFE0052D334 /* CreateGroupWireframe.swift */, + ); + path = WireFrame; + sourceTree = ""; + }; + 3A0A50B221B7FEFE0052D334 /* View */ = { + isa = PBXGroup; + children = ( + 3A0A50B321B7FEFE0052D334 /* TableView */, + 3A0A50BA21B7FEFE0052D334 /* CellWithImage.swift */, + 3A0A50BB21B7FEFE0052D334 /* ViewController */, + ); + path = View; + sourceTree = ""; + }; + 3A0A50B321B7FEFE0052D334 /* TableView */ = { + isa = PBXGroup; + children = ( + 3A0A50B421B7FEFE0052D334 /* Arrow */, + 3A0A50B721B7FEFE0052D334 /* Image */, + ); + path = TableView; + sourceTree = ""; + }; + 3A0A50B421B7FEFE0052D334 /* Arrow */ = { + isa = PBXGroup; + children = ( + 3A0A50B521B7FEFE0052D334 /* CellWithArrowCellModel.swift */, + 3A0A50B621B7FEFE0052D334 /* CellWithArrowTableViewCell.swift */, + ); + path = Arrow; + sourceTree = ""; + }; + 3A0A50B721B7FEFE0052D334 /* Image */ = { + isa = PBXGroup; + children = ( + 3A0A50B821B7FEFE0052D334 /* CellWithImageCellModel.swift */, + 3A0A50B921B7FEFE0052D334 /* CellWithImageTableViewCell.swift */, + ); + path = Image; + sourceTree = ""; + }; + 3A0A50BB21B7FEFE0052D334 /* ViewController */ = { + isa = PBXGroup; + children = ( + 3A0A50BC21B7FEFE0052D334 /* CreateGroupViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + 3A0A50BD21B7FEFE0052D334 /* Interactor */ = { + isa = PBXGroup; + children = ( + 3A0A50BE21B7FEFE0052D334 /* CreateGroupInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 3A0A50BF21B7FEFE0052D334 /* GroupInput */ = { + isa = PBXGroup; + children = ( + 3A0A50C021B7FEFE0052D334 /* Entity */, + 3A0A50C221B7FEFE0052D334 /* Protocols */, + 3A0A50C421B7FEFE0052D334 /* GroupInputViewController.swift */, + ); + path = GroupInput; + sourceTree = ""; + }; + 3A0A50C021B7FEFE0052D334 /* Entity */ = { + isa = PBXGroup; + children = ( + 3A0A50C121B7FEFE0052D334 /* GroupInputViewModel.swift */, + ); + path = Entity; + sourceTree = ""; + }; + 3A0A50C221B7FEFE0052D334 /* Protocols */ = { + isa = PBXGroup; + children = ( + 3A0A50C321B7FEFE0052D334 /* GroupInputProtocols.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 3A0A50C521B7FEFE0052D334 /* EditGroupName */ = { + isa = PBXGroup; + children = ( + 3A0A50C621B7FEFE0052D334 /* Presenter */, + 3A0A50C821B7FEFE0052D334 /* WireFrame */, + 3A0A50CA21B7FEFE0052D334 /* EditGroupNameProtocols.swift */, + ); + path = EditGroupName; + sourceTree = ""; + }; + 3A0A50C621B7FEFE0052D334 /* Presenter */ = { + isa = PBXGroup; + children = ( + 3A0A50C721B7FEFE0052D334 /* EditGroupNamePresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 3A0A50C821B7FEFE0052D334 /* WireFrame */ = { + isa = PBXGroup; + children = ( + 3A0A50C921B7FEFE0052D334 /* EditGroupNameWireframe.swift */, + ); + path = WireFrame; + sourceTree = ""; + }; + 3A0A50DF21B8198E0052D334 /* Job */ = { + isa = PBXGroup; + children = ( + 3A0A50E021B8198E0052D334 /* Job+DB.swift */, + 3A0A50E121B8198E0052D334 /* JobExtension.swift */, + ); + path = Job; + sourceTree = ""; + }; + 3A0A94D321B533D0007421AA /* Account */ = { + isa = PBXGroup; + children = ( + 3A0A94D421B53478007421AA /* AccountDAOProtocol.swift */, + 3A0A94D621B53491007421AA /* AccountDAO.swift */, + ); + 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 = ( + 3A14D83721ABEC41009CD23A /* AuthProvider.swift */, + 3A14D83C21AC136F009CD23A /* AuthProviderUIConfiguration.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 3A14D83921AC037B009CD23A /* Subviews */ = { + isa = PBXGroup; + children = ( + 3A14D83A21AC03A3009CD23A /* SearchAvailabilityView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; + 3A184D1921C0FD1D0083D367 /* InputView */ = { + isa = PBXGroup; + children = ( + 3A184D2021C0FEBC0083D367 /* ContentViewModel.swift */, + 3A184D1E21C0FD9C0083D367 /* PhoneNumberContentViewModel.swift */, + 3A184D2721C128380083D367 /* TextFieldContentViewModel.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 */, + 3AFBC22921C8D97C00D0248B /* LinkValidator.swift */, + 3A02381321C8D3A000A143FD /* SocialLinkValidator.swift */, + ); + path = Entities; + sourceTree = ""; + }; 3A1DC7371EF151B6006A8E9F /* Handlers */ = { isa = PBXGroup; children = ( @@ -6054,9 +6606,18 @@ path = Fonts; sourceTree = ""; }; + 3A37415F21B58A8600F212B9 /* ImageUploader */ = { + isa = PBXGroup; + children = ( + 3A37416021B58AAA00F212B9 /* ImageUploader.swift */, + ); + path = ImageUploader; + sourceTree = ""; + }; 3A768DE41ECB3E7600108F7C /* Library */ = { isa = PBXGroup; children = ( + 5EEB73BB2161797100D8ECE6 /* Result */, 4B7C73F5215A5522007924DB /* Debug */, B74BAFED21076ADB0049CD27 /* CircleMenuControl */, A46679EF20F10B2B00DBC6B4 /* RequestModelFactory */, @@ -6087,6 +6648,9 @@ 3A768E1C1ECD152300108F7C /* Services */ = { isa = PBXGroup; children = ( + 3A37415F21B58A8600F212B9 /* ImageUploader */, + 855A4E99219B31F200B6E90B /* SDK */, + 852BB8F721947A0800F2E8E4 /* Auth */, 851769D420D584CA008ACF6B /* Amazon */, 26ABCA3D21189DA400EA4782 /* Aps.swift */, 4BE2C5CE2142EAC500A73DD9 /* Audio */, @@ -6135,6 +6699,8 @@ F11786EF20AC5474007A9A1B /* ServiceFactory */, 859C42A6205693F100AE3797 /* SoundService */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, + 8548341921874434002064E1 /* Observable */, + 85EB37F9218365A6003A2D6F /* Statuses */, 85C16C3620D2521500EDB77E /* StickersDownloadingService */, 851872BD20CD452F007CD6CA /* StickersProvider */, E7C36C2D1FC438AC00740630 /* Storage */, @@ -6171,12 +6737,59 @@ 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 = ( + 5EEB73DC21623FED00D8ECE6 /* UIViewControllerExtensions */, 26A421CB217E026100120542 /* SnackBar */, 4B749EF2214FEABB002F3A33 /* LoginView */, - 4BB0EFBA2151347900704136 /* AlertManager.swift */, + 850A2E92219EF9A800C784D9 /* Alert */, 4BB0EFB62151347900704136 /* CustomPopup */, 8514D52020EE48750002378A /* ContextMenu */, 8514F16520EA219E00883513 /* ContextMenuOLD */, @@ -6241,10 +6854,66 @@ path = Login; sourceTree = ""; }; - 3ABCE8E41EC9330D00A80B15 = { + 3AB73FFD21B9948300D1E967 /* ContactInfoManagement */ = { isa = PBXGroup; children = ( - 85C65C6620EE58EC00C468B2 /* NynjaUIKit.xcodeproj */, + 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 = ( + 85086F5121C68BD800194361 /* ContactInfoManagementViewModel.swift */, + 3AB73FFF21B9954100D1E967 /* ContactInfoManagementViewController.swift */, + 85086F4E21C68AFF00194361 /* InputView */, + ); + 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 = ( + 85C65C6620EE58EC00C468B2 /* NynjaUIKit.xcodeproj */, A4A242442060370E00B0A804 /* Shared */, 3ABCE8EF1EC9330D00A80B15 /* Nynja */, 357809A41F9765CF00C9680C /* Nynja-Share */, @@ -6286,6 +6955,7 @@ 3ABCE9021EC9357900A80B15 /* Resources */ = { isa = PBXGroup; children = ( + 3A0AEA6921AFF0FD0066CBBA /* profile.bert */, F127E92020A44F7B006C03CF /* Nynja.entitlements */, A4F3DAA22084935400FF71C7 /* Constants.swift */, 00F7B347202B316F00E443E1 /* timezones.json */, @@ -6349,6 +7019,51 @@ path = WheelContainer; sourceTree = ""; }; + 3AE2F98821B6B4A00068C3BC /* DeleteAccount */ = { + isa = PBXGroup; + children = ( + 3AE2F98B21B6B5B30068C3BC /* DeleteAccountProtocols.swift */, + 3AE2F99321B6B5BA0068C3BC /* View */, + 3AE2F99421B6B5C60068C3BC /* Presenter */, + 3AE2F99521B6B5D10068C3BC /* Interactor */, + 3AE2F99621B6B5EE0068C3BC /* WIreframe */, + 3A0A4C5921B91D7600BA0D09 /* Entities */, + ); + 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 = ( @@ -6437,41 +7152,15 @@ path = WireFrame; sourceTree = ""; }; - 43D5323E27F49A5C95BBB6D6 /* View */ = { - isa = PBXGroup; - children = ( - 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */, - C9C6952F2023639C00A57297 /* SelectCountryViewControllerLayout.swift */, - C9C695062022318500A57297 /* TableView */, - ); - path = View; - sourceTree = ""; - }; - 4645720B5E0E5A8B5B0B0F39 /* WireFrame */ = { - isa = PBXGroup; - children = ( - AA27F453DD5811D59708B747 /* CreateGroupWireframe.swift */, - ); - path = WireFrame; - sourceTree = ""; - }; - 48CBD0E1B8BFC875AB252183 /* WireFrame */ = { - isa = PBXGroup; - children = ( - A977DE2CFBD3AB55AB05CF71 /* MyGroupAliasWireframe.swift */, - ); - path = WireFrame; - sourceTree = ""; - }; 49E75E252CE2F3C96A626230 /* Modules */ = { isa = PBXGroup; children = ( + FEA6555D2167777E00B44029 /* Wallet Flows */, ED33B21C6D660153B22D18BF /* AddContact */, 80CA53AB5B009455E0ECDC30 /* AddContactByUsername */, C4AE70AB74CA331DD03D830B /* AddContactViaPhone */, 3AED33923C943D0FCE611C55 /* AddParticipants */, B77C11D5210923F200CCB42E /* AssigningInterpreter */, - 4B749F0E214FEFC8002F3A33 /* Auth */, C0F3DD8B2859372188498348 /* Call */, A43B25C120AB1EE300FF8107 /* Channel */, A408A0B420C173DB0029F54B /* ChannelsList */, @@ -6485,9 +7174,6 @@ 51D6048350255DBFEC3969A5 /* Favorites */, F105C690209F71BE0091786A /* Flows */, 850FC5EA2032F1D900832D87 /* ForwardSelector */, - D25E638A8B9B267255AD766A /* GroupRules */, - 8ED0F3C21FBC5CF1004916AB /* GroupsList */, - 91732B7DCE35ABC02702095D /* GroupStorage */, F370781E99F6E0BC86F82844 /* History */, 82A72F9E24BABF2B9ACE8532 /* ImagePreview */, B79FA03021091EA400F286BF /* Interpretation */, @@ -6502,21 +7188,21 @@ A45F10AE20B4218D00F45004 /* Message */, 26C1A3DD2031A9330009F7F0 /* OtherUser */, 267BE2971FE13AB600C47E18 /* Participants */, + 8ED0F3C21FBC5CF1004916AB /* GroupsList */, + D25E638A8B9B267255AD766A /* GroupRules */, + 91732B7DCE35ABC02702095D /* GroupStorage */, 84514F06BFDB6825147021F5 /* Profile */, 876B96AB0ABCBF19F269E019 /* QRCodeGenerator */, 14929D916183E29FEAFA6221 /* QRCodeReader */, 264638181FFFC537002590E6 /* Replies */, E61C394BD0E94E3DCF853D4F /* ScheduleMessage */, - 115A968821FB24FA3C58A6D5 /* SelectCountry */, 859B86352048224B003272B2 /* Settings */, 267BE27D1FDE900900C47E18 /* SettingsGroup */, - 4FBB666690A18EEA5438EAB7 /* Splash */, 855AC52C208E435700DC2335 /* Stickers */, 975DB2471671357A9EEBF65B /* TimeZoneSelector */, 2528D43000589CBC2A877417 /* TopUpAccount */, 71C1C60F76F3395F30D450E1 /* Tutorial */, 739A28E989DE50705AA26959 /* VideoPreview */, - FEA6555D2167777E00B44029 /* Wallet Flows */, 85433F1C204D593100B373A7 /* WebFullScreen */, ); path = Modules; @@ -6625,6 +7311,7 @@ 4B058F02204EA928004C7D9F /* DAOProtocol.swift */, 4B058EFF204EA762004C7D9F /* Profile */, A45F59A9205825EC00EAA780 /* Roster */, + 3A0A94D321B533D0007421AA /* Account */, 4B8996C6204ECE8500DCB183 /* Contact */, 4B058F09204EAEA7004C7D9F /* Room */, 2633EF6C205212DF00DB3868 /* Member */, @@ -6806,6 +7493,7 @@ 4B2C502F21B56AE900FBA9B1 /* CorrectMessageIdTypeInStarTable.swift */, 4B2C503121B56B2300FBA9B1 /* RemoveRoomMemberTable.swift */, 4B2C503821B573A100FBA9B1 /* RemoveP2pAndMucTables.swift */, + 3AB73FF921B962F200D1E967 /* AddAccountAndContactInfoTables.swift */, ); path = Migrations; sourceTree = ""; @@ -7045,13 +7733,20 @@ path = Interactor; sourceTree = ""; }; - 4B749F0E214FEFC8002F3A33 /* Auth */ = { + 4B749F0E214FEFC8002F3A33 /* Auth Flow */ = { isa = PBXGroup; children = ( + 5EEB73A9215D406400D8ECE6 /* AuthCoordinator.swift */, + 4FBB666690A18EEA5438EAB7 /* Splash */, + 5EEB73BE216199DE00D8ECE6 /* AuthModule */, + 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */, + 5E07BC45216F64DB000E4558 /* CreateProfile */, + 850EE29821A75E260051F873 /* SelectCountry */, + 852BB8C7219424EA00F2E8E4 /* Facebook */, 3AB452082A8DAEAD93F689D8 /* Login */, 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, ); - path = Auth; + path = "Auth Flow"; sourceTree = ""; }; 4B752B4521639F4900E852B9 /* BaseChatCellModel */ = { @@ -7202,15 +7897,6 @@ path = Roster; sourceTree = ""; }; - 4B8288AA21B301FF00EEA8A7 /* Job */ = { - isa = PBXGroup; - children = ( - 263529142075729400DC6FBD /* Job+DB.swift */, - 005A877E2034C22200372B03 /* JobExtension.swift */, - ); - path = Job; - sourceTree = ""; - }; 4B8771752195ABE80014AD09 /* MessageHandler */ = { isa = PBXGroup; children = ( @@ -7368,50 +8054,17 @@ 4BBAEBBB21AC68F00089B703 /* Validator */ = { isa = PBXGroup; children = ( - 4BBAEBBC21AC68FD0089B703 /* Validator.swift */, + 4BBAEBBC21AC68FD0089B703 /* MTIValidator.swift */, 4BBAEBB921AC62740089B703 /* LengthValidator.swift */, 4BBAEBBE21AC6DF10089B703 /* ClosureValidator.swift */, + 3A184D1C21C0FD8C0083D367 /* EmailValidator.swift */, + 3A184D1A21C0FD800083D367 /* UsernameValidator.swift */, + 8516219A21D9453100EB7F58 /* FirstNameValidator.swift */, + 8516219C21D9455900EB7F58 /* LastNameValidator.swift */, ); path = Validator; sourceTree = ""; }; - 4BBAEBC221ADA9E00089B703 /* CreateGroupFlow */ = { - isa = PBXGroup; - children = ( - 4BBAEBC321ADAA040089B703 /* GroupInput */, - 6BAAF8CD92351F9115795AAC /* CreateGroup */, - E57956502ACFC6A27ACC9EB9 /* MyGroupAlias */, - AC2BC09EF5DBC423CCC26475 /* EditGroupName */, - ); - path = CreateGroupFlow; - sourceTree = ""; - }; - 4BBAEBC321ADAA040089B703 /* GroupInput */ = { - isa = PBXGroup; - children = ( - 4BBAEBC921ADAB7F0089B703 /* Entity */, - 4BBAEBC821ADAAEF0089B703 /* Protocols */, - 4BBAEBC421ADAA190089B703 /* GroupInputViewController.swift */, - ); - path = GroupInput; - sourceTree = ""; - }; - 4BBAEBC821ADAAEF0089B703 /* Protocols */ = { - isa = PBXGroup; - children = ( - 4BBAEBC621ADAA470089B703 /* GroupInputProtocols.swift */, - ); - path = Protocols; - sourceTree = ""; - }; - 4BBAEBC921ADAB7F0089B703 /* Entity */ = { - isa = PBXGroup; - children = ( - 4BBAEBCA21ADAB900089B703 /* GroupInputViewModel.swift */, - ); - path = Entity; - sourceTree = ""; - }; 4BBAEBCC21AE9F510089B703 /* ValidatorFactory */ = { isa = PBXGroup; children = ( @@ -7690,22 +8343,279 @@ path = View; sourceTree = ""; }; - 6002E0651297C852142C0DEF /* View */ = { + 5E07BC45216F64DB000E4558 /* CreateProfile */ = { isa = PBXGroup; children = ( - 8B772E08B9E40EB48DD87082 /* EditUsernameViewController.swift */, + 5E07BC4C216F64EC000E4558 /* CreateProfileProtocols.swift */, + 5E07BC48216F64DB000E4558 /* View */, + 5E07BC46216F64DB000E4558 /* Presenter */, + 5E07BC4A216F64DB000E4558 /* Interactor */, + 5E07BC47216F64DB000E4558 /* Wireframe */, + 85739FB92190A3A5001C4EC8 /* Entities */, + ); + 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 = ( + 5E07BC4E216F659E000E4558 /* CreateProfileViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 5E07BC4A216F64DB000E4558 /* Interactor */ = { + isa = PBXGroup; + children = ( + 5E07BC50216F6617000E4558 /* CreateProfileInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 5EDD454621885EC400C50BC8 /* Account Flow */ = { + isa = PBXGroup; + children = ( + 5EDD454721885EC400C50BC8 /* Coordinator */, + 5EDD454821885EC400C50BC8 /* AccountSettings */, + 3AB73FFD21B9948300D1E967 /* ContactInfoManagement */, + 3AE2F98821B6B4A00068C3BC /* DeleteAccount */, + 851452A421A5865C00DF10A6 /* LoginOptions */, + 3A80BF9121A8637F0016285E /* AuthProvider */, + ); + path = "Account Flow"; + sourceTree = ""; + }; + 5EDD454721885EC400C50BC8 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 5EDD454E21885ED200C50BC8 /* AccountSettingsCoordinator.swift */, + 851452A221A5865100DF10A6 /* LoginOptionsCoordinator.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; + 5EDD454821885EC400C50BC8 /* AccountSettings */ = { + isa = PBXGroup; + children = ( + 5EDD455021885EE300C50BC8 /* AccountSettingsProtocols.swift */, + 5EDD454B21885EC400C50BC8 /* View */, + 5EDD454921885EC400C50BC8 /* Presenter */, + 5EDD454C21885EC400C50BC8 /* Interactor */, + 5EDD454A21885EC400C50BC8 /* Wireframe */, + 5EDD454D21885EC400C50BC8 /* Entities */, + ); + path = AccountSettings; + sourceTree = ""; + }; + 5EDD454921885EC400C50BC8 /* Presenter */ = { + isa = PBXGroup; + children = ( + 5E7D5D37218C40B6009B5D8D /* AccountSettingsPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 5EDD454A21885EC400C50BC8 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 5EDD455221885F7800C50BC8 /* AccountSettingsWireframe.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 5EDD454B21885EC400C50BC8 /* View */ = { + isa = PBXGroup; + children = ( + 5EDD45542188601400C50BC8 /* AccountSettingsViewController.swift */, + 3A06B08D21CB99E400E7964B /* ContactInfoSectionViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 5EDD454C21885EC400C50BC8 /* Interactor */ = { + isa = PBXGroup; + children = ( + 5E7D5D39218C42D0009B5D8D /* AccountSettingsInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 5EDD454D21885EC400C50BC8 /* Entities */ = { + isa = PBXGroup; + children = ( + 5E7D5D3C218C59F1009B5D8D /* AccountStatus.swift */, + 3A6D7D3321CA996B00E1EF90 /* AccountTimeout.swift */, + 3A6D7D3121CA993300E1EF90 /* AccountSettingsViewModel.swift */, + 3A06B08F21CBA84500E7964B /* ContactInfoSectionItem.swift */, + 3A6D7D2F21CA7B4B00E1EF90 /* ContactInfoViewModel.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 5EEB73AB216046EA00D8ECE6 /* CodeConfirmation */ = { + isa = PBXGroup; + children = ( + 5EEB73B1216046FE00D8ECE6 /* CodeConfirmationProtocols.swift */, + 5EEB73AE216046EA00D8ECE6 /* View */, + 5EEB73AC216046EA00D8ECE6 /* Presenter */, + 5EEB73AF216046EA00D8ECE6 /* Interactor */, + 5EEB73AD216046EA00D8ECE6 /* Wireframe */, + 5EEB73B0216046EA00D8ECE6 /* Entities */, + ); + 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 = ""; }; - 600FE315263A064FB7B99F15 /* Presenter */ = { + 5EEB73AF216046EA00D8ECE6 /* Interactor */ = { + isa = PBXGroup; + children = ( + 5EEB73B721604DD900D8ECE6 /* CodeConfirmationInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 5EEB73B0216046EA00D8ECE6 /* Entities */ = { + isa = PBXGroup; + children = ( + 85739FBC2190AAC3001C4EC8 /* ConfirmationData.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 5EEB73BB2161797100D8ECE6 /* Result */ = { + isa = PBXGroup; + children = ( + 5EEB73BC2161797900D8ECE6 /* Result.swift */, + ); + name = Result; + path = Library/Result; + sourceTree = ""; + }; + 5EEB73BE216199DE00D8ECE6 /* AuthModule */ = { isa = PBXGroup; children = ( - CA78C91DFDF5884E382D38FA /* EditGroupNamePresenter.swift */, + 5EEB73C4216199ED00D8ECE6 /* AuthProtocols.swift */, + 5EEB73C1216199DE00D8ECE6 /* View */, + 5EEB73BF216199DE00D8ECE6 /* Presenter */, + 5EEB73C2216199DE00D8ECE6 /* Interactor */, + 5EEB73C0216199DE00D8ECE6 /* Wireframe */, + 5EEB73C3216199DE00D8ECE6 /* Entities */, + ); + 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 /* LoginFlow.swift */, + 3ABD5BFC21E4C11A00DAE935 /* AuthFlowDetails.swift */, + 850B9DAC219C7ADA00EA0CF4 /* PlainLoginOption.swift */, + 851FFA69219EB29A0015F073 /* PhoneNumberTextController.swift */, + 8541995121A2B003004009F7 /* PhoneNumberFormatter.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 5EEB73CE2161CDF700D8ECE6 /* Subviews */ = { + isa = PBXGroup; + children = ( + 5EEB73CF2161CE2700D8ECE6 /* LoginOptionsView.swift */, + 5EEB73D32161D5C500D8ECE6 /* AuthHeaderView.swift */, + 3A9635EA21AC4EE300ABC2C5 /* DetailContainerView.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 = ( + 8B772E08B9E40EB48DD87082 /* EditUsernameViewController.swift */, + ); + path = View; + sourceTree = ""; + }; 60A8AAD06B2EBD471B63ADA9 /* View */ = { isa = PBXGroup; children = ( @@ -7794,18 +8704,6 @@ path = Presenter; sourceTree = ""; }; - 6BAAF8CD92351F9115795AAC /* CreateGroup */ = { - isa = PBXGroup; - children = ( - FDE9DC6ADA0E71241C49A328 /* CreateGroupProtocols.swift */, - ECA328331A1B420DBE7E5506 /* View */, - 0E2F86AFA19D5CA844D6AC27 /* Presenter */, - 12396B05D93D1CA3A8410766 /* Interactor */, - 4645720B5E0E5A8B5B0B0F39 /* WireFrame */, - ); - path = CreateGroup; - sourceTree = ""; - }; 6DAC1D101F7A63F50022E5A5 /* NotificationView */ = { isa = PBXGroup; children = ( @@ -7833,14 +8731,6 @@ path = View; sourceTree = ""; }; - 71705E487C071CDEB8D9E7B3 /* WireFrame */ = { - isa = PBXGroup; - children = ( - 273EABBCA8570D21A8683273 /* EditGroupNameWireframe.swift */, - ); - path = WireFrame; - sourceTree = ""; - }; 71C1C60F76F3395F30D450E1 /* Tutorial */ = { isa = PBXGroup; children = ( @@ -8248,6 +9138,14 @@ path = UserSettings; sourceTree = ""; }; + 85086F4E21C68AFF00194361 /* InputView */ = { + isa = PBXGroup; + children = ( + 85086F4F21C68B5600194361 /* PhoneNumberContactInfoViewModel.swift */, + ); + path = InputView; + sourceTree = ""; + }; 8509547D20481AF900905B46 /* ThemePicker */ = { isa = PBXGroup; children = ( @@ -8270,6 +9168,34 @@ path = Files; sourceTree = ""; }; + 850A2E92219EF9A800C784D9 /* Alert */ = { + isa = PBXGroup; + children = ( + 4BB0EFBA2151347900704136 /* AlertManager.swift */, + 850A2E93219EF9B800C784D9 /* AlertDisplayable.swift */, + 85086F4A21C672FD00194361 /* Alert+Defaults.swift */, + ); + path = Alert; + 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 = ( @@ -8324,6 +9250,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 = ( @@ -8439,6 +9454,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 = ( @@ -8541,6 +9611,18 @@ path = Reply; sourceTree = ""; }; + 852037EB21A5BD100085CF1F /* Forms */ = { + isa = PBXGroup; + children = ( + 853567BA21A6B00100AAEEF9 /* Form.swift */, + 3A2CDAC021C944CD00B5E397 /* FormHeaderView.swift */, + 3A4D098321DCCA7400103E95 /* FormContainer.swift */, + 8542FBF421A6EDD400CC295B /* FieldRowItem */, + 8542FBF521A6EDE800CC295B /* Items */, + ); + path = Forms; + sourceTree = ""; + }; 8524C4D42177715E003BF374 /* Member */ = { isa = PBXGroup; children = ( @@ -8578,6 +9660,60 @@ 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 = ""; + }; + 852BB8F721947A0800F2E8E4 /* Auth */ = { + isa = PBXGroup; + children = ( + 852BB8F821947A3A00F2E8E4 /* GoogleAuthService.swift */, + 852BB8FA2194807500F2E8E4 /* GoogleAuthServiceUIDelegate.swift */, + 852BB8FC21949F2C00F2E8E4 /* GoogleAuthError.swift */, + ); + path = Auth; + sourceTree = ""; + }; 852C3DD0216E3A4300447878 /* Messaging */ = { isa = PBXGroup; children = ( @@ -8795,6 +9931,37 @@ 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 */, + 3A2CDAC221C9648800B5E397 /* DestructiveActionRowItem.swift */, + 3A2CDABE21C9405E00B5E397 /* AvatarRowItemView.swift */, + 852037E521A5AD4A0085CF1F /* TextRowItemView.swift */, + 852037E921A5B4230085CF1F /* TextFieldRowItemView.swift */, + 85086F5321C6AD3600194361 /* PickerRowItemView.swift */, + 852037E721A5B1E00085CF1F /* SwitchRowItemView.swift */, + ); + path = Items; + sourceTree = ""; + }; + 8542FBF621A6F0D100CC295B /* Entities */ = { + isa = PBXGroup; + children = ( + 8542FBF721A6FFE200CC295B /* LoginOption.swift */, + ); + path = Entities; + sourceTree = ""; + }; 85433F1C204D593100B373A7 /* WebFullScreen */ = { isa = PBXGroup; children = ( @@ -8865,39 +10032,15 @@ path = Documents; sourceTree = ""; }; - 854A4B392080E5D500759152 /* TableView */ = { - isa = PBXGroup; - children = ( - 854A4B3B2080E5E900759152 /* Arrow */, - 854A4B3C2080E5EF00759152 /* Image */, - ); - path = TableView; - sourceTree = ""; - }; - 854A4B3A2080E5DF00759152 /* ViewController */ = { - isa = PBXGroup; - children = ( - 7C2CBB5F32D209160D00F744 /* CreateGroupViewController.swift */, - ); - path = ViewController; - sourceTree = ""; - }; - 854A4B3B2080E5E900759152 /* Arrow */ = { - isa = PBXGroup; - children = ( - 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */, - 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */, - ); - path = Arrow; - sourceTree = ""; - }; - 854A4B3C2080E5EF00759152 /* Image */ = { + 8548341921874434002064E1 /* Observable */ = { isa = PBXGroup; children = ( - 854A4B2F2080D6C400759152 /* CellWithImageCellModel.swift */, - 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */, + 8548341C218744AC002064E1 /* Observable.swift */, + 8548341A2187449F002064E1 /* ObservableContainer.swift */, + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */, + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */, ); - path = Image; + path = Observable; sourceTree = ""; }; 854D13D6211B2E6200E139FC /* CollectionView */ = { @@ -8950,6 +10093,27 @@ path = StickerPackHeader; sourceTree = ""; }; + 855A4E99219B31F200B6E90B /* SDK */ = { + isa = PBXGroup; + children = ( + 850B9DA4219C2B7C00EA0CF4 /* App */, + 850B9D9D219C11BF00EA0CF4 /* Session */, + 855A4E9C219B323200B6E90B /* Auth */, + 859ECA6421A438E8003630A0 /* Account */, + ); + path = SDK; + sourceTree = ""; + }; + 855A4E9C219B323200B6E90B /* Auth */ = { + isa = PBXGroup; + children = ( + 859ECA6D21A441D8003630A0 /* Service */, + 859ECA6E21A441E8003630A0 /* Entities */, + 3A0AEA6C21AFF3FE0066CBBA /* ProfileMockProvider.swift */, + ); + path = Auth; + sourceTree = ""; + }; 855AC52C208E435700DC2335 /* Stickers */ = { isa = PBXGroup; children = ( @@ -9017,6 +10181,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 = ( @@ -9048,6 +10238,14 @@ path = Cells; sourceTree = ""; }; + 85739FB92190A3A5001C4EC8 /* Entities */ = { + isa = PBXGroup; + children = ( + 3A4D098521DCCCDA00103E95 /* CreateProfileViewModel.swift */, + ); + path = Entities; + sourceTree = ""; + }; 85788C3A20442263003600C9 /* BuildNumber */ = { isa = PBXGroup; children = ( @@ -9137,9 +10335,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; @@ -9148,8 +10347,8 @@ 8580BAD520BD98E600239D9D /* Model */ = { isa = PBXGroup; children = ( - 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, 8580BAD620BD98E600239D9D /* ChatListMessageCellModel.swift */, + 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, ); path = Model; sourceTree = ""; @@ -9370,6 +10569,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 */, + 85086F5721C6CCD400194361 /* PhoneNumberLabel.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 */, + ); + path = Entities; + sourceTree = ""; + }; 859F9B4A2035C555009D017A /* Cell */ = { isa = PBXGroup; children = ( @@ -9569,6 +10816,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 = ( @@ -9944,9 +11201,14 @@ isa = PBXGroup; children = ( 8503B51A205036F2006F0593 /* BaseNynjaButton.swift */, + 855A4E802199C16F00B6E90B /* RoundNynjaButton.swift */, E761A0DB1F8B8F3900C088E0 /* NynjaButton.swift */, 8503B51820503683006F0593 /* NynjaCellButton.swift */, 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */, + 855A4E7E2199B4FE00B6E90B /* NynjaImageButton.swift */, + 8542FBF221A6ECC100CC295B /* NynjaSwitch.swift */, + 85086F5521C6B7E700194361 /* DestructiveNynjaButton.swift */, + 3A4D098121DCB9A700103E95 /* NynjaCheckBox.swift */, ); path = NynjaButton; sourceTree = ""; @@ -10394,8 +11656,6 @@ A43B257120AB1DFA00FF8107 /* TextInput */ = { isa = PBXGroup; children = ( - A460324E2105C9A1009783DA /* InputsCachePolicy.swift */, - 8580BAE320BD99DC00239D9D /* UITextInput+Cursor.swift */, A432CF0320B4347C00993AFB /* Material */, A43B257220AB1DFA00FF8107 /* InputBar */, A43B257920AB1DFA00FF8107 /* TextView */, @@ -10449,7 +11709,6 @@ A43B257C20AB1DFA00FF8107 /* InputField */ = { isa = PBXGroup; children = ( - A460324B2105C2CE009783DA /* TextField */, A4679BB620B305360021FE9C /* LinkField */, A43B257D20AB1DFA00FF8107 /* CodeField.swift */, A43B257F20AB1DFA00FF8107 /* NynjaSearchField.swift */, @@ -10459,13 +11718,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; @@ -10720,11 +11977,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 = ""; @@ -10911,8 +12168,8 @@ A45F115720B422AF00F45004 /* Contact */ = { isa = PBXGroup; children = ( + 3A0A50E421B819A60052D334 /* Contact+Desc.swift */, A45F115820B422AF00F45004 /* Contact+DB.swift */, - A438DB9120763AFB00AA86A2 /* Contact+Desc.swift */, ); path = Contact; sourceTree = ""; @@ -10936,14 +12193,6 @@ name = Roster; sourceTree = ""; }; - A460324B2105C2CE009783DA /* TextField */ = { - isa = PBXGroup; - children = ( - A43B258720AB1DFA00FF8107 /* TextField.swift */, - ); - path = TextField; - sourceTree = ""; - }; A46679EF20F10B2B00DBC6B4 /* RequestModelFactory */ = { isa = PBXGroup; children = ( @@ -11046,8 +12295,8 @@ A4679BB620B305360021FE9C /* LinkField */ = { isa = PBXGroup; children = ( - A4679BB720B305360021FE9C /* LinkValidator.swift */, - A4679BB820B305360021FE9C /* LinkField.swift */, + A4679BB720B305360021FE9C /* ChannelLinkValidator.swift */, + A4679BB820B305360021FE9C /* NynjaLinkField.swift */, ); path = LinkField; sourceTree = ""; @@ -11407,16 +12656,6 @@ path = View; sourceTree = ""; }; - AC2BC09EF5DBC423CCC26475 /* EditGroupName */ = { - isa = PBXGroup; - children = ( - 6FFA60D8A9D5F677A9AAAF57 /* EditGroupNameProtocols.swift */, - 600FE315263A064FB7B99F15 /* Presenter */, - 71705E487C071CDEB8D9E7B3 /* WireFrame */, - ); - path = EditGroupName; - sourceTree = ""; - }; B015BD1159A1ABE336C043BB /* WireFrame */ = { isa = PBXGroup; children = ( @@ -11773,14 +13012,6 @@ path = Validation; sourceTree = ""; }; - B8DCBB4ACE8A650987F2D234 /* Presenter */ = { - isa = PBXGroup; - children = ( - DC9D0CBC2BAD6DC6C7047A26 /* MyGroupAliasPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; C0F3DD8B2859372188498348 /* Call */ = { isa = PBXGroup; children = ( @@ -11924,27 +13155,6 @@ path = SwipeBackHelper; sourceTree = ""; }; - C9C695062022318500A57297 /* TableView */ = { - isa = PBXGroup; - children = ( - C9C695022022306D00A57297 /* SelectCountryTableDataSource.swift */, - C9DF57492023A29A006B990A /* SelectCountryTableDelegate.swift */, - C9C69504202230DD00A57297 /* SelectCountryCell.swift */, - C9C6952D202349DA00A57297 /* SelectCountryCellLayout.swift */, - C9DF574B2023BE92006B990A /* SelectCountryHeaderView.swift */, - ); - path = TableView; - sourceTree = ""; - }; - C9C6952920232BB900A57297 /* SortableObject */ = { - isa = PBXGroup; - children = ( - C9C6952520232B0100A57297 /* SortableObject.swift */, - C9C6952720232B7000A57297 /* Array+SortableObject.swift */, - ); - path = SortableObject; - sourceTree = ""; - }; CAB5F1F6E675E93CA821CC51 /* Presenter */ = { isa = PBXGroup; children = ( @@ -11969,14 +13179,6 @@ path = WireFrame; sourceTree = ""; }; - CADE0A8BB5BE972F51CE1E2F /* WireFrame */ = { - isa = PBXGroup; - children = ( - B05863F1D1FC27487D496750 /* SelectCountryWireframe.swift */, - ); - path = WireFrame; - sourceTree = ""; - }; CE9D96E59FD1D607EFA72FCE /* View */ = { isa = PBXGroup; children = ( @@ -12130,16 +13332,6 @@ path = View; sourceTree = ""; }; - E57956502ACFC6A27ACC9EB9 /* MyGroupAlias */ = { - isa = PBXGroup; - children = ( - 8606C1D61AA46EB77821B1B0 /* MyGroupAliasProtocols.swift */, - B8DCBB4ACE8A650987F2D234 /* Presenter */, - 48CBD0E1B8BFC875AB252183 /* WireFrame */, - ); - path = MyGroupAlias; - sourceTree = ""; - }; E61C394BD0E94E3DCF853D4F /* ScheduleMessage */ = { isa = PBXGroup; children = ( @@ -12257,6 +13449,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 */, @@ -12301,6 +13495,8 @@ children = ( E79061B21FBF1057009FD83A /* Base */, E70938361FBEDA2B006CCDC6 /* ProfileTable.swift */, + 3A0AEA7421B028120066CBBA /* AccountTable.swift */, + 3A0E865821B130DC00BAF80B /* ContactInfoTable.swift */, E709383C1FBEE176006CCDC6 /* ServiceTable.swift */, E79061B71FBF2243009FD83A /* FeatureTable.swift */, E79061B51FBF1C8C009FD83A /* DescTable.swift */, @@ -12642,8 +13838,8 @@ 4B8BEDDF2049798C00C7D625 /* ImagesView */, E74597761FA2226900D3C88C /* NavigationView */, E79117891F97874D00462D68 /* GradientView.swift */, - E70402BC1FF6972B00182D81 /* BaseView.swift */, F11DF05E20BD93FB00F3E005 /* UIViewExtensions.swift */, + 3ABA188E21BFF3D40026B96B /* GradientContainerView.swift */, ); path = View; sourceTree = ""; @@ -12660,7 +13856,6 @@ A416DA5E207533FB00FBF1BA /* CoreLocation */, 2648C3E52069B48F00863614 /* UITextField+Extension.swift */, 26B32B8B1FE20B0400888A0A /* MQTTModels */, - C9C6952920232BB900A57297 /* SortableObject */, E7A77FD91FACC375004AE609 /* Models */, E7A77FD71FACC360004AE609 /* UIDeviceExtension.swift */, E77FBDDC1FFE828400BDB255 /* AVURLAsset+Duration.swift */, @@ -12687,7 +13882,7 @@ A45F115720B422AF00F45004 /* Contact */, 853E595220D6AF6A007799B9 /* Desc */, 853E595320D6AF7E007799B9 /* Feature */, - 4B8288AA21B301FF00EEA8A7 /* Job */, + 3A0A50DF21B8198E0052D334 /* Job */, A48C154020EF76DB002DA994 /* Link */, 8524C4D42177715E003BF374 /* Member */, A45F115B20B422AF00F45004 /* Message */, @@ -12700,6 +13895,7 @@ 266F04CE201541BC00B97A83 /* StarExtension.swift */, 853E595C20D71E73007799B9 /* StickerPack */, E757B53C1FE9225C00467BA2 /* TypingExtension.swift */, + 3A0E865B21B14ECB00BAF80B /* AccountExtension.swift */, 4B8288AB21B3024100EEA8A7 /* HistoryExtension.swift */, ); path = Models; @@ -12822,15 +14018,6 @@ path = WireFrame; sourceTree = ""; }; - ECA328331A1B420DBE7E5506 /* View */ = { - isa = PBXGroup; - children = ( - 854A4B392080E5D500759152 /* TableView */, - 854A4B3A2080E5DF00759152 /* ViewController */, - ); - path = View; - sourceTree = ""; - }; ED33B21C6D660153B22D18BF /* AddContact */ = { isa = PBXGroup; children = ( @@ -12862,7 +14049,11 @@ F105C690209F71BE0091786A /* Flows */ = { isa = PBXGroup; children = ( - 4BBAEBC221ADA9E00089B703 /* CreateGroupFlow */, + 3AAA92AD21B1A6C800EF5F1E /* AppCoordinator.swift */, + 4B749F0E214FEFC8002F3A33 /* Auth Flow */, + 5EDD454621885EC400C50BC8 /* Account Flow */, + 3A0E425A21BFB69C001A3F3C /* Search Flow */, + 3A0A50A521B7FEFE0052D334 /* CreateGroupFlow */, F105C691209F71BE0091786A /* CameraFlow */, F11786F420ACF017007A9A1B /* CameraSettingsFlow */, 4BFED75C21A6CE38003CF1B3 /* ExtendedImage.swift */, @@ -13091,7 +14282,8 @@ F11786B320A8A5EB007A9A1B /* Coordinators */ = { isa = PBXGroup; children = ( - F11786BA20A8A63F007A9A1B /* CoordinatorProtocol.swift */, + F11786BA20A8A63F007A9A1B /* Coordinator.swift */, + 3A14D83E21AC1F07009CD23A /* NavigationError.swift */, ); path = Coordinators; sourceTree = ""; @@ -13211,6 +14403,10 @@ children = ( 4B055C35219C30F5001FE077 /* FileDownloaderFactory */, F11786F020AC5482007A9A1B /* ServiceFactory.swift */, + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */, + 851C6A53218B560B0062B148 /* MQTTFactoryProtocol.swift */, + 850B9DA2219C208100EA0CF4 /* MobileSDKFactoryProtocol.swift */, + 3A0A94D821B544B4007421AA /* DAOFactoryProtocol.swift */, ); name = ServiceFactory; path = Services/ServiceFactory; @@ -13442,7 +14638,8 @@ F13EACD920B86B7F007104D6 /* Wireframe */ = { isa = PBXGroup; children = ( - F13EACDA20B86B8C007104D6 /* WireframeProtocol.swift */, + F13EACDA20B86B8C007104D6 /* Wireframe.swift */, + 854574CB21933190001D43CF /* NavigableWireframeProtocol.swift */, ); path = Wireframe; sourceTree = ""; @@ -14542,6 +15739,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 */, @@ -14644,11 +15842,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; @@ -14668,14 +15868,14 @@ "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.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", "${BUILT_PRODUCTS_DIR}/MQTTClient/MQTTClient.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", @@ -14683,6 +15883,11 @@ "${BUILT_PRODUCTS_DIR}/SocketRocket/SocketRocket.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.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 = ( @@ -14692,12 +15897,12 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.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", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MQTTClient.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", @@ -14707,6 +15912,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.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}/MotionAnimator.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MotionInterchange.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -14725,15 +15935,22 @@ "${BUILT_PRODUCTS_DIR}/AutoScrollLabel/AutoScrollLabel.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", - "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.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}/MQTTClient/MQTTClient.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", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", "${BUILT_PRODUCTS_DIR}/SocketRocket/SocketRocket.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 = ( @@ -14742,15 +15959,22 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AutoScrollLabel.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.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}/MQTTClient.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", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.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; @@ -14784,6 +16008,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", @@ -14792,6 +16017,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", @@ -14881,6 +16107,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 */, @@ -14942,7 +16169,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 */, @@ -14958,7 +16184,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 */, @@ -14969,6 +16194,7 @@ E757B53E1FE92C9E00467BA2 /* TypingModel.swift in Sources */, A42CE5A620692EDB000889CC /* Contact.swift in Sources */, A42CE5B820692EDB000889CC /* Job_Spec.swift in Sources */, + 3A0A50E621B81A100052D334 /* JobExtension.swift in Sources */, A42CE5BE20692EDB000889CC /* Search_Spec.swift in Sources */, 35B1AB821F9FB06500E65233 /* Attachment.swift in Sources */, A42CE5D420692EDB000889CC /* cur_Spec.swift in Sources */, @@ -15076,7 +16302,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 */, @@ -15111,7 +16336,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 */, 4B483ABA21BED06A00FCF879 /* MQTTConnectionHandler.swift in Sources */, @@ -15129,11 +16353,9 @@ 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 */, 4BF2C3FD218AFF6300E59F6C /* FullNameRepresentable.swift in Sources */, - 26352916207572AA00DC6FBD /* JobExtension.swift in Sources */, 8566BB12215BC39D00320E15 /* Feed.swift in Sources */, 8520040120D4672E007C0036 /* StickerPack.swift in Sources */, 4B7E93382170D1BC001558CF /* RootNavigationController.swift in Sources */, @@ -15161,7 +16383,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 */, @@ -15169,6 +16390,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 */, 4B1162FC21BFC1FB003859ED /* AutoReconnectConnectionHandler.swift in Sources */, 4BAE7DDB21B69AC70018D3C2 /* TimerHandler.swift in Sources */, @@ -15191,6 +16413,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 */, @@ -15203,11 +16426,14 @@ 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 */, + 3A0A50CE21B7FEFE0052D334 /* CreateGroupPresenter.swift in Sources */, F119E66E20D24BBF0043A532 /* MultiplePreviewWireframe.swift in Sources */, 2611CEF72182090900FFD4DD /* LogWriterProtocol.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 */, @@ -15233,7 +16459,6 @@ F11DF05F20BD93FB00F3E005 /* UIViewExtensions.swift in Sources */, 4B1D7E112029FF5000703228 /* Array+WheelItemModel.swift in Sources */, A46C36342121999100172773 /* DDMechanism.swift in Sources */, - 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */, 4B483AB621BEC0C100FCF879 /* UserIdentifiers.swift in Sources */, 3A8045D81F60C98200AED866 /* MQTTService+Helper.swift in Sources */, 8E9601971FF2EC8100E0C21D /* GroupFilesListVC.swift in Sources */, @@ -15248,14 +16473,18 @@ 4B1D7E072029D00000703228 /* OtherUserProfileItemsFactory.swift in Sources */, 855AC532208E441500DC2335 /* StickersInputPresenter.swift in Sources */, 3A1DC73C1EF15330006A8E9F /* HandlerService.swift in Sources */, + 852BB8D12194256600F2E8E4 /* FacebookAuthInteractor.swift in Sources */, + 5EDD45552188601400C50BC8 /* AccountSettingsViewController.swift in Sources */, F117871120ACF018007A9A1B /* CameraQualitySettingsInteractor.swift in Sources */, 8503B51920503683006F0593 /* NynjaCellButton.swift in Sources */, 8596CEF62048AEB8006FC65D /* ThemeItemsFactory.swift in Sources */, 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 */, + 852BB8FD21949F2C00F2E8E4 /* GoogleAuthError.swift in Sources */, E7EED2341F740BF3005DAE20 /* ChatsItem.swift in Sources */, 260313A620A0A4BA009AC66D /* SwitchableActionCellViewModel.swift in Sources */, A42D51AE206A361400EEB952 /* Message.swift in Sources */, @@ -15264,6 +16493,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 */, @@ -15277,6 +16507,7 @@ 3A27B0A71EF307A900B4B3CB /* DeleteUserModel.swift in Sources */, 3A1F74FA1F5ED344009A11E4 /* PushService.swift in Sources */, FEA656042167777F00B44029 /* WalletBalancesInteractor.swift in Sources */, + 85739FBD2190AAC3001C4EC8 /* ConfirmationData.swift in Sources */, 261F2E2E200EB0AD007D0813 /* RepliesVC+CellDelegate.swift in Sources */, 4B0CC1FD2195B52000E0BA61 /* IoHandlerDelegate.swift in Sources */, A45F110620B4218D00F45004 /* MessageConfiguration.swift in Sources */, @@ -15286,6 +16517,7 @@ F1AC0DE3207252E1001C68F7 /* Testable.swift in Sources */, A408A0BD20C174040029F54B /* ChannelsListInteractor.swift in Sources */, A458FABD20EB8B320075D55E /* MessageChannelActionsProtocol.swift in Sources */, + 3A9635EB21AC4EE300ABC2C5 /* DetailContainerView.swift in Sources */, 4BE2C5D92142EAC500A73DD9 /* AudioPlayer.swift in Sources */, F11786EE20AC39E9007A9A1B /* Job_Spec.swift in Sources */, 0008E92420347A8E003E316E /* DBJobMessage.swift in Sources */, @@ -15303,12 +16535,15 @@ FB0B721320907DB5003B9757 /* MessageEditService.swift in Sources */, 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 */, 0062D93F2062EC4100B915AC /* InviteFriendsCellLayout.swift in Sources */, 001F0CF5202C38FA006B4304 /* TimeZoneCell.swift in Sources */, 4B8FC3182163CC0E00602D6B /* ChatCellModel.swift in Sources */, + 3A0A50E521B819A60052D334 /* Contact+Desc.swift in Sources */, C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */, 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */, A49381AA21355EE1006D28DD /* MessageInteractor+Forward.swift in Sources */, @@ -15319,6 +16554,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 */, @@ -15343,6 +16579,7 @@ F105C6BA20A1347E0091786A /* PhotoPreviewProtocols.swift in Sources */, 85458CF5212D770100BA8814 /* Message+Files.swift in Sources */, E721306F1F9A384900D88103 /* AlignableLabel.swift in Sources */, + 3A0A50CF21B7FEFE0052D334 /* CreateGroupWireframe.swift in Sources */, A4C9300420B323B700D6FB0F /* RoomExtension.swift in Sources */, 4B06D30E2028A349003B275B /* ChatsItemsFactory.swift in Sources */, 266F04CF201541BC00B97A83 /* StarExtension.swift in Sources */, @@ -15361,6 +16598,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 */, @@ -15380,6 +16618,7 @@ F10B0E2120B4CF3800528E7A /* CameraCoordinator.swift in Sources */, 2657BE532012405600F21935 /* ImageActionItemModel.swift in Sources */, 26A0CFE12005138C006F6617 /* MemberExtension+BERT.swift in Sources */, + 3A80BF9721A864220016285E /* AuthProviderPresenter.swift in Sources */, 2652D6181FA85B28005E62C7 /* ImageSelector.swift in Sources */, 26DE8D9120FE1AF500C41096 /* ChatCellFooterView.swift in Sources */, 6D485DE51F0AD96D00E12FB1 /* Localizable.swift in Sources */, @@ -15387,6 +16626,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 */, @@ -15394,13 +16634,14 @@ 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 */, 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */, - C9DF574A2023A29A006B990A /* SelectCountryTableDelegate.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 */, 4B1D7E052029CF2900703228 /* ShareContactsItemsFactory.swift in Sources */, @@ -15423,6 +16664,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 */, @@ -15430,7 +16672,6 @@ A42D52CA206A53AB00EEB952 /* muc_Spec.swift in Sources */, 850C301D204DA87A00DB26C2 /* PrivacyListProtocols.swift in Sources */, B77C11DF2109242200CCB42E /* AssigningInterpreterWireFrame.swift in Sources */, - 4BBAEBC521ADAA1A0089B703 /* GroupInputViewController.swift in Sources */, 8520040D20D513B8007C0036 /* OpponentMessageStickerRepliedView.swift in Sources */, A458FAC420EBA58A0075D55E /* MuteChatService.swift in Sources */, 2661D12F1F373D1700F3E125 /* BorderView.swift in Sources */, @@ -15438,7 +16679,6 @@ F117870D20ACF018007A9A1B /* CameraQualitySettingsPresenter.swift in Sources */, 26EAA2CB20D2497F005697CB /* TranslationAutoView.swift in Sources */, 85D669EA20BD95FA00FBD803 /* MessagePresenter+MentionUnreadCounter.swift in Sources */, - 854A4B312080D6C400759152 /* CellWithImageCellModel.swift in Sources */, FBCE841020E525A6003B7558 /* HTTPHeader+Authorization.swift in Sources */, 4B71AC4221622A6A00E4583B /* AppNotificationsProvider.swift in Sources */, 855EF425202CCADB00541BE3 /* ExtendedStarHandler.swift in Sources */, @@ -15448,10 +16688,13 @@ 4B02130220372C5700650298 /* OtherItemView.swift in Sources */, 853801282052CCAD002C6960 /* SoundCellModel.swift in Sources */, A458FABB20EB87BF0075D55E /* ActionContainerContent.swift in Sources */, + 5EEB73DE21623FF900D8ECE6 /* UIViewControllerExtensions.swift in Sources */, 00E9824C205C1E19008BF03D /* ActiveSessionsItemsFactory.swift in Sources */, 26342CA920ECBAEF00D2196B /* TranscribeNetworkClient.swift in Sources */, 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 */, @@ -15480,6 +16723,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 */, @@ -15488,23 +16732,30 @@ A43B259D20AB1DFA00FF8107 /* PhoneField.swift in Sources */, 267BE90820693DE700153FB8 /* DBManagerProtocol.swift in Sources */, 26342CAB20ECBB0100D2196B /* TranscribeNetworkService.swift in Sources */, + 5E07BC57216F6722000E4558 /* CreateProfileWireframe.swift in Sources */, A44B4D5820CE9BDF00CA700A /* AvatarCell.swift in Sources */, A418DA3820EE1AFD00FE780B /* CountAppearanceModel.swift in Sources */, B763DD9320AA1C3400A30B63 /* ContactCellLayout.swift in Sources */, A45F110820B4218D00F45004 /* ChatCheckpoint.swift in Sources */, + 3A0A50D721B7FEFE0052D334 /* CreateGroupInteractor.swift in Sources */, 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 */, + 851452B621A5A2E100DF10A6 /* ActionRowItemView.swift in Sources */, D30EB73829E48C0B1C1FD1C9 /* LoginViewController.swift in Sources */, FEA65471216775CD00B44029 /* ServiceWalletExtension.swift in Sources */, FEA655E82167777E00B44029 /* TransferHistoryProtocols.swift in Sources */, 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 */, 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */, A4679BA920B2DD100021FE9C /* SubscribersTableDelegate.swift in Sources */, 2648C41E2069B5B300863614 /* ChangeNumberItemsFactory.swift in Sources */, @@ -15514,13 +16765,16 @@ 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 */, 26DCB24B2064B9CC001EF0AB /* ContactCell.swift in Sources */, A42D52B8206A53AA00EEB952 /* act_Spec.swift in Sources */, F11786CE20A8E4FD007A9A1B /* MuteState.swift in Sources */, 85D66A1120BD965300FBD803 /* UserMentionCellModel.swift in Sources */, 0062D9472062EC4100B915AC /* InviteFriendsViewController.swift in Sources */, + 5EEB73B821604DD900D8ECE6 /* CodeConfirmationInteractor.swift in Sources */, 4B752B512163A04800E852B9 /* Array+BaseChatCellModel.swift in Sources */, 4B2D063C202E1A1500010A0C /* ContactsExpandedItemsFactory.swift in Sources */, 85D66A2120BD970400FBD803 /* BBCodeEntity.swift in Sources */, @@ -15530,6 +16784,8 @@ E791178A1F97874D00462D68 /* GradientView.swift in Sources */, FEA655EE2167777E00B44029 /* TransferHistoryCell.swift in Sources */, E79117901F97A3BC00462D68 /* ProfileDetailsView.swift in Sources */, + 851452AC21A586E900DF10A6 /* LoginOptionsProtocols.swift in Sources */, + 3A0A50D621B7FEFE0052D334 /* CreateGroupViewController.swift in Sources */, 265F5D24209B6987008ACCC8 /* Place.swift in Sources */, E734831A1F9F39400090A4DB /* CellModel.swift in Sources */, B74BB00321076AFA0049CD27 /* CircleMenuItem.swift in Sources */, @@ -15554,6 +16810,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 */, @@ -15573,16 +16830,17 @@ 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 */, 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 */, A458FAC220EBA5450075D55E /* MuteChatServiceProtocol.swift in Sources */, A42D51C3206A361400EEB952 /* ok2.swift in Sources */, + 3A0A50DB21B7FEFE0052D334 /* EditGroupNamePresenter.swift in Sources */, 267BE2AF1FE13AB600C47E18 /* ParticipantsPresenter.swift in Sources */, 2648C4112069B52100863614 /* ChangeNumberCodeView.swift in Sources */, 35F2DA611F73CAD400777920 /* NotificationManager.swift in Sources */, @@ -15594,6 +16852,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 */, @@ -15603,6 +16862,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 */, @@ -15616,7 +16876,10 @@ 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 */, 8EC2AF6B20053FC300807B20 /* GroupCollectionCell.swift in Sources */, 4BBAEBD021AE9F9D0089B703 /* ValidatorFactory.swift in Sources */, 4B8996D8204EDA7700DCB183 /* JobDAOProtocol.swift in Sources */, @@ -15626,7 +16889,10 @@ 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 */, B74BAFFC21076AFA0049CD27 /* SectionView.swift in Sources */, 8ED0F3CF1FBC5CF2004916AB /* GroupsListPresenter.swift in Sources */, 850A2BB22035AE5E00D68FDF /* ForwardCellViewModel.swift in Sources */, @@ -15636,6 +16902,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 */, @@ -15647,19 +16914,25 @@ A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, + 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 */, 6D36F8E71F0BBFC300FA1AC8 /* ContactManager.swift in Sources */, 32868DD51F31CADF0028B260 /* ChatsListProtocols.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 */, 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 */, 005886CD2030F41700FE2E89 /* NynjaTimeAmPmDelegate.swift in Sources */, 8514F17820EA219F00883513 /* ContextMenu.swift in Sources */, @@ -15668,15 +16941,18 @@ 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 */, 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 */, @@ -15689,6 +16965,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 */, 8502DB512061030100613C8C /* WheelPositionPickerProtocols.swift in Sources */, 00F7B3402029DD6200E443E1 /* TextItemView.swift in Sources */, @@ -15697,7 +16974,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 */, @@ -15713,10 +16989,12 @@ 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 */, 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 */, @@ -15726,11 +17004,13 @@ E749C5671FD4490E0048DEAC /* DefaultMessageProcessingManager.swift in Sources */, FEA655FF2167777F00B44029 /* WalletBalancesPresenter.swift in Sources */, B7B546B0210DC68E002DCA55 /* CircleLoadingView.swift in Sources */, + 3A0A50D421B7FEFE0052D334 /* CellWithImageTableViewCell.swift in Sources */, 85482848204EA56600DCBEC8 /* PrivacyListDataSource.swift in Sources */, 2600DAD6203479D000A2D4F7 /* ReturnToHomeHeaderView.swift in Sources */, 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 */, @@ -15759,6 +17039,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 */, @@ -15776,6 +17057,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 */, @@ -15801,6 +17083,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 */, @@ -15811,15 +17094,18 @@ 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 */, 850A0C6520469AED004F79AD /* UserSettingsRespondable.swift in Sources */, + 3A0A50E221B8198E0052D334 /* Job+DB.swift in Sources */, E7598F6C1FA1D8B90082FBE7 /* ProfileContactCellLayout.swift in Sources */, 265F5D2C209B8B74008ACCC8 /* MessageEditActionDAOProtocol.swift in Sources */, 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 */, @@ -15828,6 +17114,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 */, 8513F06C218D053F003B901B /* BERTEncodable.swift in Sources */, 850C301F204DA87A00DB26C2 /* PrivacyListWireFrame.swift in Sources */, @@ -15835,7 +17122,7 @@ 850571232050B0AD00EDF794 /* NotificationAlertSoundsProtocols.swift in Sources */, B592FE29097F622601D5084C /* AddContactInteractor.swift in Sources */, A42D5206206A4B5E00EEB952 /* TimerHandler.swift in Sources */, - 4BBAEBBD21AC68FD0089B703 /* Validator.swift in Sources */, + 4BBAEBBD21AC68FD0089B703 /* MTIValidator.swift in Sources */, B06A314CF727CB6D3C9AF2A4 /* AddContactWireframe.swift in Sources */, D764CA9732E3D09DE3DD8EDB /* QRCodeGeneratorProtocols.swift in Sources */, E3F2A61FC911D43EC6B8BB4C /* QRCodeGeneratorViewController.swift in Sources */, @@ -15847,14 +17134,17 @@ 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 */, FBCE841420E525A6003B7558 /* NetworkService.swift in Sources */, A409B1CF2108D48E0051C20B /* QueryFactory.swift in Sources */, 4B87717D2195AFF50014AD09 /* StaticDelegating.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 */, 9BD8E3F120EF7898001384EC /* CallInProgressViewController.swift in Sources */, @@ -15871,7 +17161,6 @@ A45F111920B4218D00F45004 /* MessageContactView.swift in Sources */, 4B06D3242028B209003B275B /* WCBaseItemsFactory.swift in Sources */, 0014FADA20603D6400A4022F /* NoInternetNavigationView.swift in Sources */, - 005A877F2034C22200372B03 /* JobExtension.swift in Sources */, A45F110420B4218D00F45004 /* MessageWireframe.swift in Sources */, 85788C4420442385003600C9 /* BuildNumberPresenter.swift in Sources */, 3AE0A84D1F20321A008A04F3 /* WheelItemView.swift in Sources */, @@ -15888,12 +17177,12 @@ 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 */, E7A3DAB51F9DEAC400856133 /* ProfileSectionModel.swift in Sources */, 2603139F20A0A4BA009AC66D /* ChatLanguageSettingsWireframe.swift in Sources */, - 8580BAE420BD99DD00239D9D /* UITextInput+Cursor.swift in Sources */, A4F3DAA32084935400FF71C7 /* Constants.swift in Sources */, E74FD69D1FC5D06200656611 /* TransactionObserverExtension.swift in Sources */, B7121EB8205045F300AABBE6 /* MediaDownloadManager.swift in Sources */, @@ -15908,16 +17197,21 @@ F10B0E1720B4401500528E7A /* GalleryPresenter.swift in Sources */, 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 */, 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 */, 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 */, 38182BD2C2E0C783796C8AA1 /* QRCodeReaderInteractor.swift in Sources */, 2603139520A0A4B9009AC66D /* LanguageSelectorWireframe.swift in Sources */, A42D51CF206A361400EEB952 /* Contact.swift in Sources */, @@ -15937,7 +17231,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 */, @@ -15963,6 +17256,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 */, @@ -15977,13 +17271,16 @@ FEA655FA2167777F00B44029 /* TransferDetailsPresenter.swift in Sources */, 4B87717B2195AF9D0014AD09 /* TypingHandlerDelegate.swift in Sources */, 8ED0F3CE1FBC5CF2004916AB /* GroupsListInteractor.swift in Sources */, + 3A0A50CC21B7FEFE0052D334 /* MyGroupAliasWireframe.swift in Sources */, 267BE2851FDE983400C47E18 /* SettingsGroupVC.swift in Sources */, 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 */, + 855A4E9E219B336000B6E90B /* AppBundleCredentials.swift in Sources */, E764628C1FCD67AC0091FC2E /* DBModelConvertible.swift in Sources */, 85D66A0620BD963C00FBD803 /* MessagePayloadRendererInput.swift in Sources */, 85D66A2420BD970400FBD803 /* BBCodeParser.swift in Sources */, @@ -16008,6 +17305,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 */, @@ -16024,6 +17322,8 @@ 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 */, 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */, @@ -16036,6 +17336,8 @@ 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 */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */, A42D51AC206A361400EEB952 /* Auth.swift in Sources */, @@ -16053,6 +17355,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 */, 4B030F2F2195BFF300F293B7 /* AuthHandler.swift in Sources */, 4B78E30421C3F7BB001B2F0E /* CallsItemsFactory.swift in Sources */, @@ -16067,15 +17370,19 @@ 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 */, 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 */, 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 */, @@ -16099,8 +17406,8 @@ A94B03A70E016BDA759B0703 /* EditProfileViewController.swift in Sources */, 8562853920D166E5000C9739 /* CollectionPreviewState.swift in Sources */, 4B7C73F2215A5509007924DB /* MotionManager.swift in Sources */, + F11786BB20A8A63F007A9A1B /* Coordinator.swift in Sources */, 4B030F322195CD4500F293B7 /* MQTTServiceDelegate.swift in Sources */, - F11786BB20A8A63F007A9A1B /* CoordinatorProtocol.swift in Sources */, F105C6BE20A1347E0091786A /* PhotoPreviewInteractor.swift in Sources */, F18AEAFD20C15792004FE01C /* SelectAvatarCoordinator.swift in Sources */, 85D66A1220BD965300FBD803 /* UserMentionTableViewCell.swift in Sources */, @@ -16109,27 +17416,28 @@ 4B030F3E2195D88100F293B7 /* UserInfoImpl.swift in Sources */, 85BA176120BEA7BD001EF8AC /* StickerPreviewContainerView.swift in Sources */, 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */, + 855A4E7F2199B4FE00B6E90B /* NynjaImageButton.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 */, 26C061C01FEAA04A00A2EBE4 /* FeatureExtension+BERT.swift in Sources */, 2651093F20ADB81100F1B38B /* NotificationSettingProtocol.swift in Sources */, 26ED2C1A2004276B002DBBE8 /* RepliesCollectionViewDelegate.swift in Sources */, - 4BBAEBCB21ADAB900089B703 /* GroupInputViewModel.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 */, 852E847121345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift in Sources */, A4330A6E2109EBA70060BD93 /* CountriesProvider.swift in Sources */, 26142B1320473BFD004E5FE4 /* DBMessageLink.swift in Sources */, - 4BBAEBC721ADAA470089B703 /* GroupInputProtocols.swift in Sources */, 68B66BDEEFD73CDC331AC840 /* EditProfilePresenter.swift in Sources */, + 853567BD21A6B76600AAEEF9 /* AnyFieldRowItem.swift in Sources */, 4B06D3082028A200003B275B /* WCItemsFactory.swift in Sources */, - C9C69505202230DD00A57297 /* SelectCountryCell.swift in Sources */, 2648C4102069B52100863614 /* ChangeNumberStep3Wireframe.swift in Sources */, E785F1551FF3DDC8006C52D9 /* GroupRulesViewControllerConstants.swift in Sources */, 853FB0692049B193000996C5 /* SupportInteractor.swift in Sources */, @@ -16144,25 +17452,31 @@ 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 */, F105C69C209F71BF0091786A /* CameraPresenter.swift in Sources */, 5BC1D37920D3B4A8002A44B3 /* GroupCollectionViewCell.swift in Sources */, + 3A0A50D921B7FEFE0052D334 /* GroupInputProtocols.swift in Sources */, 26610F5B2015476C00609F77 /* LocationFullWheelItemModel.swift in Sources */, 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 */, + 85086F4921C64D6D00194361 /* SearchContactResult.swift in Sources */, 2606F3BC20BFE20500CF7F15 /* MessageInteractor+Translation.swift in Sources */, ACD15567460FFE46A0AAF51E /* EditProfileInteractor.swift in Sources */, 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 */, @@ -16187,9 +17501,11 @@ 8580BAF020BD9AAE00239D9D /* ConstraintMaker+Extensions.swift in Sources */, 850A2BB0203584B000D68FDF /* SearchActionsView.swift in Sources */, 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */, + 850B9DAD219C7ADA00EA0CF4 /* PlainLoginOption.swift in Sources */, A4679B8920B2DA550021FE9C /* Array+ChannelSubscriber.swift in Sources */, A4ED79AC20C7056C00A41F67 /* AllChannelsItemsFactory.swift in Sources */, E707C4AF1FA0F6E700B86137 /* ProfileActionCell.swift in Sources */, + 3A0A50D321B7FEFE0052D334 /* CellWithImageCellModel.swift in Sources */, 4BFED75A21A6BE49003CF1B3 /* CLLocation+GPSMetadata.swift in Sources */, 4B877137219315770014AD09 /* QueryInterfaceRequestExtension.swift in Sources */, 2648C4172069B52100863614 /* ChangeNumberStep2Wireframe.swift in Sources */, @@ -16200,8 +17516,8 @@ 986BE2204D6D0813B13618B1 /* AddContactViaPhonePresenter.swift in Sources */, 265AEA171FE9AFD400AC4806 /* MemberModel.swift in Sources */, 2605311D21274116002E1CF1 /* LogOutputView.swift in Sources */, - 263529152075729400DC6FBD /* Job+DB.swift in Sources */, FEA655D02167777E00B44029 /* SeedVerificationWalletInteractor.swift in Sources */, + 3A0A50DC21B7FEFE0052D334 /* EditGroupNameWireframe.swift in Sources */, 8520040B20D4FB06007C0036 /* ReplyInfoView.swift in Sources */, E77D58991F98B94E00FBE926 /* ProfileTablewViewDS.swift in Sources */, E72906E72011156B007C5C5B /* UITableViewExtensions.swift in Sources */, @@ -16215,19 +17531,23 @@ 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 */, + 852BB8FB2194807500F2E8E4 /* GoogleAuthServiceUIDelegate.swift in Sources */, + 5EEB73C5216199ED00D8ECE6 /* AuthProtocols.swift in Sources */, 260313AA20A0A4BA009AC66D /* ChatLanguageSettingsViewController.swift in Sources */, 859B862F204820DC003272B2 /* ThemePickerInteractor.swift in Sources */, 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 */, 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 */, @@ -16236,10 +17556,11 @@ 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 */, + 852037ED21A5BD380085CF1F /* LoginOptionSwitchRowItemView.swift in Sources */, 4B749F05214FEE4F002F3A33 /* VerifyNumberPresenter.swift in Sources */, 263C04EB2132E56E00B8F0BE /* TranscribeOperation.swift in Sources */, A42CE5AD20692EDB000889CC /* StringAtom.swift in Sources */, @@ -16256,16 +17577,15 @@ 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 */, + 3A0E426021BFBE99001A3F3C /* SearchContactPresenter.swift in Sources */, A4CE80C020C9318700400713 /* EmptyStateTableViewDS.swift in Sources */, A458FAC920ECDB480075D55E /* Cloneable.swift in Sources */, E7598F571FA1CDB20082FBE7 /* ProfileActionModel.swift in Sources */, 8557987F2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift in Sources */, 5C468A609C445962C0D19DD3 /* HistoryViewController.swift in Sources */, - 854A4B2D2080D68200759152 /* CellWithArrowCellModel.swift in Sources */, FEA655E62167777E00B44029 /* SeedBackupWalletInputParams.swift in Sources */, C9405149204C7FAF00D72B04 /* DataAndStoragePresenter.swift in Sources */, 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */, @@ -16281,6 +17601,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 */, 2648C41C2069B52100863614 /* ChangeNumberStep2Interactor.swift in Sources */, 0DE4B40440737CF42D3E0204 /* HistoryWireframe.swift in Sources */, @@ -16289,10 +17610,13 @@ 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 */, 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 */, A45F113E20B4218D00F45004 /* MessageInteractor+Utils.swift in Sources */, @@ -16312,18 +17636,21 @@ 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 */, 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 */, B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */, A42D51CD206A361400EEB952 /* operation.swift in Sources */, 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, + 3A184D1D21C0FD8C0083D367 /* EmailValidator.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */, F10AFEBC20F7B1D200C7CE83 /* WheelPreviewFactory.swift in Sources */, @@ -16339,8 +17666,10 @@ 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 */, + 3A2CDAC321C9648800B5E397 /* DestructiveActionRowItem.swift in Sources */, F117871220ACF018007A9A1B /* CameraSettingsProtocols.swift in Sources */, 8503B526205046A6006F0593 /* NotificationSettingsViewController.swift in Sources */, 8580BAC820BD983400239D9D /* MentionCounterInteractive.swift in Sources */, @@ -16352,13 +17681,16 @@ 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 */, + 3AB7400421B9954100D1E967 /* ContactInfoManagementViewController.swift in Sources */, 26B5F7421FB0FF7B00CEC6AE /* FileManager.swift in Sources */, 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 */, @@ -16368,6 +17700,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 */, @@ -16378,6 +17711,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 */, @@ -16388,7 +17722,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 */, @@ -16420,6 +17753,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 */, @@ -16434,6 +17768,8 @@ FEA655CB2167777E00B44029 /* SeedVerificationWalletPresenter.swift in Sources */, 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 */, @@ -16442,6 +17778,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 */, @@ -16470,6 +17807,8 @@ 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 */, 264638291FFFE835002590E6 /* RepliesInteractor.swift in Sources */, @@ -16483,37 +17822,46 @@ 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 */, 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 */, + 3A06B08E21CB99E400E7964B /* ContactInfoSectionViewController.swift in Sources */, 26B32B961FE20BAB00888A0A /* DescExtension+BERT.swift in Sources */, 4B1D7E0B2029D8CD00703228 /* GroupOptionsItemsFactory.swift in Sources */, CCF8AA193F15D4191EC99051 /* SplashProtocols.swift in Sources */, 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 */, 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 */, + 3A0A50D821B7FEFE0052D334 /* GroupInputViewModel.swift in Sources */, 8528E50C2072724600A8644A /* StarDateConverter.swift in Sources */, - C9C695302023639C00A57297 /* SelectCountryViewControllerLayout.swift in Sources */, + 3A14D83821ABEC41009CD23A /* AuthProvider.swift in Sources */, E791178C1F978ACF00462D68 /* ImagePreviewViewControllerLayout.swift in Sources */, A43B25D820AB1EE400FF8107 /* NewChannelViewController.swift in Sources */, 850C301E204DA87A00DB26C2 /* PrivacyListInteractor.swift in Sources */, 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 */, + 3AAA92AE21B1A6C800EF5F1E /* AppCoordinator.swift in Sources */, 6B3D349607A18D5650BF47E6 /* SplashInteractor.swift in Sources */, 859B863720485F01003272B2 /* CarouselPickerViewController.swift in Sources */, 8566771E20C1579C00DD4204 /* StorageSubscriberReference.swift in Sources */, @@ -16525,12 +17873,14 @@ 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 */, 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 */, 4B2C503421B56BC200FBA9B1 /* MigrationsProvider.swift in Sources */, @@ -16541,6 +17891,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 */, @@ -16548,8 +17899,10 @@ 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 */, 26F47052201B7248005D3192 /* ReturnToCallView.swift in Sources */, A42D51C6206A361400EEB952 /* Tag.swift in Sources */, 8526187D20D05BF700824357 /* StickerGridPlaceholderCellModel.swift in Sources */, @@ -16586,7 +17939,6 @@ 859B8630204820DC003272B2 /* ThemePickerWireFrame.swift in Sources */, 263D66301FE8D20100A509F8 /* TypingExtension+BERT.swift in Sources */, F117872720ACF2DB007A9A1B /* CameraSourceFlow.swift in Sources */, - C90EE13E20246E2700FDB873 /* SelctCountryDelegate.swift in Sources */, 4BBAEBBF21AC6DF10089B703 /* ClosureValidator.swift in Sources */, 0062D9422062EC4100B915AC /* InviteFriendsSelectionViewModel.swift in Sources */, A4679BA520B2DD0F0021FE9C /* SubscribersSelectorPresenter.swift in Sources */, @@ -16594,10 +17946,12 @@ A43B25A620AB1DFA00FF8107 /* RecordingAudioWaveform.swift in Sources */, 26C1A3EB2031AAD20009F7F0 /* OtherUserInteractor.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 */, 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 */, @@ -16614,6 +17968,7 @@ 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 */, @@ -16621,6 +17976,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 */, @@ -16628,6 +17984,7 @@ 265D5F44203F0CE600FFB513 /* ScheduleItemProtocol.swift in Sources */, E73315F01FB0B60E00C273FF /* ContainerViewController.swift in Sources */, 0062D93E2062EC4100B915AC /* InviteFriendsProtocols.swift in Sources */, + 3A0A50D121B7FEFE0052D334 /* CellWithArrowCellModel.swift in Sources */, FEA655E22167777E00B44029 /* SeedBackupWalletWireFrame.swift in Sources */, A43B25D920AB1EE400FF8107 /* NewChannelViewControllerLayout.swift in Sources */, E485DED76CE7933E643691D6 /* VideoPreviewInteractor.swift in Sources */, @@ -16635,6 +17992,7 @@ 4B8996CF204ED33D00DCB183 /* StarDAO.swift in Sources */, 26DAE5D41FFAF91100EDF412 /* DefaultBackgroundModeService.swift in Sources */, 5E278E14F45F56BACB71271C /* VideoPreviewWireframe.swift in Sources */, + 3A0A50D521B7FEFE0052D334 /* CellWithImage.swift in Sources */, A45F111820B4218D00F45004 /* TimeCell.swift in Sources */, C8C6310F83825D7385C3A6E4 /* MapProtocols.swift in Sources */, A44B4D5320CE9BDF00CA700A /* SettingViewModelProtocol.swift in Sources */, @@ -16655,6 +18013,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 */, @@ -16688,6 +18047,7 @@ 85D66A1020BD965300FBD803 /* MentionPanelView.swift in Sources */, 4B7C73F1215A5509007924DB /* SMSCodeProviding.swift in Sources */, 45F60C4B14438C65076457AB /* EditUsernameProtocols.swift in Sources */, + 3A184D1B21C0FD800083D367 /* UsernameValidator.swift in Sources */, 00F7B33E2029DD4B00E443E1 /* AudioItemView.swift in Sources */, 4B7C73F9215A5522007924DB /* DebugLogs.swift in Sources */, 0062D9412062EC4100B915AC /* InviteFriendsSelectionCell.swift in Sources */, @@ -16714,7 +18074,6 @@ 9BB33F3E2146A14B009FB252 /* HoldToSpeakView.swift in Sources */, 850C0B5420E0369E003341D0 /* ChatListMessageCellModelDelegate.swift in Sources */, A432CF1220B4347D00993AFB /* InputInfoProvider.swift in Sources */, - A43B25A320AB1DFA00FF8107 /* CountryModel.swift in Sources */, A43B25A820AB1DFA00FF8107 /* ALTextInputBarDelegate.swift in Sources */, FEA655F92167777F00B44029 /* PaymentModel.swift in Sources */, A42D51C8206A361400EEB952 /* iterator.swift in Sources */, @@ -16729,13 +18088,18 @@ 4BEF0F6C21C290A10012B6E1 /* FavoritesP2pItemsFactory.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 */, 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 */, 2910A0129CA29C35161DD692 /* EditPhotoInteractor.swift in Sources */, 705B483A1FCDEA2273CEFE2C /* EditPhotoWireframe.swift in Sources */, - A43B25A420AB1DFA00FF8107 /* TextField.swift in Sources */, + 5EEB73D82162227B00D8ECE6 /* PhoneNumberLoginView.swift in Sources */, E743B58F1FB0A32700F72F92 /* ParticipantsHeaderView.swift in Sources */, A4679BAA20B2DD100021FE9C /* SubscribersTableDataSource.swift in Sources */, 4B2C503021B56AE900FBA9B1 /* CorrectMessageIdTypeInStarTable.swift in Sources */, @@ -16744,6 +18108,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 */, @@ -16752,6 +18117,7 @@ 6C25C4720B043D98729C02C8 /* TopUpAccountPresenter.swift in Sources */, A42D51CE206A361400EEB952 /* Search.swift in Sources */, B77C11DB2109242200CCB42E /* AssigningInterpreterPresenter.swift in Sources */, + 3A0A50D021B7FEFE0052D334 /* CreateGroupProtocols.swift in Sources */, 26342CB220ECDDC400D2196B /* Encodable+Dictionary.swift in Sources */, 3CDA490701EC3FEAAC2E9AFE /* TopUpAccountInteractor.swift in Sources */, 26B32B691FE1715500888A0A /* WeakRef.swift in Sources */, @@ -16769,12 +18135,12 @@ 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 */, 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 */, @@ -16794,18 +18160,16 @@ 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 */, B745F2E42109B9E500488A91 /* LanguagePickerDelegate.swift in Sources */, - 990A25B2C84CE09B4CE64533 /* MyGroupAliasPresenter.swift in Sources */, 4BE2C5E82142EB5A00A73DD9 /* NynjaRingingService.swift in Sources */, 4B06D3202028A9B1003B275B /* P2pChatItemsFactory.swift in Sources */, A432CF1820B4347D00993AFB /* MaterialTextField.swift in Sources */, 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 */, @@ -16813,25 +18177,26 @@ 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 */, A44B4D5720CE9BDF00CA700A /* AvatarCellViewModel.swift in Sources */, A4F3DAA9208493C100FF71C7 /* StorageService.swift in Sources */, 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 */, 4BC95A7721C0077100B462AC /* LocalizableConstants.swift in Sources */, 8ED0F3D11FBC5CF2004916AB /* GroupsListViewController.swift in Sources */, - BBF46945EB64E07C58817ACA /* EditGroupNameProtocols.swift in Sources */, 8520040020D466CE007C0036 /* StickerPack_Spec.swift in Sources */, - 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */, - 43F333D298934DCBAC8D8192 /* EditGroupNamePresenter.swift in Sources */, + 3A0AEA7521B028120066CBBA /* AccountTable.swift in Sources */, A45F112C20B4218D00F45004 /* MessageBaseImageView.swift in Sources */, 8524C4D92177741A003BF374 /* Service+Construct.swift in Sources */, E77764BD1FBDA9B60042541D /* ImageFullWheelItemView.swift in Sources */, B723C627204D86AF00884FFD /* SettingsDataAndStorageProtocols.swift in Sources */, + 855A4E9B219B321000B6E90B /* AuthServiceImpl.swift in Sources */, 260313A120A0A4BA009AC66D /* DirectableActionCell.swift in Sources */, + 3A14D83B21AC03A3009CD23A /* SearchAvailabilityView.swift in Sources */, 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */, 263A60AE1FB51C22006F9D52 /* MemberExtension.swift in Sources */, E709383F1FBEE41D006CCDC6 /* Describable.swift in Sources */, @@ -16840,7 +18205,6 @@ 2648C40A2069B52100863614 /* ChangeNumberStep1Presenter.swift in Sources */, F11786CA20A8E4FD007A9A1B /* CameraVideoPreviewPresenter.swift in Sources */, A43B259C20AB1DFA00FF8107 /* NynjaSearchField.swift in Sources */, - 6E7CD38810BC3B896070C819 /* EditGroupNameWireframe.swift in Sources */, 4B8996EA204EF34000DCB183 /* MessageActionDAOProtocol.swift in Sources */, 731181233D84FD4F41936981 /* EditGroupPhotoProtocols.swift in Sources */, 855798802093200D007050B8 /* StickerMenuActionCellModel.swift in Sources */, @@ -16861,30 +18225,32 @@ 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 */, 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 */, - 99B9D27D2F0EFE051E6581ED /* CreateGroupProtocols.swift in Sources */, + 3A0E426121BFBE99001A3F3C /* SearchContactViewController.swift in Sources */, A4213AF320D9240100B6BE7D /* PHFetchOptions+Utils.swift in Sources */, - A438DB9220763AFB00AA86A2 /* Contact+Desc.swift in Sources */, 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 */, A42D52C9206A53AB00EEB952 /* operation_Spec.swift in Sources */, A42D51B7206A361400EEB952 /* push.swift in Sources */, 4B7C73FA215A5522007924DB /* UIView+Debug.swift in Sources */, E7C36C3B1FC46B4300740630 /* FeatureExtension.swift in Sources */, B723C630204D9E1500884FFD /* PickableEnum.swift in Sources */, A432CF1F20B44C0000993AFB /* MaterialTextContainer.swift in Sources */, - 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */, + 859ECA6A21A43FE4003630A0 /* AccountService.swift in Sources */, + 852BB8D22194256600F2E8E4 /* FacebookAuthWireframe.swift in Sources */, A43B259520AB1DFA00FF8107 /* InputContentProtocol.swift in Sources */, 8509FC89215908B300734D93 /* AppGroupFlagObserver.swift in Sources */, 2625DBF620EFC52E00E01C05 /* AudioFileConvertOperation.swift in Sources */, @@ -16895,12 +18261,10 @@ 4B3B35D9217119BF005A214A /* AmazonInitializerImpl.swift in Sources */, B7B546AE210D9C8C002DCA55 /* CircleView.swift in Sources */, E79061B81FBF2243009FD83A /* FeatureTable.swift in Sources */, - 5B5EE777EF301CFC1FDCF307 /* CreateGroupInteractor.swift in Sources */, 8580BAD820BD98E700239D9D /* CounterView.swift in Sources */, 26A421CD217E027600120542 /* SnackBar.swift in Sources */, A4E6EDBB20724AAA004B456C /* BadgeNumberServiceProtocol.swift in Sources */, A415132820DBE40F00C2C01F /* LinkTable.swift in Sources */, - C02DD71CA3832908D422B83C /* CreateGroupWireframe.swift in Sources */, A45F110E20B4218D00F45004 /* PositionType.swift in Sources */, 26EEA5472091F84E0066D3B0 /* CollectionsExtensions.swift in Sources */, 1CCEA1165C5D016C6768E5DC /* GroupRulesProtocols.swift in Sources */, @@ -16908,6 +18272,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 */, @@ -16915,6 +18280,7 @@ 4B055C3B219C4101001FE077 /* MQTTServiceProtocol.swift in Sources */, 2F2A5C12A7202E7834F923DC /* GroupRulesWireframe.swift in Sources */, 2625DBF820EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift in Sources */, + 851C6A54218B560B0062B148 /* MQTTFactoryProtocol.swift in Sources */, D3A30AF05BD7C46A9A8C1FC1 /* GroupStorageProtocols.swift in Sources */, 8520040720D4F436007C0036 /* StickerPreviewConfig.swift in Sources */, F1607B1D20B20F7800BDF60A /* GridView.swift in Sources */, @@ -16928,24 +18294,22 @@ C921738220BADAFC00519A2D /* TextInputValidationService.swift in Sources */, 9763CCDFE5AF7B58C21CDED9 /* GroupStoragePresenter.swift in Sources */, 16A903BE16E0899FD3E5D232 /* GroupStorageInteractor.swift in Sources */, + 3A0A50E321B8198E0052D334 /* JobExtension.swift in Sources */, 85D66A0820BD963C00FBD803 /* MentionInputFilter.swift in Sources */, 8514F17A20EA219F00883513 /* ContextMenuArrowView.swift in Sources */, B3D0F59E1E7BDB7E485AE662 /* GroupStorageWireframe.swift in Sources */, A45F114120B4218D00F45004 /* MessageInteractor+StorageSubscriber.swift in Sources */, - FEA59F90B93C7B49BAF99F9C /* SelectCountryProtocols.swift in Sources */, + 5E7D5D3D218C59F1009B5D8D /* AccountStatus.swift in Sources */, 26ABCA3E21189DA400EA4782 /* Aps.swift in Sources */, 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */, 260313AB20A0A4BA009AC66D /* ChatLanguageSettingsInteractor.swift in Sources */, 4B8288A621B2F39100EEA8A7 /* FeedCreator.swift in Sources */, - 7C51CDC1260CE191C07EE46C /* SelectCountryViewController.swift in Sources */, 8596CEF22048A763006FC65D /* ThemeCellModel.swift in Sources */, 4B030F3B2195CF8100F293B7 /* Host.swift in Sources */, 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */, - A1AD6864F4F49D9FC8997D59 /* SelectCountryPresenter.swift in Sources */, - 32E5A25AD25BF752EB3864AB /* SelectCountryInteractor.swift in Sources */, + 3A4D098621DCCCDA00103E95 /* CreateProfileViewModel.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 */, @@ -16957,8 +18321,12 @@ 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 */, + 3A2C2DFC21C26708006A53BB /* SearchContactResponse.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 */, 26053123212741C2002E1CF1 /* LogOutputWireFrame.swift in Sources */, @@ -16967,7 +18335,9 @@ 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 */, + 3A0AEA7121B018380066CBBA /* DBAccount.swift in Sources */, A45F113920B4218D00F45004 /* AvatarViewLayout.swift in Sources */, B16EC832C763628A2EBBD383 /* MapSearchProtocols.swift in Sources */, A43B25A720AB1DFA00FF8107 /* ALKeyboardObservingView.swift in Sources */, @@ -16981,6 +18351,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 */, 4B2C503221B56B2300FBA9B1 /* RemoveRoomMemberTable.swift in Sources */, 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */, @@ -16990,6 +18361,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 */, @@ -17021,6 +18393,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 */, @@ -17037,9 +18410,10 @@ 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 */, + 851452AB21A586E900DF10A6 /* LoginOptionsViewController.swift in Sources */, F922EF38E4C1662D54CE533D /* LanguageSettingsWireframe.swift in Sources */, 26A421CF217E541800120542 /* SnackBarLayoutGuide.swift in Sources */, E8AFC57E49EED25C3F5001B7 /* ActiveSessionsProtocols.swift in Sources */, @@ -17049,9 +18423,11 @@ 4BF090C521635E8600DCCA5C /* Message+LinkedId.swift in Sources */, C6B308C6734EFB77892832A0 /* ActiveSessionsPresenter.swift in Sources */, A42D52B4206A53AA00EEB952 /* ok_Spec.swift in Sources */, + 851452B921A5A91E00DF10A6 /* FieldRowItem.swift in Sources */, B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */, 8E54E93EA25B11D417A6100E /* ActiveSessionsInteractor.swift in Sources */, A43B259420AB1DFA00FF8107 /* InputBar.swift in Sources */, + 3A0A50D221B7FEFE0052D334 /* CellWithArrowTableViewCell.swift in Sources */, F11DF06820BD996200F3E005 /* NavigationProtocol.swift in Sources */, 855C9FE62125B4C0000E3429 /* MessageHandlerSubscriber.swift in Sources */, 2CB54DD94DA23D7160F36472 /* ActiveSessionsWireframe.swift in Sources */, @@ -17081,7 +18457,6 @@ A4CB153B21039C1100C3B68B /* JailbreakDetectorProtocol.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 */, @@ -17114,8 +18489,8 @@ 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 */, + 850B9DA1219C1E8A00EA0CF4 /* SessionStorage.swift in Sources */, A4AB8E532105EC4B005F9B0C /* TextView.swift in Sources */, FB816EFA20B5B8B000093DCD /* Member.swift in Sources */, 4B8C05902164A9AA0034D8F3 /* ReversableDataSourceTests.swift in Sources */, @@ -17310,9 +18685,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)"; @@ -17695,9 +19082,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)"; @@ -17729,9 +19167,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"; @@ -17795,6 +19245,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; @@ -17816,9 +19267,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)"; @@ -17997,9 +19460,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)"; @@ -18265,9 +19740,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)"; @@ -18447,9 +19934,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"; @@ -18642,9 +20141,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)"; @@ -18668,7 +20179,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/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index 93abd7815e6f754497f983cdc29deea2ca760728..4fcd7aa6232079e4adf4f166c0de3214d40d722b 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -11,11 +11,13 @@ import Crashlytics import Fabric import GoogleMaps import GooglePlaces +import GoogleSignIn import AWSCore import AWSS3 import UserNotifications import Firebase import Intercom +import NynjaUIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { @@ -28,15 +30,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD imgView.contentMode = .scaleAspectFill return imgView }() + + private let serviceFactory = ServiceFactory() + + private let storageService = StorageService.sharedInstance private let antiDebuggingService = AntiDebuggingService() - private var storageService: StorageService { - return .sharedInstance - } + private(set) var appCoordinator: AppCoordinatorInput! - // FIXME: need to be removed from here when share extension won't require new mqtt connection. - private var appGroupObserver: AppGroupFlagObserver? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { UNUserNotificationCenter.current().delegate = self @@ -47,10 +49,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD wipeKeychain() + configureAppearance() + configureWindow() LogService.log(topic: .system) { return "Avaliable logs:\n\(LogServiceTopic.allValuesStrings)" } MotionManager.shared.startAccelerometers() + return true } @@ -64,6 +69,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return NynjaJoinByLinkService.shared().handleJoin(by: url) } + 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]) + } + func applicationWillResignActive(_ application: UIApplication) { window?.addSubview(backgroundImageView) } @@ -87,22 +98,35 @@ 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 - self.window = UIWindow(frame: UIScreen.main.bounds) let navigation = UINavigationController() navigation.isNavigationBarHidden = true - SplashWireFrame().presentSplash(navigation: navigation) - self.window?.rootViewController = navigation - self.window?.makeKeyAndVisible() + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = navigation + window?.makeKeyAndVisible() + + appCoordinator = AppCoordinator(navigation: navigation, serviceFactory: serviceFactory) + appCoordinator.start() } - private func configureDependencies() { + func configureDependencies() { + setupNynjaSDK() setupTestFairy() setupCrashlytics() setupGoogleMaps() + setupGoogleSignIn() setupAmazon() setupIntercom() try? AudioSessionManager.shared.configureDefaultSession() @@ -113,7 +137,7 @@ private extension AppDelegate { FileManagerService.sharedInstance.createDirectory(dirName: Constants.Folders.downloads) } - private func wipeKeychain() { + func wipeKeychain() { if !storageService.wasRun { LogService.log(topic: .db) { return "Clear storage: AppDelegate - if it is first runs" } storageService.wipeKeychain() @@ -125,9 +149,20 @@ private extension AppDelegate { // MARK: - Setup third party services 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) } @@ -141,12 +176,20 @@ private extension AppDelegate { GMSServices.provideAPIKey(key) GMSPlacesClient.provideAPIKey(key) } + + func setupGoogleSignIn() { + let clientId = ThirdPartyServicesFactory.googleSignIn.serviceConfig.clientId + let serverClientId = ThirdPartyServicesFactory.googleSignIn.serviceConfig.serverClientId + + GIDSignIn.sharedInstance().clientID = clientId + GIDSignIn.sharedInstance().serverClientID = serverClientId + } func setupAmazon() { ServiceFactory().makeAmazonInitializer().initialize() } - private func setupIntercom() { + func setupIntercom() { let intercomServiceConfig = ThirdPartyServicesFactory.intercom.serviceConfig Intercom.setApiKey(intercomServiceConfig.apiKey, forAppId: intercomServiceConfig.appId) } diff --git a/Nynja/Auth/GoogleAuthError.swift b/Nynja/Auth/GoogleAuthError.swift new file mode 100644 index 0000000000000000000000000000000000000000..7f24924e9dbffc9b4ea5c396cdc7e36e0d087f49 --- /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 0000000000000000000000000000000000000000..14993dd601477c39f625f5378708d78f548bbdea --- /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 0000000000000000000000000000000000000000..e07dfae9e12d1740aae013cac697c4ddf932967a --- /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/BadgeNumberService.swift b/Nynja/BadgeNumberService.swift index 639f1f0b75c53074be99a4ae0e6c7696b3a8170e..f85950699fcc554672ea4ba59e01529fc9625b85 100644 --- a/Nynja/BadgeNumberService.swift +++ b/Nynja/BadgeNumberService.swift @@ -59,7 +59,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 f7021e3e7c16351117574f5b2eb6a7ab14428396..37a9b5dc8f435fc37d86f1eb914f7fa49af16e18 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/ChatService/ContactFinder.swift b/Nynja/ChatService/ContactFinder.swift index 04269f278813d3a342f892ba12c633a3026539e7..af60e972d509a2dc5a4e94a655594eec7b4c877f 100644 --- a/Nynja/ChatService/ContactFinder.swift +++ b/Nynja/ChatService/ContactFinder.swift @@ -15,7 +15,7 @@ final class ContactFinder: InitializeInjectable { init(dependencies: Dependencies) { mqttService = dependencies.mqttService - IoHandler.delegate = self + IoHandler.shared.delegate = self } @@ -35,7 +35,7 @@ final class ContactFinder: InitializeInjectable { } func updateSubscribes() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } } diff --git a/Nynja/ContactsItemsFactory.swift b/Nynja/ContactsItemsFactory.swift index ae42d24a0cf1335042fe0e223e8ccd98f59ba5c9..893f476ca19af887e2ad7c6b7904c69aec1f0692 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/Coordinators/CoordinatorProtocol.swift b/Nynja/Coordinators/Coordinator.swift similarity index 73% rename from Nynja/Coordinators/CoordinatorProtocol.swift rename to Nynja/Coordinators/Coordinator.swift index 9de25563f01e3ea2f6fa3853ca81b61a52e4a05a..0600b49b7adb31d1d532d3a23d4bf92c523b0e5f 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/Coordinators/NavigationError.swift b/Nynja/Coordinators/NavigationError.swift new file mode 100644 index 0000000000000000000000000000000000000000..4eb4b567f123177ac9d1b0e0cb7cbf41a320736e --- /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/CountriesProvider.swift b/Nynja/CountriesProvider.swift index e00258d91f1534e92b76721324904e1c7a101114..25a529748991a8caba3d55842817e6b12cc09299 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,8 +18,19 @@ 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 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 + 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 72e3338030ccbf9a04d06a5ed0de4acc438203a8..b2fff14791ecfc46e3015ba83742a1a5bccb0139 100644 --- a/Nynja/CountriesProviding.swift +++ b/Nynja/CountriesProviding.swift @@ -6,6 +6,14 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol CountriesProviding { - func fetchCountries() -> [CountryModel] +protocol CountrySearchProviding { + func fetchCountry(by countryCode: String) -> Country? +} + +protocol LocalCountryProviding { + func fetchDefaultCountry() -> Country +} + +protocol CountriesProviding: CountrySearchProviding, LocalCountryProviding { + func fetchCountries() -> [Country] } diff --git a/Nynja/DB/Models/DBAccount.swift b/Nynja/DB/Models/DBAccount.swift new file mode 100644 index 0000000000000000000000000000000000000000..a9e816c28604599f00e8a0941ad0c0a5f99c5f05 --- /dev/null +++ b/Nynja/DB/Models/DBAccount.swift @@ -0,0 +1,170 @@ +// +// 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? { + 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) + 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) + return try DBAccount.filter(profileIdColumn == profileId).fetchAllConstructed(db) + } +} diff --git a/Nynja/DB/Models/DBContactInfo.swift b/Nynja/DB/Models/DBContactInfo.swift new file mode 100644 index 0000000000000000000000000000000000000000..48cd60ca2d666bac17ca1882faec273a618523ae --- /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/DBMember.swift b/Nynja/DB/Models/DBMember.swift index b8c7bb876e2c986c1f7c506f5d083358ed88a2b0..7f745fda972085b942616e978ee866763f2c47b4 100644 --- a/Nynja/DB/Models/DBMember.swift +++ b/Nynja/DB/Models/DBMember.swift @@ -10,20 +10,20 @@ import GRDBCipher final class DBMember: Record, DBModel { - var id: Int64 - var roomId: String? - var container: String? - var prev: Int64? - var next: Int64? - var phoneId: String - var avatar: String? - var names: String - var surnames: String? - var alias: String - var reader: Int64 - var update: Int64 - var presence: String? - var status: String? + let id: Int64 + private(set) var roomId: String? + let container: String? + let prev: Int64? + let next: Int64? + let phoneId: String + let avatar: String? + let names: String + let surnames: String? + let alias: String + let reader: Int64 + let update: Int64 + let presence: String? + let status: String? var features: [DBFeature] = [] var services: [DBService] = [] @@ -70,7 +70,9 @@ final class DBMember: Record, DBModel { super.init() } + // MARK: - Record + override static var databaseTableName: String { return MemberTable.name } @@ -111,7 +113,9 @@ final class DBMember: Record, DBModel { container[Column.status] = status } - // MARK: + + // MARK: - DBModel + func saveAggregate(_ db: Database) throws { if roomId == nil { roomId = try DBMember @@ -126,20 +130,32 @@ final class DBMember: Record, DBModel { try services.forEach { try $0.save(db) } } - static func member(from db: Database, rowId: Int64) throws -> DBMember? { - guard let member = try DBMember.filter(Column.rowID == rowId).fetchOne(db) else { - return nil - } + func construct(_ db: Database) throws { + let memberId = String(id) + self.features = (try? DBFeature.request(targetId: memberId, targetType: .member).fetchAll(db)) ?? [] + self.services = (try? DBService.request(targetId: memberId, targetType: .member).fetchAll(db)) ?? [] + } + + @discardableResult + func deleteAggregate(_ db: Database) throws -> Bool { + let memberId = String(id) + try DBFeature.deleteAll(db, targetId: memberId, targetType: .member) + try? DBService.deleteAll(db, targetId: memberId, targetType: .member) - try member.construct(db) - return member + return try self.delete(db) + } + + + // MARK: - Query + + static func member(from db: Database, rowId: Int64) throws -> DBMember? { + return try DBMember.filter(Column.rowID == rowId).fetchOneConstructed(db) } static func member(from db: Database, id: Int64) throws -> DBMember? { guard let member = try DBMember.fetchOne(db, key: id) else { return nil } - try member.construct(db) return member } @@ -168,18 +184,11 @@ final class DBMember: Record, DBModel { .fetchAllConstructed(db) } - func construct(_ db: Database) throws { - let memberId = "\(self.id)" - self.features = (try? DBFeature.request(targetId: memberId, targetType: .member).fetchAll(db)) ?? [] - self.services = (try? DBService.request(targetId: memberId, targetType: .member).fetchAll(db)) ?? [] - } - - @discardableResult - func deleteAggregate(_ db: Database) throws -> Bool { - let memberId = "\(self.id)" - try DBFeature.deleteAll(db, targetId: memberId, targetType: .member) - try? DBService.deleteAll(db, targetId: memberId, targetType: .member) - - return try self.delete(db) + static func memberAlias(from db: Database, roomId: String, phoneId: String) throws -> String? { + return try DBMember + .filter(Column.roomId == roomId) + .filter(Column.phoneId == phoneId) + .fetchOne(db)? + .alias } } diff --git a/Nynja/DB/Models/DBProfile.swift b/Nynja/DB/Models/DBProfile.swift index 5c89cf9ffb064066b87d090ffc1b3e0d8d55043f..46c08fe5b49327b76206b912e540059de550f37f 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 0000000000000000000000000000000000000000..5f7843b25d22b9e9a1e95aeffd28d4976f7e1340 --- /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 0000000000000000000000000000000000000000..975d7d6fa33fb30b72c22cff80630751a72250d8 --- /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/DBObserver.swift b/Nynja/DBObserver.swift index 280fa9d6faadf53635f494b444898ff4fe7cc687..0556c007adcd0b2a1ed0f21fd2ecba04a38391b2 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/DatabaseManager.swift b/Nynja/DatabaseManager.swift index c803157bb477ad0e8d8644d26d111211d0af4d84..ad23574f73509bfce10c87978b69a1764119dbf4 100644 --- a/Nynja/DatabaseManager.swift +++ b/Nynja/DatabaseManager.swift @@ -243,7 +243,10 @@ extension DatabaseManager { StickerPackTable.self, ConvertMessageTable.self, - StarActionTable.self + StarActionTable.self, + + AccountTable.self, + ContactInfoTable.self ] } diff --git a/Nynja/ExtendedStarHandler.swift b/Nynja/ExtendedStarHandler.swift index 184b5d6a093c462ff1dd25e627971968f3473d87..593df5997d00ca33bc08c9f7fa6f4bebd342917c 100644 --- a/Nynja/ExtendedStarHandler.swift +++ b/Nynja/ExtendedStarHandler.swift @@ -10,11 +10,21 @@ import Foundation final class ExtendedStarHandler: BaseHandler { - private static var storageService: StorageService { - return .sharedInstance - } + // MARK: - Dependencies + + private let storageService = StorageService.sharedInstance + + + // MARK: - Singleton + + static let shared = ExtendedStarHandler() + + private init() {} + + + // MARK: - Handler - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { let extendedStars = data.elements.compactMap { get_ExtendedStar().parse(bert: $0) as? ExtendedStar } let stars = extendedStars.compactMap { extendedStar -> DBStar? in diff --git a/Nynja/Extensions/Bundle+Keys.swift b/Nynja/Extensions/Bundle+Keys.swift index e3ea530efbdc4834eb348fce67d3c56830c66486..c7e77805a208c11ff7d921e16343cc11254e0efe 100644 --- a/Nynja/Extensions/Bundle+Keys.swift +++ b/Nynja/Extensions/Bundle+Keys.swift @@ -75,4 +75,20 @@ extension Bundle { var associatedDomain: String { return object(forInfoDictionaryKey: "AssociatedDomain") as! String } + + var googleClientId: String { + return object(forInfoDictionaryKey: "GoogleClientId") as! String + } + + 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/Extensions/CollectionsExtensions.swift b/Nynja/Extensions/CollectionsExtensions.swift index 038ceac38d56311e1939ce0a11167f5f5cb88069..3a6d3d0160329f126b3fc3e97ae12759ba456b90 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/Models/AccountExtension.swift b/Nynja/Extensions/Models/AccountExtension.swift new file mode 100644 index 0000000000000000000000000000000000000000..fad0c3c1c12a79eaf92a48a872be795211f6873d --- /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 6b633fe577195c740e822460c3d989e9fd70648f..5062ea4e2ac2a42c5d03589f3f41eb27a0da8093 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/Extensions/Models/StarExtension.swift b/Nynja/Extensions/Models/StarExtension.swift index 3317259a779047b308ea3e1285187b8003787896..11b196beb479a5352673ce670ff0d948f73196e9 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/Extensions/SortableObject/Array+SortableObject.swift b/Nynja/Extensions/SortableObject/Array+SortableObject.swift deleted file mode 100644 index c731576d006ddf1e4396e2f75725112039419d6d..0000000000000000000000000000000000000000 --- 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 aa9eb96ce368fb3e0890ccd478cee96d20037d7b..0000000000000000000000000000000000000000 --- 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/Extensions/SwiftLibrary/Array/Array+Grouped.swift b/Nynja/Extensions/SwiftLibrary/Array/Array+Grouped.swift index b7ed6e2c7777f841658de508966b5604e8021c46..2f71029b0f471126ea1fa5a1c4c5c12359ef4d95 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/Extensions/SwiftLibrary/Array/ArrayExtension.swift b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift index 913a8beb614e73655ac14edc9291e7262d8404b5..85240c83e68cf5f84ee730c94ff4522f94dce202 100644 --- a/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift +++ b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift @@ -54,3 +54,34 @@ extension Array { return temp } } + +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/Generated/AssetsConstants.swift b/Nynja/Generated/AssetsConstants.swift index a69f5cd74ad4e24c8f55efe8d5279c197503364f..5585cf2b4fae425e86123955b29718907af915f6 100644 --- a/Nynja/Generated/AssetsConstants.swift +++ b/Nynja/Generated/AssetsConstants.swift @@ -104,6 +104,10 @@ internal extension Image { /// "contact_separatop" static var contactSeparatop: ImageAsset { return ImageAsset(name: "contact_separatop") } } + /// "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") } @@ -126,12 +130,20 @@ internal extension Image { static var group1: ImageAsset { return ImageAsset(name: "Group_1") } /// "Group_2" static var group2: ImageAsset { return ImageAsset(name: "Group_2") } + /// "Icons_General_ic_accept_call" + static var iconsGeneralIcAcceptCall: ImageAsset { return ImageAsset(name: "Icons_General_ic_accept_call") } /// "Icons_General_ic_close" static var iconsGeneralIcClose: ImageAsset { return ImageAsset(name: "Icons_General_ic_close") } + /// "Icons_General_ic_email" + static var iconsGeneralIcEmail: ImageAsset { return ImageAsset(name: "Icons_General_ic_email") } /// "Icons_General_ic_eye" static var iconsGeneralIcEye: ImageAsset { return ImageAsset(name: "Icons_General_ic_eye") } + /// "Icons_General_ic_google" + 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 { @@ -247,8 +259,8 @@ internal extension Image { static var newContact: ImageAsset { return ImageAsset(name: "new_contact") } /// "new_group" static var newGroup: ImageAsset { return ImageAsset(name: "new_group") } - /// "next_bttn" - static var nextBttn: ImageAsset { return ImageAsset(name: "next_bttn") } + /// "next_bttn1" + static var nextBttn1: ImageAsset { return ImageAsset(name: "next_bttn1") } /// "search" static var search: ImageAsset { return ImageAsset(name: "search") } /// "send" @@ -431,8 +443,6 @@ internal extension Image { static var messageBttn: ImageAsset { return ImageAsset(name: "message-bttn") } /// "microphone-bttn" static var microphoneBttn: ImageAsset { return ImageAsset(name: "microphone-bttn") } - /// "next-bttn" - static var nextBttn: ImageAsset { return ImageAsset(name: "next-bttn") } /// "outgoing_dark" static var outgoingDark: ImageAsset { return ImageAsset(name: "outgoing_dark") } /// "outgoing_light" @@ -443,8 +453,8 @@ internal extension Image { static var outgoingVideoLight: ImageAsset { return ImageAsset(name: "outgoing_video_light") } /// "phone-number" static var phoneNumber: ImageAsset { return ImageAsset(name: "phone-number") } - /// "qr-code" - static var qrCode: ImageAsset { return ImageAsset(name: "qr-code") } + /// "qr-code1" + static var qrCode1: ImageAsset { return ImageAsset(name: "qr-code1") } /// "rec-bar" static var recBar: ImageAsset { return ImageAsset(name: "rec-bar") } /// "rec-process" @@ -626,12 +636,6 @@ internal extension Image { static var wheelRightImage: ImageAsset { return ImageAsset(name: "wheel_right_image") } } } - enum WheelPosition { - /// "wheel_left_image" - static var wheelLeftImage: ImageAsset { return ImageAsset(name: "wheel_left_image") } - /// "wheel_right_image" - static var wheelRightImage: ImageAsset { return ImageAsset(name: "wheel_right_image") } - } /// "arrow_collapse" static var arrowCollapse: ImageAsset { return ImageAsset(name: "arrow_collapse") } /// "arrow_expand" @@ -670,6 +674,8 @@ internal extension Image { static var emojiWhite: ImageAsset { return ImageAsset(name: "emoji_white") } /// "frame" static var frame: ImageAsset { return ImageAsset(name: "frame") } + /// "ic_add" + static var icAdd: ImageAsset { return ImageAsset(name: "ic_add") } /// "ic_add_participants" static var icAddParticipants: ImageAsset { return ImageAsset(name: "ic_add_participants") } /// "ic_add_photo_placeholder" @@ -686,8 +692,6 @@ internal extension Image { static var icBackNavigation: ImageAsset { return ImageAsset(name: "ic_back_navigation") } /// "ic_bottom_arrow" static var icBottomArrow: ImageAsset { return ImageAsset(name: "ic_bottom_arrow") } - /// "ic_camera_frame" - static var icCameraFrame: ImageAsset { return ImageAsset(name: "ic_camera_frame") } /// "ic_change_camera_ios" static var icChangeCameraIos: ImageAsset { return ImageAsset(name: "ic_change_camera_ios") } /// "ic_checkmark_red" @@ -708,6 +712,12 @@ internal extension Image { static var icEditDone: ImageAsset { return ImageAsset(name: "ic_edit_done") } /// "ic_email_storage" static var icEmailStorage: ImageAsset { return ImageAsset(name: "ic_email_storage") } + /// "ic_empty_avatar" + 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" @@ -740,10 +750,10 @@ internal extension Image { static var icMicDarkGray: ImageAsset { return ImageAsset(name: "ic_mic_dark_gray") } /// "ic_mute_member" static var icMuteMember: ImageAsset { return ImageAsset(name: "ic_mute_member") } - /// "ic_new_group" - static var icNewGroup: ImageAsset { return ImageAsset(name: "ic_new_group") } /// "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" @@ -754,8 +764,6 @@ internal extension Image { static var icQrCode: ImageAsset { return ImageAsset(name: "ic_qr_code") } /// "ic_scheduled_msg_check" static var icScheduledMsgCheck: ImageAsset { return ImageAsset(name: "ic_scheduled_msg_check") } - /// "ic_search" - static var icSearch: ImageAsset { return ImageAsset(name: "ic_search") } /// "ic_search_empty" static var icSearchEmpty: ImageAsset { return ImageAsset(name: "ic_search_empty") } /// "ic_send_as_file" @@ -766,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" @@ -848,6 +858,8 @@ internal extension Image { static var leftButton: ImageAsset { return ImageAsset(name: "left_button") } /// "lock" static var lock: ImageAsset { return ImageAsset(name: "lock") } + /// "logo-2" + static var logo2: ImageAsset { return ImageAsset(name: "logo-2") } /// "maximaze" static var maximaze: ImageAsset { return ImageAsset(name: "maximaze") } /// "minimaze" diff --git a/Nynja/Generated/ColorsConstants.swift b/Nynja/Generated/ColorsConstants.swift index 98d2940735e34184ab450f73a26cfc3cd883aea4..a0f198e1728df688b333aee32c94839c8e11e493 100644 --- a/Nynja/Generated/ColorsConstants.swift +++ b/Nynja/Generated/ColorsConstants.swift @@ -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) @@ -51,10 +47,20 @@ 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) + /// 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) 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) @@ -105,6 +111,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/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 2ff98ee062f7ab43a5101abc0d8896bc5d75dc72..2bddfd44f7262774b959bd6ab0595a6463e81c26 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -78,6 +78,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 @@ -284,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) @@ -460,6 +460,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 @@ -728,12 +730,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 @@ -914,7 +922,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") } @@ -1168,6 +1176,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 @@ -1330,12 +1340,16 @@ internal extension String { static var wheelDataAndStorage: String { return localizable.tr("Localizable", "wheel_data_and_storage") } /// Invite Friends static var wheelInviteFriends: String { return localizable.tr("Localizable", "wheel_invite_friends") } + /// 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 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 @@ -1408,6 +1422,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 @@ -1422,8 +1438,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 @@ -1438,8 +1452,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 @@ -1450,8 +1462,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 @@ -1500,6 +1510,222 @@ 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 + 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") } + /// 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 + 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") } + /// 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") } + /// 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 + 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") } + /// 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 + 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") } + /// 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. + 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 + 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") } + /// 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 + 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") } + /// 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/HandlerFactory.swift b/Nynja/HandlerFactory.swift index 6313b5934bd936ade160a0f24d101b126c343010..360e11b451ce1c2f023fcf96d5854fc0c02df97a 100644 --- a/Nynja/HandlerFactory.swift +++ b/Nynja/HandlerFactory.swift @@ -8,39 +8,38 @@ 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 .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/HomeItemsFactory.swift b/Nynja/HomeItemsFactory.swift index 90c7cfd1cc38f79c199aaf3bb6cd3158dd2ff5d2..7f96a85da04ee12cadcce2835af50c4100681b5e 100644 --- a/Nynja/HomeItemsFactory.swift +++ b/Nynja/HomeItemsFactory.swift @@ -13,44 +13,40 @@ 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] - if !FeatureFlags.isNotImplementedForLive.value { - edit.subitems.append(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) { [weak navigateDelegate] (item, indexPath) in - let call = ImageActionItemModel(nameImage: "ic_calls", navItem: .call, action: { [weak navigateDelegate] (item, indexPath) in - if FeatureFlags.isNotImplementedForLive.value { navigateDelegate?.conferenceVoiceCall(indexPath: indexPath) } else { navigateDelegate?.call(indexPath: indexPath) } - }) + } if !FeatureFlags.isNotImplementedForLive.value { call.subitems = [videoCall, voiceCall] } - 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) - }) + } return [call, edit, myQR, help] } // MARK: - Calls var voiceCall: ImageFilledItemModel { - return ImageFilledItemModel(nameImage: "ic_calls", navItem: .voiceCall, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in + return ImageFilledItemModel(nameImage: "ic_calls", navItem: .voiceCall, isSelectable: false) { [weak navigateDelegate] (item, indexPath) in navigateDelegate?.conferenceVoiceCall(indexPath: indexPath) - }) + } } var videoCall: ImageActionItemModel { @@ -63,30 +59,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: ImageActionItemModel { - return ImageFilledItemModel(navItem: .phoneNumber) { [weak navigateDelegate] (item, indexPath) in - navigateDelegate?.showChangeNumber(indexPath: indexPath) + var loginOptions: ImageFilledItemModel { + return ImageFilledItemModel(navItem: .loginOptions, isSelectable: false) { [weak navigateDelegate] item, indexPath in + navigateDelegate?.loginOptions(indexPath: indexPath) } } - + // MARK: - Items @@ -101,5 +85,4 @@ class HomeItemsFactory: WCBaseItemsFactory { item.state = .selected return item } - } diff --git a/Nynja/ImageSelector.swift b/Nynja/ImageSelector.swift index bfa583c1b12b83d5054bca2f95e97c36052c5adf..7f38f8141c45690351d5a082a6dab8a37d752f93 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/ImageUploader/ImageUploader.swift b/Nynja/ImageUploader/ImageUploader.swift new file mode 100644 index 0000000000000000000000000000000000000000..f71f7fb180884929f54ed7490f465f8da55c7d2d --- /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, quality: .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/Improvements/StorageObserver.swift b/Nynja/Improvements/StorageObserver.swift index b3f77b228ed01ec3f0d4a9c5c200c3c1139e10d1..184ae3bbddbf0da96d5c7184e352a46a38f4579b 100644 --- a/Nynja/Improvements/StorageObserver.swift +++ b/Nynja/Improvements/StorageObserver.swift @@ -22,10 +22,9 @@ extension StorageObserver { func register(subscriber: StorageSubscriber, type: SubscribeType) { isolationQueue.async { [weak self, weak subscriber] in - guard let `self` = self, let subscriber = subscriber else { + guard let self = self, let subscriber = subscriber else { return } - let reference = StorageSubscriberReference(subscriber) if var subs = self.subscribers[type] { subs.append(reference) @@ -44,10 +43,9 @@ extension StorageObserver { func unregister(subscriber: StorageSubscriber, type: SubscribeType) { isolationQueue.async { [weak self, weak subscriber] in - guard let `self` = self, let subscriber = subscriber else { + guard let self = self, let subscriber = subscriber else { return } - self.subscribers[type] = self.subscribers[type]?.filter { $0.subscriber != nil && $0.subscriber?.id != subscriber.id } } } diff --git a/Nynja/Improvements/StorageSubscriber.swift b/Nynja/Improvements/StorageSubscriber.swift index 7ef5878f3242038ae9ea5b14bf60e81e8de03e39..fb245d07265e4184a2cec825d5a8a3024636dd08 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/JobHandler.swift b/Nynja/JobHandler.swift index 32343977dfa260d929574bd390e964fc11b93cf2..a2eaa8db3263ff32b8a9711cc3896ed401461861 100644 --- a/Nynja/JobHandler.swift +++ b/Nynja/JobHandler.swift @@ -8,16 +8,24 @@ final class JobHandler: BaseHandler { - private static var storageService: StorageService { - return .sharedInstance - } + // MARK: - Dependencies + + private let storageService = StorageService.sharedInstance + + + // MARK: - Singleton + + static let shared = JobHandler() - static func executeHandle(data: BertTuple) { - guard let job = get_Job().parse(bert: data) as? Job, - let status = StringAtom.string(job.status) else { - return + 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 } - switch status { case "pending", "update": try? storageService.perform(action: .save, with: job) @@ -28,4 +36,3 @@ final class JobHandler: BaseHandler { } } } - diff --git a/Nynja/Library/Interfaces/NavigationProtocol.swift b/Nynja/Library/Interfaces/NavigationProtocol.swift index 81a48066a8b63a590f3209a5718107802b6e5ac7..f673afd9fd13c268b6787ab8ed824d41c8a74a93 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/Library/Result/Result.swift b/Nynja/Library/Result/Result.swift new file mode 100644 index 0000000000000000000000000000000000000000..f824f8d8244747a7aad0bd02aa1b2591cf92dd43 --- /dev/null +++ b/Nynja/Library/Result/Result.swift @@ -0,0 +1,149 @@ +// +// Result.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +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, CustomDebugStringConvertible { + + public var description: String { + switch self { + case .success: + return "SUCCESS" + case .failure: + return "FAILURE" + } + } + + 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/Alert/Alert+Defaults.swift b/Nynja/Library/UI/Alert/Alert+Defaults.swift new file mode 100644 index 0000000000000000000000000000000000000000..13920cb1f07a3ea144f8b62a65baad2f656f8d2f --- /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/Alert/AlertDisplayable.swift b/Nynja/Library/UI/Alert/AlertDisplayable.swift new file mode 100644 index 0000000000000000000000000000000000000000..8c2d336b99df3cebe13159de1dc1bb01d4fb7f76 --- /dev/null +++ b/Nynja/Library/UI/Alert/AlertDisplayable.swift @@ -0,0 +1,34 @@ +// +// AlertDisplayable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +protocol AlertDisplayable: class { + func present(_ alert: Alert, completion: (() -> Void)?) + func present(_ alert: Alert) +} + +extension AlertDisplayable { + func present(_ alert: Alert) { + present(alert, completion: nil) + } +} + +// MARK: - Wireframe + Coordinator + +protocol NavigationContainer: AlertDisplayable { + var navigation: UINavigationController! { get } +} + +extension NavigationContainer { + func present(_ alert: Alert, completion: (() -> Void)?) { + let alertController = alert.makeAlertController() + navigation?.present(alertController, 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/Library/UI/BaseVC/BaseVC.swift b/Nynja/Library/UI/BaseVC/BaseVC.swift index 77f9889868982794c95936a36ee11172b0845b2c..0f6da76188d99df70de3f24e93eec069b19cad1d 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/BaseVC/LoadingInteractive.swift b/Nynja/Library/UI/BaseVC/LoadingInteractive.swift index 23e6d7fd11a4cd291c880c31663d70e430ff1ecf..69ad07c9af2d197a3deb156e72ca8e3e89bf49af 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/Buttons/NynjaButton/BaseNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift index 3c6d9ec9d8b2a9a967ec8a05f9f7b1084a37d39d..806c2caa3f9de34b4b5b43e6642620c4e2266f27 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,26 +20,47 @@ 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 { + setupTextColor() + } + } + + 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) } + 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() @@ -55,13 +69,17 @@ class BaseNynjaButton: UIButton { // MARK: - Setup - private func baseSetup() { - self.backgroundColor = defaultColor - - self.setTitleColor(UIColor.nynja.white, for: .normal) - self.setTitleColor(UIColor.nynja.white.withAlphaComponent(0.3), for: .disabled) + func baseSetup() { + setContentHuggingPriority(.required, for: .vertical) + setContentCompressionResistancePriority(.required, for: .vertical) + backgroundColor = defaultColor + setupTextColor() - 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) } + private func setupTextColor() { + setTitleColor(textColor, for: .normal) + setTitleColor(textColor.withAlphaComponent(0.5), for: .disabled) + } } diff --git a/Nynja/Library/UI/Buttons/NynjaButton/DestructiveNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/DestructiveNynjaButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..2ff95d8db0c2d7eced356799cbf038f8f1ab2bf0 --- /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/NynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaButton.swift index 78abccc6525c69527f542adc99600759d9f5e139..9838dba4e00397b0ae1f6918bb3dcef84a049871 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 699f0cf386bc6508b2241a88613508347d0d0ab4..ed3a35e33248e7b729ea3ff74f7d6a3cabce3792 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCellButton.swift @@ -8,47 +8,30 @@ 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) - 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/Buttons/NynjaButton/NynjaCheckBox.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaCheckBox.swift new file mode 100644 index 0000000000000000000000000000000000000000..3b5b5f754c7d0feadc01c7ed76d4ed6c43d31572 --- /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/Buttons/NynjaButton/NynjaImageButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaImageButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..589671af94d2df6edeea1ed4c2b7e6f762b7d5a5 --- /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/NynjaSwitch.swift b/Nynja/Library/UI/Buttons/NynjaButton/NynjaSwitch.swift new file mode 100644 index 0000000000000000000000000000000000000000..0f379272f1271a8c2941edf351d0304b796a7a4a --- /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/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..4124cc34d37891dff429782ec4373386779383b6 --- /dev/null +++ b/Nynja/Library/UI/Buttons/NynjaButton/RoundNynjaButton.swift @@ -0,0 +1,20 @@ +// +// 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 + layer.masksToBounds = true + } +} diff --git a/Nynja/Library/UI/Buttons/PartialCheckableButton/PartialCheckableButton.swift b/Nynja/Library/UI/Buttons/PartialCheckableButton/PartialCheckableButton.swift index 670020688d339e073f3ba1be6331cbf5bb920c8c..cc9e65cccf0dda2e6a5bee36087f1acca22880b7 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/ControlsContainer/NynjaControlContainerView.swift b/Nynja/Library/UI/ControlsContainer/NynjaControlContainerView.swift index 9dcddc9a7f26ac63cbe3eacbeca260e06960fbb0..df9ffcd71cf4f57f888680883429c871484f886d 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 @@ -42,6 +44,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) { @@ -86,14 +90,12 @@ 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 maker.left.right.equalToSuperview() maker.bottom.equalTo(containerView.snp.top) - maker.height.equalTo(Constraints.gradientView.defaultHeight) + maker.height.equalTo(gradientHeight) } } @@ -103,7 +105,6 @@ final class NynjaControlContainerView: UIView { @objc private func actionCloseButtonTapped(sender: UIButton) { closeButtonHandler?(sender) } - } // MARK: - Layout @@ -125,5 +126,4 @@ extension NynjaControlContainerView { static let defaultHeight = CGFloat(28.0).adjustedByWidth } } - } diff --git a/Nynja/Library/UI/Extensions/UI/LabelExtensions.swift b/Nynja/Library/UI/Extensions/UI/LabelExtensions.swift index 9380c77a75a085b1ec7f033b9d4552c98134bd99..7d00ae11459502aa9b6be84b0b9689fc0f51c873 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/Extensions/UI/UIFontExtension.swift b/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift index abec040d443abfb9740f0c9ded36b1b2611b2a72..d646ff8a7343690b832a26abf7b44d0aabd13d7b 100644 --- a/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift +++ b/Nynja/Library/UI/Extensions/UI/UIFontExtension.swift @@ -17,6 +17,26 @@ 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)) + } + + 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/Extensions/UI/UIImageView/UIImageExtensions.swift b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift index 497ab22deedd269e53eb8665738a61deefa0b139..8b7a12ed22402e51d1c026da7fced22fd95ff917 100644 --- a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift +++ b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageExtensions.swift @@ -217,4 +217,22 @@ 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 + } } diff --git a/Nynja/Library/UI/Extensions/URLExtensions.swift b/Nynja/Library/UI/Extensions/URLExtensions.swift index 569ace4b7c0155ae50d2e0e8b82db6a6cd87b8ac..2d07c490daaa3bc1732f8734099893e71e20680f 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/Library/UI/Lists/EmptyStateView/EmptyStateView.swift b/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift index 97a8adaa4092beb184596aebd315fd94e52adaab..ea7d85c8287c58d5ee08c712cb9c6d5567bdb669 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/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index 8813713608074f271d566dcad503ac9953097cc9..53e85b008f11b1e970360c20c04526979663086d 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: [NSAttributedString.Key: Any] = [ - .foregroundColor: UIColor.nynja.manatee, - .font: type(of: self).contentFont - ] - let boldAttributes: [NSAttributedString.Key: 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.. 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 6a892dfd38eef4a2f6a092ea289344df050dd6ed..edc7daea1561ab028d52febe8ba56accb6689bed 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/Library/UI/TextInput/InputField/MyTextField.swift b/Nynja/Library/UI/TextInput/InputField/MyTextField.swift index d29d876548789dfaabb85b1285fe2144a68d0cc4..2c1ea0c69e9710ac1c6f689c1c2e6637ffa50b88 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 f8e36ee99268d059c9bdf56de1d58a9ef52ad2d1..075f26218580b201bfb1b3d81234521bf93d5749 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 72eea07c5365f84a75a7f2d5964b97b31d7db3aa..f19fbffa6e5e514fc7cca9403225eb17432f91ea 100644 --- a/Nynja/Library/UI/TextInput/InputField/PhoneField.swift +++ b/Nynja/Library/UI/TextInput/InputField/PhoneField.swift @@ -7,7 +7,7 @@ // import UIKit -import libPhoneNumber_iOS +import NynjaUIKit protocol PhoneFieldDelegate: class { func phoneChanged(text: String) -> Bool @@ -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 d0e37e66cd498a52924d75812700ba1f62ecdaca..2bb9966f3d4e67ab4810efc1b2f79bed941bd163 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/TextInput/Material/Base/MaterialTextContainer.swift b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextContainer.swift index 3bc815867bffdc12c08669fb232bad9d90908060..c9e7e31b42989130a2d6fef073c0118df5f4311b 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() @@ -41,6 +42,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/Base/MaterialTextInput.swift b/Nynja/Library/UI/TextInput/Material/Base/MaterialTextInput.swift index 2e1b381fb3babcdc62d7af32966f085c87f2b3e7..14d7eb344c35a6e4f5a20a2acad15a64c598d33c 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/Config/NynjaMTIConfig.swift b/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift index ff8a5dcdab510b259b4d27478d2932fed811b8ba..aac04a918202a570a68565b7f5cd3c391c5897ee 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 90f7d8e78d0978360c1c935c8e09548d5dbfb736..5dd27885c71637af2801e91fc5f7e1d3a2a0431e 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 { @@ -32,6 +33,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 +61,13 @@ class MaterialTextField: MaterialTextContainer { var autocapitalizationType: UITextAutocapitalizationType = UITextAutocapitalizationType.sentences { didSet { textField.autocapitalizationType = autocapitalizationType } } - - func setTextFieldFirstResponder() { - textField.becomeFirstResponder() + + var textContentType: UITextContentType! { + didSet { textField.textContentType = textContentType } } + + var returnHandler: ((MaterialTextField) -> Bool)? + // MARK: - Views @@ -87,9 +99,13 @@ class MaterialTextField: MaterialTextContainer { } override func becomeFirstResponder() -> Bool { - return self.textField.becomeFirstResponder() + return textField.becomeFirstResponder() } + override func resignFirstResponder() -> Bool { + return textField.resignFirstResponder() + } + // MARK: - Actions @@ -111,6 +127,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/Library/UI/TextInput/Material/Private/FloatingPlaceholderContainer/FloatingPlaceholderContainer.swift b/Nynja/Library/UI/TextInput/Material/Private/FloatingPlaceholderContainer/FloatingPlaceholderContainer.swift index cafe631aa93fd84db8b12a37d0965ca4c09d7b4b..5e90120005cc5eb145ca3d451d31b0853bca1868 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/InputInfo.swift b/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfo.swift index 9b27debe9fdf8bec72a40ce2bc9fe66290b99862..aa320b832a0d0f9c93107e10e3bc2799ef6daaac 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/Private/InputInfoContainer/InputInfoContainer.swift b/Nynja/Library/UI/TextInput/Material/Private/InputInfoContainer/InputInfoContainer.swift index 37a2d77ecbdd166323dce59521f32f4b932c0f9f..c10ca6aedba46be3c3708c742be29c7cd0d33af5 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/TextInput/Material/Validator/ClosureValidator.swift b/Nynja/Library/UI/TextInput/Material/Validator/ClosureValidator.swift index 9d6ca91b323f2a7a74f5c46b3023980ee4e072c0..85a3589c19af5f4cf424984f460de215033f07aa 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 0000000000000000000000000000000000000000..a03cec1f3d883a1a2176b02c53b25b472e56c166 --- /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 0000000000000000000000000000000000000000..bc16a91e199bf1096c7dce44b051e6df68d0621e --- /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 0000000000000000000000000000000000000000..fa59da8319ab166bcab7ec470780d3897c1d22f2 --- /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 e87f216a1ea8b28afe87e08d5994c6a6b19b2a02..e2a49c139d98f0bfd72f32097b332176cf6eec46 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 @@ -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/Validator.swift b/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift similarity index 89% rename from Nynja/Library/UI/TextInput/Material/Validator/Validator.swift rename to Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift index 90ad3062073c9fb150d504ae44d030bfcae343af..de8b02eb1734b12321b9212dbacf2620103cbe4d 100644 --- a/Nynja/Library/UI/TextInput/Material/Validator/Validator.swift +++ b/Nynja/Library/UI/TextInput/Material/Validator/MTIValidator.swift @@ -1,5 +1,5 @@ // -// Validator.swift +// MTIValidator.swift // Nynja // // Created by Volodymyr Hryhoriev on 11/26/18. 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 0000000000000000000000000000000000000000..77768fe5794d90d08f28b7bf17ee93e156b2e74d --- /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/Library/UI/TextInput/TextView/TextView.swift b/Nynja/Library/UI/TextInput/TextView/TextView.swift index cf9a427dadbc150e04db045bbbd655eee48b9dab..c32a752fc13b6c4cfca6c10deaf73e352b7d45ec 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/Library/UI/UIInterfaces/Configurable.swift b/Nynja/Library/UI/UIInterfaces/Configurable.swift index 06c7a598d0f0c250ec8f665fdbc0cb65e07cf414..df73b9c2845ac4b031dc8518da5105b5d6dfb48c 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/Library/UI/UIViewControllerExtensions/UIViewControllerExtensions.swift b/Nynja/Library/UI/UIViewControllerExtensions/UIViewControllerExtensions.swift new file mode 100644 index 0000000000000000000000000000000000000000..2e0a9e8682331ee0613446bc3271700dfcd91a9b --- /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/BaseView.swift b/Nynja/Library/UI/View/BaseView.swift deleted file mode 100644 index 55360529efee293ecbb357b35090791ebd9cab00..0000000000000000000000000000000000000000 --- 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/GradientContainerView.swift b/Nynja/Library/UI/View/GradientContainerView.swift new file mode 100644 index 0000000000000000000000000000000000000000..bbc882bb10f3c65ffa82c2cba64bc71415b73471 --- /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 b3bd9aaf70a155dbb2d7d8e537c20fb87778c0a5..cd4ee2f108fbd02d877beb5957b634280f9bf29d 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/Library/UI/View/ImagesView/ImagesView.swift b/Nynja/Library/UI/View/ImagesView/ImagesView.swift index 2fe8dbdcf821e0dbf609f3192e48fe54e265ba71..72862dd0048b90df2bda46bb626b25d9c336885e 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 77e2bf686d0c673f6a817d43cfb217b8ac6f08fb..a523b3c451dd2f31229940026d2a32d9914a480f 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/View/UIViewExtensions.swift b/Nynja/Library/UI/View/UIViewExtensions.swift index 7fbc08b38453498fa99fa5131c1c8040617efc04..145507a25fdafe7b01765a1a48f339c0f90fb0b3 100644 --- a/Nynja/Library/UI/View/UIViewExtensions.swift +++ b/Nynja/Library/UI/View/UIViewExtensions.swift @@ -11,11 +11,18 @@ import UIKit // MARK: - Factory methods extension UIView { - static func makeStatusBarBackgroundView(on view: UIView) -> UIView { - struct StatusBarBackgroundLayout { - static let height = 20 + 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 { let background = UIView() view.addSubview(background) @@ -23,29 +30,55 @@ 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 } + + 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.clear + let navigationView = makeHeaderViewWithoutConstraints(on: view, config: config) navigationView.snp.makeConstraints { (maker) in 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/Library/UI/WheelContainer/Wheel/Factory/WheelItemViewFactory.swift b/Nynja/Library/UI/WheelContainer/Wheel/Factory/WheelItemViewFactory.swift index f65fe5115423091bc303e2ffc4e1ec8f5dbbd93f..ee1f95c7dad8a779b67747c20c4b587de41a5442 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 385779fc698c7762532d173f7fa416bd53351e0e..5ab564521cd5b8ddfba5c896d1396f7b08ace4e7 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/Library/UI/WheelContainer/Wheel/ItemViews/Location/PlaceDescriptionView.swift b/Nynja/Library/UI/WheelContainer/Wheel/ItemViews/Location/PlaceDescriptionView.swift index 83754c4b4f6fd3a838787d961070442c059b7d5a..7507bb5df00e338f2ebf0c37694ccf8a9b038bc2 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/LinkHandler.swift b/Nynja/LinkHandler.swift index 620ad386d0779de2aacb8a9d9822e069f01263ba..1e774840b8cbf7cf4fa26c5f2b3317ed861c4f69 100644 --- a/Nynja/LinkHandler.swift +++ b/Nynja/LinkHandler.swift @@ -8,9 +8,18 @@ final class LinkHandler: BaseHandler, StaticDelegating { - static weak var delegate: LinkHandlerDelegate? + // MARK: - Singleton - static func executeHandle(data: BertTuple, codes: StatusCodes) { + static let shared = LinkHandler() + + private init() {} + + + // MARK: - Handler + + weak var delegate: LinkHandlerDelegate? + + func executeHandle(data: BertTuple, codes: StatusCodes) { guard let link = get_Link().parse(bert: data) as? Link else { return } @@ -22,7 +31,7 @@ final class LinkHandler: BaseHandler, StaticDelegating { } } - private static func handle(link: Link) { + private func handle(link: Link) { guard let status = link.originalStatus else { return } @@ -38,7 +47,7 @@ final class LinkHandler: BaseHandler, StaticDelegating { } } - private static func handle(codes: StatusCodes, link: Link) { + private func handle(codes: StatusCodes, link: Link) { let statusCodeManager = StatusCodeManager.shared codes diff --git a/Nynja/MQTTModels/TypingExtension+BERT.swift b/Nynja/MQTTModels/TypingExtension+BERT.swift index 3a793308f138b9e46528f0829348598db179c936..f5a39fd095982d2ec39f9742c72f60bf124df33b 100644 --- a/Nynja/MQTTModels/TypingExtension+BERT.swift +++ b/Nynja/MQTTModels/TypingExtension+BERT.swift @@ -12,13 +12,18 @@ extension Typing: BERTEncodable { 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/MemberHandler.swift b/Nynja/MemberHandler.swift index d6a52401101fdda7b713af00474762979ca4053a..f0e87a31627f9259b651a71c4d91f90c1ec71d37 100644 --- a/Nynja/MemberHandler.swift +++ b/Nynja/MemberHandler.swift @@ -6,13 +6,23 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class MemberHandler: BaseHandler { +final class MemberHandler: BaseHandler { - private static var storageService: StorageService { - return .sharedInstance - } + // MARK: - Dependencies + + private let storageService = StorageService.sharedInstance + + + // MARK: - Singleton + + static let shared = MemberHandler() - static func executeHandle(data: BertTuple) { + 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 @@ -23,11 +33,10 @@ class MemberHandler: BaseHandler { if let id = member.id, let oldMember = MemberDAO.findMemberBy(id: id) { member.status = oldMember.status } - try? storageService.perform(action: .save, with: member) default: return - } + } } } diff --git a/Nynja/MigrationManager/Migrations/AddAccountAndContactInfoTables.swift b/Nynja/MigrationManager/Migrations/AddAccountAndContactInfoTables.swift new file mode 100644 index 0000000000000000000000000000000000000000..0a3a8611abfe303eb6c053ec776795b91ad447c3 --- /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 8f43f03ed735cbc6e36d7fc959ee89de4929b6d1..bd205802e96abd4d291faaa6044e2db63e8df7c2 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 cdc25bc2c2495c713caef13532ec351436d56ee1..4e7d9026278a21a2c605adf4a57e2ee32b24f6ef 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() ] } diff --git a/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift b/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift index 170fa07f9578d6d48b227e34a879ff9450e928e8..f21673f79e970bae7a1c789e52b7ef745ee32b1e 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/AddContactByUsernameProtocols.swift b/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift index b455f0e6aa90dd0d671bcb470a67cd5692a656cf..f92e615bdc3cae067c0ff5033ad822c09e4d4fd4 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 aff9af4f3fb2e6bad9f07d4e1e4e1892e8cc1942..28f8c0d0f2d65b5ef567b660aa821ff701e01688 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 { @@ -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 b8ae4b2e3ad0287514d8a5c78cb43be19f9e3eac..e93c77965df69d7bc353796cf301751239e8f7dd 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 44f91475bc51af3cdb82ac010901b17cebb20d56..87fec6d6452ec0956700ed66ae3ec8abd8e6b8ab 100644 --- a/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift +++ b/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift @@ -18,16 +18,24 @@ protocol AddContactViaPhoneWireFrameProtocol: class { func presentAddContact(contact: Contact) func showMyProfile() - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) + func showSelectCountry() } -protocol AddContactViaPhoneViewProtocol: LoadingInteractiveView { +protocol AddContactViaPhoneWireFrameOutput: class { + func didSelectCountry(_ country: Country) +} + +protocol AddContactViaPhoneViewProtocol: class { var presenter: AddContactViaPhonePresenterProtocol! { get set } /** * Add here your methods for communication PRESENTER -> VIEW */ + + func setupSelectedCountry(_ country: Country) + func showSpinner() + func hideSpinner() } protocol AddContactViaPhonePresenterProtocol: BasePresenterProtocol { @@ -40,7 +48,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 +64,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/Interactor/AddContactViaPhoneInteractor.swift b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift index 370b84a39ceee6b8afaee7d08b25c4c2bcd69f6e..032292783cfe92e5a7420638f00bf580e51333f2 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 { @@ -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 531074978c97f39e6a4979936dd59e15830b91e3..94cb8f25d83118646ce23ef1f7eaf989140c9a93 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,15 +32,19 @@ final class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresen self.wireFrame.showMyProfile() } - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) { - self.wireFrame.showSelectCountry(selectCountryDelegate) + func showSelectCountry() { + self.wireFrame.showSelectCountry() } - func showHUD() { + func showLoading() { view.showSpinner() } - func hideHUD() { + func hideLoading() { 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 8e7e3a02ba081d37bb2199698433ed17902282c7..ae03099c4f1559928de74ec555a8729cc764f1de 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) } @@ -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) { @@ -256,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() @@ -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 c1c6b5f0cb9b5c91c6cd6238622563e964787498..9318ae10c0885eb6dbcdfcf12e6b9ce72f50ef6c 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/Call/View/CallInProgressView.swift b/Nynja/Modules/Call/View/CallInProgressView.swift index 9fb80ebf14589a4c45331b14261873d66387376b..a0ef90f8490d943918a485f18f947fdabeadd657 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/Channel/NewChannel/Interactor/NewChannelInteractor.swift b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift index d6b00320d8dc01a3bfd0cef2f17f61dd0ceb2ff0..347f482e0285bc616b088ff36613256a4ebf87a0 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/Channel/NewChannel/NewChannelProtocols.swift b/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift index 81fe468023334be70192c87a757d1750f891e2df..d555355397c44e70e84905e4fcbe2637789f457c 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 a039f2a173464f9b3dfbab64c782336fded62dab..5ae72b4a81f7468635fa6d67d84a2e11a0db7ac7 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 3677cebb36349c097ccccff76836616d8dfe23a9..62217b091873526a1bfa666580fe23ceb5cab801 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/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index c5aafbdb5f878ebd48702a72ae76ec78f9e495c3..e9ee87810c69f9aefa230b6442d161b9da9ba0eb 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 6dde34fcda20a9cb35b2bb00a572d87ec0458c22..525e83262473da1bbb7b21edc28b9f2dbe5f27ec 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -12,6 +12,7 @@ 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 = "" @@ -20,16 +21,27 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini return storageService.rosterId } + 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) } @@ -41,6 +53,10 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchChats() } @@ -52,6 +68,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 6456463ebd7fe386e9047eda0385b601e0f07b05..2db4c11d7c668fe9fc3cf33157c434f6aa80b22b 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 1744972294bf83d00dd33afb6facb07e20f90c84..1a8c94bc17d96b09608f3524f7dd019592ab83bc 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 6d2e8c0bc1322347c9d2845a90d0773aa1fc8e8a..a7fefb16cde93bd0e246bf4e1fc8cf375973b984 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 d54d9eb7b2f985efd06d0e5052aa7915e1f59805..6f7fc5ed5f9400685d7f3d275166113be631004f 100644 --- a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift +++ b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift @@ -14,14 +14,18 @@ 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: ServiceFactory().makeConversationsProvider())) - + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) + self.main = main // Connecting diff --git a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift index bf794f6955c7b1b3881561e1cbd892b976e06fe3..c403849bafe23606c7910b742691be01fe7eab04 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/Contacts/View/InviteFriendHeaderView.swift b/Nynja/Modules/Contacts/View/InviteFriendHeaderView.swift index 465b4c4a0d7c3d2bc8e8707c4abc168fd045e253..831e5d21dc1a5e606c8849e8f3e2c2b5ebb3d0ce 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 c8cc88e56bfbb333ce9e0ccd1afb5f72357ebac2..877ffc758f11ffc1fc2d77f40e0f017f1a5378a8 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 0c072f526df7d489f6a666bfac47487e76a2947c..cb57dddc5a70fe02c881565f06cb34497b2ad21b 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 6309d4b216de2da00e8a1da382201ba1fd333ab8..c4adb2abb0e395acff44c91cf2b7e028c47d092f 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/EditUsername/Interactor/EditUsernameInteractor.swift b/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift index cb9d7bd2566bcc8ca12ac5b3d51122cb4d78271e..f7319c2ccd56edec806617abe20f189ed881f1c4 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/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..80e00417956bfc85b7c40f1c8d48f74466210361 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/AccountSettingsProtocols.swift @@ -0,0 +1,57 @@ +// +// AccountSettingsProtocols.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +// MARK: - Wireframe + +protocol AccountSettingsWireframeProtocol: AlertDisplayable { + func dismiss() + + 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) +} + +// MARK: - View + +protocol AccountSettingsViewInput: LoadingInteractive { + func setup(form: Form) +} + +// MARK: - Presenter + +protocol AccountSettingsPresenterProtocol: BasePresenterProtocol, NavigationProtocol { + func save() +} + +// MARK: - Interactor + +// MARK: Input +protocol AccountSettingsInteractorInput: BaseInteractorProtocol { + + var identityId: String { get } + + var accountId: String { get } + + var availableStatuses: [AccountStatus] { get } + + var availableTimeouts: [AccountTimeout] { get } + + var availableContactInfoTypes: [ContactInfoInputModel.InputType] { get } + + 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 0000000000000000000000000000000000000000..7da7b0ea92ddc2f2e85315bec94f3e27ecfe677a --- /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 0000000000000000000000000000000000000000..882f31661ba969668442fd44825bf1fab7831f37 --- /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 0000000000000000000000000000000000000000..138b79be4907361d4d7f4250e0424f63bf0f3b4d --- /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/ContactInfoSectionItem.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/ContactInfoSectionItem.swift new file mode 100644 index 0000000000000000000000000000000000000000..43107a40b763aa8dda2ccb3a6a28c62b7bad4f6b --- /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 0000000000000000000000000000000000000000..2033ec0dcaa0a497fc63fb3886a570a034aa6628 --- /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/UserProfile.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserProfile.swift new file mode 100644 index 0000000000000000000000000000000000000000..385a05c706b45fe6ce0e1187d17e80adec2dee68 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Entities/UserProfile.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/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..e96fa33394a476c81df6c701c8c5016f231debe5 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Interactor/AccountSettingsInteractor.swift @@ -0,0 +1,176 @@ +// +// AccountSettingsInteractor.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AccountSettingsInteractor: BaseInteractor, AccountSettingsInteractorInput, InitializeInjectable { + + private weak var presenter: AccountSettingsInteractorOutput? + + override var subscribes: [SubscribeType]? { + return [.account(accountId), .contactInfo] + } + + // MARK: - User Info + + let identityId: String + + let accountId: String + + var availableStatuses: [AccountStatus] { + return AccountStatus.allCases + } + + var availableTimeouts: [AccountTimeout] { + return [ + .time(5 * 60), + .time(15 * 60), + .time(30 * 60), + .time(60 * 60), + .never + ] + } + + var availableContactInfoTypes: [ContactInfoInputModel.InputType] { + return ContactInfoInputModel.InputType.allCases + } + + private var account: DBAccount? + + + // MARK: - Services + + private let accountDAO: AccountDAOProtocol + + private let accountService: AccountService + + private let imageUploader: ImageUploader + + + // MARK: - Init + + struct Dependencies { + let presenter: AccountSettingsInteractorOutput + let identityId: String + let accountId: String + let accountDAO: AccountDAOProtocol + let accountService: AccountService + let imageUploader: ImageUploader + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + identityId = dependencies.identityId + accountId = dependencies.accountId + accountDAO = dependencies.accountDAO + accountService = dependencies.accountService + imageUploader = dependencies.imageUploader + } + + override func loadData() { + super.loadData() + fetchAccount() + } + + private func fetchAccount() { + guard let account = accountDAO.fetchAccount(byId: accountId) else { + return + } + self.account = account + setup(account) + } + + private func setup(_ account: DBAccount) { + 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 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, settings: settings, completion: completion) + case let .failure(error): + completion(.failure(error)) + } + } + case let .url(avatarURL): + updateAccount(avatarURL: avatarURL, settings: settings, completion: completion) + } + } + + private func updateAccount(avatarURL: URL?, settings: AccountSettingsViewModel, completion: @escaping (Result) -> Void) { + // FIXME: save birthday + let accountInfo = AccountInfo( + accountId: accountId, + avatar: avatarURL?.absoluteString, + accountMark: settings.profileMessage, + accountName: nil, + firstName: settings.firstName, + lastName: settings.lastName, + username: settings.username, + accountStatus: .enabled, + roles: nil, + qrCode: accountId, + 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)) + } + } + } + + + // 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 new file mode 100644 index 0000000000000000000000000000000000000000..786bf9f043a50523c2c68f2f32a652e16330122a --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Presenter/AccountSettingsPresenter.swift @@ -0,0 +1,304 @@ +// +// AccountSettingsPresenter.swift +// Nynja +// +// Created by Ash on 11/2/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class AccountSettingsPresenter: BasePresenter, AccountSettingsPresenterProtocol, SetInjectable, AccountSettingsInteractorOutput { + + private weak var view: AccountSettingsViewInput? + private var wireframe: AccountSettingsWireframeProtocol! + private var interactor: AccountSettingsInteractorInput! { + didSet { + _interactor = interactor + } + } + + private var viewModel: AccountSettingsViewModel! + + + // MARK: - View Model + + 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) + } + + private func makeTopSection(for viewModel: AccountSettingsViewModel) -> Form.Section { + 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 { + 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 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 + 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: CGFloat(32).adjustedByWidth) + + 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 + self?.viewModel?.firstName = text + } + + 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 + self?.viewModel?.lastName = text + } + + let birthdayItem = TextFieldRowItem(height: CGFloat(64).adjustedByWidth) + birthdayItem.placeholder = String.localizable.accountSettingsBirthdayFieldPlaceholder + birthdayItem.returnKeyType = .next + + return Form.Section(header: header, rows: [fistNameItem, lastNameItem, birthdayItem]) + } + + private func makeUsernameSection(for viewModel: AccountSettingsViewModel) -> Form.Section { + let header = FormHeader(title: String.localizable.accountSettingsHeaderUsername, height: CGFloat(32).adjustedByWidth) + + 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 + self?.viewModel?.username = text + } + + let textItem = TextRowItem(text: String.localizable.accountSettingsUsernameDescription) + + return Form.Section(header: header, rows: [usernameItem, textItem]) + } + + private func makeContactInfoSection(for viewModel: AccountSettingsViewModel) -> Form.Section { + let header = FormHeader(title: String.localizable.accountSettingsHeaderContactInformation, height: CGFloat(32).adjustedByWidth) + + var items: [AnyFieldRowItem] = [] + + 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) + } + } + 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) + } + } + let contactInfoSectionItem = ContactInfoSectionItem(data: contactInfo) + items.append(contactInfoSectionItem) + } + + return Form.Section(header: header, rows: items) + } + + private func makeDeleteAccountSection() -> Form.Section { + 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 + } + 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]) + } + + + // MARK: - Presenter + + 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 back() { + wireframe?.dismiss() + } + + + // 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) + } + + 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) + } + + 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) + } + + 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) + } + + + // MARK: - Interactor Output + + func didUpdate(_ viewModel: AccountSettingsViewModel) { + self.viewModel = viewModel + setup(viewModel) + } +} + +// MARK: - Injection + +extension AccountSettingsPresenter { + struct Dependencies { + let view: AccountSettingsViewInput + let wireframe: AccountSettingsWireframe + let interactor: AccountSettingsInteractorInput + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + wireframe = dependencies.wireframe + interactor = dependencies.interactor + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..b445a9390f81cb85a20413e4ff0dfde0ecc3c14f --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/View/AccountSettingsViewController.swift @@ -0,0 +1,142 @@ +// +// AccountSettingsViewController.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class AccountSettingsViewController: BaseVC, AccountSettingsViewInput, FormContainer, LoadingDisplayable, InitializeInjectable { + + private let presenter: AccountSettingsPresenterProtocol + + + // MARK: - Views + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + // MARK: Container + + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.contentInset.bottom = CGFloat(28.0.adjustedByWidth) + + 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 + }() + + private(set) lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + + contentView.addSubview(stackView) + stackView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return stackView + }() + + // MARK: Control + + private lazy var controlContainerView: NynjaControlContainerView = { + let containerView = NynjaControlContainerView(contentView: saveButton) + + 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 containerView + }() + + 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.addTarget(self, action: #selector(saveAction(sender:)), for: .touchUpInside) + + button.snp.makeConstraints { maker in + maker.height.equalTo(44.0.adjustedByWidth) + } + + return button + }() + + var form: Form? + + + // MARK: - Init + + struct Dependencies { + let presenter: AccountSettingsPresenterProtocol + } + + 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") + } + + + // MARK: - Life Cycle + + override func initialize() { + super.initialize() + + screenTitle = String.localizable.accountSettingsScreenTitle + + navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: true, + isVisibleBackButton: true, + title: screenTitle, + navigationHandler: presenter, + backButtonImage: UIImage.nynja.icBackNavigation.image) + ) + + controlContainerView.addGradientView() + scrollView.snp.makeConstraints { maker in + maker.bottom.equalTo(saveButton.snp.top) + } + + view.bringSubviewToFront(controlContainerView) + } + + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + view.endEditing(true) + } + + + // MARK: - Actions + + @objc private func saveAction(sender: UIButton) { + presenter.save() + } +} 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 0000000000000000000000000000000000000000..b4cf9771db0c81d67fe07e84351ad4c9ab4e44ca --- /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/Wireframe/AccountSettingsWireframe.swift b/Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..2622a876755645bf6331426498f8f18d164a2eb0 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AccountSettings/Wireframe/AccountSettingsWireframe.swift @@ -0,0 +1,85 @@ +// +// AccountSettingsWireframe.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +protocol AccountSettingsCoordinatorProtocol: AlertDisplayable { + func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) +} + +final class AccountSettingsWireframe: Wireframe, AccountSettingsWireframeProtocol { + + private let coordinator: AccountSettingsCoordinatorProtocol + + init(coordinator: AccountSettingsCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let identityId: String + let accountId: String + } + + struct Dependencies { + let accountDAO: AccountDAOProtocol + let accountService: AccountService + let imageUploader: ImageUploader + } + + enum State { + 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) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = AccountSettingsPresenter() + + let view = AccountSettingsViewController(dependencies: .init(presenter: presenter)) + + let interactor = AccountSettingsInteractor(dependencies: .init( + presenter: presenter, + identityId: parameters.identityId, + accountId: parameters.accountId, + accountDAO: dependencies.accountDAO, + accountService: dependencies.accountService, + imageUploader: dependencies.imageUploader) + ) + + presenter.inject(dependencies: .init(view: view, wireframe: self, interactor: interactor)) + + return view + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } + + func present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) + } + + func chooseAvatar(from source: ImageSource, completion: @escaping (UIImage?) -> Void) { + coordinator.wireframe(self, didEndWithState: .chooseAvatar(source: source, completion: completion)) + } + + func addContactInfo(ofType contactInfoType: ContactInfoInputModel.InputType, accountId: String) { + coordinator.wireframe(self, didEndWithState: .addContactInfo(type: contactInfoType, accountId: accountId)) + } + + func editContactInfo(_ contactInfo: ContactInfoInputModel.InputInfo, accountId: String) { + coordinator.wireframe(self, didEndWithState: .editContactInfo(contactInfo, accountId: accountId)) + } + + func deleteAccount(identityId: String, accountId: String) { + coordinator.wireframe(self, didEndWithState: .deleteAccount(identityId: identityId, accountId: accountId)) + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..7b1bbb63b22e60ebc71da5993bcbb6336018a3ca --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/AuthProviderProtocols.swift @@ -0,0 +1,60 @@ +// +// AuthProviderProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/23/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +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 } + + func setupContentView(with configuration: AuthProviderUIConfiguration) + func setNextActionEnabled(_ isEnabled: Bool) + + func select(country: Country) +} + +// MARK: - Presenter + +protocol AuthProviderPresenterProtocol: BasePresenterProtocol, NavigationProtocol { + + var authProvider: AuthProvider { get } + + func viewDidLoad() + + func setAvailableForSearch(_ isAvailable: Bool) + func selectCountry() + func next(inputText: String) +} + +// MARK: - Interactor + +// MARK: Input +protocol AuthProviderInteractorInput: class { + + func fetchDefaultCountry() -> Country + func fetchCountry(by code: String) -> Country? + + func addEmailProvider(_ email: String) + func addPhoneNumberProvider(_ phoneNumber: PhoneNumberInfo) +} + +// MARK: Output +protocol AuthProviderInteractorOutput: class { + func didAddAuthProvider(with confirmationData: ConfirmationData) + func didReceiveFailure(_ error: Error?) +} diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProvider.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProvider.swift new file mode 100644 index 0000000000000000000000000000000000000000..d1b637df47f9c35648abd35dedd652f1541268c5 --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Entities/AuthProviderUIConfiguration.swift new file mode 100644 index 0000000000000000000000000000000000000000..7c533e8c6ad5dfaceb7dbdc1e1339939755f471e --- /dev/null +++ b/Nynja/Modules/Flows/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(validator: EmailValidator) + case phoneNumber(controller: PhoneNumberTextController, country: Country, selectionHandler: () -> Void) + } + + let content: Content + let isAvailableForSearch: Bool +} diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..8697c14f86375a227bfd35a0ced346323095ccf2 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/Interactor/AuthProviderInteractor.swift @@ -0,0 +1,50 @@ +// +// 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 } + } + + func addEmailProvider(_ email: String) { + presenter?.didAddAuthProvider(with: .email(email)) + } + + func addPhoneNumberProvider(_ phoneNumber: PhoneNumberInfo) { + presenter?.didAddAuthProvider(with: .phoneNumber(phoneNumber)) + } +} diff --git a/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..19a211570e14cfb8d958ec670a5f2fb043d72a55 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/Presenter/AuthProviderPresenter.swift @@ -0,0 +1,159 @@ +// +// 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? + + private var selectedCountry: Country? { + return phoneNumberController?.country + } + + + // 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 = makeEmailValidator() + + content = .email(validator: 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(countryProvider: CountriesProvider()) + controller.country = country + + controller.validationAction = { [weak view] result in + view?.setNextActionEnabled(result) + } + + return controller + } + + private func makeEmailValidator() -> EmailValidator { + let validator = EmailValidator() + + validator.validationHandler = { [weak view] isValid in + view?.setNextActionEnabled(isValid) + } + + return validator + } + + 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) { + let inputText = inputText.trimmed() + + switch authProvider { + case .email: + view?.showLoading() + interactor.addEmailProvider(inputText) + + case .phoneNumber: + guard let country = selectedCountry else { + return + } + view?.showLoading() + + let rawNumber = inputText.replacingOccurrences(of: " ", with: "") + let phoneNumber = PhoneNumberInfo(country: country, number: rawNumber) + interactor.addPhoneNumberProvider(phoneNumber) + } + } + + func back() { + wireframe.dismiss() + } + + + // MARK: - Interactor Output + + func didAddAuthProvider(with confirmationData: ConfirmationData) { + view?.hideLoading() + wireframe.confirm(data: confirmationData) + } + + func didReceiveFailure(_ error: Error?) { + view?.hideLoading() + } +} + +// 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/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..e2e7b478a91303af4aa219e5d4d06706aa7e3ca8 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/AuthProviderViewController.swift @@ -0,0 +1,215 @@ +// +// AuthProviderViewController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/23/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit +import NynjaUIKit + +final class AuthProviderViewController: BaseVC, AuthProviderViewInput, LoadingDisplayable { + + private let presenter: AuthProviderPresenterProtocol + + + // MARK: - Views + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + private lazy var formContainer: UIView = { + let top = Constraints.formContainer.top.adjustedByWidth + + let container = UIView() + + view.addSubview(container) + container.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + + maker.top.equalTo(navigationView.snp.bottom).offset(top) + maker.left.right.equalToSuperview().inset(horizontal) + } + + 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(validator): + let emailView = EmailLoginView(validator: validator) + 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) + } +} + +// 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/Flows/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/View/Subviews/SearchAvailabilityView.swift new file mode 100644 index 0000000000000000000000000000000000000000..ae7cde9bf31353ade98839b7512958863c6895b5 --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift b/Nynja/Modules/Flows/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..15894cb2597f697e38a6914c7cc9acb5a5b6d825 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/AuthProvider/Wireframe/AuthProviderWireframe.swift @@ -0,0 +1,64 @@ +// +// 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) + case confirmProvider(ConfirmationData) + } + + 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)) + } + + func confirm(data: ConfirmationData) { + coordinator.wireframe(self, didEndWithState: .confirmProvider(data)) + } +} 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 0000000000000000000000000000000000000000..b1f2ba9e5e5deca70729c717e456e8d87c515d46 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/ContactInfoManagementProtocols.swift @@ -0,0 +1,49 @@ +// +// 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 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() +} + +// MARK: - Interactor + +// MARK: Input +protocol ContactInfoManagementInteractorInput: class { + 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 didSaveContactInfo() + 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 new file mode 100644 index 0000000000000000000000000000000000000000..df7824e3bbb37fe3cac04ed4dccde9f42df308f2 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/ContactInfoInputModel.swift @@ -0,0 +1,59 @@ +// +// ContactInfoInputModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/7/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum ContactInfoInputModel { + case empty(InputType) + case data(InputInfo) + + enum InputType: CaseIterable { + case phoneNumber + case email + case social(SocialProvider) + + static var allCases: [InputType] { + var allCases: [InputType] = [.phoneNumber, .email] + allCases.append(contentsOf: SocialProvider.allCases.map { .social($0) }) + return allCases + } + } + + enum InputInfo { + case phoneNumber(PhoneNumber) + case email(String) + 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 + + var displayName: String { + switch self { + case .facebook: + return String.localizable.facebook + case .twitter: + return String.localizable.twitter + } + } + } +} 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 0000000000000000000000000000000000000000..91ea4efb6a36daa4379119c6685d0acbe6f93112 --- /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 0000000000000000000000000000000000000000..2811d19a75d4c5075f2426c3a2fdf4cedaa0f9c4 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Entities/SocialLinkValidator.swift @@ -0,0 +1,44 @@ +// +// SocialLinkValidator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/18/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class 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 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) + + 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/Interactor/ContactInfoManagementInteractor.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..a8ae2de0fe624c132024a431e1754d72f4a9dd40 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Interactor/ContactInfoManagementInteractor.swift @@ -0,0 +1,142 @@ +// +// 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 + + private let accountDAO: AccountDAOProtocol + + + // MARK: - Init + + struct Dependencies { + 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.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 new file mode 100644 index 0000000000000000000000000000000000000000..b72cb720e3ffda70540641f7244a179518ef307b --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Presenter/ContactInfoManagementPresenter.swift @@ -0,0 +1,341 @@ +// +// 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! + + 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: EmailValidator(), 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 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 + } + } + + 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) + } + } + + 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 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 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) + } +} + +// MARK: - Injection + +extension ContactInfoManagementPresenter: SetInjectable { + + struct Dependencies { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..1300e3b0fbefb26110803ad5288955915f593c3c --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewController.swift @@ -0,0 +1,197 @@ +// +// 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 inputContainerView: UIView = { + let containerView = UIView() + + view.addSubview(containerView) + containerView.snp.makeConstraints { maker in + maker.top.equalTo(navigationView.snp.bottom) + maker.left.right.equalToSuperview() + } + + return containerView + }() + + private(set) lazy var deleteButton: DestructiveNynjaButton = { + let height = Constraints.deleteButton.height + let fontHeight = Constraints.deleteButton.fontHeight + let horizontal = Constraints.deleteButton.horizontal + + let button = DestructiveNynjaButton(font: FontFamily.NotoSans.medium, fontHeight: fontHeight) + + view.addSubview(button) + button.snp.makeConstraints { maker in + maker.top.equalTo(inputContainerView.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 fontHeight = Constraints.deleteButton.fontHeight + let horizontal = Constraints.saveButton.horizontal + + let button = RoundNynjaButton(font: FontFamily.NotoSans.medium, fontHeight: fontHeight) + button.setTitle(String.localizable.save, for: .normal) + + 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 viewDidLoad() { + super.viewDidLoad() + presenter.viewDidLoad() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unregisterForKeyboardNotifications() + } + + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + view.endEditing(true) + } + + + // 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: - 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 + } + } + + + // 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/View/ContactInfoManagementViewModel.swift b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/View/ContactInfoManagementViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..5a5184deadfe0972bd85c1ac2cd884a5e48f29b9 --- /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 0000000000000000000000000000000000000000..7706ba1a4fc9a68383f75b72269d866fa4e3c7ec --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..9200c57e848dbc36e8250ab323edb0c57cdbf0fd --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/ContactInfoManagement/Wireframe/ContactInfoManagementWireframe.swift @@ -0,0 +1,79 @@ +// +// 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 + 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(inputData: parameters.inputData) + + let view = ContactInfoManagementContainerViewController(dependencies: .init(presenter: presenter)) + + let interactor = ContactInfoManagementInteractor(dependencies: .init( + presenter: presenter, + accountId: parameters.accountId, + accountService: dependencies.accountService, + accountDAO: dependencies.accountDAO) + ) + + 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 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 new file mode 100644 index 0000000000000000000000000000000000000000..d6a47162d1221d764f04d310c4108ece9d189ec2 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/Coordinator/AccountSettingsCoordinator.swift @@ -0,0 +1,204 @@ +// +// AccountSettingsCoordinator.swift +// Nynja +// +// Created by Ash on 10/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AccountSettingsCoordinatorDelegate: class { + func accountSettingsCoordinator(_ coordinator: AccountSettingsCoordinator, didFinishWithState 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 + + 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() + + guard let identityId = storageService.identityId, let accountId = storageService.accountId else { + return + } + + let wireframe = AccountSettingsWireframe(coordinator: self) + + let view = wireframe.prepareModule( + parameters: .init(identityId: identityId, accountId: accountId), + dependencies: .init( + accountDAO: serviceFactory.makeAccountDAO(), + accountService: serviceFactory.makeAccountService(), + imageUploader: serviceFactory.makeImageUploader() + ) + ) + + navigation.pushViewController(view, animated: true) + } + + func end() { + end(with: .dismissed) + } + + private func end(with state: 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) + } +} + +// MARK: - Account Settings + +extension AccountSettingsCoordinator: AccountSettingsCoordinatorProtocol { + + func wireframe(_ wireframe: AccountSettingsWireframe, didEndWithState state: AccountSettingsWireframe.State) { + switch state { + case .dismiss: + end(with: .dismissed) + + case let .addContactInfo(contactInfoType, accountId): + showContactInfoManagment(with: .empty(contactInfoType), for: accountId) + + case let .editContactInfo(contactInfo, accountId): + showContactInfoManagment(with: .data(contactInfo), for: accountId) + + case let .chooseAvatar(imageSource, completion): + showAvatarSelector(source: imageSource, completion: completion) + + case let .deleteAccount(identityId, accountId): + showDeleteAccount(identityId: identityId, accountId: accountId) + } + } +} + +// MARK: - Contact Info Management + +extension AccountSettingsCoordinator: ContactInfoManagementCoordinatorProtocol { + + func wireframe(_ wireframe: ContactInfoManagementWireframe, didEndWithState state: ContactInfoManagementWireframe.State) { + switch state { + case .dismiss, .finish: + navigation.popViewController(animated: true) + case let .selectCountry(callback): + selectCountryCallback = callback + showCountrySelector() + } + } +} + +// 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)) + } + 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) + } + } +} + +// MARK: - Presentation + +private extension AccountSettingsCoordinator { + + 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!, + serviceFactory: serviceFactory, + completion: { url in completion(UIImage.sd_image(with: try? Data(contentsOf: url))) } + ) + + let coordinator = SelectAvatarFlowCoordinator(dependencies: coordinatorDependencies) + coordinator.start() + } + + func showDeleteAccount(identityId: String, accountId: String) { + let wireframe = DeleteAccountWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init( + identityId: identityId, + accountId: accountId + ), + dependencies: .init( + accountService: serviceFactory.makeAccountService() + ) + ) + + navigation.pushViewController(view, animated: true) + } + + 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/Coordinator/LoginOptionsCoordinator.swift b/Nynja/Modules/Flows/Account Flow/Coordinator/LoginOptionsCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..3f84cd773da47db410451988bc4c6174fcdcb340 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/Coordinator/LoginOptionsCoordinator.swift @@ -0,0 +1,119 @@ +// +// 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 { + + let 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) + + case let .confirmProvider(confirmationData): + let wireframe = CodeConfirmationWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(confirmationData: confirmationData, isLogoVisible: false), + dependencies: .init(storageService: serviceFactory.makeStorageService(), + authService: serviceFactory.makeAuthService(), + accountService: serviceFactory.makeAccountService()) + ) + + 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) + } +} + +// MARK: - Code Confirmation + +extension LoginOptionsCoordinator: CodeConfirmationCoordinatorProtocol { + + func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) { + switch state { + case .back: + navigation.popViewController(animated: true) + case .loggedIn: + break + case .registered: + break + } + } +} diff --git a/Nynja/Modules/Flows/Account Flow/DeleteAccount/DeleteAccountProtocols.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/DeleteAccountProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..860c862241541e8d8441700e21030f0574b0c689 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/DeleteAccount/DeleteAccountProtocols.swift @@ -0,0 +1,40 @@ +// +// 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() + func handleAccountDeletion() +} + +// 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/Flows/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Entities/DeleteAccountErrors.swift new file mode 100644 index 0000000000000000000000000000000000000000..d0bb822277375fadec63af6bde37250a80589d4b --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..93c9569ff05a9bb0ade532bac7ee564e259b0b07 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Interactor/DeleteAccountInteractor.swift @@ -0,0 +1,58 @@ +// +// 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 identityId: String + + private let accountId: String + + + // MARK: - Services + + private let accountService: AccountService + + + // MARK: - Init + + 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 + } + + + // MARK: - Interactor Input + + 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.deleteIdentity(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/Flows/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..4932df23e7c4fdc9724cee2e1e0d61b2809d1b3e --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/DeleteAccount/Presenter/DeleteAccountPresenter.swift @@ -0,0 +1,81 @@ +// +// DeleteAccountPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class DeleteAccountPresenter: BasePresenter, DeleteAccountPresenterProtocol, DeleteAccountInteractorOutput { + + private weak var view: DeleteAccountViewInput? + private var interactor: DeleteAccountInteractorInput! + private var wireframe: DeleteAccountWireframeProtocol! + + + // MARK: - Presenter + + func 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() { + wireframe.dismiss() + } + + + // MARK: - Interactor Output + + func accountDidDelete() { + view?.hideLoading() + } + + 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) + } + } + } +} + +// 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/Flows/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/View/DeleteAccountViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..b197fc82278ee4cd8a4568d5c430b10cdbcd0464 --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift b/Nynja/Modules/Flows/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..a9260c4346e44be33ce34274d7c4d2c2829c02eb --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/DeleteAccount/WIreframe/DeleteAccountWireframe.swift @@ -0,0 +1,67 @@ +// +// 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 identityId: String + let accountId: String + } + + struct Dependencies { + let accountService: AccountService + } + + enum State { + case dismiss + case accountDeleted + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = DeleteAccountPresenter() + + let view = DeleteAccountViewController(dependencies: .init(presenter: presenter)) + + let interactor = DeleteAccountInteractor(dependencies: .init( + presenter: presenter, + identityId: parameters.identityId, + 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) + } + + func handleAccountDeletion() { + coordinator.wireframe(self, didEndWithState: .accountDeleted) + } +} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/Entities/LoginOption.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Entities/LoginOption.swift new file mode 100644 index 0000000000000000000000000000000000000000..151cadf2fc37f12fa252e7f55d545727cb1f4a7f --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Interactor/LoginOptionsInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..823b755c717c848bcb73f21576ceed8baa0b6fa8 --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/LoginOptions/LoginOptionsProtocols.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/LoginOptionsProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..215c59dd44c150d7567ad0886cccc842f8fbea90 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/LoginOptionsProtocols.swift @@ -0,0 +1,48 @@ +// +// LoginOptionsProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +// MARK: - Wireframe + +protocol LoginOptionsWireframeProtocol: AlertDisplayable { + func dismiss() + func addAuthProvider(ofType provider: AuthProvider) +} + +// MARK: - View + +protocol LoginOptionsViewInput: class { + 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 } + + func fetchLoginOptions() -> [LoginOption] + + func delete(_ loginOption: LoginOption) + func update(_ loginOption: LoginOption, isAvailableForSearch: Bool) +} + +// MARK: Output +protocol LoginOptionsInteractorOutput: class { + func didDelete(_ loginOption: LoginOption) + func didUpdate(_ loginOption: LoginOption) +} diff --git a/Nynja/Modules/Flows/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..9974373c426dadc96daca47bfb4fa5a4c7343be5 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/Presenter/LoginOptionsPresenter.swift @@ -0,0 +1,185 @@ +// +// LoginOptionsPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +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 = { + let fontHeight = CGFloat(22).adjustedByWidth + + return ActionRowItem( + text: String.localizable.loginOptionsAddLabel, + font: UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: fontHeight)!, + textColor: UIColor.nynja.white, + icon: UIImage.nynja.icAdd.image, + height: rowHeight, + action: { [weak self] _ in self?.addLoginOption() } + ) + }() + + 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: fontHeight)!, + 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 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 + + 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: [Alert.Action] = [ + Alert.Action(title: String.localizable.delete, style: .destructive) { [weak self] _ in + self?.delete(loginOption) + }, + Alert.Action(title: String.localizable.cancel, style: .cancel, handler: nil) + ] + let alert = Alert(style: .actionSheet, actions: actions) + wireframe.present(alert) + } + + + // MARK: - Actions + + private func addLoginOption() { + let actions: [Alert.Action] = [ + Alert.Action(title: String.localizable.loginOptionsPhoneNumberOptionActionText, style: .default) { [weak self] _ in + self?.wireframe.addAuthProvider(ofType: .phoneNumber) + }, + Alert.Action(title: String.localizable.loginOptionsEmailOptionActionText, style: .default) { [weak self] _ in + self?.wireframe.addAuthProvider(ofType: .email) + }, + Alert.Action(title: String.localizable.cancel, style: .cancel, handler: nil) + ] + let alert = Alert(title: String.localizable.loginOptionsAddAlertTitle, style: .actionSheet, actions: actions) + wireframe.present(alert) + } + + private func toggle(_ loginOption: LoginOption, isOn: Bool) { + interactor.update(loginOption, isAvailableForSearch: isOn) + } + + private func delete(_ loginOption: LoginOption) { + 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) + } + ] + let alert = Alert(message: String.localizable.loginOptionsDeleteOptionAlertMessage, style: .alert, actions: actions) + wireframe.present(alert) + } + + + // 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/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift new file mode 100644 index 0000000000000000000000000000000000000000..49fc21e3c682844d08dffcce8e361dcb45d96acf --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/AnyFieldRowItem.swift @@ -0,0 +1,40 @@ +// +// 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 additionalInset: UIEdgeInsets? { get } + + func becomeFirstResponder() -> Bool + func resignFirstResponder() -> Bool + + func makeView() -> UIView +} + +extension AnyFieldRowItem { + + var height: CGFloat? { + return nil + } + + var additionalInset: UIEdgeInsets? { + return nil + } + + func becomeFirstResponder() -> Bool { + return 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 new file mode 100644 index 0000000000000000000000000000000000000000..893d78d63fabcb0eea7883e7bbc28bbe0dde0351 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FieldRowItem/FieldRowItem.swift @@ -0,0 +1,16 @@ +// +// 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() -> UIView + func configure(_ view: 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 new file mode 100644 index 0000000000000000000000000000000000000000..c26f6eae47119b0d6a1805b7003a52411a0db1b2 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Form.swift @@ -0,0 +1,35 @@ +// +// 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: FormHeader? + var rows: [AnyFieldRowItem] + var contentInset: UIEdgeInsets + + init(header: FormHeader? = nil, rows: [AnyFieldRowItem], contentInset: UIEdgeInsets = .zero) { + self.header = header + self.rows = rows + self.contentInset = contentInset + } + } + + 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/Flows/Account Flow/LoginOptions/View/Forms/FormContainer.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormContainer.swift new file mode 100644 index 0000000000000000000000000000000000000000..cd97539e8e3908787178cc21e8ffd62214d7866d --- /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/FormHeaderView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/FormHeaderView.swift new file mode 100644 index 0000000000000000000000000000000000000000..0f601030dd842dbd64b90e2d5a34724db5581cc7 --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..1f283778f1c84ff537c7a37594d3753bbf74188b --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/ActionRowItemView.swift @@ -0,0 +1,163 @@ +// +// 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 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 + } + } + let action: Action? + + private weak var view: View? + + 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 + self.icon = icon + self.height = height + self.action = action + } + + func makeView() -> UIView { + 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/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 0000000000000000000000000000000000000000..d24667d39de02eb33a993f3cb2c9ca21607b9e31 --- /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 0000000000000000000000000000000000000000..b7e0b24a63450a2823e8dd1df4223902b4f9e967 --- /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 0000000000000000000000000000000000000000..f21b631428917d363ea314b1ae216d11d6c2c967 --- /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/SwitchRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/SwitchRowItemView.swift new file mode 100644 index 0000000000000000000000000000000000000000..42d9d86d05a11e6824b03d04ca9e54decb2880e1 --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift new file mode 100644 index 0000000000000000000000000000000000000000..ba29e5b76b3ca5a81cabb1a0050559d001087877 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextFieldRowItemView.swift @@ -0,0 +1,170 @@ +// +// 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 = (_ 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? { + 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 + 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 + + var edges: UIEdgeInsets = UIEdgeInsets( + top: 0, + left: CGFloat(16.0.adjustedByWidth), + bottom: 0, + right: CGFloat(16.0.adjustedByWidth) + ) + + 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: MTIValidator? = nil, height: CGFloat? = nil, additionalInset: UIEdgeInsets? = nil) { + self.validator = validator + self.height = height + self.additionalInset = additionalInset + } + + 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) { + let textField = view.textField + + textField.returnHandler = { [weak self] _ in + self?.returnHandler?() + return false + } + + textField.textChanged = { [weak self] field in + self?.text = field.text + } + + textField.font = font + + 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 + } + + 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 - + +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/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift new file mode 100644 index 0000000000000000000000000000000000000000..43f927ce44b605978744b980a0a0013491dddbe7 --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Forms/Items/TextRowItemView.swift @@ -0,0 +1,119 @@ +// +// 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 + 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) { + 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 + self.backgroundColor = backgroundColor + self.edges = edges + } + + func makeView() -> UIView { + 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/Flows/Account Flow/LoginOptions/View/LoginOptionsViewController.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/LoginOptionsViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..4ac8b049bb4dfc8c8c7ed422d3d6596ad693d2ee --- /dev/null +++ b/Nynja/Modules/Flows/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/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/View/Subviews/LoginOptionSwitchRowItemView.swift new file mode 100644 index 0000000000000000000000000000000000000000..dd2d7ab4b6937fa7ef81451798d7f909501e3ba1 --- /dev/null +++ b/Nynja/Modules/Flows/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() -> UIView { + 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/Flows/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift b/Nynja/Modules/Flows/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..04a7d00545c4b32f9f6c142b2dcf0b9ba95cf50b --- /dev/null +++ b/Nynja/Modules/Flows/Account Flow/LoginOptions/Wireframe/LoginOptionsWireframe.swift @@ -0,0 +1,55 @@ +// +// LoginOptionsWireframe.swift +// Nynja +// +// Created by Anton Poltoratskyi on 21.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +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) { + 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 present(_ alert: Alert, completion: (() -> Void)?) { + coordinator.present(alert, completion: completion) + } + + func dismiss() { + coordinator.wireframe(self, didEndWithState: .dismiss) + } + + func addAuthProvider(ofType provider: AuthProvider) { + coordinator.wireframe(self, didEndWithState: .addAuthProvider(provider)) + } +} diff --git a/Nynja/Modules/Flows/AppCoordinator.swift b/Nynja/Modules/Flows/AppCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..c1845c7fce6d219ecdf238272527ee5462055181 --- /dev/null +++ b/Nynja/Modules/Flows/AppCoordinator.swift @@ -0,0 +1,56 @@ +// +// AppCoordinator.swift +// Nynja +// +// Created by Anton Poltoratskyi on 11/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +protocol AppCoordinatorInput: Coordinator { + func logout() +} + +final class AppCoordinator: AppCoordinatorInput { + + private let navigation: UINavigationController + + private let serviceFactory: ServiceFactoryProtocol + + + // MARK: - Init + + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { + self.navigation = navigation + self.serviceFactory = serviceFactory + } + + func start() { + let authCoordinator = makeAuthCoordinator() + authCoordinator.start() + } + + func logout() { + let authCoordinator = makeAuthCoordinator() + authCoordinator.restart() + } + + func end() { } + + private func makeAuthCoordinator() -> AuthCoordinatorInput { + let authCoordinator = AuthCoordinator(navigation: navigation, serviceFactory: serviceFactory) + authCoordinator.delegate = self + return authCoordinator + } +} + +// MARK: - Auth Delegate + +extension AppCoordinator: AuthCoordinatorDelegate { + + func authCoordinatorDidFinish(_ coordinator: AuthCoordinator) { + MainWireFrame().presentMain(navigation: navigation, isRegistered: true) + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..e578e2d4e2df82b89fa3d9551a908f3b89181d59 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthCoordinator.swift @@ -0,0 +1,315 @@ +// +// AuthCoordinator.swift +// Nynja +// +// Created by Ash on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import SDWebImage + +protocol AuthCoordinatorDelegate: class { + func authCoordinatorDidFinish(_ coordinator: AuthCoordinator) +} + +protocol AuthCoordinatorInput: Coordinator { + func restart() +} + +final class AuthCoordinator: AuthCoordinatorInput, NavigationContainer { + + weak var delegate: AuthCoordinatorDelegate? + + let navigation: UINavigationController! + + private let serviceFactory: ServiceFactoryProtocol + + private var selectCountryCallback: ((Result) -> Void)? + + private var facebookAuthCodeCallback: ((Result) -> Void)? + + init(navigation: UINavigationController, serviceFactory: ServiceFactoryProtocol) { + self.navigation = navigation + self.serviceFactory = serviceFactory + } + + func start() { + let view = makeSplashView() + navigation.pushViewController(view, animated: false) + } + + func restart() { + let view = makeAuthView() + navigation.setViewControllers([view], animated: true) + } + + func end() { + 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() + ) + ) + return view + } +} + +// MARK: - Splash + +extension AuthCoordinator: SplashCoordinatorProtocol { + + func wireframe(_ wireframe: SplashWireframe, didEndWithState state: SplashWireframe.State) { + switch state { + case .showTutorial, .showAuth, .showCreateProfile: + // TODO: show (tutorial / create profile) if needed in new UI flow. + showAuth() + + case let .showMain(isRegistered): + let wireframe = MainWireFrame() + wireframe.presentMain(navigation: navigation, isRegistered: isRegistered) + } + } + + @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() + 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 + +extension AuthCoordinator: AuthCoordinatorProtocol { + + func wireframe(_ wireframe: AuthWireframe, didEndWithState state: AuthWireframe.State) { + switch state { + case let .continueLogin(loginFlow): + continueLoginProcess(with: loginFlow) + + case let .selectCountry(callback): + selectCountryCallback = callback + showCountrySelector() + + case let .showFacebookAuth(callback): + facebookAuthCodeCallback = callback + showFacebookAuth() + + case let .present(viewController): + navigation.present(viewController, animated: true) + + case let .dismiss(viewController): + viewController.dismiss(animated: true) + } + } + + private func continueLoginProcess(with loginFlow: LoginFlow) { + switch loginFlow { + case let .email(email): + showCodeConfirmation(with: .email(email)) + + case let .phoneNumber(numberInfo): + showCodeConfirmation(with: .phoneNumber(numberInfo)) + + case let .google(authFlowDetails), let .facebook(authFlowDetails): + switch authFlowDetails.authenticationType { + case .register: + showCreateProfile(with: authFlowDetails.accountId, prefillInfo: authFlowDetails.prefillInfo) + case .login: + end() + } + } + } + + 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 showCountrySelector() { + let wireframe = SelectCountryWireFrame(coordinator: self) + let view = wireframe.prepareModule( + dependencies: .init( + countriesProvider: serviceFactory.makeCountriesProvider() + ) + ) + navigation.pushViewController(view, animated: true) + } + + private func showFacebookAuth() { + let wireframe = FacebookAuthWireframe(coordinator: self) + let view = wireframe.prepareModule() + navigation.pushViewController(view, animated: true) + } +} + +// MARK: - Facebook Auth + +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 let .selected(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): + showCreateProfile(with: accountId) + } + } + + private func showCreateProfile(with accountId: String, prefillInfo: AuthPrefillInfo? = nil) { + let wireframe = CreateProfileWireframe(coordinator: self) + let view = wireframe.prepareModule( + parameters: .init(accountId: accountId, prefillInfo: prefillInfo), + dependencies: .init( + storageService: serviceFactory.makeStorageService(), + imageUploader: serviceFactory.makeImageUploader(), + 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() + + case let .chooseAvatar(completion): + guard let rootViewController = navigation.viewControllers.last else { return } + + 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() + } + + case let .openTerms(url): + WebFullScreenWireFrame().presentWebFullScreen(navigation: navigation, + title: String.localizable.createProfileTermsOfUse.uppercased(), + inputURL: url) + } + } + + // FIXME: should be in presenter + 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/Flows/Auth Flow/AuthModule/AuthProtocols.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..11bf72a479069dbefa3b2e78350e5dc49deb95e2 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/AuthProtocols.swift @@ -0,0 +1,60 @@ +// +// AuthProtocols.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +// MARK: - Wireframe + +protocol AuthWireframeProtocol: AlertDisplayable { + func selectCountry(completion: @escaping (Result) -> Void) + func showFacebookAuth(completion: @escaping (Result) -> Void) + func continueLogin(loginFlow: LoginFlow) + + func present(_ viewController: UIViewController) + func dismiss(_ viewController: UIViewController) +} + +// MARK: - View + +protocol AuthViewInput: LoadingInteractive where Self: UIViewController { + func select(country: Country) +} + +// MARK: - Presenter + +protocol AuthPresenterProtocol: class { + var loginOption: PlainLoginOption { get } + + func switchLoginOption() + + func loginViaFacebook() + func loginViaGoogle() + func loginViaEmail(_ email: String) + func loginViaPhoneNumber(_ phoneNumber: String, country: Country) + + func selectCountry() +} + +// MARK: - Interactor + +// MARK: Input +protocol AuthInteractorInput: class { + func fetchDefaultCountry() -> Country + + func loginViaPhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) + func loginViaEmail(_ email: String) + func loginViaFacebook(code: String) + func loginViaGoogle() +} + +// MARK: Output +protocol AuthInteractorOutput: class { + func didAuthenticated(with loginFlow: LoginFlow) + func didReceiveAuthenticationFailure(_ error: Error?) +} 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 0000000000000000000000000000000000000000..81830cac60ccd1d1baba8abe9becac471d75989c --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..b393172e10748309040d0e523c1fe82c8825b8f4 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/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(AuthFlowDetails) + case google(AuthFlowDetails) +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberFormatter.swift new file mode 100644 index 0000000000000000000000000000000000000000..2aa4b1ac7c6250a8afe6a50d4ed943646eac0ba9 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/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/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift new file mode 100644 index 0000000000000000000000000000000000000000..1d2c870651c661576674e436f94df85810b681fd --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PhoneNumberTextController.swift @@ -0,0 +1,119 @@ +// +// PhoneNumberTextController.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit +import libPhoneNumber_iOS + +struct PhoneAutoFillInfo { + let country: Country +} + +final class PhoneNumberTextController: NSObject, UITextFieldDelegate { + + var country: Country { + didSet { + self.formatter = CountryMaskFormatter(country: country) + } + } + + var validationAction: ((Bool) -> Void)? + + var autofillHandler: ((PhoneAutoFillInfo) -> Void)? + + private(set) var isValid: Bool = false + + + // MARK: - Dependencies + + private let countryProvider: CountrySearchProviding + + private let phoneNumberUtil = NBPhoneNumberUtil.sharedInstance()! + + private var formatter: PhoneNumberFormatter + + + // MARK: - Init + + init(countryProvider: CountrySearchProviding & LocalCountryProviding) { + self.country = countryProvider.fetchDefaultCountry() + self.countryProvider = countryProvider + self.formatter = CountryMaskFormatter(country: country) + } + + + // MARK: - UITextFieldDelegate + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + defer { validationAction?(isValid) } + + let currentText = textField.text ?? "" + + 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) + + country = countryProvider.fetchCountry(by: countryCode) ?? country + + let autofillInfo = PhoneAutoFillInfo(country: country) + autofillHandler?(autofillInfo) + + isValid = check(nationalNumber) + + textField.text = formattedResult(for: nationalNumber, country: country) + + return false + + } catch { } + } + + // 971231212 - without country code + let result = (currentText as NSString).replacingCharacters(in: range, with: string) + let nationalNumber = formattedResult(for: result, country: country) + + isValid = check(nationalNumber) + + let shouldChange = string == " " + + if !shouldChange { + textField.text = nationalNumber +// textField.cursorPosition = TextInputUtils.updatedCursor(for: string, in: range, currentText: currentText) + } + + return shouldChange + } + + + // MARK: - Utils + + private func extractCountryCode(from possibleNumber: String) -> String { + return phoneNumberUtil.extractCountryCode(possibleNumber, nationalNumber: nil).stringValue + } + + 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 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/Flows/Auth Flow/AuthModule/Entities/PlainLoginOption.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PlainLoginOption.swift new file mode 100644 index 0000000000000000000000000000000000000000..25718767dcbd8b76fb023a8a6e22d161a500be7b --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Entities/PlainLoginOption.swift @@ -0,0 +1,12 @@ +// +// PlainLoginOption.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum PlainLoginOption { + case phoneNumber(String) + case email(String) +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..6ecc4dfe26ace5d8182a7f96528c5274351856f8 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Interactor/AuthInteractor.swift @@ -0,0 +1,150 @@ +// +// AuthInteractor.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AuthInteractor: AuthInteractorInput, InitializeInjectable { + + private weak var presenter: AuthInteractorOutput? + + // MARK: - Services + + private let authService: AuthService + + private let accountService: AccountService + + private let googleAuthService: GoogleAuthService + + private let countriesProvider: CountriesProviding + + + // MARK: - Init + + struct Dependencies { + let presenter: AuthInteractorOutput + let authService: AuthService + let accountService: AccountService + let googleAuthService: GoogleAuthService + let countriesProvider: CountriesProviding + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + authService = dependencies.authService + accountService = dependencies.accountService + googleAuthService = dependencies.googleAuthService + countriesProvider = dependencies.countriesProvider + } + + + // MARK: - Interactor Input + + func fetchDefaultCountry() -> Country { + return countriesProvider.fetchDefaultCountry() + } + + 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) + } + } + } + + func loginViaEmail(_ email: String) { + 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 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(authDetails): + self.presenter?.didAuthenticated(with: .facebook(authDetails)) + case let .failure(error): + self.presenter?.didReceiveAuthenticationFailure(error) + } + } + } + } + + func loginViaGoogle() { + googleAuthService.signIn { [weak self] result in + switch result { + 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(authDetails): + self.presenter?.didAuthenticated(with: .google(authDetails)) + case let .failure(error): + self.presenter?.didReceiveAuthenticationFailure(error) + } + } + } + } + + /// Generic method for social auth processing + /// + /// - Parameters: + /// - 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: + accountService.getAccount(accountId: 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(details)) + } catch { + completion(.failure(error)) + } + case let .failure(error): + completion(.failure(error)) + } + } + case .register: + completion(.success(details)) + } + 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 new file mode 100644 index 0000000000000000000000000000000000000000..597aaee4a7fc0849a7c90034bd61643283f04cf2 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Presenter/AuthPresenter.swift @@ -0,0 +1,164 @@ +// +// AuthPresenter.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +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("") + + func switchLoginOption() { + switch loginOption { + case .email: + loginOption = .phoneNumber("") + case .phoneNumber: + loginOption = .email("") + } + } + + func loginViaFacebook() { + wireframe.showFacebookAuth { [weak self] result in + guard case let .success(code) = result else { + return + } + self?.view?.showLoading() + self?.interactor?.loginViaFacebook(code: code) + } + } + + func loginViaGoogle() { + view?.showLoading() + interactor.loginViaGoogle() + } + + func loginViaEmail(_ email: String) { + let email = email.trimmed() + + confirmInputData(for: .email(email)) { isConfirmed in + if isConfirmed { + self.view?.showLoading() + self.interactor.loginViaEmail(email) + } + } + } + + func loginViaPhoneNumber(_ phoneNumber: String, country: Country) { + let numberInfo = PhoneNumberInfo(country: country, number: phoneNumber.replacingOccurrences(of: " ", with: "")) + + confirmInputData(for: .phoneNumber(numberInfo.displayString)) { isConfirmed in + if isConfirmed { + self.view?.showLoading() + self.interactor.loginViaPhoneNumber(numberInfo) + } + } + } + + func selectCountry() { + wireframe.selectCountry { [weak self] result in + guard case let .success(country) = result else { + return + } + self?.view?.select(country: country) + } + } + + 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.authPopupConfirmAction, 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 + +extension AuthPresenter { + + func googleAuthWillStart(_ googleAuthService: GoogleAuthService) { + view?.hideLoading() + } + + 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 { + + func didAuthenticated(with loginFlow: LoginFlow) { + view?.hideLoading() + wireframe?.continueLogin(loginFlow: loginFlow) + } + + func didReceiveAuthenticationFailure(_ error: Error?) { + view?.hideLoading() + + 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) + } +} + +// MARK: - Injection + +extension AuthPresenter: SetInjectable { + struct Dependencies { + let view: AuthViewInput + let interactor: AuthInteractorInput + let wireframe: AuthWireframeProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..1560ffe4a6ca494690c7390370ff2eff121b02ed --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/AuthViewController.swift @@ -0,0 +1,372 @@ +// +// AuthViewController.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +final class AuthViewController: UIViewController, AuthViewInput, InitializeInjectable, KeyboardInteractive, LoadingDisplayable { + + private let presenter: AuthPresenterProtocol + + private let phoneNumberTextController: PhoneNumberTextController + + private let emailValidator = EmailValidator() + + + // MARK: - Views + + private lazy var backgroundImageView: UIImageView = { + let backgroundImageView = UIImageView() + + view.addSubview(backgroundImageView) + backgroundImageView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return backgroundImageView + }() + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + + // MARK: Scroll Container + + 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 { maker in + maker.top.left.right.equalToSuperview() + } + + return header + }() + + // MARK: Center Content + + private lazy var loginContainerView: UIView = { + let containerView = UIView() + + contentView.addSubview(containerView) + containerView.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + + maker.top.equalTo(headerView.snp.bottom) + maker.left.right.equalToSuperview().inset(horizontal) + } + + return containerView + }() + + private lazy var emailContainerView = makeEmailLoginView(on: loginContainerView) + + private lazy var phoneContainerView = makePhoneNumberLoginView(on: loginContainerView, country: phoneNumberTextController.country) + + 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) + + 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 + let horizontal = Constraints.horizontal + + maker.top.equalTo(loginContainerView.snp.bottom).offset(24) + maker.left.right.equalToSuperview().inset(horizontal) + maker.height.equalTo(44) + } + + return button + }() + + // 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(nextButton.snp.bottom).offset(32) + maker.top.equalTo(nextButton.snp.bottom).offset(32).priority(.high) + } + + return label + }() + + private lazy var bottomView: LoginOptionsView = { + let bottomView = LoginOptionsView() + + bottomView.configure(config: LoginOptionsView.Config( + loginOption: presenter.loginOption, + switchLoginAction: { [unowned self] () -> PlainLoginOption in + self.presenter.switchLoginOption() + + let loginOption = self.presenter.loginOption + + switch loginOption { + case .email: + self.showEmailLogin(animated: true) + case .phoneNumber: + self.showPhoneNumberLogin(animated: true) + } + + return loginOption + }, + facebookLoginAction: { [weak presenter] in presenter?.loginViaFacebook() }, + googleLoginAction: { [weak presenter] in presenter?.loginViaGoogle() }) + ) + + contentView.addSubview(bottomView) + bottomView.snp.makeConstraints { maker in + let horizontal = Constraints.horizontal + + maker.top.equalTo(alternativeLabel.snp.bottom).offset(32) + maker.left.right.equalToSuperview().inset(horizontal) + maker.bottom.equalToSuperview().inset(30 + UIWindow.safeAreaBottomPadding()) + } + + return bottomView + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: AuthPresenterProtocol + let phoneNumberController: PhoneNumberTextController + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + phoneNumberTextController = dependencies.phoneNumberController + super.init(nibName: nil, bundle: nil) + } + + 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, contentView, bottomView] + + nextButton.isEnabled = false + + showPhoneNumberLogin(animated: false) + + emailContainerView.snp.makeConstraints { maker in + maker.height.equalTo(phoneContainerView) + } + + enableKeyboardHidingWhenTappedAround() + + setupPhoneNumberController() + setupEmailController() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unregisterForKeyboardNotifications() + } + + + // MARK: - Setup + + private func setupPhoneNumberController() { + phoneNumberTextController.validationAction = { [weak self] result in + self?.nextButton.isEnabled = result + } + } + + private func setupEmailController() { + emailValidator.validationHandler = { [weak self] result in + self?.nextButton.isEnabled = result + } + } +} + +// MARK: - View Input + +extension AuthViewController { + + func select(country: Country) { + phoneNumberTextController.country = country + phoneNumberLoginView.selectCountry(country) + } +} + +// MARK: - KeyboardInteractive + +extension AuthViewController { + + func keyboardNotified(endFrame: CGRect) { + scrollView.contentInset.bottom = view.bounds.height - endFrame.minY + } +} + +// MARK: - Animations + +private extension AuthViewController { + + @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: phoneNumberTextController.country) + } + } + + 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 { + let nextButtonEnabled = phoneNumberTextController.isValid + animateChangingViews(first: emailContainerView, second: phoneContainerView, isNextActionEnabled: nextButtonEnabled) + } else { + emailContainerView.isHidden = true + phoneContainerView.isHidden = false + } + } + + func showEmailLogin(animated: Bool) { + if animated { + let nextButtonEnabled = emailValidator.isValid + animateChangingViews(first: phoneContainerView, second: emailContainerView, isNextActionEnabled: nextButtonEnabled) + } else { + emailContainerView.isHidden = false + phoneContainerView.isHidden = true + } + } + + func makeEmailLoginView(on view: UIView) -> DetailContainerView { + let loginView = EmailLoginView(validator: emailValidator) + + let container = DetailContainerView(contentView: loginView) + container.detailsLabel.text = String.localizable.authEnterEmailAddressComment + + view.addSubview(container) + container.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + return container + } + + func makePhoneNumberLoginView(on view: UIView, country: Country) -> DetailContainerView { + let loginView = PhoneNumberLoginView(textController: phoneNumberTextController) + + let container = DetailContainerView(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() + } + )) + + 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/AuthHeaderView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift new file mode 100644 index 0000000000000000000000000000000000000000..e613e99732f43458cf63fb0b4fd6f3742762380d --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/AuthHeaderView.swift @@ -0,0 +1,94 @@ +// +// AuthHeaderView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class AuthHeaderView: UIView, Configurable { + + // MARK: - Views + + private(set) lazy var welcomeLabel = makeWelcomeLabel() + private(set) lazy var logoImageView = makeLogoImageView() + + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + + welcomeLabel.setContentHuggingPriority(.required, for: .vertical) + welcomeLabel.setContentCompressionResistancePriority(.required, for: .vertical) + + _ = [welcomeLabel, logoImageView] + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Layout + +private extension AuthHeaderView { + + 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 + + addSubview(label) + label.snp.makeConstraints { maker in + maker.top.equalToSuperview().offset(top) + maker.centerX.equalToSuperview() + } + + return label + } + + func makeLogoImageView() -> UIImageView { + let imageView = UIImageView() + + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage.nynja.logo2.image + + addSubview(imageView) + imageView.snp.makeConstraints { maker in + maker.centerX.equalToSuperview() + 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 + +extension AuthHeaderView { + + func configure(config: Config) { + backgroundColor = UIColor.nynja.clear + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/DetailContainerView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/DetailContainerView.swift new file mode 100644 index 0000000000000000000000000000000000000000..1a77de3f0757a353b94d8578447090a870292003 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/DetailContainerView.swift @@ -0,0 +1,68 @@ +// +// DetailContainerView.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 DetailContainerView: 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() + maker.right.equalToSuperview() + maker.bottom.lessThanOrEqualToSuperview() + maker.bottom.equalToSuperview().priority(.high) + } + + return label + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift new file mode 100644 index 0000000000000000000000000000000000000000..a27cdf83425b4d0d0074d3ffc87a2b109bb41d47 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/EmailLoginView.swift @@ -0,0 +1,74 @@ +// +// EmailLoginView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class EmailLoginView: UIView { + + // MARK: - Views + + private(set) lazy var inputField = makeInputField() + + private let validator: EmailValidator + + + // MARK: - Initvalidator + + init(validator: EmailValidator) { + self.validator = validator + + super.init(frame: .zero) + + inputField.isHidden = false + + setup() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Setup + + private func setup() { + inputField.validators = [validator] + } +} + +// MARK: - Layout + +private extension EmailLoginView { + + 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 + + addSubview(textField) + textField.snp.makeConstraints { maker in + maker.top.bottom.equalToSuperview() + maker.left.equalToSuperview() + maker.right.equalToSuperview() + maker.height.equalTo(64) + } + + return textField + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift new file mode 100644 index 0000000000000000000000000000000000000000..6a045ec91df5a56a7ec364500617d65c5cdda278 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/LoginOptionsView.swift @@ -0,0 +1,158 @@ +// +// LoginOptionsView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class LoginOptionsView: UIView, Configurable { + + // MARK: - Views + + private lazy var switchLoginButton = makeSwitchLoginButton() + private lazy var loginWithFacebook = makeLoginWithFacebookButton() + private lazy var loginWithGoogle = makeLoginWithGoogleButton() + + private var switchLoginAction: (() -> PlainLoginOption)? + private var facebookLoginAction: (() -> Void)? + private var googleLoginAction: (() -> Void)? + + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + switchLoginButton.isHidden = false + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Configurable + +extension LoginOptionsView { + struct Config { + let loginOption: PlainLoginOption + let switchLoginAction: () -> PlainLoginOption + let facebookLoginAction: () -> Void + let googleLoginAction: () -> Void + } + + func configure(config: LoginOptionsView.Config) { + backgroundColor = UIColor.nynja.clear + + switchLoginAction = config.switchLoginAction + facebookLoginAction = config.facebookLoginAction + googleLoginAction = config.googleLoginAction + + updateSwitchButton(loginOption: config.loginOption) + + _ = [switchLoginButton, loginWithFacebook, loginWithGoogle] + } +} + +// MARK: - Actions + +private extension LoginOptionsView { + @objc func switchLogin(sender: UIButton) { + guard let loginOption = switchLoginAction?() else { + return + } + updateSwitchButton(loginOption: loginOption) + } + + @objc func loginWithFacebook(sender: UIButton) { + facebookLoginAction?() + } + + @objc func loginWithGoogle(sender: UIButton) { + googleLoginAction?() + } + + func updateSwitchButton(loginOption: PlainLoginOption) { + switch loginOption { + case .email: + switchLoginButton.setTitle(String.localizable.authLoginWithPhoneNumber.uppercased(), for: .normal) + switchLoginButton.setImage(UIImage.nynja.iconsGeneralIcAcceptCall.image, for: .normal) + case .phoneNumber: + switchLoginButton.setTitle(String.localizable.authLoginWithEmail.uppercased(), for: .normal) + switchLoginButton.setImage(UIImage.nynja.iconsGeneralIcEmail.image, for: .normal) + } + } +} + +// 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() + maker.right.equalToSuperview() + 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() + maker.right.equalToSuperview() + 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() + maker.right.equalToSuperview() + maker.height.equalTo(44) + } + + return button + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift new file mode 100644 index 0000000000000000000000000000000000000000..59e389ee2635f0f42f9aa075f27b28354c59f28e --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/View/Subviews/PhoneNumberLoginView.swift @@ -0,0 +1,212 @@ +// +// PhoneNumberLoginView.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +final class PhoneNumberLoginView: UIView, Configurable { + + // MARK: - Views + + private lazy var countrySelector = makeCountrySelector() + + private lazy var countryCodeContainer = makeCountryCodeContainer() + private lazy var countryCodeField = makeCountryCodeField() + + private lazy var phoneNumberContainer = makePhoneNumberContainer() + private(set) lazy var phoneNumberTextField = makePhoneNumberTextField() + + private let textController: PhoneNumberTextController + + private var country: Country? + private var countrySelectorAction: (() -> Void)? + + + // MARK: - Init + + init(textController: PhoneNumberTextController) { + self.textController = textController + super.init(frame: .zero) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Configurable + +extension PhoneNumberLoginView { + struct Config { + let country: Country + let countrySelectorAction: (() -> Void)? + } + + func configure(config: Config) { + country = config.country + countrySelectorAction = config.countrySelectorAction + phoneNumberTextField.delegate = textController + selectCountry(config.country) + + textController.autofillHandler = { [weak self] autofillInfo in + self?.updatePhoneCountry(autofillInfo.country) + } + + _ = [countrySelector, countryCodeContainer, countryCodeField, phoneNumberContainer, phoneNumberTextField] + } +} + +// MARK: - Public + +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) + + // FIXME: remove default mask + let placeholder = "".updateWithMask(placeHolder: country.placeHolder ?? country.defaultMask) + phoneNumberTextField.attributedPlaceholder = NSAttributedString( + string: placeholder, + attributes: [ + .font: phoneNumberTextField.font!, + .foregroundColor: phoneNumberTextField.textColor! + ] + ) + } + + private func updatePhoneCountry(_ country: Country) { + updateCountryInfo(for: country) + } + + func selectCountry(_ country: Country) { + updatePhoneCountry(country) + phoneNumberTextField.text = "" + } + + func updatePhoneNumber(_ phoneNumberInfo: PhoneNumberInfo) { + updatePhoneCountry(phoneNumberInfo.country) + phoneNumberTextField.text = phoneNumberInfo.number + } +} + +// MARK: - Actions + +private extension PhoneNumberLoginView { + @objc func changeCountry(sender: UIButton) { + countrySelectorAction?() + } +} + +// 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.top.equalToSuperview() + maker.left.equalToSuperview() + maker.right.equalToSuperview() + maker.height.equalTo(64) + } + + 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.bottom.equalToSuperview() + maker.width.equalTo(84) + 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.centerY.equalToSuperview() + maker.left.equalToSuperview() + maker.right.equalToSuperview().inset(16) + } + + return field + } + + func makePhoneNumberContainer() -> UIView { + let container = UIView() + container.backgroundColor = UIColor.nynja.clear + + let left = countryCodeContainer + + 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) + } + + return container + } + + 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.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 + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(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 new file mode 100644 index 0000000000000000000000000000000000000000..1145eca49ee78968c9151cf119306b93a3e9eab7 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/AuthModule/Wireframe/AuthWireframe.swift @@ -0,0 +1,86 @@ +// +// AuthWireframe.swift +// Nynja +// +// Created by Ash on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +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) { + self.coordinator = coordinator + } + + struct Dependencies { + let authService: AuthService + let accountService: AccountService + let googleAuthService: GoogleAuthService + let countriesProvider: CountriesProviding + } + + enum State { + case selectCountry(callback: (Result) -> Void) + case showFacebookAuth(callback: (Result) -> Void) + case continueLogin(loginFlow: LoginFlow) + case present(UIViewController) + case dismiss(UIViewController) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = AuthPresenter() + + let view = AuthViewController( + dependencies: .init( + presenter: presenter, + phoneNumberController: PhoneNumberTextController(countryProvider: dependencies.countriesProvider) + ) + ) + + let interactor = AuthInteractor(dependencies: .init( + presenter: presenter, + authService: dependencies.authService, + accountService: dependencies.accountService, + googleAuthService: dependencies.googleAuthService, + countriesProvider: dependencies.countriesProvider) + ) + + presenter.inject(dependencies: .init(view: view, interactor: interactor, wireframe: self)) + + 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 showFacebookAuth(completion: @escaping (Result) -> Void) { + coordinator.wireframe(self, didEndWithState: .showFacebookAuth(callback: completion)) + } + + func continueLogin(loginFlow: LoginFlow) { + coordinator.wireframe(self, didEndWithState: .continueLogin(loginFlow: loginFlow)) + } + + func present(_ viewController: UIViewController) { + coordinator.wireframe(self, didEndWithState: .present(viewController)) + } + + func dismiss(_ viewController: UIViewController) { + coordinator.wireframe(self, didEndWithState: .dismiss(viewController)) + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..6f24ab621f191e3e5e5143f86462c0099ecae013 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/CodeConfirmationProtocols.swift @@ -0,0 +1,62 @@ +// +// CodeConfirmationProtocols.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +// MARK: - Wireframe + +protocol CodeConfirmationWireframeProtocol: AlertDisplayable { + func continueSignUpFlow(with accountId: String) + func continueSuccessAuthentication() + func back() +} + +// MARK: - View + +protocol CodeConfirmationViewInput: LoadingInteractive where Self: UIViewController { + func updateTimerLabel(text: String) + func showButtons() +} + +// 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 CodeConfirmationInteractorInput: 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 CodeConfirmationInteractorOutput: class { + func didResendCode() + func didReceiveResendCodeFailure(_ error: Error) + + func didConfirmCode(response: AuthResponse) + func didSaveAccount() + func didReceiveFailure(_ error: Error?) +} diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift new file mode 100644 index 0000000000000000000000000000000000000000..5b2853ba8987f103617db95710fa783ec8e6fcb4 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Entities/ConfirmationData.swift @@ -0,0 +1,12 @@ +// +// ConfirmationData.swift +// Nynja +// +// Created by Anton Poltoratskyi on 05.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum ConfirmationData { + case email(String) + case phoneNumber(PhoneNumberInfo) +} diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..c5cb65028bf5ff09cce74ff7329dea9dc3211d72 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Interactor/CodeConfirmationInteractor.swift @@ -0,0 +1,116 @@ +// +// CodeConfirmationInteractor.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class CodeConfirmationInteractor: CodeConfirmationInteractorInput, InitializeInjectable { + + private weak var presenter: CodeConfirmationInteractorOutput? + + let confirmationData: ConfirmationData + + var address: String { + switch confirmationData { + case let .email(email): + return email + case let .phoneNumber(phoneNumberInfo): + return phoneNumberInfo.displayString + } + } + + + // MARK: - Services + + private let storageService: StorageService + + private let authService: AuthService + + private let accountService: AccountService + + + // MARK: - Init + + struct Dependencies { + let presenter: CodeConfirmationInteractorOutput + 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 + } + + + // MARK: - Interactor Input + + func sendConfirmationCode(_ code: String) { + authService.confirmNynjaCode(code) { [weak self] result in + switch result { + case let .success(response): + self?.presenter?.didConfirmCode(response: response) + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + } + + func loadAccount(by accountId: String) { + accountService.getAccount(accountId: 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?.didReceiveFailure(error) + } + } + } + + func resendCode() { + switch confirmationData { + 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() { + guard case let .phoneNumber(phoneNumberInfo) = confirmationData else { + return + } + authService.login(by: phoneNumberInfo, confirmVia: .call) { [weak self] result in + self?.handleResendCodeResponse(result) + } + } + + private func handleResendCodeResponse(_ result: Result) { + switch result { + case .success: + presenter?.didResendCode() + case let .failure(error): + presenter?.didReceiveResendCodeFailure(error) + } + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..30c9f417c0bc57016bd55c6c12775598ed070cd9 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Presenter/CodeConfirmationPresenter.swift @@ -0,0 +1,162 @@ +// +// CodeConfirmationPresenter.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +final class CodeConfirmationPresenter: CodeConfirmationPresenterProtocol, CodeConfirmationInteractorOutput, SetInjectable { + + private weak var view: CodeConfirmationViewInput? + private var interactor: CodeConfirmationInteractorInput! + private var wireframe: CodeConfirmationWireframe! + + private var timer: Timer? + + private var timerValue = 0 { + didSet { + if timerValue > 60 { + 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: String.localizable.codeConfirmationShouldReceiveInSeconds(timerValue)) + } + + if timerValue == 0 { + view?.showButtons() + invalidateTimer() + } + } + } + + var canAskForCall: Bool { + if case .phoneNumber = interactor.confirmationData { + return true + } + return false + } + + var address: String { + return interactor.address + } + + var descriptionText: String { + switch interactor.confirmationData { + case .phoneNumber: + return String.localizable.codeConfirmationCodeSentToPhone + case .email: + return String.localizable.codeConfirmationCodeSentToEmail + } + } + + + deinit { + invalidateTimer() + } + + private func invalidateTimer() { + timer?.invalidate() + timer = nil + } + + func viewDidLoad() { + switch interactor.confirmationData { + case .email: + timerValue = 15 * 60 + case .phoneNumber: + timerValue = 60 + } + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.timerValue = self.timerValue - 1 + } + } + + func viewDidDisappear() { + invalidateTimer() + } + + func sendConfirmationCode(_ code: String) { + view?.showLoading() + interactor.sendConfirmationCode(code) + } + + func resendCode() { + view?.showLoading() + interactor?.resendCode() + } + + func askForCall() { + guard canAskForCall else { + return + } + view?.showLoading() + interactor?.askForCall() + } + + func back() { + wireframe?.back() + } +} + +// MARK: - Interactor Output + +extension CodeConfirmationPresenter { + + func didResendCode() { + view?.hideLoading() + } + + func didReceiveResendCodeFailure(_ error: Error) { + view?.hideLoading() + } + + 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.continueSuccessAuthentication() + } + + func didReceiveFailure(_ error: Error?) { + view?.hideLoading() + + 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) + } +} + +// MARK: - SetInjectable + +extension CodeConfirmationPresenter { + struct Dependencies { + let view: CodeConfirmationViewInput + let interactor: CodeConfirmationInteractorInput + let wireframe: CodeConfirmationWireframe + } + + func inject(dependencies: CodeConfirmationPresenter.Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..bf905fb653379c16430d2bf0143a0ae7d41c607c --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/View/CodeConfirmationViewController.swift @@ -0,0 +1,270 @@ +// +// CodeConfirmationViewController.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +final class CodeConfirmationViewController: UIViewController, CodeConfirmationViewInput, LoadingDisplayable, InitializeInjectable { + + private let presenter: CodeConfirmationPresenterProtocol + + private let isLogoVisible: Bool + + + // MARK: - Views + + private lazy var backButton = makeBackButton(on: view, target: self, selector: #selector(back(sender:))) + + 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 codeInputView: SecureCodeContainerView = { + let titleFontHeight = Constraints.titleLabel.fontHeight + let textFontHeight = Constraints.codeInputView.fontHeight + let descriptionFontHeight = Constraints.descriptionLabel.fontHeight + + 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) + maker.centerX.equalToSuperview() + maker.left.greaterThanOrEqualToSuperview() + maker.right.lessThanOrEqualToSuperview() + } + + return codeInputView + }() + + private lazy var timerLabel = makeTimerLabel(on: view) + + private weak var resendCodeButton: UIButton? + private weak var callMeButton: UIButton? + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + + // MARK: - Init + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + isLogoVisible = dependencies.isLogoVisible + super.init(nibName: nil, bundle: nil) + } + + 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() + + _ = [headerView, codeInputView, timerLabel, resendCodeButton, callMeButton, backButton] + view.backgroundColor = UIColor.nynja.backgroundColor + + headerView.isHidden = !isLogoVisible + + 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() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + presenter.viewDidDisappear() + } +} + +// MARK: - View Input + +extension CodeConfirmationViewController { + + func updateTimerLabel(text: String) { + timerLabel.text = text + } + + func showButtons() { + timerLabel.isHidden = true + resendCodeButton = makeResendCodeButton(on: view, target: self, selector: #selector(resendCode(sender:))) + + if presenter.canAskForCall { + callMeButton = makeCallMeButton(on: view, top: resendCodeButton!, target: self, selector: #selector(callMe(sender:))) + } + } +} + +// MARK: - Actions + +private 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 + let isLogoVisible: Bool + } +} + +// MARK: - Layout + +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 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: fontHeight) + + 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 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: fontHeight) + 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 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: fontHeight) + 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 + } + + // FIXME: add adjust if needed + enum Constraints { + + enum titleLabel { + static let fontHeight: CGFloat = 22.0 + } + + enum descriptionLabel { + static let fontHeight: CGFloat = 20.0 + } + + enum codeInputView { + 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/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..4250610a49f750e25830f7d777a8bb57ba2d1de2 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CodeConfirmation/Wireframe/CodeConfirmationWireframe.swift @@ -0,0 +1,74 @@ +// +// CodeConfirmationWireframe.swift +// Nynja +// +// Created by Ash on 9/30/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaUIKit + +protocol CodeConfirmationCoordinatorProtocol: AlertDisplayable { + func wireframe(_ wireframe: CodeConfirmationWireframe, didEndWith state: CodeConfirmationWireframe.State) +} + +final class CodeConfirmationWireframe: Wireframe, CodeConfirmationWireframeProtocol { + + private let coordinator: CodeConfirmationCoordinatorProtocol + + init(coordinator: CodeConfirmationCoordinatorProtocol) { + self.coordinator = coordinator + } + + struct Parameters { + let confirmationData: ConfirmationData + let isLogoVisible: Bool + } + + struct Dependencies { + let storageService: StorageService + let authService: AuthService + let accountService: AccountService + } + + enum State { + 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, isLogoVisible: true)) + + let interactor = CodeConfirmationInteractor(dependencies: .init( + presenter: presenter, + confirmationData: parameters.confirmationData, + storageService: dependencies.storageService, + authService: dependencies.authService, + 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 continueSignUpFlow(with accountId: String) { + coordinator.wireframe(self, didEndWith: .registered(accountId: accountId)) + } + + func continueSuccessAuthentication() { + coordinator.wireframe(self, didEndWith: .loggedIn) + } + + func back() { + coordinator.wireframe(self, didEndWith: .back) + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..65c3227fa246d3b7d9dd186426fec8fd15ef7e16 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/CreateProfileProtocols.swift @@ -0,0 +1,49 @@ +// +// CreateProfileProtocols.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +// MARK: - Wireframe + +protocol CreateProfileWireframeProtocol: class { + func back() + func end() + func chooseAvatar(completion: @escaping (UIImage?) -> Void) + func open(url: URL) +} + +// MARK: - View + +protocol CreateProfileViewInput: LoadingInteractive { + func setup(form: Form) + func setTermsAccepted(_ isAccepted: Bool) + func setActionEnabled(_ isEnabled: Bool) +} + +// MARK: - Presenter + +protocol CreateProfilePresenterProtocol: NavigationProtocol { + func viewDidLoad() + + func open(url: URL) + func acceptTerms(_ isAccepted: Bool) + func createAccount() +} + +// MARK: - Interactor + +// MARK: Input +protocol CreateProfileInteractorInput: class { + func createAccount(from viewModel: CreateProfileViewModel) +} + +// MARK: Output +protocol CreateProfileInteractorOutput: class { + 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 0000000000000000000000000000000000000000..0f59891f2832e056f11a3b0371726d3996ade3ab --- /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/Interactor/CreateProfileInteractor.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..b17033e9f66ac0bbea0e86986a5726aa90d1223c --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Interactor/CreateProfileInteractor.swift @@ -0,0 +1,99 @@ +// +// CreateProfileInteractor.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class CreateProfileInteractor: CreateProfileInteractorInput, InitializeInjectable { + + private weak var presenter: CreateProfileInteractorOutput? + + // MARK: - User Info + + private let accountId: String + + + // MARK: - Services + + private let storageService: StorageService + + private let imageUploader: ImageUploader + + private let authService: AuthService + + private let accountService: AccountService + + + // MARK: - Init + + struct Dependencies { + let presenter: CreateProfileInteractorOutput + let accountId: String + let storageService: StorageService + let imageUploader: ImageUploader + let authService: AuthService + let accountService: AccountService + } + + init(dependencies: CreateProfileInteractor.Dependencies) { + presenter = dependencies.presenter + accountId = dependencies.accountId + storageService = dependencies.storageService + imageUploader = dependencies.imageUploader + authService = dependencies.authService + accountService = dependencies.accountService + } + + + // MARK: - Interactor Input + + 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, viewModel: viewModel) + case let .failure(error): + self?.presenter?.didReceiveFailure(error) + } + } + case let .url(avatarURL): + createAccount(withAvatar: avatarURL, viewModel: viewModel) + } + } + + private func createAccount(withAvatar avatarURL: URL?, viewModel: CreateProfileViewModel) { + let accountInfo = AccountInfo(accountId: accountId, + avatar: avatarURL?.absoluteString, + accountMark: nil, + accountName: nil, + firstName: viewModel.firstName, + lastName: viewModel.lastName, + username: viewModel.username, + accountStatus: .enabled, + roles: nil, + qrCode: accountId, + 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) + } + } + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..4498f285485b367f1ed5b33d694f448cef66bf60 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Presenter/CreateProfilePresenter.swift @@ -0,0 +1,169 @@ +// +// CreateProfilePresenter.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class CreateProfilePresenter: CreateProfileInteractorOutput, CreateProfilePresenterProtocol, SetInjectable { + + private weak var view: CreateProfileViewInput? + private var interactor: CreateProfileInteractorInput! + private var wireframe: CreateProfileWireframeProtocol! + + 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 + + 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 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 back() { + wireframe.back() + } + + func open(url: URL) { + wireframe.open(url: url) + } + + 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) + } + + 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() + } + + func didReceiveFailure(_ error: Error?) { + view?.hideLoading() + } +} + +// MARK: - Injection + +extension CreateProfilePresenter { + struct Dependencies { + let wireframe: CreateProfileWireframe + let interactor: CreateProfileInteractorInput + let view: CreateProfileViewInput + } + + func inject(dependencies: CreateProfilePresenter.Dependencies) { + wireframe = dependencies.wireframe + interactor = dependencies.interactor + view = dependencies.view + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..500f6eed9a91bd21c328cd7278fcef90d02aeca8 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/View/CreateProfileViewController.swift @@ -0,0 +1,314 @@ +// +// CreateProfileViewController.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +final class CreateProfileViewController: BaseVC, CreateProfileViewInput, FormContainer, KeyboardInteractive, LoadingDisplayable, InitializeInjectable { + + private let presenter: CreateProfilePresenterProtocol + + // MARK: - Views + + private(set) lazy var progressHUD = makeProgressHUD(on: view) + + // 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 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 termsTextView: UITextView = { + let textView = makeTermsOfUseTextView(on: termsContainerView) + + textView.snp.makeConstraints { maker in + let left = Constraints.termsTextView.left + maker.centerY.equalToSuperview() + maker.left.equalTo(checkBox.snp.right).offset(left) + maker.right.equalToSuperview() + } + + return textView + }() + + // MARK: Button + + private let bottomInset: CGFloat = Constraints.createButton.bottom + + private lazy var controlContainerView: NynjaControlContainerView = { + let containerView = NynjaControlContainerView(contentView: createButton) + + view.addSubview(containerView) + containerView.snp.makeConstraints { maker in + maker.top.equalTo(scrollView.snp.bottom) + maker.left.right.equalToSuperview() + adjustVerticalInset(.bottom, make: maker, offset: -bottomInset) + } + + 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 + }() + + + // MARK: - Init + + struct Dependencies { + let presenter: CreateProfilePresenterProtocol + } + + 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 initialize() { + super.initialize() + setupUI() + presenter.viewDidLoad() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + registerForKeyboardNotifications() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unregisterForKeyboardNotifications() + } + + override func tapOnScreen(recognizer: UITapGestureRecognizer) { + super.tapOnScreen(recognizer: recognizer) + view.endEditing(true) + } + + + // 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) + ) + + _ = [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) + + termsTextView.delegate = self + } + + + // MARK: - Actions + + @objc func createAccount(sender: UIButton) { + presenter.createAccount() + } +} + +// MARK: - View Input + +extension CreateProfileViewController { + + func setTermsAccepted(_ isAccepted: Bool) { + checkBox.isChecked = isAccepted + } + + func setActionEnabled(_ isEnabled: Bool) { + createButton.isEnabled = isEnabled + } +} + +// MARK: - KeyboardInteractive + +extension CreateProfileViewController { + + func keyboardNotified(endFrame: CGRect) { + if endFrame.origin.y >= UIScreen.main.bounds.size.height { + updateToHide(view: controlContainerView, offset: -bottomInset) + } else { + updateToShow(view: controlContainerView, offset: -bottomInset - endFrame.height) + } + } +} + +// 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: - Layout + +private extension CreateProfileViewController { + + 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/Wireframe/CreateProfileWireframe.swift b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..9f73b8fc030865c16e4d7cc4495555e9b698d366 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/CreateProfile/Wireframe/CreateProfileWireframe.swift @@ -0,0 +1,78 @@ +// +// CreateProfileWireframe.swift +// Nynja +// +// Created by Ash on 10/11/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol CreateProfileCoordinatorProtocol: class { + func wireframe(_ wireframe: CreateProfileWireframe, didEndWithState state: CreateProfileWireframe.State) +} + +final class CreateProfileWireframe: Wireframe, CreateProfileWireframeProtocol { + + enum State { + case back + case next + case chooseAvatar(completion: (UIImage?) -> Void) + case openTerms(url: URL) + } + + struct Parameters { + let accountId: String + let prefillInfo: AuthPrefillInfo? + } + + struct Dependencies { + let storageService: StorageService + let imageUploader: ImageUploader + let authService: AuthService + let accountService: AccountService + } + + private let coordinator: CreateProfileCoordinatorProtocol + + init(coordinator: CreateProfileCoordinatorProtocol) { + self.coordinator = coordinator + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = CreateProfilePresenter(prefillInfo: parameters.prefillInfo) + + let view = CreateProfileViewController(dependencies: .init( + presenter: presenter) + ) + + let interactor = CreateProfileInteractor(dependencies: .init( + presenter: presenter, + accountId: parameters.accountId, + storageService: dependencies.storageService, + imageUploader: dependencies.imageUploader, + authService: dependencies.authService, + accountService: dependencies.accountService) + ) + + presenter.inject(dependencies: .init(wireframe: self, interactor: interactor, view: view)) + + 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)) + } + + func open(url: URL) { + coordinator.wireframe(self, didEndWithState: .openTerms(url: url)) + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/Facebook/FacebookAuthProtocols.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/FacebookAuthProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..08d58a8c1ec0af81de00c57f9150580736256567 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Facebook/FacebookAuthProtocols.swift @@ -0,0 +1,44 @@ +// +// FacebookAuthProtocols.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +// MARK: - Wireframe + +protocol FacebookAuthWireframeProtocol: class { + func finishAuthentication(code: String) + func dismiss() +} + +// MARK: - View + +protocol FacebookAuthViewInput: class { + func load(_ request: URLRequest) +} + +// MARK: - Presenter + +protocol FacebookAuthPresenterProtocol: class { + func viewDidLoad() + func dismiss() + func handleRedirect(to url: URL) -> Bool +} + +// MARK: - Interactor + +// MARK: Input +protocol FacebookAuthInteractorInput: class { + func authenticate() + func handleRedirect(to url: URL) -> Bool +} + +// MARK: Output +protocol FacebookAuthInteractorOutput: class { + func load(_ request: URLRequest) + func didAuthenticated(code: String) +} diff --git a/Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/Intreractor/FacebookAuthInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..01c7110092ed07695a194fb4dbdb70882a5bf032 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/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: FacebookAuthInteractorInput, InitializeInjectable { + + private(set) weak var presenter: FacebookAuthInteractorOutput! + + // MARK: - API + + private let loginURL = "https://www.facebook.com/v3.1/dialog/oauth" + + private enum Request { + static let fbRedirectURL = "https://web.dev-eu.nynja.net/oauth/facebook" + static let fbClientId = "306118319994975" + } + + 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: FacebookAuthInteractorOutput + } + + init(dependencies: Dependencies) { + presenter = dependencies.presenter + } + + + // MARK: - FacebookAuthInteractorInput + + 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/Flows/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..93808d6cbd1b86234d73834c0556e532d8b0e302 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Facebook/Presenter/FacebookAuthPresenter.swift @@ -0,0 +1,59 @@ +// +// FacebookAuthPresenter.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class FacebookAuthPresenter: FacebookAuthPresenterProtocol, FacebookAuthInteractorOutput { + + private(set) weak var view: FacebookAuthViewInput? + private(set) var interactor: FacebookAuthInteractorInput! + private(set) var wireframe: FacebookAuthWireframeProtocol! + + + // MARK: - Presenter + + func viewDidLoad() { + interactor.authenticate() + } + + func dismiss() { + wireframe.dismiss() + } + + func handleRedirect(to url: URL) -> Bool { + return interactor.handleRedirect(to: url) + } + + + // MARK: - Interactor Output + + func load(_ request: URLRequest) { + view?.load(request) + } + + func didAuthenticated(code: String) { + wireframe.finishAuthentication(code: code) + } +} + +// MARK: - Injection + +extension FacebookAuthPresenter: SetInjectable { + + struct Dependencies { + let view: FacebookAuthViewInput + let interactor: FacebookAuthInteractorInput + let wireframe: FacebookAuthWireframeProtocol + } + + func inject(dependencies: Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireframe = dependencies.wireframe + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/Facebook/View/FacebookAuthViewController.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/View/FacebookAuthViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..9a9f2180bbb195b27a46df68a4d403dc006ee00f --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/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, FacebookAuthViewInput { + + 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.facebookBackground + + 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: - View Input + + 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/Flows/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift b/Nynja/Modules/Flows/Auth Flow/Facebook/Wireframe/FacebookAuthWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..29405b53bae6049a4084d654a5ea16fec191d630 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/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) + } +} diff --git a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift b/Nynja/Modules/Flows/Auth Flow/Login/Interactor/LoginInteractor.swift similarity index 98% rename from Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/Login/Interactor/LoginInteractor.swift index 7215b961b2d72de465c75674abf76c78360b1e70..6b60e3159af2246d8648e9d6addb98d178bc0624 100644 --- a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/Login/Interactor/LoginInteractor.swift @@ -29,7 +29,7 @@ class LoginInteractor: BaseInteractor, LoginInteractorInputProtocol, IoHandlerDe // MARK: - Configure func configure() { - IoHandler.delegate = self + IoHandler.shared.delegate = self mqttService.addSubscriber(self) } diff --git a/Nynja/Modules/Auth/Login/Interactor/Modelka.swift b/Nynja/Modules/Flows/Auth Flow/Login/Interactor/Modelka.swift similarity index 100% rename from Nynja/Modules/Auth/Login/Interactor/Modelka.swift rename to Nynja/Modules/Flows/Auth Flow/Login/Interactor/Modelka.swift diff --git a/Nynja/Modules/Auth/Login/LoginProtocols.swift b/Nynja/Modules/Flows/Auth Flow/Login/LoginProtocols.swift similarity index 96% rename from Nynja/Modules/Auth/Login/LoginProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/Login/LoginProtocols.swift index 8555912942b1ec407942649913d98e7c8e783d4e..bfde50a795276b42d5d1498535dfc2164f9cfee9 100644 --- a/Nynja/Modules/Auth/Login/LoginProtocols.swift +++ b/Nynja/Modules/Flows/Auth Flow/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/Presenter/LoginPresenter.swift b/Nynja/Modules/Flows/Auth Flow/Login/Presenter/LoginPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/Login/Presenter/LoginPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/Login/Presenter/LoginPresenter.swift diff --git a/Nynja/Modules/Auth/Login/View/LoginViewController.swift b/Nynja/Modules/Flows/Auth Flow/Login/View/LoginViewController.swift similarity index 98% rename from Nynja/Modules/Auth/Login/View/LoginViewController.swift rename to Nynja/Modules/Flows/Auth Flow/Login/View/LoginViewController.swift index cb3357cca1fa841c49c973b6ce4e07c06251d728..6be7d162d9f8b2e10c42259093e83e2bbb50090c 100644 --- a/Nynja/Modules/Auth/Login/View/LoginViewController.swift +++ b/Nynja/Modules/Flows/Auth Flow/Login/View/LoginViewController.swift @@ -76,7 +76,7 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro // MARK: View lifecycle override func initialize() { - let countryModels = StorageService.sharedInstance.countries + let countryModels = CountriesProvider().fetchCountries() configureLoginView() configureContainer(with: countryModels) @@ -101,7 +101,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) @@ -353,7 +353,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/Flows/Auth Flow/Login/View/LoginWheelContainerDataSource.swift similarity index 97% rename from Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift rename to Nynja/Modules/Flows/Auth Flow/Login/View/LoginWheelContainerDataSource.swift index 725fcd2322753135ac5163e28da4470eb7af0796..f126ce2257f5e8a1a27dbffaa6d95a43a7b6df32 100644 --- a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift +++ b/Nynja/Modules/Flows/Auth Flow/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/Flows/Auth Flow/Login/View/LoginWheelContainerDelegate.swift similarity index 98% rename from Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift rename to Nynja/Modules/Flows/Auth Flow/Login/View/LoginWheelContainerDelegate.swift index 6aad13e1288451a74e321a8ef54958662fabdc46..f7dec8ca26a0586fe4f1ca909b74397947a049b4 100644 --- a/Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift +++ b/Nynja/Modules/Flows/Auth Flow/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/Auth/Login/WireFrame/LoginWireframe.swift b/Nynja/Modules/Flows/Auth Flow/Login/WireFrame/LoginWireframe.swift similarity index 100% rename from Nynja/Modules/Auth/Login/WireFrame/LoginWireframe.swift rename to Nynja/Modules/Flows/Auth Flow/Login/WireFrame/LoginWireframe.swift diff --git a/Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/CountriesSection.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/CountriesSection.swift new file mode 100644 index 0000000000000000000000000000000000000000..db504f4fdb4f6fe1e52e921da4e39b3dce8b5672 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/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: [Country] +} diff --git a/Nynja/Library/UI/TextInput/InputField/CountryModel.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/Country.swift similarity index 60% rename from Nynja/Library/UI/TextInput/InputField/CountryModel.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/Country.swift index 215852f10d8f5adcb0891563598a5dc77d31bd61..ef01331f0c2d06f51f4b1ce3104499b2430bed7f 100644 --- a/Nynja/Library/UI/TextInput/InputField/CountryModel.swift +++ b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Entities/Country.swift @@ -1,18 +1,22 @@ // -// 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 let code: String let placeHolder: String? + var defaultMask: String { + return "XXX XX XX" + } + var hasPhonePattern: Bool { return placeHolder != nil } @@ -23,9 +27,14 @@ final class CountryModel: 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/Flows/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..f924d2de5fa413b058c4597d0e8a778f9a3c90f0 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Interactor/SelectCountryInteractor.swift @@ -0,0 +1,60 @@ +// +// SelectCountrySelectCountryInteractor.swift +// Nynja +// +// Created by Roman Chopovenko on 30/01/2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class SelectCountryInteractor: SelectCountryInteractorInputProtocol, InitializeInjectable { + + private weak var presenter: SelectCountryInteractorOutputProtocol! + + private(set) lazy var countries: [Country] = { + return countriesProvider.fetchCountries() + }() + + + // 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 filter(with text: String) { + let filteredCountries = countries.filter { $0.name.contains(substring: text, options: .caseInsensitive) } + setup(filteredCountries) + } + + 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.didFetch(sections: sections) + } +} diff --git a/Nynja/Modules/Flows/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..a6063a9d2d1cb1ffa09e8c87d2971700f5ca2817 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/SelectCountry/Presenter/SelectCountryPresenter.swift @@ -0,0 +1,52 @@ +// +// SelectCountrySelectCountryPresenter.swift +// Nynja +// +// Created by Roman Chopovenko on 30/01/2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class SelectCountryPresenter: SelectCountryPresenterProtocol { + + private weak var view: SelectCountryViewProtocol! + private var interactor: SelectCountryInteractorInputProtocol! + private var wireframe: SelectCountryWireFrameProtocol! + + func getCountries() { + interactor.getCountries() + } + + func filter(with text: String) { + interactor.filter(with: text) + } + + func didSelect(country: Country) { + wireframe.didSelect(country: country) + } + + func dismiss() { + wireframe.dismiss() + } +} + +extension SelectCountryPresenter: SelectCountryInteractorOutputProtocol { + + 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/Flows/Auth Flow/SelectCountry/SelectCountryProtocols.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/SelectCountryProtocols.swift new file mode 100644 index 0000000000000000000000000000000000000000..22fb6f8e1098617e5ed122cbe79f24bccc743aab --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/SelectCountry/SelectCountryProtocols.swift @@ -0,0 +1,42 @@ +// +// SelectCountrySelectCountryProtocols.swift +// Nynja +// +// Created by Roman Chopovenko on 30/01/2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +// MARK: - Wireframe + +protocol SelectCountryWireFrameProtocol: class { + func didSelect(country: Country) + func dismiss() +} + +// MARK: - View + +protocol SelectCountryViewProtocol: class { + func setup(sections: [CountriesSection]) +} + +// MARK: - Presenter + +protocol SelectCountryPresenterProtocol: class { + func getCountries() + func filter(with text: String) + func didSelect(country: Country) + func dismiss() +} + +// MARK: - Interactor + +protocol SelectCountryInteractorInputProtocol: class { + func getCountries() + func filter(with text: String) +} + +protocol SelectCountryInteractorOutputProtocol: class { + func didFetch(sections: [CountriesSection]) +} diff --git a/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..90bc540cec6b53cba3448f0947e27b6236a6fec5 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryCellModel.swift @@ -0,0 +1,23 @@ +// +// CountryCellModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 06.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import NynjaUIKit + +struct CountryCellModel: CellViewModel { + + var accessibilityIdentifier: String { + return "select_country_cell" + } + + let country: Country + + 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/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift similarity index 50% rename from Nynja/Modules/SelectCountry/View/TableView/SelectCountryCell.swift rename to Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Cell/CountryTableViewCell.swift index 4ec8b776d17642f7e5a3d477459b769aadb3c066..8ad772fa5e8c515209ab96999594c481844eb25d 100644 --- a/Nynja/Modules/SelectCountry/View/TableView/SelectCountryCell.swift +++ b/Nynja/Modules/Flows/Auth Flow/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: UITableViewCell.CellStyle, 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/Flows/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/TableView/Header/SelectCountryHeaderView.swift new file mode 100644 index 0000000000000000000000000000000000000000..90c9b1aa953031d87c7153a4db858447e9bb6746 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/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/Flows/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..167e4a609485e60fb77a6f434b5244f99068cd5c --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/SelectCountry/View/ViewController/SelectCountryViewController.swift @@ -0,0 +1,219 @@ +// +// SelectCountrySelectCountryViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 30/01/2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import NynjaUIKit + +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: [CountriesSection] = [] + + private(set) var scrollBar: ScrollBar? + var scrollOffset: CGFloat = 0 + + + // MARK: - Views + + private lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.keyboardDismissMode = .interactive + tableView.backgroundColor = UIColor.nynja.clear + tableView.separatorStyle = .none + tableView.showsHorizontalScrollIndicator = false + tableView.tableFooterView = UIView() + + tableView.sectionHeaderHeight = SelectCountryHeaderView.Constraints.height.adjustedByWidth + tableView.estimatedSectionHeaderHeight = tableView.sectionHeaderHeight + + tableView.rowHeight = CountryCellModel.Cell.Constraints.height.adjustedByWidth + tableView.estimatedRowHeight = tableView.rowHeight + + tableView.contentInset.top = topInset + + view.addSubview(tableView) + tableView.snp.makeConstraints { make in + make.top.equalTo(navigationView.snp.bottom) + make.left.right.equalToSuperview() + } + + return tableView + }() + + private lazy var controlContainerView: NynjaControlContainerView = { + let containerView = NynjaControlContainerView(contentView: searchField) + + view.addSubview(containerView) + containerView.snp.makeConstraints { maker in + maker.left.right.equalToSuperview() + maker.top.equalTo(self.tableView.snp.bottom) + adjustVerticalInset(.bottom, make: maker, offset: -bottomInset) + } + + containerView.addGradientView() + + return containerView + }() + + private lazy var searchField: NynjaSearchField = { + let searchField = NynjaSearchField() + searchField.searchTextChangeHandler = { [weak self] searchQuery in + let text = searchQuery ?? "" + self?.presenter.filter(with: text) + } + return searchField + }() + + + // MARK: - Lifecycle + + 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) + + navigationView.configure(config: config) + } + + private func setupTableView(){ + tableView.register(viewModel: CountryCellModel.self) + tableView.register(headerFooter: SelectCountryHeaderView.self) + + tableView.dataSource = self + tableView.delegate = self + + presenter.getCountries() + + scrollBar = ScrollBar(scrollView: tableView) + + controlContainerView.isHidden = false + } + + + // MARK: - SelectCountryViewProtocol + + func setup(sections: [CountriesSection]){ + self.sections = sections + tableView.reloadData() + } + + + // MARK: - Actions + + func back() { + view.endEditing(true) + presenter.dismiss() + } + + + // MARK: - Keyboard + + func keyboardNotified(endFrame: CGRect) { + if endFrame.origin.y >= UIScreen.main.bounds.size.height { + updateToHide(view: controlContainerView, offset: -bottomInset) + } else { + updateToShow(view: controlContainerView, offset: -bottomInset - endFrame.height) + } + } +} + + +// MARK: - TableView + +// MARK: UITableViewDataSource +extension SelectCountryViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].countries.count + } + + 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].symbol + } + + private func country(at indexPath: IndexPath) -> Country { + let section = sections[indexPath.section] + return section.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) + presenter.didSelect(country: selectedCountry) + } + + 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/Flows/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift b/Nynja/Modules/Flows/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift new file mode 100644 index 0000000000000000000000000000000000000000..7a6a4371d6ba58a8665742e081eb037ae0758205 --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/SelectCountry/WireFrame/SelectCountryWireframe.swift @@ -0,0 +1,62 @@ +// +// SelectCountrySelectCountryWireframe.swift +// Nynja +// +// Created by Roman Chopovenko on 30/01/2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol CountrySelectorCoordinatorProtocol: class { + func wireframe(_ wireframe: SelectCountryWireFrame, endWithState state: SelectCountryWireFrame.State) +} + +final class SelectCountryWireFrame: Wireframe, SelectCountryWireFrameProtocol { + + // MARK: - Init + + private let coordinator: CountrySelectorCoordinatorProtocol + + init(coordinator: CountrySelectorCoordinatorProtocol) { + self.coordinator = coordinator + } + + + // MARK: - Wireframe + + struct Dependencies { + let countriesProvider: CountriesProviding + } + + enum State { + case dismiss + case selected(country: Country) + } + + func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController { + let presenter = SelectCountryPresenter() + + let view = SelectCountryViewController() + view.presenter = presenter + + 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: Country) { + coordinator.wireframe(self, endWithState: .selected(country: country)) + } + + func dismiss() { + coordinator.wireframe(self, endWithState: .dismiss) + } +} 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 0000000000000000000000000000000000000000..a054829e41a50bcc5a5c890a79b9ec57dfcfc2eb --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Splash/Interactor/SplashInteractor.swift @@ -0,0 +1,124 @@ +// +// 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) + } + + // FIXME: connect when +// 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() + 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 0000000000000000000000000000000000000000..4c09b9b3675d35508dfd2f8f6b34b7135bbcdf2f --- /dev/null +++ b/Nynja/Modules/Flows/Auth Flow/Splash/Presenter/SplashPresenter.swift @@ -0,0 +1,59 @@ +// +// SplashSplashPresenter.swift +// Nynja +// +// Created by Anton Makarov on 23/08/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +final class SplashPresenter: BasePresenter, SplashPresenterProtocol, SplashInteractorOutput { + + 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 0000000000000000000000000000000000000000..b1a1a0bedf091460cb61c9d6eb99556eab99b9b0 --- /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/Splash/View/SplashViewController.swift b/Nynja/Modules/Flows/Auth Flow/Splash/View/SplashViewController.swift similarity index 96% rename from Nynja/Modules/Splash/View/SplashViewController.swift rename to Nynja/Modules/Flows/Auth Flow/Splash/View/SplashViewController.swift index 8bd60fbdad351add87fa5368bb443a13dd365a5d..a8dd8da8c27feeecf25ce0d763c38966fafed37c 100644 --- a/Nynja/Modules/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 0000000000000000000000000000000000000000..c7b5c09e605ac5857751d1b196ada6bfa3ff1f00 --- /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) + } + + 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)) + } + + func showCreateProfile() { + coordinator.wireframe(self, didEndWithState: .showCreateProfile) + } +} diff --git a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift similarity index 99% rename from Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift index 7425b121d6088ae606a2306e6c9c2f505076640f..748fa73437d72ae295e29b31a2e701b0cc884382 100644 --- a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift +++ b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/Interactor/VerifyNumberInteractor.swift @@ -38,7 +38,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/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/Presenter/VerifyNumberPresenter.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/VerifyNumberProtocols.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/View/VerifyNumberViewController.swift diff --git a/Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift b/Nynja/Modules/Flows/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift similarity index 100% rename from Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift rename to Nynja/Modules/Flows/Auth Flow/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift b/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift index 0f1c5038f089b07d44cf73fde6392d2c3fef03d9..f8aa1378a729361c6e6a7d4487fdd730d175874d 100644 --- a/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift +++ b/Nynja/Modules/Flows/CameraFlow/Camera/CameraProtocols.swift @@ -8,12 +8,13 @@ import UIKit -protocol CameraWireFrameProtocol: WireframeProtocol { +protocol CameraWireFrameProtocol: class { func openSettings(on view: UIViewController) func back(from view: UIViewController) func openImagePreview(from view: UIViewController, image: ExtendedImage) func openVideoPreview(from view: UIViewController, videoURL: URL) func openQRPreview(from view: UIViewController, text: String) + func end(from view: UIViewController) } protocol CameraViewProtocol: class { @@ -61,14 +62,16 @@ protocol CameraPresenterProtocol: NavigationProtocol { } protocol CameraInteractorInputProtocol: class { + var gpsMetadata: GPSMetadata { get } func configure() func reset() - + func getAvailableCameraModes() -> [UIImagePickerController.CameraCaptureMode] func getCurrentCaptureMode() -> UIImagePickerController.CameraCaptureMode func getVideoQuality() -> UIImagePickerController.QualityType + func getMediaTypes() -> [String] func getFlashMode() -> UIImagePickerController.CameraFlashMode diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/Presenter/CameraPresenter.swift b/Nynja/Modules/Flows/CameraFlow/Camera/Presenter/CameraPresenter.swift index 0486c2e1b35214127297cb9a999a869ee40de1b4..e603c06f25938cfb238f3f055166ad443e19e8f0 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 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/View/Subviews/QRNotificationVIew.swift b/Nynja/Modules/Flows/CameraFlow/Camera/View/Subviews/QRNotificationVIew.swift index c9afb9ed23905b763d0247788460e776ae001022..bfd15fe69dda95fc895e6a6c0028d0a57220f82b 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) diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/Wireframe/CameraWireframe.swift b/Nynja/Modules/Flows/CameraFlow/Camera/Wireframe/CameraWireframe.swift index de5f25a678743941088097d6683d5b074524b37a..f202bbdb54923f8c60823af987c301a9955fe409 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/CameraCoordinator.swift b/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift index f60eae879329d6d3c1b466ad3d24396646d1e895..e0df40b17e0ea4c5b816b6219861557f89de07c8 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/CameraFlow/PhotoPreview/PhotoPreviewProtocols.swift b/Nynja/Modules/Flows/CameraFlow/PhotoPreview/PhotoPreviewProtocols.swift index d3dcf212c5013036bafe31eff97d4edbdd9f39f5..9405976eeecbddecf221b7fba86cca8ebe540db1 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 d165d7b5eb6b5b006893d8a5358b24dc36eb262d..48e71a0292bbfa4ced42903cfe5ba35d9b897df6 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 85856507f1153cbf594c8bc6700a0f9e0c47a8f4..a61199a9ca86fca4348978599c5226e04e77e14a 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 d81a7f8927b159ccfc785f9439525d7109f61456..33f1f2721e240f56faba5fdf48e762f0ae236404 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 4348635633311dc8494ef257502f3a16c5228cdd..02d679977845a7ada7407f8c80fcdf0d7710f386 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 a9dfe437bcebccdb6f8b11acc88ddace38a8438d..28a86e33539ef1201a8e3509b7fa9bfe0a2b478a 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 440faaabe0286c4b1a1543c2026d3c935ac58af9..71a23bc6243e2ed75b2ae5676a232116ea91fde6 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 var interactor: CameraVideoPreviewInputInteractorProtocol! - private var wireframe: CameraVideoPreviewWireframe! private weak var view: CameraVideoPreviewViewProtocol! + private var interactor: CameraVideoPreviewInputInteractorProtocol! + 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 f82dbb9b0402da1db78d4ac91977c2cf4d143475..125023e98b87e1a6246591aca34269769276916c 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 0046fd2ebee97b289c50c9bf8ec98627b8307c31..fce540a9c8ee1a53090169d9c785ebc76ddf8da8 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 7f88297f1c7969e0583163e53feb4a909a3d4720..c81ac5eb0cd3ec0d4ae3c44d085a985205e07f32 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 86a59480ec28997164ca4ea12edf986e9932172a..b4661c24d3fa394471f79eb66a1bfd561d2ad73e 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 effb46d2b5ccacf54f510061c5c7357552b11183..767ab8304ff9f437a168f90310615519d6fee584 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 24441805f4551033089b79be2c6a7687a74d874a..7bd4c4b2a06c6a126dc53b793ead696039534d3e 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/CameraSettingsFlow/CameraSettings/Wireframe/CameraSettingsWireframe.swift b/Nynja/Modules/Flows/CameraSettingsFlow/CameraSettings/Wireframe/CameraSettingsWireframe.swift index 9c4d0e4ff039633e07e10de3387406fe2a761307..b9e4bce0b4e79994ce1f675e7da0aba666083d68 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 @@ -37,7 +36,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 889029740cb349730f6bdf2e8706953fe51e75fc..61af9eff46d8d5bda635fe5a2aff78eeb86f2cc3 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! @@ -23,9 +23,11 @@ final class CameraSettingsFlowCoordinator: CameraSettingsFlowCoordinatorProtocol extension CameraSettingsFlowCoordinator { func start() { let wireframe = CameraSettingsWireFrame(coordinator: self) - let view = wireframe.prepareModule(parameters: NSNull(), dependencies: CameraSettingsWireFrame.Dependencies( + + let view = wireframe.prepareModule(dependencies: .init( cameraSettingsService: serviceFactory.makeCameraSettingsService(with: sourceFlow), - locationService: serviceFactory.makeLocationService())) + locationService: serviceFactory.makeLocationService()) + ) mainFlowVC = view diff --git a/Nynja/Modules/Flows/CreateGroupFlow/CreateGroup/View/CellWithImage.swift b/Nynja/Modules/Flows/CreateGroupFlow/CreateGroup/View/CellWithImage.swift index fac1f86d525a5f29f99a8254605a0cecfe965487..ca7059d1fbf1d49e1c0fb2185c8d4cae7cfd8bf5 100644 --- a/Nynja/Modules/Flows/CreateGroupFlow/CreateGroup/View/CellWithImage.swift +++ b/Nynja/Modules/Flows/CreateGroupFlow/CreateGroup/View/CellWithImage.swift @@ -33,9 +33,7 @@ class CellWithImage: UITableViewCell { if let img = image { self.arrow.image = img } else { - self.arrow.image = .avatarPlaceholder + self.arrow.image = UIImage.nynja.Contacts.avaPlaceholder.image } } } - - diff --git a/Nynja/Modules/Flows/GalleryFlow/Gallery/GalleryProtocols.swift b/Nynja/Modules/Flows/GalleryFlow/Gallery/GalleryProtocols.swift index 5517ae1424da298f5d18a06aa5798f6026344bf7..66baab09d46507c1a7b69a69f06dc48abc898c32 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 f875666d45055e3cd7e24ad1393b21cc43f0d4df..bf994ac8e02fdcd09d03bb53d9c11de91eaa873f 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 5935116cda7e7e09852815cab6bf44cdd6c31340..3b09dcb30d6a3ed255f930fed066d57c14a01b59 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/GalleryCoordinator.swift b/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift index 47e224fde4cc787586bb27bb3c0105f16c260442..e3242502e30e940f27699d7ae9a6b2193972ca9e 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/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift index 30561ea9b4e0e9dc8eeabe2121dfd330a2a3fc70..01292ac00b175b2c5d69cd192bc0ea0ed92f9afb 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 da0cf91dfcc2a17e43b0df34d0adbf9a14ae36a5..da1e7c4ae292ec5b361514b117f241b1dc7def81 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 c81b267c86c1e2221806888e2217a66e81709f52..c6519e2d3c5963c423fe021f8b02feaa4b0ce470 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/Modules/Flows/Search Flow/Entities/SearchContactResponse.swift b/Nynja/Modules/Flows/Search Flow/Entities/SearchContactResponse.swift new file mode 100644 index 0000000000000000000000000000000000000000..c2358187dea657e965e888961011c32a7f7baf1f --- /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 0000000000000000000000000000000000000000..af1c1b049cc5e0bd1466b208c9eff42b24d8ca3e --- /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 0000000000000000000000000000000000000000..f6f34cbf42a226a562a04e9948541a7b8569798c --- /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 0000000000000000000000000000000000000000..6259f25cd59817f668817d0b3da1550f05d0c405 --- /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 0000000000000000000000000000000000000000..0cc414a06fc03b54eee9b98ac070f678cd6dfa7b --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/Presenter/SearchContactPresenter.swift @@ -0,0 +1,258 @@ +// +// 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 = TextFieldContentViewModel(validator: EmailValidator()) + viewModel.placeholder = String.localizable.searchContactEmailPlaceholder + viewModel.textContentType = .emailAddress + viewModel.keyboardType = .emailAddress + viewModel.returnKeyType = .search + + 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 = TextFieldContentViewModel(validator: UsernameValidator()) + viewModel.placeholder = String.localizable.searchContactUsernamePlaceholder + viewModel.keyboardType = .default + viewModel.returnKeyType = .search + + 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 TextFieldContentViewModel where inputMode == .username: + interactor.searchByUsername(viewModel.inputText) + + case let viewModel as TextFieldContentViewModel where inputMode == .email: + 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) + 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]) { + 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 0000000000000000000000000000000000000000..ac35ed049943261a2b711f02309429f63f18a4f8 --- /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 0000000000000000000000000000000000000000..f35eaa61509982e83ecc44cf15d3fc40c79157e3 --- /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 0000000000000000000000000000000000000000..c384250bdfd15fc59983b93d53cdd72c30b8561d --- /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/PhoneNumberContentViewModel.swift b/Nynja/Modules/Flows/Search Flow/View/InputView/PhoneNumberContentViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..1c032aae05241893fddc7f0f6c2c173e9f8b4682 --- /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 0000000000000000000000000000000000000000..7273f805526e4218977cc6b7317ddd8c97cfab7d --- /dev/null +++ b/Nynja/Modules/Flows/Search Flow/View/InputView/TextFieldContentViewModel.swift @@ -0,0 +1,119 @@ +// +// TextFieldContentViewModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class TextFieldContentViewModel: ContentViewModel { + + private var validator: MTIValidator + + var returnHandler: (() -> Void)? + + private let initialText: String? + + var inputText: String { + 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 { + 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, 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 + + 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.validationHandler = validationHandler + + textField.returnHandler = { [weak self, weak textField] textInput in + self?.returnHandler?() + textField?.endEditing(true) + return false + } + + 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/Search Flow/View/SearchContactViewController.swift b/Nynja/Modules/Flows/Search Flow/View/SearchContactViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..38e48137f710e4046c57ca36609750d2a889b997 --- /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 0000000000000000000000000000000000000000..cab8ad17c2099c1bc2a654bb50f4e22a0b55891e --- /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 0000000000000000000000000000000000000000..87e8b4d02fdbb57f356dc1026cab98d0462d2f7d --- /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 0000000000000000000000000000000000000000..fd8df226c74e8dd9e959f3e3803f46bf21b766bc --- /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 0000000000000000000000000000000000000000..dc752b30636482fdfb6df99f78f89b321fb28792 --- /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/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift b/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift index 72be2351e49e54f8dfaa2e25ddc6d37f6a0c91d7..58336cbca4a405ce5e4781b250266adc17a789e0 100644 --- a/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift +++ b/Nynja/Modules/Flows/SelectAvatarFlow/SelectAvatarCoordinator.swift @@ -9,24 +9,24 @@ import Foundation import UIKit -enum SelectAvatarFlowCoordinatorSource { +enum ImageSource { case camera case gallery } -protocol SelectAvatarFlowCoordinatorProtocol: CoordinatorProtocol, CameraCoordinatorProtocol, GalleryCoordinatorProtocol, +protocol SelectAvatarFlowCoordinatorProtocol: Coordinator, CameraCoordinatorProtocol, GalleryCoordinatorProtocol, 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 @@ -41,7 +41,7 @@ final class SelectAvatarFlowCoordinator: SelectAvatarFlowCoordinatorProtocol, In } -// MARK: - CoordinatorProtocol +// MARK: - Coordinator extension SelectAvatarFlowCoordinator { func start() { diff --git a/Nynja/Modules/GroupsList/GroupsListProtocols.swift b/Nynja/Modules/GroupsList/GroupsListProtocols.swift index 6f973d06534f6e384d568f2e5da4bb991337f171..f10ba5bd2d0163a3d41bebaca0cf2d9507690a5e 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 c44ceff9fe342dc75255fd197c952de1cdc05b61..7381eb4d267962cd600bc8a1dbed6f40140e59ef 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -12,6 +12,7 @@ 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 = "" @@ -20,18 +21,30 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I return storageService.rosterId } + 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]? { @@ -44,6 +57,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchGroups() } @@ -55,6 +72,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 4cd181f2e7f20fdc369cff1379529dbd428de83b..8600c93aeca2a43e631a22c70b9b1e8edde24ac6 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 c7a9dbc10d80184dd2741e665c44be015c07d305..49470a986166bbe9c2901de3ee5653e7803c0665 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 20b33a03d85a5d0b2675948859237a462b775fbd..22fc61e87d484d54b44183948cf349a78a9ae7a4 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 85443764430a72b1f1343e1306a7c7df313610e2..65549cafca60ebb0a39383f4a64f7fc03d9b3984 100644 --- a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift +++ b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift @@ -17,12 +17,15 @@ class GroupsListWireFrame: GroupsListWireFrameProtocol { self.navigation = navigation self.main = main - // Compomentes + let serviceFactory = ServiceFactory() + let view = GroupsListViewController() let presenter = GroupsListPresenter() let interactor = GroupsListInteractor( - dependencies: .init(storageService: StorageService.sharedInstance, - conversationsProvider: ServiceFactory().makeConversationsProvider())) + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) // Connecting view.presenter = presenter diff --git a/Nynja/Modules/History/View/HistoryCell.swift b/Nynja/Modules/History/View/HistoryCell.swift index b0ce0acd454ff69f22eebd28665ddf232421399e..1e4565b315d7f66a2dc445838f72ed20d1c837e6 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/Entity/PhoneContact.swift b/Nynja/Modules/InviteFriends/Entity/PhoneContact.swift index e37ca11fc1cd13e8b746246c607b4c36257021d0..0335caf1ff5cf97a0dc465cee9e84baaa7392380 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 0c3cf1677c85ac39567cdd09ffc95595a64cfc76..aacbdbdb80e5b1de80d82a0159f008c958b9a270 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 { @@ -24,7 +23,7 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } //MARK: - BaseInteractor @@ -35,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) @@ -77,7 +76,7 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto } func isSelected(contact: PhoneContact) -> Bool { - return selectedContacts.contains(contact) + return selectedContacts.contains { $0 === contact } } func hasSelectedContacts() -> Bool { @@ -89,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/Header/ShareNynjaHeaderView.swift b/Nynja/Modules/InviteFriends/View/Cell/Header/ShareNynjaHeaderView.swift index 33400098f358ded2650420e0bff4419af169299c..8cbdc88acc76d9861bdd3cd28ae67d5e2c26564e 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/InviteFriends/View/Cell/InviteFriendsCell.swift b/Nynja/Modules/InviteFriends/View/Cell/InviteFriendsCell.swift index 100f8ebbfc5ed1c773407ab145cdf99014f760c2..fc6a27404a29b1e963942a21008e68373dd0111c 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 2f0508d1056d9eafdfa1fe753dcb4b411f42807f..f12f32572b625ebea09c5e32c98ba4a1b18174d5 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/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift b/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift index 919c7f77c6113b1ce7af0fa419751a024d25afdf..41cdbf94691587a1eab0f1b63744f04a983d00af 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/LeaveVoiceMessage/View/LeaveVoiceMessageViewController.swift b/Nynja/Modules/LeaveVoiceMessage/View/LeaveVoiceMessageViewController.swift index e2e16327cfa81a694003aee415d6c4fa659e3cd8..d0964bc21cd97ac348a605055d5f5661a2f41cbd 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/Interactor/MainInteractor.swift b/Nynja/Modules/Main/Interactor/MainInteractor.swift index 6829492127ad6cf912179f89e1fbc6853394f3d5..fd5ea1129777e67f1ae85d853c741e7dbaa79287 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/Main/MainProtocols.swift b/Nynja/Modules/Main/MainProtocols.swift index 41734f5f1c16b14a318de16c128774f73d654b58..660e6b58cee9cbfd8bf8b62be44c26b7aadd49c3 100644 --- a/Nynja/Modules/Main/MainProtocols.swift +++ b/Nynja/Modules/Main/MainProtocols.swift @@ -85,10 +85,8 @@ protocol MainWireFrameProtocol: class { func openMapView() func showMySelfChat(contact: Contact) func showQRGenerator() - func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) + func showAddContactByEmail() func showAddContactByUserName() - func showEditName() - func showEditUsername() func showWallet(for profile: Profile) func getRecentsLocation() -> [LocationType] func getStarredLocation() -> [LocationType] @@ -97,6 +95,10 @@ protocol MainWireFrameProtocol: class { func showFavorites(mode: FavoritesMode) + // Edit Profile + func showAccountSettings() + func showLoginOptions() + // Group func showAddParticipants() func showGroupsList() @@ -133,7 +135,6 @@ protocol MainViewProtocol: WheelOutProtocol { func hidePartnerVideoView() func showQRReader() func loadFromCamera() - func showEditPhoto() func showUILocker() func hideUILocker() @@ -191,8 +192,12 @@ protocol MainPresenterProtocol: BasePresenterProtocol { func showSettingsDataAndStorage(with usageMode: DataDownloadAndUsageMode) func showQRGenerator() - func showEditName() - func showEditUsername() + + // Edit Profile + func showAccountSettings() + func showLoginOptions() + + func showAddContactByEmail() func showAddContactByUserName() func showFavorites(mode: FavoritesMode) diff --git a/Nynja/Modules/Main/Presenter/MainPresenter.swift b/Nynja/Modules/Main/Presenter/MainPresenter.swift index e71794ba2b4924e162f4c9f1af38c9fd69a5cd07..c1c4760044a386efcea1632106025d6f13c34b17 100644 --- a/Nynja/Modules/Main/Presenter/MainPresenter.swift +++ b/Nynja/Modules/Main/Presenter/MainPresenter.swift @@ -268,17 +268,21 @@ final class MainPresenter: BasePresenter, MainPresenterProtocol, MainInteractorO func showQRGenerator() { self.wireFrame.showQRGenerator() } + + func showAddContactByEmail() { + wireFrame.showAddContactByEmail() + } func showAddContactByUserName() { 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 cb92fed3ab5ae20281567f254d649bb825dd6164..1e8a0e77afe31fb9d2017d0975396cacd3cb1960 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" // Calls section case callContact = "wheel_item_call_contact" @@ -83,6 +82,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" @@ -107,7 +107,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 a34b1606914d73add10b7673c6d588d5961dffde..418de3d18787aa7766bf8826c5d5500664dea9ea 100644 --- a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift @@ -186,18 +186,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) } @@ -231,6 +226,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/MainViewController.swift b/Nynja/Modules/Main/View/MainViewController.swift index bb46be6bd68fc91e6f9bf1d920be87cc013aa490..76f23098a70978af42fb5f8d69f1cf83a85440ea 100644 --- a/Nynja/Modules/Main/View/MainViewController.swift +++ b/Nynja/Modules/Main/View/MainViewController.swift @@ -602,15 +602,6 @@ final class MainViewController: BaseVC, MainViewProtocol, HitTestDelegate, UINav } } - 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/MainWheelContainerDataSource.swift b/Nynja/Modules/Main/View/MainWheelContainerDataSource.swift index f7c48354cded33d42b78248dbe51835c9b8fe1d4..dc841f5a60553aa52dfc127a352a13a38213fb47 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/Main/View/NavigateProtocol.swift b/Nynja/Modules/Main/View/NavigateProtocol.swift index 5b8e708ee5545c233c60e2a3c8c5d23781b56bab..6cb3b438e1dd523bff02fca37e3e88c608b8a26d 100644 --- a/Nynja/Modules/Main/View/NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/NavigateProtocol.swift @@ -64,9 +64,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?) @@ -78,6 +78,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/View/ScheduleView/ScheduleView.swift b/Nynja/Modules/Main/View/ScheduleView/ScheduleView.swift index bc49da4732bfa0bdd50495db6ef01d9f944c3200..12c57770392ad1f7ecac4fbb5b5196b9f0c3c085 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/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 9b90f3d781e85ad07ebeddc5b17eb718ee77359e..2d81a1cd10bc2e8d33b49e3ed47e0a44584aceb1 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -124,36 +124,18 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega } } - 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) } } - 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!) } } - - 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) @@ -387,9 +369,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 @@ -399,9 +399,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() { @@ -468,15 +467,22 @@ final class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelega // MARK: Edit Profile - - func showEditName() { - EditProfileWireFrame().presentEditProfile(navigation: navigation!, main: self) + + func showAccountSettings() { + guard let navigation = navigation else { return } + + let coordinator = AccountSettingsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.delegate = self + coordinator.start() } - - func showEditUsername() { - EditUsernameWireFrame().presentEditUsername(navigation: navigation!, main: self) + + func showLoginOptions() { + guard let navigation = navigation else { return } + + let coordinator = LoginOptionsCoordinator(navigation: navigation, serviceFactory: ServiceFactory()) + coordinator.start() } - + func updateAvatar(image: UIImage) { let editingDelegate = view?.presenter.interactor as? EditPhotoDelegate EditPhotoWireFrame().presentEditPhoto(navigation: navigation!, image: image, delegate:editingDelegate) @@ -665,3 +671,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() + } + } +} diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift index 0551903b9e078d2291f328ad5e683078bb8167f4..2db26c36484f3827e19595d8d31f8551861815ec 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift @@ -133,9 +133,8 @@ extension MessageInteractor { text: result.text, from: nil, to: input.language.language, - timeout: TranslationService.Constants.translationTimeout) { (translatedText, error) in - - guard var translatedText = translatedText else { + timeout: TranslationService.Constants.translationTimeout) { response in + guard case var .success(translatedText) = response else { handler(.error(.failed)) return } @@ -232,9 +231,8 @@ extension MessageInteractor { text: result.text, from: nil, to: translationLanguage, - timeout: TranslationService.Constants.translationTimeout) { [weak self] (translatedText, error) in - guard var translatedText = translatedText, !translatedText.isEmpty else { - handler?(.error(.failed)) + timeout: TranslationService.Constants.translationTimeout) { [weak self] response in + guard case var .success(translatedText) = response, !translatedText.isEmpty else { self?.finishTranslation(for: id, with: .failed) return } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 72a18a72ea1e450f205f2a94173288bf25f922ea..ec6b44787aee18eba7f05ac9241a31b7b8541d4e 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, HistoryHandlerSubscriber, TypingHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { +final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerSubscriber, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { private var callService = NynjaCommunicatorService.sharedInstance @@ -97,6 +97,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let stickersProvider: StickersProviding private var presenceProvider: PresenceStatusProvider! + + private let typingProvider: TypingProvider private let historyRequestFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() @@ -171,6 +173,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) + typingProvider = TypingProviderImpl.shared super.init() @@ -179,20 +182,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() } @@ -215,8 +224,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H func configure() { processingManager.delegate = self - TypingHandler.delegate = self - isAfterConnectionAppeared = false prepareInitialValues() @@ -429,6 +436,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) { @@ -945,21 +959,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 { @@ -987,6 +986,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 c53f3c85666260e79809f68a61e48b9da0d6efb3..1c207c69c6a8aa883e4735ac6d82bab3eb257adc 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/Mention/InputController/MentionController.swift b/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift index a7dc95f7bd014bacc6c6ca6e35d50f683a19eeb4..b60a2fa7a87763cbb8f21c36634076fe48c41fc2 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) } diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift new file mode 100644 index 0000000000000000000000000000000000000000..1cedf26b2931fc81fcc70fc975128c82712b7c5f --- /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 0000000000000000000000000000000000000000..dfd9c1d6f789120c16ed5d90e94c80dfa497f77a --- /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 5c3c25c454490bbb3e2fc649e15aae7f93279073..3ba25b8fa845982d4ebb203c47e7baf7193c9925 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 bfb75367e4bee7d1789358c94646fe30759b3ceb..a65d33387b05a36a4c3930d9f6c56301edf67a8c 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 7ab43601746749ae72151073726623b202fda2ac..3a5ad85bc44168b699da906507aed058d0f707fa 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 0ca4539282e832258f9c8495287c5d49109759e5..505a26c2e413de3fb81be2795a3ef23359bc0967 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) @@ -76,11 +106,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! { @@ -956,29 +988,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 actionStatusChanged(_ status: ActionStatus) { - if case .done = status { - restoreStatus() - } else { - view.updateHeaderStatus(status.title) - } - } - - func restoreStatus() { - view.updateHeaderStatus(lastStatus) + func didReceiveTyping(_ typing: TypingDisplayModel) { + headerStatus.typing = typing } func messageSent(_ localId: MessageLocalId, serverId: MessageServerId) { @@ -1026,11 +1044,6 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract // 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 d28f6d5a47a4ae2c04e76e5d5bf4b61f03647ad6..1d915ed9203b1088dfb189c496629d10c1fd1e01 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, serverId: MessageServerId) func messageRead(_ localId: MessageLocalId, serverId: MessageServerId) @@ -251,6 +250,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func sender(for message: Message) -> MessageSender func askForInternetStatus() + func askForTypingStatus() func editMessage(_ message: InputTextMessage) func clearEditMessageObject() @@ -301,8 +301,9 @@ protocol MessageViewProtocol: class { func scrollToBottomIfNeeded() func scrollToBottom() - func updateHeaderStatus(_ status: String) + func updateHeaderStatus(_ status: ChatStatusDisplayInfo) func updateDeliveryStatus(_ status: DeliveryStatus, localId: MessageLocalId, serverId: MessageServerId) + func removeMessage(_ messageId: MessageLocalId, isForAllUsers: Bool) func update(sender: MessageSender, inMessagesWithIds messageIds: [String]) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 3c2023c65efc9530fe1efa7891f8a31ed0f3ebec..e79463d31ff46aba9dc79143c183fecb5fd5b757 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 }() @@ -1179,7 +1177,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 b39e1d1f860a61f80e637c36631317bd8dd3ef55..fae8c725233505da6bd34d394fddc07ec88a3b15 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 c8c2d2ce67581d08da39f147af08b18c5ca854f4..44212f7e316536bcd12f85bf0291a4efb7a5233e 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 7ee65abf2316487906980ad3aab67b58e0e06203..7a5ca994f7a23a358db75795d4ea7ab4198f9460 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/Message/View/Views/CollectionView/Cells/Views/Base/MessageContentView.swift b/Nynja/Modules/Message/View/Views/CollectionView/Cells/Views/Base/MessageContentView.swift index cd73fb3f8747f24413221fc7dd4db1174dd4eddb..31a3f6be455eb46d6bc9cd6554ac738ab516f73f 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 0a365e53d34ec895374c6e8878bf00734d4aab7e..3a1130dc5d7a1630ac747b40768fb9c81c9a46ca 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 39d0530fcba02423ae4f58fa3ca2d2521fac179e..433400abac16d1de66d8f9cacd15f1ec647190c9 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 af7a256d2eb7ace7d6c257bd55bbb96c3bccf44a..5c8ecf9141cdb8dd6a206304452a2aa6eef0af0c 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 644566615fa661a7ed7036b84e92613c32de8ece..d4be939769d85081bb531eaecbcabf7fc8cc0018 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 059ee84d7ae884df215029a92b3f590c75963fbd..6cf1858ee41efbf193475a794b4569185fa73a3b 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 b9a02c8d8c9fabbf4c978c0cb0e931d8603a4783..c1a7bf2c07a6b2bb96adacd2ffb5c21525db7f06 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 b15d94a3b81b21109da702ce573259fe4ee7e593..f0068cc951f699cb469e5112df3ef5e71795ae16 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 8d971ad2bbf05c2caf41403ced30e51742711f0e..8cc6d37ac3e33e5354c8f9b0037ce49dddf88d33 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 15ea3399ee56d9f4c47b937b408f40b823e8c2ec..61c88c584f37db39fafe6f54cdbda65e03d1632c 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/Participants/Interactor/ParticipantsInteractor.swift b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift index 3ec546980a205a3127ede2d7f5423df95d65125a..4905c52198957b1ce3d0f0a5c2107f93607deb22 100644 --- a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift +++ b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift @@ -12,7 +12,7 @@ class ParticipantsInteractor: BaseInteractor, ParticipantsInteractorInputProtoco weak var presenter: ParticipantsInteractorOutputProtocol! private var participants: [Participant] = [] - + private var userInfo: UserInfo! private var mqttService: MQTTService! @@ -25,8 +25,8 @@ class ParticipantsInteractor: BaseInteractor, ParticipantsInteractorInputProtoco override func loadData() { super.loadData() - IoHandler.delegate = self - self.loadContacts() + IoHandler.shared.delegate = self + loadContacts() } diff --git a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift index 613cb6390408365aef6abf6583c3e5b33a4fcaf7..8b20b64151c9cebe66d8fbe2078d85b91ea8c194 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 homeDataProvider: HomeDataProvider! private var contactsProvider: ContactsProviding! + 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, @@ -158,11 +186,15 @@ fileprivate extension ProfileInteractor { } + +// MARK: - SetInjectable + extension ProfileInteractor: SetInjectable { func inject(dependencies: ProfileInteractor.Dependencies) { presenter = dependencies.presenter homeDataProvider = dependencies.homeDataProvider contactsProvider = dependencies.contactsProvider + typingProvider = dependencies.typingProvider mqttService = dependencies.mqttService } @@ -170,6 +202,7 @@ extension ProfileInteractor: SetInjectable { let presenter: ProfileInteractorOutputProtocol let homeDataProvider: HomeDataProvider let contactsProvider: ContactsProviding + 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 1001ca59aeb9e432b1d2f2e98a154dc044ad537a..cc1dd1d371b340f71cc594f8de703625bc079761 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 c247a31722dd7aa697d48ac9af1eeed5409be46b..608d2b2796261f2f1d2277f9ecf3e13fd5071b6c 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 4728e19edefba80a0a68ec439717a31d421f0946..806d35cd7d1dc976c6406a8291ac9242669489f8 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 1e8cd4a007e965ab9f45977c8bf2fc6d98d10e18..7934b0313680e0a895bd6fc8e98d5f2dfecd578f 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 045570511bde444a5aa7d25a45d4ddd110921327..83626aca30517ed7203d1b36fce04fafbf3cb66b 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 66a9ac98dcc090f90d8790d6cc6ebc9ad22749cc..a9ea8140bf796566d7aa4af2b1ff32307871a51c 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 38591f0c88b835cc3a0b3b47a4c1b57ca2ef03d5..33c9b7617f05ffbf17743b3af6680b76c4e3ebec 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 1681b365a43df9f63401cb33ae83c60f8a5a8f68..4955c8a600cad914107542ac3176a629cc8b9f77 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 f5b2e40fad90b69dc2a2f5eec217d916f23ce76d..1c7cf6a6b032c1d08dc47318442c742f51c10f0e 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 73ccb453ff539bce8756c65fd57b71db09dcd95e..2e329e7c6939f6b9852c2a72ad3ab7f22c98c2a8 100644 --- a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift +++ b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift @@ -15,6 +15,7 @@ class ProfileWireFrame: ProfileWireFrameProtocol { func presentProfile(navigation: UINavigationController, main: MainWireFrame?) { + let serviceFactory = ServiceFactory() // Components @@ -27,6 +28,7 @@ class ProfileWireFrame: ProfileWireFrameProtocol { presenter: presenter, homeDataProvider: serviceFactory.makeHomeDataProvider(limit: 5), contactsProvider: serviceFactory.makeContactsProvider(), + typingProvider: serviceFactory.makeTypingProvider(), mqttService: serviceFactory.makeMQTTService())) presenter.inject( dependencies: .init( diff --git a/Nynja/Modules/QRCodeGenerator/Interactor/QRCodeGeneratorInteractor.swift b/Nynja/Modules/QRCodeGenerator/Interactor/QRCodeGeneratorInteractor.swift index 48393decb9db85e5a1ba785f224480e308af3202..f22b7945cd03ba1d2cfb77444878e7fadee49676 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 63c54a892943161e8b1d681e0bb622f01096d832..57b80fc0b68a4c9df92731b8ec2614870e3435af 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 e40f87369517b7078abfe2149f46f52e967ec371..583854e8dd2fc26a2223b8196d2d8719ee379c78 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 1fd0fa81fcb4724ca08c6085304233e09f612aa1..08d601b9a58146039c34a5e1a7920ac71ddfa38c 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 71400cb8a06f2866328640c6bf2602f23297b04c..043ec4ff559352f2c782b3a8d317e5ce604e85a4 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 0f7e8eaaf8aad61fc960c1dcedcddf46a4f070d5..8592e1981e285b8a0ba2683f9aa6507136ac1fc1 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 ownQRCode: String - init() { - IoHandler.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 ownQRCode: String + let accountService: AccountService + let accountDAO: AccountDAOProtocol } - func contactQRNotFound() { - self.presenter.getContactFailed() + init(dependencies: Dependencies) { + presenter = dependencies.presenter + ownQRCode = dependencies.ownQRCode + accountService = dependencies.accountService + accountDAO = dependencies.accountDAO } + + // MARK: - Interactor Input + + func search(by qrCode: String) { + guard qrCode != ownQRCode else { + presenter?.didReceiveOwnAccount() + return + } + + if let account = accountDAO.fetchAccount(byQRCode: 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 e4ac0afaa50ce41128f4d3e7de2e04b373f35e87..bbfd66f2a0302c4728bcd11f773d2c9a191b7245 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(qrСode: String) { + interactor.search(by: qrСode) } - func getContactSuccess(contact: Contact) { - wireFrame.showOtherUserProfile(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 1ebff330c287888509a8a06f95b82e5120059b28..a7074094d223f538bf7bce756003f8b088e1d013 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(qrСode: 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 eeae4ad9580b7849dc8d027910c32ff517fa8785..cdf84d9656c57715e01d8b733e2509bd40e3dc55 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(qrСode: 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 875b0d14b155d41391701f85cba634e21b2f14be..3730250b2505c4a59f17b0214737e371e993090c 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 ownQRCode: 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, + ownQRCode: parameters.ownQRCode, + 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( + ownQRCode: accountId + ), + dependencies: .init( + accountService: serviceFactory.makeAccountService(), + accountDAO: serviceFactory.makeAccountDAO() + ) + ) + + navigation.pushViewController(view, animated: true) } func showOtherUserProfile(_ contact: Contact) { diff --git a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift index dec068752168e6dbf8ddf503faa8ba341d79b067..788fc8f48e606eed2f834bf6ef1a3c4887137d0c 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/ScheduleMessage/View/Views/DateTime/TimeZoneItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/DateTime/TimeZoneItemView.swift index da57fb00a1723d126e509eb4b8727738c85c31dc..25e7601b75a3b73107def7534d353dccbf99a1a9 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 d9dd77e7bd2b13ec8101a3ba0c547262aade224c..65fafbb7ba9c795f61f923e75de77030d4e3bf97 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 2233183eb5d3cee15c588e95bd1c7bf4f48c2bb8..2357f1222b4f0e619297596683be74c66cb4fb6e 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 c4d0115ca725d8dd199f8712833ca9cd34a9795b..bfe3eedec62661edd703d694e9c4ff444215a5d4 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 03c6d329217eb2b3b9bb73fd2c402f1d57200aa3..467485105cd2426e558ede30948d7fe32d0558c0 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/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift deleted file mode 100644 index 2713cf184a0cc8c93d431a911a7c39aae163c347..0000000000000000000000000000000000000000 --- a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// SelectCountrySelectCountryInteractor.swift -// Nynja -// -// Created by Roman Chopovenko on 30/01/2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -class SelectCountryInteractor: SelectCountryInteractorInputProtocol { - - weak var presenter: SelectCountryInteractorOutputProtocol! - var countriesData: [CountryModel] = [] - - func getCountriesList() { - let countries = StorageService.sharedInstance.countries - self.countriesData = countries - self.prepareCountriesData(countries) - } - - func filterList(with text: String) { - let filtered = self.countriesData.filter { $0.name.contains(substring: text, options: .caseInsensitive) } - self.prepareCountriesData(filtered) - } - - // MARK: 🔐 Private - private func prepareCountriesData(_ countries: [CountryModel]) { - let preparedData: KeysListAndSectionsDictionaty = countries.sorted(withSortingMode: .ascending).splitBySections(withSortingMode: .ascending) - self.presenter.preparedCountriesList(preparedData) - } -} diff --git a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift b/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift deleted file mode 100644 index 065e1225379867bff717d399171333366ded81ab..0000000000000000000000000000000000000000 --- a/Nynja/Modules/SelectCountry/Presenter/SelectCountryPresenter.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SelectCountrySelectCountryPresenter.swift -// Nynja -// -// Created by Roman Chopovenko on 30/01/2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -class SelectCountryPresenter: SelectCountryPresenterProtocol { - - weak var view: SelectCountryViewProtocol! - var interactor: SelectCountryInteractorInputProtocol! - var wireFrame: SelectCountryWireFrameProtocol! - - func getCountries() { - self.interactor.getCountriesList() - } - - func filterList(withText text: String) { - self.interactor.filterList(with: text) - } - - func dismiss() { - self.wireFrame.dismiss() - } -} - -extension SelectCountryPresenter: SelectCountryInteractorOutputProtocol { - - func preparedCountriesList(_ countries: KeysListAndSectionsDictionaty) { - self.view.updateCountriesList(with: countries) - } -} diff --git a/Nynja/Modules/SelectCountry/SelctCountryDelegate.swift b/Nynja/Modules/SelectCountry/SelctCountryDelegate.swift deleted file mode 100644 index ed9db66167012be583c75a2eca51de44d866ef24..0000000000000000000000000000000000000000 --- 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 deleted file mode 100644 index 478fff1104122c572f7bf247fb24544a303aba76..0000000000000000000000000000000000000000 --- a/Nynja/Modules/SelectCountry/SelectCountryProtocols.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// SelectCountrySelectCountryProtocols.swift -// Nynja -// -// Created by Roman Chopovenko on 30/01/2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -protocol SelectCountryWireFrameProtocol: class { - - func presentSelectCountry(navigation: UINavigationController, main: MainWireFrame?, selectCountryDelegate: SelectCountryDelegate) - - /** - * Add here your methods for communication PRESENTER -> WIREFRAME - */ - - func dismiss() -} - -protocol SelectCountryViewProtocol: class { - - var presenter: SelectCountryPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ - - func updateCountriesList(with data: KeysListAndSectionsDictionaty) -} - -protocol SelectCountryPresenterProtocol: class { - - var view: SelectCountryViewProtocol! { get set } - var interactor: SelectCountryInteractorInputProtocol! { get set } - var wireFrame: SelectCountryWireFrameProtocol! { get set } - - /** - * Add here your methods for communication VIEW -> PRESENTER - */ - func getCountries() - func filterList(withText text: String) - func dismiss() -} - -protocol SelectCountryInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ - - func preparedCountriesList(_ countries: KeysListAndSectionsDictionaty) -} - -protocol SelectCountryInteractorInputProtocol: class { - - var presenter: SelectCountryInteractorOutputProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ - func getCountriesList() - func filterList(with text: String) -} diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift deleted file mode 100644 index b1ec7e01d66ed7338bfde7b4c0fda0ff73f47390..0000000000000000000000000000000000000000 --- a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// SelectCountrySelectCountryViewController.swift -// Nynja -// -// Created by Roman Chopovenko on 30/01/2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardInteractive { - - var presenter: SelectCountryPresenterProtocol! - - private let topInset = Constraints.tableView.topInset.adjustedByWidth - private let bottomInset = Constraints.controlsContainerView.bottomInset.adjustedByWidth - - var countriesTableDataSource: SelectCountryTableDataSource! - var countriesTableDelegate: SelectCountryTableDelegate! - var scrollBar: ScrollBar! - - 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.tableFooterView = UIView() - table.showsHorizontalScrollIndicator = false - table.rowHeight = SelectorCell.Constraints.height - table.estimatedRowHeight = table.rowHeight - - self.view.addSubview(table) - table.snp.makeConstraints({ (make) in - make.top.equalTo(navigationView.snp.bottom).offset(topInset) - make.left.right.equalToSuperview() - }) - - return table - }() - - private lazy var controlContainerView: NynjaControlContainerView = { - let containerView = NynjaControlContainerView(contentView: searchField) - - view.addSubview(containerView) - containerView.snp.makeConstraints { maker in - maker.left.right.equalToSuperview() - maker.top.equalTo(self.tableView.snp.bottom) - adjustVerticalInset(.bottom, make: maker, offset: -bottomInset) - } - - containerView.addGradientView() - - containerView.addCloseButton { [weak self] _ in - self?.onCloseButtonTapped() - } - - return containerView - }() - - private lazy var searchField: NynjaSearchField = { - let searchField = NynjaSearchField() - searchField.searchTextChangeHandler = { [weak self] searchQuery in - let text = searchQuery ?? "" - self?.presenter.filterList(withText: text) - } - return searchField - }() - - - // MARK: - 🌎 Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - screenTitle = Constants.LocalizableKeys.selectCountry.localized.uppercased() - - self.setupData() - self.setupLayout() - } - - private func setupData(){ - self.tableView.register(SelectCountryCell.self, forCellReuseIdentifier: SelectCountryCell.cellId) - - self.countriesTableDataSource = SelectCountryTableDataSource() - self.tableView.dataSource = self.countriesTableDataSource - - self.countriesTableDelegate = SelectCountryTableDelegate() - self.countriesTableDelegate.cellDelegate = self - self.tableView.delegate = self.countriesTableDelegate - - self.presenter.getCountries() - - self.scrollBar = ScrollBar(scrollView: self.tableView) - self.countriesTableDelegate.scrollBar = self.scrollBar - } - - private func setupLayout() { - navigationView.isSeparatorVisible = true - controlContainerView.isHidden = false - } - - - // MARK: - SelectCountryViewProtocol - - func updateCountriesList(with data: KeysListAndSectionsDictionaty) { - self.countriesTableDataSource.sections = data.keysList - self.countriesTableDataSource.countriesDictionary = data.keySectionedDictionary - self.tableView.reloadData() - } - - - // MARK: - 🚀 Actions - - @objc private func onCloseButtonTapped() { - self.view.endEditing(true) - self.presenter.dismiss() - } - - - // MARK: - ⌨️ Keyboard - - func keyboardNotified(endFrame: CGRect) { - if endFrame.origin.y >= UIScreen.main.bounds.size.height { - updateToHide(view: controlContainerView, offset: -bottomInset) - } else { - updateToShow(view: controlContainerView, offset: -bottomInset - endFrame.height) - } - } -} - -// MARK: - SelectCountryCellDelegate -extension SelectCountryViewController: SelectCountryCellDelegate { - func didSelect(country: CountryModel, at indexPath: IndexPath) { - self.presenter.dismiss() - self.countryDelegate?.selected(country) - } -} diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewControllerLayout.swift b/Nynja/Modules/SelectCountry/View/SelectCountryViewControllerLayout.swift deleted file mode 100644 index 113a455db7af402860937335da86c97a146e651b..0000000000000000000000000000000000000000 --- 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/SelectCountryCellLayout.swift b/Nynja/Modules/SelectCountry/View/TableView/SelectCountryCellLayout.swift deleted file mode 100644 index 5f377a762df87afb516c1b57f4157507acae5f15..0000000000000000000000000000000000000000 --- 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 3c3109138a9c24080baf44fb2c7fa1a378fc4dde..0000000000000000000000000000000000000000 --- 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 13e5b3e0c9c82a28ffc939bba56878a7b340da2a..0000000000000000000000000000000000000000 --- 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 89d87428d4614da67f6eaa539c4513b323007d35..0000000000000000000000000000000000000000 --- 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 deleted file mode 100644 index 812cb1e8f241380e11aa66bbe29d406aef3c27a2..0000000000000000000000000000000000000000 --- a/Nynja/Modules/SelectCountry/WireFrame/SelectCountryWireframe.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SelectCountrySelectCountryWireframe.swift -// Nynja -// -// Created by Roman Chopovenko on 30/01/2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class SelectCountryWireFrame: SelectCountryWireFrameProtocol { - - weak var navigation : UINavigationController? - weak var main: MainWireFrame? - - func presentSelectCountry(navigation: UINavigationController, main: MainWireFrame?, selectCountryDelegate: SelectCountryDelegate) { - let view = SelectCountryViewController() - let presenter = SelectCountryPresenter() - let interactor = SelectCountryInteractor() - - self.navigation = navigation - self.main = main - - // Connecting - view.presenter = presenter - view.countryDelegate = selectCountryDelegate - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - - view.modalTransitionStyle = .crossDissolve - view.modalPresentationStyle = .overCurrentContext - navigation.pushViewController(view as UIViewController, animated: true) - } - - func dismiss() { - navigation?.popToRootViewController(animated: true) - } -} diff --git a/Nynja/Modules/Settings/ActiveSessions/Interactor/ActiveSessionsInteractor.swift b/Nynja/Modules/Settings/ActiveSessions/Interactor/ActiveSessionsInteractor.swift index ad11b2d1b0c50592d028708374500c39192830e0..5d3f2f038a0c7630162822aefa3251d88bf6b4be 100644 --- a/Nynja/Modules/Settings/ActiveSessions/Interactor/ActiveSessionsInteractor.swift +++ b/Nynja/Modules/Settings/ActiveSessions/Interactor/ActiveSessionsInteractor.swift @@ -15,8 +15,8 @@ final class ActiveSessionsInteractor: ActiveSessionsInteractorInputProtocol, Aut 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/Modules/Settings/ChangeNumber/ChangeNumberStep2/ChangeNumberStep2Protocols.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/ChangeNumberStep2Protocols.swift index e2be97c2a88295684658bea83c48ad960d0af4ef..5c20f6e08200e244b1deb3fffb9dec4e0a55f9f1 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 85e22e98e3d032bfad2b7bf55d51d40ad65deb8e..2829b3754ed732ebe4ecaa262e8e6a62165b4256 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 d33c50bfa6b6fdb079faecd1e8f90a747091a0f6..913f159863225f5ffc591416e13c0d4e66a0bd17 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 e02b2032157a01c6d6504b3952ad567bc7f92bc8..a6d1ffe7c4cf9904a8de7594d9bfaccaa7fb62f0 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 b3fe8260d90012cf8e32196c42754d59d5c88fc9..dbc960588b278f82dece96efe74505e7a3d005b7 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberView.swift @@ -7,9 +7,10 @@ // import Foundation +import NynjaUIKit class ChangeNumberView: UIView, UserSettingsRespondable, UITextFieldDelegate, TestableViewProtocol { - var countryModel: CountryModel? { + var countryModel: Country? { didSet { if let model = countryModel { let text = "".updateWithMask(placeHolder: model.placeHolder) @@ -169,7 +170,7 @@ class ChangeNumberView: UIView, UserSettingsRespondable, UITextFieldDelegate, Te lazy var checkButton: UIButton = { let btn = UIButton() - let img = UIImage.nynja.MainWheel.nextBttn.image + let img = UIImage.nynja.MainWheel.nextBttn1.image btn.setBackgroundImage(img, for: .normal) self.addSubview(btn) setupCheckButton(btn) diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep3/View/ChangeNumberCodeView.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep3/View/ChangeNumberCodeView.swift index 3c8eb562d833b9c4e2b0afa3da46e30aaccde78e..e9de108360336b15d65877018753a591bce5731f 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/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift deleted file mode 100644 index 7a740f09462182b60531de1e36cd5febd81072ef..0000000000000000000000000000000000000000 --- a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift +++ /dev/null @@ -1,91 +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() - presenter.showAuth() - } -} diff --git a/Nynja/Modules/Splash/Presenter/SplashPresenter.swift b/Nynja/Modules/Splash/Presenter/SplashPresenter.swift deleted file mode 100644 index 8a75ead4a6132517bafb3020153b47e091b950c0..0000000000000000000000000000000000000000 --- a/Nynja/Modules/Splash/Presenter/SplashPresenter.swift +++ /dev/null @@ -1,42 +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 { - - 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/Splash/SplashProtocols.swift b/Nynja/Modules/Splash/SplashProtocols.swift deleted file mode 100644 index 93800181eae0cb56ecbd7c2dc4d330be875a2a7d..0000000000000000000000000000000000000000 --- a/Nynja/Modules/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/Splash/WireFrame/SplashWireframe.swift b/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift deleted file mode 100644 index 84636f7d966617fe86b38ce7c9b469b63ceb6806..0000000000000000000000000000000000000000 --- a/Nynja/Modules/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) - } - - func showEditProfile() { - EditProfileWireFrame().presentEditProfile(navigation: navigation!, isRegistered: true, main: nil) - } -} diff --git a/Nynja/Modules/Stickers/View/ViewController/StickersInputView.swift b/Nynja/Modules/Stickers/View/ViewController/StickersInputView.swift index 472a2e7db8eb95c36881d30cccb47adbc45f73b5..2b22f359cc3c66772507cf5a962a7106d6685c9f 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 { diff --git a/Nynja/Modules/VideoPreview/View/VideoPreviewViewController.swift b/Nynja/Modules/VideoPreview/View/VideoPreviewViewController.swift index c49857e4c1158c6fc60312b5eca6c101836435c9..eca51605d941915c10bec5f7ea4418a875aaf113 100644 --- a/Nynja/Modules/VideoPreview/View/VideoPreviewViewController.swift +++ b/Nynja/Modules/VideoPreview/View/VideoPreviewViewController.swift @@ -44,7 +44,7 @@ class VideoPreviewViewController: UIViewController, VideoPreviewViewProtocol { lazy var nextButton : UIButton = { let btn = UIButton() - let img = UIImage.nynja.MainWheel.nextBttn.image + let img = UIImage.nynja.MainWheel.nextBttn1.image btn.setBackgroundImage(img, for: .normal) let width = UIScreen.main.bounds.width * 0.17 btn.layer.cornerRadius = width / 2 diff --git a/Nynja/Modules/Wallet Flows/SeedVerification/View/SeedVerificationWalletViewController.swift b/Nynja/Modules/Wallet Flows/SeedVerification/View/SeedVerificationWalletViewController.swift index 303a9e496dcbbaaf5c6a7272384ccea17e5252a1..5c43ade7242c2dff358040c33fe8de06a712edbb 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 diff --git a/Nynja/Modules/WebFullScreen/View/WebFullScreenViewController.swift b/Nynja/Modules/WebFullScreen/View/WebFullScreenViewController.swift index 4760fc1f4f34c2dc7935d0b851a8aeca35747363..786c848230d0a00dadb350776ae00cedd998d970 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/Modules/tutorial/TutorialProtocols.swift b/Nynja/Modules/tutorial/TutorialProtocols.swift index d385b843fb3502b57adb3a8ceab913de4e5fb4eb..98903084e73318765f5e126249ee46ac09b53379 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 5ca5deb81962d81e3d9652c324741b19b3f79922..73c778f8939420ecd29b8c86fd9368e86ac0392b 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/NotificationManager.swift b/Nynja/NotificationManager.swift index 51c91018de85dd51582bc9468ca90ee89d2a3d2a..1a780e98c7a1f2d78a617e9cd08561f93a09d4f3 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/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift new file mode 100644 index 0000000000000000000000000000000000000000..69bba39738cb55a239bc4e6e8e37b78f29ca2563 --- /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 0000000000000000000000000000000000000000..de9a2c1747285622c383dcac1d5b33bf548f47f4 --- /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 0000000000000000000000000000000000000000..a3d4c80218a0155562378acbd5360738746545d7 --- /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 0000000000000000000000000000000000000000..183ee0dd5f5e998cdb58d8c88530012233e8ecc9 --- /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/Assets.xcassets/ic_search.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Contents.json similarity index 84% rename from Nynja/Resources/Assets.xcassets/ic_search.imageset/Contents.json rename to Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Contents.json index 487152ca275246ef257719f5afcf3fe21da930c3..1a1d38180d3662d9f48cc0ac6bc1e9cd5ad4973e 100644 --- a/Nynja/Resources/Assets.xcassets/ic_search.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_search.pdf", + "filename" : "Disclosure_Indicator.pdf", "scale" : "1x" }, { 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Disclosure_Indicator.imageset/Disclosure_Indicator.pdf differ diff --git a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Contents.json similarity index 53% rename from Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json rename to Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Contents.json index 0fc3cb5069687694f1d48deaa3a99c26a056b024..5fff511254f1bdc6d3f90530ad23ca0ebf6ff697 100644 --- a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Contents.json @@ -2,14 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_new_group.pdf" + "filename" : "Empty_States_Images_img_empty_states_delete.pdf" } ], "info" : { "version" : 1, "author" : "xcode" - }, - "properties" : { - "preserves-vector-representation" : true } } \ 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Empty_States_Images_img_empty_states_delete.imageset/Empty_States_Images_img_empty_states_delete.pdf differ 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 0000000000000000000000000000000000000000..599c82dcf1928d3f2124128ddc4574346e7418ca --- /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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call.png differ 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..fa2948118838a122b3e275fbe97aeba2fe1f83f9 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@2x.png differ 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..f893d7451f8ee515e6cff2f55cb5d9485a469d78 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_accept_call.imageset/Icons_General_ic_accept_call@3x.png differ 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 0000000000000000000000000000000000000000..599c82dcf1928d3f2124128ddc4574346e7418ca --- /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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call.png differ 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@2x.png differ 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_email.imageset/Icons_General_ic_accept_call@3x.png differ 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 0000000000000000000000000000000000000000..24973f8b22aa852610d441e675fe5859109d0bcd --- /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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google.png differ 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@2x.png differ diff --git a/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@3x.png b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8c85573fd4157caa99a9baea61bef538e999e6d5 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_google.imageset/Icons_General_ic_google@3x.png differ 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 0000000000000000000000000000000000000000..a08e78b82cf9330cdbb030f08e7c32bdbeeb4a67 --- /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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/Icons_General_ic_mail.imageset/Icons_General_ic_mail.pdf differ 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/next-bttn.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/Contents.json deleted file mode 100644 index f9494e456da4440aef4641751f98db24be098823..0000000000000000000000000000000000000000 --- 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 Binary files a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn.png and /dev/null differ 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 Binary files a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@2x.png and /dev/null differ 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 Binary files a/Nynja/Resources/Assets.xcassets/New Folder/next-bttn.imageset/next-bttn@3x.png and /dev/null differ 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/left_image.pdf b/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf deleted file mode 100644 index a21b5c9f751612124205c9b533bb03172339cabe..0000000000000000000000000000000000000000 Binary files a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/left_image.pdf and /dev/null differ 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 Binary files a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/right_image.pdf and /dev/null differ diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json similarity index 76% rename from Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json rename to Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json index bab7f7d16c0419003b4fba7627c67b3b587c5a66..2774291293327580fa0dbf3ea5d9c8abc6bd717b 100644 --- a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_left_image.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/ic_add.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "left_image.pdf" + "filename" : "ic_add.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/ic_add.imageset/ic_add.pdf b/Nynja/Resources/Assets.xcassets/ic_add.imageset/ic_add.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9e32a8a702744ac53b7c6410c795686f51f6a06c Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_add.imageset/ic_add.pdf differ diff --git a/Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/Contents.json deleted file mode 100644 index f958d87dfa2c25244a187761efd56f6da49fcd6e..0000000000000000000000000000000000000000 --- a/Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "tile", - "width" : 1, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 31, - "top" : 31, - "right" : 30, - "left" : 32 - } - }, - "idiom" : "universal", - "filename" : "ic_camera_frame.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_contacts_empty.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_contacts_empty.imageset/Contents.json index 93f22f9036386c54aaf87ad50d0238a90a5521a3..e1079b59d26020bbd10e1712922662fcd2d98081 100644 --- a/Nynja/Resources/Assets.xcassets/ic_contacts_empty.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/ic_contacts_empty.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_contacts_empty.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "ic_contacts_empty.pdf" } ], "info" : { 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 0000000000000000000000000000000000000000..abc5db8768ce2e84e2b7857364b2a958ff47b4ed --- /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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar.png differ diff --git a/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@2x.png b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a94227560bbc0d7d55310b4ef78d3a4c04ded2b0 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@2x.png differ diff --git a/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@3x.png b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..24cbfdb0b2af9a9392c5306ac371abb670c69132 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_empty_avatar.imageset/ic_empty_avatar@3x.png differ 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 0000000000000000000000000000000000000000..d0ace947d496cb987e5095cae04408f37d40ccd7 --- /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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook.png differ diff --git a/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@2x.png b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..95f367911766eb286810361aac05a96cbdd9d00b Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@2x.png differ 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_facebook.imageset/ic_facebook@3x.png differ diff --git a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_fb.imageset/Contents.json similarity index 76% rename from Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json rename to Nynja/Resources/Assets.xcassets/ic_fb.imageset/Contents.json index 3c7f1cbf179166e031b0b0647596c78be9bb6300..7b50dd81f9c2c28bae0ba4b5d193596a113eb1d4 100644 --- a/Nynja/Resources/Assets.xcassets/WheelPosition/wheel_right_image.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/ic_fb.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "right_image.pdf" + "filename" : "ic_fb.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/ic_camera_frame.pdf b/Nynja/Resources/Assets.xcassets/ic_fb.imageset/ic_fb.pdf similarity index 78% rename from Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/ic_camera_frame.pdf rename to Nynja/Resources/Assets.xcassets/ic_fb.imageset/ic_fb.pdf index 50a7b4b496db95a98c45d25b1f64f7a29a23426c..e3ce027c01a1bad2b8ea49f10109ed60770809cb 100644 Binary files a/Nynja/Resources/Assets.xcassets/ic_camera_frame.imageset/ic_camera_frame.pdf and b/Nynja/Resources/Assets.xcassets/ic_fb.imageset/ic_fb.pdf differ 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 Binary files a/Nynja/Resources/Assets.xcassets/ic_new_group.imageset/ic_new_group.pdf and /dev/null differ 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 0000000000000000000000000000000000000000..24bb65c00db1e95fe26fabe8684598b0628564c3 --- /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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/ic_phone.imageset/ic_phone.pdf differ diff --git a/Nynja/Resources/Assets.xcassets/ic_search_empty.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_search_empty.imageset/Contents.json index a732cdea21de4862a77e2ba4105c4e675b5e8010..22438d1318658c8aaec1ccfa00aedeff0ceef10a 100644 --- a/Nynja/Resources/Assets.xcassets/ic_search_empty.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/ic_search_empty.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_search_empty.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "ic_search_empty.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/ic_twitter.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_twitter.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..4768223c79cba6b034022327b3e707f5e7c62405 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/ic_twitter.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_twitter.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_search.imageset/ic_search.pdf b/Nynja/Resources/Assets.xcassets/ic_twitter.imageset/ic_twitter.pdf similarity index 72% rename from Nynja/Resources/Assets.xcassets/ic_search.imageset/ic_search.pdf rename to Nynja/Resources/Assets.xcassets/ic_twitter.imageset/ic_twitter.pdf index 967881b11815ab328fcd947c3488a36d0fa5c404..52861da6fd2122698c0ebfdf2f0b1bfac789d9b8 100644 Binary files a/Nynja/Resources/Assets.xcassets/ic_search.imageset/ic_search.pdf and b/Nynja/Resources/Assets.xcassets/ic_twitter.imageset/ic_twitter.pdf differ diff --git a/Nynja/Resources/Assets.xcassets/img_empty_states_contact_requests.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/img_empty_states_contact_requests.imageset/Contents.json index 7f848facc80b4293665825e3b83f245594a82c07..49c6029eef61ece80c43a70d56b77cb32e893e5a 100644 --- a/Nynja/Resources/Assets.xcassets/img_empty_states_contact_requests.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/img_empty_states_contact_requests.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "img_empty_states_contact_requests.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "img_empty_states_contact_requests.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/img_empty_states_groups.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/img_empty_states_groups.imageset/Contents.json index d7d55b0e611754a0bed7aa59c7019bb6693e8f65..72322028f86b57951cdd5f138350549beeb5dcd4 100644 --- a/Nynja/Resources/Assets.xcassets/img_empty_states_groups.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/img_empty_states_groups.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "img_empty_states_groups.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "img_empty_states_groups.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/img_empty_states_p2p.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/img_empty_states_p2p.imageset/Contents.json index ea0c5f8c663956434b8efd03517e45d41aa36e89..3de5b3c428f824796c1bc8cdc0fed7c4d35f7a3f 100644 --- a/Nynja/Resources/Assets.xcassets/img_empty_states_p2p.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/img_empty_states_p2p.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "img_empty_states_p2p.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "img_empty_states_p2p.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/img_empty_states_scheduled.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/img_empty_states_scheduled.imageset/Contents.json index bd5c52c81b8091b55cc06929d84d751c5f6d77df..d2892c8c5a5c9e03a6e824901964a57f82a6d56d 100644 --- a/Nynja/Resources/Assets.xcassets/img_empty_states_scheduled.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/img_empty_states_scheduled.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "img_empty_states_scheduled.pdf", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "img_empty_states_scheduled.pdf" } ], "info" : { diff --git a/Nynja/Resources/Assets.xcassets/logo-2.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/logo-2.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..77ad60e245a5e9c053f32369ae703aefe92bcfd2 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/logo-2.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "info" : { + "author" : "xcode", + "version" : "1" + }, + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "logo.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "logo@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "logo@3x.png" + } + ] +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo.png b/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..360c88d803466260cde5f4590c2ed2b4a525d1c3 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo.png differ 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@2x.png differ diff --git a/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@3x.png b/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..311b4cf0e2ffe27dbf8afd5c81460e824179c89a Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/logo-2.imageset/logo@3x.png differ diff --git a/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Contents.json index b6d1c0875e4c58236d341874bd3092109d15b014..35ef22821cde69efd4af107670839cf24b658041 100644 --- a/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Contents.json +++ b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Contents.json @@ -1,21 +1,23 @@ { + "info" : { + "author" : "xcode", + "version" : "1" + }, "images" : [ { "idiom" : "universal", - "filename" : "table_overrides_right_overrides_checkbox_ic_unchecked.pdf", - "scale" : "1x" + "scale" : "1x", + "filename" : "Table_Overrides_Right_Overrides_Checkbox_ic_unchecked.png" }, { "idiom" : "universal", - "scale" : "2x" + "scale" : "2x", + "filename" : "Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@2x.png" }, { "idiom" : "universal", - "scale" : "3x" + "scale" : "3x", + "filename" : "Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@3x.png" } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } + ] } \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked.png b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..87553fefef6bbf619aa525ee7f14c41e4a4d8ad2 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked.png differ diff --git a/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@2x.png b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..44c61b861dbb6614f4b5b6549c50c835dab45bc0 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@2x.png differ 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 Binary files /dev/null and b/Nynja/Resources/Assets.xcassets/table_overrides_right_overrides_checkbox_ic_unchecked.imageset/Table_Overrides_Right_Overrides_Checkbox_ic_unchecked@3x.png differ diff --git a/Nynja/Resources/Colors.json b/Nynja/Resources/Colors.json index 6f8e098e93afc170a3ac0d011acee8ed9b29d027..d5a6f90b40a663ee0dc748c633c477bcbc2e92ff 100644 --- a/Nynja/Resources/Colors.json +++ b/Nynja/Resources/Colors.json @@ -45,6 +45,10 @@ "black": "#000000", "callBackground": "#2c2e33", "separatorGrayColor": "#3f3f3f", - "callGradientStart": "#2c2e33ff", - "callGradientEnd": "#2c2e3300", + "gradientStart": "#2c2e33ff", + "gradientEnd": "#2c2e3300", + "facebookBackground": "#3b5998", + "facebookHighlighted": "#213980", + "whiteHighlighted": "#d3d9e0", + "lightTransparentBlack": "#0000001A", } diff --git a/Nynja/Resources/DevAutoTests.xcconfig b/Nynja/Resources/DevAutoTests.xcconfig index 35a7a2f0e76c883ca7b18bfad79192739a01babb..e9919324a4edb8d87aa7afbd3925c21dec071f73 100644 --- a/Nynja/Resources/DevAutoTests.xcconfig +++ b/Nynja/Resources/DevAutoTests.xcconfig @@ -6,12 +6,28 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // - 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 +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/DevConfig.xcconfig b/Nynja/Resources/DevConfig.xcconfig index c48955c46660ad9d3a1ead38858307bac93c5ecc..e9919324a4edb8d87aa7afbd3925c21dec071f73 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 @@ -20,3 +19,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 = 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/Info.plist b/Nynja/Resources/Info.plist index 54727582ea570842e70b6b2525e539dfae288b22..e6132d91d48f1d93af815bd8e1dc0fcd674d11f0 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -22,8 +22,19 @@ APPL CFBundleShortVersionString 0.8.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + $(ReversedGoogleClientId) + + + CFBundleVersion - 0.8.5 + 0.8.5.multi-acc ConfServerAddress $(ConfServerAddress) ConfServerPort @@ -46,6 +57,10 @@ + GoogleClientId + $(GoogleClientId) + GoogleServerClientId + $(GoogleServerClientId) LSApplicationQueriesSchemes cydia @@ -76,6 +91,32 @@ 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 $(ServerPort) ServerURL diff --git a/Nynja/Resources/LaunchScreen.storyboard b/Nynja/Resources/LaunchScreen.storyboard index 2e44e96b2b65c1710ebabee7daa839fe186f13c9..4aa5ed7f1840fa6fec9f53031bb8ed04ec010af3 100644 --- a/Nynja/Resources/LaunchScreen.storyboard +++ b/Nynja/Resources/LaunchScreen.storyboard @@ -1,13 +1,11 @@ - + - - - + @@ -27,16 +25,16 @@ - + - + - - - + + + diff --git a/Nynja/Resources/LoadDBConfig.xcconfig b/Nynja/Resources/LoadDBConfig.xcconfig index 7b7891380aaca508808afbf6d17043c8406f720e..1a33bb98c4a15d3412dc4278134201be749ef8b5 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 20b1bc320d38ffb72a2da193096780abf7295393..98465937179a118e7f56351048d7572430b555f0 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 @@ -20,3 +19,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 = 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/PrereleaseDebugConfig.xcconfig b/Nynja/Resources/PrereleaseDebugConfig.xcconfig index af5e0d2c49dfe09f620d02c0ea9e9542093f35ce..b15700dcf2625eb670696b2adabef2ffa9296110 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 1c5970aa361ac165cc1b574c40372b3a1ff9eec7..aa395d1bc36f0ced5c4107f28d969db8aa9fd208 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.nynja.net @@ -20,3 +19,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 = 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/ThirdPartyServices.swift b/Nynja/Resources/ThirdPartyServices.swift index a7db9394b9d2423973012bc31b3f1dbb930b08aa..51feb463523b6288b7df03411dbcc930d6711d1f 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 { @@ -84,6 +85,19 @@ struct GoogleService: ThirdPartyService { } } +struct GoogleSignInService: ThirdPartyService { + struct Config { + let clientId: String + let serverClientId: String + } + + let serviceConfig: Config + + init(config: AppConfig) { + serviceConfig = Config(clientId: Bundle.main.googleClientId, serverClientId: Bundle.main.googleServerClientId) + } +} + struct IntercomService: ThirdPartyService { struct Config { let apiKey: String diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 261739e945f65f9850a9c40979afdf6053821bbf..c93c0a9500e7c62136e094de66a315faa3debbfe 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -21,6 +21,8 @@ "unmute"="Unmute"; "join"="Join"; "no_search_result" = "Sorry, no search result"; +"facebook"="Facebook"; +"twitter"="Twitter"; // MARK: alert "number_or_region_empty"="Number or region is empty"; @@ -493,9 +495,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"; @@ -503,7 +507,7 @@ // MARK: Recording Status "video"="video"; -"recording"="...recording a"; +"recording"="recording"; // MARK: Presence status "active"="active"; @@ -554,9 +558,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"; @@ -579,11 +580,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_deleteAccount"="Delete Account"; @@ -599,6 +601,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"; "wheel_item_call_contact" = "Call Сontact"; "wheel_item_dial_number" = "Dial Number"; "wheel_item_call_history" = "Call History"; @@ -822,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"; @@ -997,3 +1000,173 @@ "camera_setting_photo_quality" = "Photo quality"; "camera_setting_video_resolution" = "Video resolution"; "camera_setting_microphone_access" = "Microphone access"; + + +// 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"; + +"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 + +"create_profile.screen_title" = "CREATE PROFILE"; +"create_profile.first_name_field_placeholder" = "First Name"; +"create_profile.last_name_field_placeholder" = "Last Name"; +"create_profile.account_name_field_placeholder" = "Account Name"; +"create_profile.username_field_placeholder" = "Username"; +"create_profile.profile_message_field_placeholder" = "Profile Message"; +"create_profile.agree_at_terms" = "I agree at"; +"create_profile.terms_hint" = "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."; +"create_profile.terms_of_use" = "terms of use"; +"create_profile.create_button" = "CREATE"; + + +// MARK: - Account Edit Flow + +// MARK: Account Settings + +"account_settings.screen_title" = "ACCOUNT SETTINGS"; +"account_settings.save_button" = "SAVE"; + +"account_settings.header.personal_information" = "Perfomal Information"; +"account_settings.header.username" = "Username"; +"account_settings.header.contact_information" = "Contact Information"; + +"account_settings.status_field_title" = "Status"; +"account_settings.timeout_field_title" = "Inactive Timeout"; + +"account_settings.profile_message_field_placeholder" = "Profile Message"; +"account_settings.first_name_field_placeholder" = "First Name"; +"account_settings.last_name_field_placeholder" = "Last Name"; +"account_settings.birthday_field_placeholder" = "Birthday"; +"account_settings.username_field_placeholder" = "Username"; + +"account_settings.contact_info_field_title" = "Add Contact Info"; +"account_settings.delete_account_field_title" = "Delete Account"; + +"account_settings.status_alert_title" = "Status"; +"account_settings.timeout_alert_title" = "Inactive Timeout"; +"account_settings.add_contact_info_alert_title" = "Add Contact Info"; + +"account_settings.phone_number_input_alert_action_title" = "Phone Number"; +"account_settings.email_input_alert_action_title" = "Email"; + +"account_settings.username.description" = "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."; + +// 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."; + +// MARK: Contact Info + +"phone_number_picker.title" = "Phone number type"; +"phone_number_label.mobile" = "Mobile"; +"phone_number_label.home" = "Home"; +"phone_number_label.work" = "Work"; +"phone_number_label.add_custom" = "Add Custom"; + +"contact_info.phone_number_screen_title" = "ADD NUMBER"; +"contact_info.email_screen_title" = "ADD EMAIL"; +"contact_info.facebook_screen_title" = "ADD FACEBOOK"; +"contact_info.twitter_screen_title" = "ADD TWITTER"; + +"contact_info.phone_number_delete_button" = "Delete Phone Number"; +"contact_info.email_delete_button" = "Delete Email"; +"contact_info.social_delete_button" = "Remove Account"; + +"contact_info.phone_number_picker_alert_title" = "Phone number type"; + +"contact_info.email.placeholder" = "Email"; +"contact_info.facebook.placeholder" = "Facebook Link"; +"contact_info.twitter.placeholder" = "Twitter Link"; + +// MARK: Delete Account + +"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"; + + +// 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"; +"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/Resources/profile.bert b/Nynja/Resources/profile.bert new file mode 100644 index 0000000000000000000000000000000000000000..983836056d2b3aa85186efa00a1044a5b00ab979 Binary files /dev/null and b/Nynja/Resources/profile.bert differ diff --git a/Nynja/SDK/Account/Entities/AccountInfo.swift b/Nynja/SDK/Account/Entities/AccountInfo.swift new file mode 100644 index 0000000000000000000000000000000000000000..4ca08bb24aa9255d020cc0a441c12f7f56b3f9b1 --- /dev/null +++ b/Nynja/SDK/Account/Entities/AccountInfo.swift @@ -0,0 +1,49 @@ +// +// AccountInfo.swift +// Nynja +// +// Created by Anton Poltoratskyi on 20.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +public final class AccountInfo { + + 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?, + 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 0000000000000000000000000000000000000000..0264b5d7fafd200dd927d3f63df778f8d7335bdf --- /dev/null +++ b/Nynja/SDK/Account/Service/AccountService.swift @@ -0,0 +1,81 @@ +// +// AccountService.swift +// Nynja +// +// Created by Anton Poltoratskyi on 20.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +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 IdentityCompletion = (Result) -> Void + typealias AccountCompletion = (Result) -> Void + typealias AccountListCompletion = (Result<[NYNAccountDetails]>) -> Void + typealias StatusCompletion = (Result) -> Void + typealias SearchCompletion = (Result) -> Void + + + // MARK: - Profile (Identity) + + func getIdentity(by identityId: String, completion: @escaping IdentityCompletion) + + func deleteIdentity(_ identityId: String, completion: @escaping StatusCompletion) + + // MARK: Login Option + + func addAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, to identityId: String, completion: @escaping StatusCompletion) + + func deleteAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, from identityId: String, completion: @escaping StatusCompletion) + + + // MARK: - Account + + func completePendingAccountCreation(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) + + func updateAccount(_ accountInfo: AccountInfo, completion: @escaping AccountCompletion) + + func deleteAccount(_ accountId: String, completion: @escaping StatusCompletion) + + func getAccount(accountId: String, completion: @escaping AccountCompletion) + + func getAccount(qrCode: String, completion: @escaping AccountCompletion) + + func getAccount(username: String, completion: @escaping AccountCompletion) + + func getAccount(authenticationIdentifier: String, authType: NYNAccountAuthenticationType, completion: @escaping AccountCompletion) + + func getAllAccounts(by identityId: String, completion: @escaping AccountListCompletion) + + + // MARK: - Account's Contact Info + + func addContactInfo(_ contactDetails: NYNContactDetails, to accountId: String, completion: @escaping StatusCompletion) + + func editContactInfo(_ editInfo: UpdateInfo, in accountId: String, completion: @escaping StatusCompletion) + + func deleteContactInfo(_ contactDetails: NYNContactDetails, from accountId: String, 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 new file mode 100644 index 0000000000000000000000000000000000000000..8b9576b866529e79339ec81f78eb24d95efbb7ea --- /dev/null +++ b/Nynja/SDK/Account/Service/AccountServiceImpl.swift @@ -0,0 +1,451 @@ +// +// 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 storage: SessionStorage + + private let processingQueue: DispatchQueue + + + // MARK: - Handlers + + // MARK: Identity + + private var getIdentityCompletion: IdentityCompletion? + private var deleteIdentityCompletion: StatusCompletion? + private var addAuthProviderToIdentityCompletion: StatusCompletion? + private var deleteAuthProviderFromIdentityCompletion: StatusCompletion? + + // MARK: Account + + private var completePendingAccountCompletion: AccountCompletion? + private var updateAccountCompletion: AccountCompletion? + private var deleteAccountCompletion: StatusCompletion? + 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 + } + + + // MARK: - Init + + public struct Dependencies { + let accountManager: AccountManager + let storage: SessionStorage + let processingQueue: DispatchQueue = .main + } + + public init(dependencies: Dependencies) { + accountManager = dependencies.accountManager + storage = dependencies.storage + processingQueue = dependencies.processingQueue + + super.init() + + accountManager.delegate = self + } + + + // MARK: - Profile (Identity) + + public func getIdentity(by identityId: String, completion: @escaping IdentityCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getIdentityCompletion) + + accountManager.sendGetIdentity(identityId, withAccessToken: token) + } + + public func deleteIdentity(_ identityId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteIdentityCompletion) + + accountManager.sendDeleteIdentity(identityId, withAccessToken: token) + } + + public func addAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, to identityId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.addAuthProviderToIdentityCompletion) + + accountManager.sendAddAuthenticationProvider(toIdentity: identityId, withAccessToken: token, with: authProviderDetails) + } + + public func deleteAuthenticationProvider(_ authProviderDetails: NYNAuthProviderDetails, from identityId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteAuthProviderFromIdentityCompletion) + + accountManager.sendDeleteAuthenticationProvider(fromIdentity: identityId, withAccessToken: token, with: authProviderDetails) + } + + + // MARK: - Account + + public 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) + } + + public 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) + } + + public func deleteAccount(_ accountId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteAccountCompletion) + + accountManager.sendDeleteAccount(withAccountId: accountId, withAccessToken: token) + } + + public func getAccount(accountId: String, completion: @escaping AccountCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getAccountByIdCompletion) + + accountManager.sendGetAccount(withAccountId: accountId, withAccessToken: token) + } + + 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 + } + bind(completion, to: \AccountServiceImpl.getAccountByAuthProviderCompletion) + + accountManager.sendGetAccount(withAuthProvider: authenticationIdentifier, withAccessToken: token, with: authType) + } + + public func getAllAccounts(by identityId: String, completion: @escaping AccountListCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.getAllAccountsCompletion) + + accountManager.sendGetAllAccounts(withIdentity: identityId, withAccessToken: token) + } + + + // MARK: - Account's Contact Info + + public func addContactInfo(_ contactDetails: NYNContactDetails, to accountId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.addContactInfoCompletion) + + accountManager.sendAddContactInfoToAccount(withAccountId: accountId, withAccessToken: token, with: contactDetails) + } + + 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(_ contactDetails: NYNContactDetails, from accountId: String, completion: @escaping StatusCompletion) { + guard let token = token else { + return + } + bind(completion, to: \AccountServiceImpl.deleteContactInfoCompletion) + + 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 + } + } + + 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) + } + } + + 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)) + } + } +} + + +// MARK: - NYNAccountManagerDelegate + +extension AccountServiceImpl { + + // MARK: Identity + + public func getIdentityDidFinish(with profileDetails: NYNProfileDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getIdentityCompletion) { completion in + self.processResponseBody(profileDetails, error: error, completion: completion) + } + } + + @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) + } + } + + 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 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?) { + // FIXME: deprecated + } + + public func completePendingAccountCreationDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.completePendingAccountCompletion) { completion in + self.processResponseBody(accountDetails, error: error, completion: completion) + } + } + + public func updateAccountDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.updateAccountCompletion) { completion in + 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 + self.processResponseBody(status, error: error, completion: completion) + } + } + + public func getAccountByIdDidFinish(with accountDetails: NYNAccountDetails?, withError error: Error?) { + 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 + self.processResponseBody(accountDetails, error: error, completion: completion) + } + } + + public func getAllAccountsByIdentityDidFinish(withDetails accountDetailsArray: [NYNAccountDetails]?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.getAllAccountsCompletion) { completion in + self.processResponseBody(accountDetailsArray, error: error, completion: completion) + } + } + + // MARK: Account's Contact Info + + public func addContactInfoToAccountDidFinish(withStatus status: String, withError error: Error?, withAccountId accountId: String) { + handleResponse(nil, to: \AccountServiceImpl.addContactInfoCompletion) { completion in + 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?, withAccountId accountId: String) { + handleResponse(nil, to: \AccountServiceImpl.deleteContactInfoCompletion) { completion in + self.processResponseBody(status, error: error, completion: completion) + } + } + + + // MARK: Search + + public func searchByPhoneNumberDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.searchByPhoneCompletion) { completion in + self.processResponseBody(searchResultDetails, error: error, completion: completion) + } + } + + public func searchByEmailDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.searchByEmailCompletion) { completion in + self.processResponseBody(searchResultDetails, error: error, completion: completion) + } + } + + public func searchByUsernameDidFinish(with searchResultDetails: NYNSearchResultDetails?, withError error: Error?) { + handleResponse(nil, to: \AccountServiceImpl.searchByUsernameCompletion) { completion in + self.processResponseBody(searchResultDetails, error: error, completion: completion) + } + } + + 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/App/AppBundleCredentials.swift b/Nynja/SDK/App/AppBundleCredentials.swift new file mode 100644 index 0000000000000000000000000000000000000000..81d2390c54ec18d35f63026c55172b8f44d58631 --- /dev/null +++ b/Nynja/SDK/App/AppBundleCredentials.swift @@ -0,0 +1,14 @@ +// +// 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 + let appToken: String +} diff --git a/Nynja/SDK/App/AppConfigurationProvider.swift b/Nynja/SDK/App/AppConfigurationProvider.swift new file mode 100644 index 0000000000000000000000000000000000000000..4e9fe7933900cd6de5ac216853e9be2c7db4044f --- /dev/null +++ b/Nynja/SDK/App/AppConfigurationProvider.swift @@ -0,0 +1,52 @@ +// +// 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, + appToken: "" + ) + } + + 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 0000000000000000000000000000000000000000..b90d4f0e0ae865fa959b2cdeecdf72ef42ae1608 --- /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/Entities/AuthConfirmationType.swift b/Nynja/SDK/Auth/Entities/AuthConfirmationType.swift new file mode 100644 index 0000000000000000000000000000000000000000..024d4dee842e30c5e32dcc81ed117c93bea7077c --- /dev/null +++ b/Nynja/SDK/Auth/Entities/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/Entities/AuthResponse.swift b/Nynja/SDK/Auth/Entities/AuthResponse.swift new file mode 100644 index 0000000000000000000000000000000000000000..11e4b5f12ec1d46b536902ac9119157bf047aacc --- /dev/null +++ b/Nynja/SDK/Auth/Entities/AuthResponse.swift @@ -0,0 +1,19 @@ +// +// AuthResponse.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// 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/Entities/AuthTokenData.swift b/Nynja/SDK/Auth/Entities/AuthTokenData.swift new file mode 100644 index 0000000000000000000000000000000000000000..ca22a319b2a01dd4e112361e2e6a813df8f63fef --- /dev/null +++ b/Nynja/SDK/Auth/Entities/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/SDK/Auth/Entities/AuthenticationType.swift b/Nynja/SDK/Auth/Entities/AuthenticationType.swift new file mode 100644 index 0000000000000000000000000000000000000000..c254b8892e26e3fe308bf4d50fcb03206bd1c0a4 --- /dev/null +++ b/Nynja/SDK/Auth/Entities/AuthenticationType.swift @@ -0,0 +1,15 @@ +// +// AuthenticationType.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum AuthenticationType { + /// User hasn't been registered yet + case register + + /// User has already registered + case login +} diff --git a/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift b/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift new file mode 100644 index 0000000000000000000000000000000000000000..1779af37fb1cb19c8f2be3acbab8950aa5109734 --- /dev/null +++ b/Nynja/SDK/Auth/Entities/PhoneNumberInfo.swift @@ -0,0 +1,41 @@ +// +// 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 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 { + return number + } + var temp = number + + return mask.reduce(into: "") { result, char in + result.append(char == " " ? " " : temp.removeFirst()) + } + } +} diff --git a/Nynja/SDK/Auth/Entities/PhoneNumberLabel.swift b/Nynja/SDK/Auth/Entities/PhoneNumberLabel.swift new file mode 100644 index 0000000000000000000000000000000000000000..7bb2a3f10086077da27dbb40d7f1fbc109522f20 --- /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/ProfileMockProvider.swift b/Nynja/SDK/Auth/ProfileMockProvider.swift new file mode 100644 index 0000000000000000000000000000000000000000..de81844fe4714e99be8ba3556e37a31bd4255c10 --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..359df28357baa8b24f200dfb7ac415ada17ff678 --- /dev/null +++ b/Nynja/SDK/Auth/Service/AuthService.swift @@ -0,0 +1,33 @@ +// +// 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 StatusCompletion = (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 CodeConfirmationCompletion) + + func loginByGoogle(serverCode: 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) + + func processAuthenticatedAccount(_ account: Account) throws +} diff --git a/Nynja/SDK/Auth/Service/AuthServiceImpl.swift b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift new file mode 100644 index 0000000000000000000000000000000000000000..8b7ec03976a3319f9c70ee7cf8696aec925a0333 --- /dev/null +++ b/Nynja/SDK/Auth/Service/AuthServiceImpl.swift @@ -0,0 +1,294 @@ +// +// AuthServiceImpl.swift +// Nynja +// +// Created by Anton Poltoratskyi on 13.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import NynjaSDK + +final class AuthServiceImpl: NSObject, AuthService, InitializeInjectable, NYNLoginManagerDelegate { + + enum AuthError: Error { + case invalidResponse + } + + // MARK: - Dependencies + + private let loginManager: LoginManager + + private let storage: StorageService + + private let appConfigurationProvider: AppConfigurationProvider + + private let processingQueue: DispatchQueue + + + // MARK: - Handlers + + private var loginByPhoneCompletion: LoginCompletion? + private var loginByEmailCompletion: LoginCompletion? + private var loginByFacebookCompletion: LoginCompletion? + private var loginByGoogleCompletion: LoginCompletion? + private var confirmCodeCompletion: CodeConfirmationCompletion? + private var verifyAuthProviderCompletion: StatusCompletion? + private var refreshTokenCompletion: RefreshTokenCompletion? + + // MARK: - Properties + + private var appToken: String { + return appConfigurationProvider.sdkCredentials.appToken + } + + + // MARK: - Init + + struct Dependencies { + let loginManager: LoginManager + let storage: StorageService + let appConfigurationProvider: AppConfigurationProvider + let processingQueue: DispatchQueue = .main + } + + init(dependencies: Dependencies) { + loginManager = dependencies.loginManager + storage = dependencies.storage + appConfigurationProvider = dependencies.appConfigurationProvider + processingQueue = dependencies.processingQueue + + super.init() + + loginManager.delegate = self + initialize() + } + + private func initialize() { + let credentials = appConfigurationProvider.sdkCredentials + + loginManager.initialize(withDeviceId: credentials.deviceId, + withInstanceId: credentials.instanceId, + withAppClass: String(describing: type(of: self)), + withOrgId: credentials.bundleId) + } + + + // MARK: - API + + func login(by email: String, completion: @escaping LoginCompletion) { + bind(completion, to: \AuthServiceImpl.loginByEmailCompletion) + loginManager.sendLogin(byEmail: email, withAppToken: appToken) + } + + func login(by numberInfo: PhoneNumberInfo, confirmVia authConfirmationType: AuthConfirmationType, completion: @escaping LoginCompletion) { + let phone = numberInfo.formattedForRequest() + + bind(completion, to: \AuthServiceImpl.loginByPhoneCompletion) + loginManager.sendLogin(byPhone: phone, withAppToken: appToken, withSendTokenVia: authConfirmationType.sdkValue) + } + + 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: 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)) + } + } + } + + private func _loginByGoogle(completion: @escaping LoginCompletion) { + bind(completion, to: \AuthServiceImpl.loginByGoogleCompletion) + loginManager.sendLoginByGooglePlus(withAppToken: appToken) + } + + func confirmNynjaCode(_ code: String, completion: @escaping CodeConfirmationCompletion) { + confirm(code: code, with: "", completion: completion) + } + + func confirmSocialServerAuthCode(_ code: String, completion: @escaping CodeConfirmationCompletion) { + confirm(code: "", with: code, completion: completion) + } + + private func confirm(code: String, with socialToken: String?, completion: @escaping CodeConfirmationCompletion) { + bind(completion, to: \AuthServiceImpl.confirmCodeCompletion) + 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) + } + + func processAuthenticatedAccount(_ account: Account) throws { + let account = DBAccount(account: account) + + guard case let passcode = account.profileId, !passcode.isEmpty else { + assertionFailure("Unable to setup database") + return + } + storage.setupDatabase(with: passcode, application: UIApplication.shared) + + storage.wasLogined = true + storage.clientId = appConfigurationProvider.sdkCredentials.deviceId + 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 + + try storage.perform(action: .save, with: profile) + } + + + // MARK: - Delegate + + func sendLogin(byEmailDidFinish error: Error?) { + handleResponse(nil, to: \AuthServiceImpl.loginByEmailCompletion) { completion in + self.processResponseBody((), error: error, completion: completion) + } + } + + func sendLogin(byPhoneDidFinish error: Error?) { + handleResponse(nil, to: \AuthServiceImpl.loginByPhoneCompletion) { completion in + self.processResponseBody((), error: error, completion: completion) + } + } + + func sendLogin(byFacebookDidFinish error: Error?) { + handleResponse(nil, to: \AuthServiceImpl.loginByFacebookCompletion) { completion in + self.processResponseBody((), error: error, completion: completion) + } + } + + func sendLogin(byGooglePlusDidFinish error: Error?) { + handleResponse(nil, to: \AuthServiceImpl.loginByGoogleCompletion) { completion in + self.processResponseBody((), error: error, completion: completion) + } + } + + func confirmCodeDidFinish(withAccoutnId accountId: String, + withAccessToken accessToken: String, + 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 { + self.storage.clearToken() + completion?(.failure(error)) + return + } + self.storage.save(accessToken: accessToken, refreshToken: refreshToken) + + 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)) + } + } + + 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?, + withError error: Error?) { + handleResponse(nil, to: \AuthServiceImpl.refreshTokenCompletion) { completion in + if let error = error { + self.storage.clearToken() + completion?(.failure(error)) + return + } + self.storage.save(accessToken: accessToken, refreshToken: refreshToken) + + 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) + } + + 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) + } + } + + 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 + +private extension AuthConfirmationType { + + var sdkValue: NYNVerifyTokenSendMethod { + switch self { + case .sms: + return .VTSM_SMS + case .call: + return .VTSM_CALL + } + } +} diff --git a/Nynja/SDK/Session/SessionStorage.swift b/Nynja/SDK/Session/SessionStorage.swift new file mode 100644 index 0000000000000000000000000000000000000000..1a89bf33c0c819e6d761d8c18fd22bc1fda6530c --- /dev/null +++ b/Nynja/SDK/Session/SessionStorage.swift @@ -0,0 +1,19 @@ +// +// SessionStorage.swift +// Nynja +// +// Created by Anton Poltoratskyi on 14.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol SessionStorage: class { + + 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/SearchModel.swift b/Nynja/SearchModel.swift index c5cc6b6fbde327d74ec649b9584f6eb8afe1548a..bb8d5e549f581cedd3731c157d888ef93d2b6ebe 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/ServerModel/Model/Typing.swift b/Nynja/ServerModel/Model/Typing.swift index f37b1826744e18814b877285cb2192b3f194f42f..e807bc87c674d730598f78c0e0dc87e1b840f8c1 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 0af94781c52a8b09bfadef3a11e479ca94bbbb6f..d6116cd2e85fb14e05b9c406a6a8f011c987beca 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 41ceda8a5b1b019907e27ad81475ccf598b5e188..bdc43d54c3686453112917f6a1e008d29f500b0d 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/ContactManager.swift b/Nynja/Services/ContactManager.swift index e3857083c49ecb9c16caffc9feb30e79bc92f2ed..3b87b1aa06b014d9ac4f2bb751df96d4ff4237cd 100644 --- a/Nynja/Services/ContactManager.swift +++ b/Nynja/Services/ContactManager.swift @@ -7,11 +7,10 @@ // import Contacts -import libPhoneNumber_iOS -class ContactManager { +final class ContactManager { - static var shared = ContactManager() + static let shared = ContactManager() private init() {} @@ -23,12 +22,14 @@ 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) @@ -50,37 +51,100 @@ 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/HandleServices/ContactHandler.swift b/Nynja/Services/HandleServices/ContactHandler.swift index 9741d74a6e6fef6bce822d347fcf07d7221cc146..947910262c9238c4c4ea0f18ec1731440dc87792 100644 --- a/Nynja/Services/HandleServices/ContactHandler.swift +++ b/Nynja/Services/HandleServices/ContactHandler.swift @@ -12,21 +12,23 @@ final class ContactHandler: BaseHandler { // MARK: - Dependencies - private static var storageService: StorageService { - return .sharedInstance - } + private let storageService = StorageService.sharedInstance - private static var notificationManager: NotificationManager { - return .shared - } + private let notificationManager = NotificationManager.shared + + + // MARK: - Singleton + + static let shared = ContactHandler() + + private init() {} // MARK: - Handler - static func executeHandle(data: BertTuple) { - guard let contact = get_Contact().parse(bert: data) as? Contact, - let status = contact.originalStatus else { - return + func executeHandle(data: BertTuple) { + guard let contact = get_Contact().parse(bert: data) as? Contact, let status = contact.originalStatus else { + return } switch status { @@ -50,11 +52,11 @@ final 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) @@ -62,11 +64,11 @@ final 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), @@ -82,16 +84,18 @@ final class ContactHandler: BaseHandler { try storageService.perform(action: .save, with: contact) if [.request, .authorization, .ignore].contains(prevStatus) { - let friendRequestType: InAppNotificationType.FriendRequestType = prevStatus == .request ? .outcoming : .incoming - notificationManager.handle(bert: data, - type: .friend(friendRequestType) ) + let friendRequestType: InAppNotificationType.FriendRequestType = prevStatus == .request + ? .outcoming + : .incoming + + notificationManager.handle(bert: data, type: .friend(friendRequestType) ) } } catch { LogService.log(topic: .db) { return "Storage Service Error: can't save contact with status 'friend'" } } } - private static func handleAuthorization(_ contact: Contact, data: BertTuple) { + private func handleAuthorization(_ contact: Contact, data: BertTuple) { do { try storageService.perform(action: .save, with: contact) notificationManager.handle(bert: data, type: .request) @@ -100,7 +104,7 @@ final 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 79ba1e79fd64357855c156bf7f6385af32983121..8d22f83d6bb94956aa7ec3c124c4cc67d409d836 100644 --- a/Nynja/Services/HandleServices/HistoryHandler.swift +++ b/Nynja/Services/HandleServices/HistoryHandler.swift @@ -10,13 +10,20 @@ import Foundation final class HistoryHandler: BaseHandler { + // MARK: - Singleton + + static let shared = HistoryHandler() + + private init() {} + + // MARK: - Subscribers - private static let subscribersQueue = DispatchQueue(label: String.label(withSuffix: "history-handler.subscribers-queue")) + private let subscribersQueue = DispatchQueue(label: String.label(withSuffix: "history-handler.subscribers-queue")) - private static var subscribers = [WeakRef]() + private var subscribers = [WeakRef]() - private static func notify(block: @escaping (HistoryHandlerSubscriber) -> Void) { + private func notify(block: @escaping (HistoryHandlerSubscriber) -> Void) { subscribersQueue.sync { subscribers.forEach { weak in guard let subscriber = weak.value as? HistoryHandlerSubscriber else { @@ -30,43 +37,39 @@ final class HistoryHandler: BaseHandler { } } - static func addSubscriber(_ subscriber: HistoryHandlerSubscriber) { - subscribersQueue.async { [weak subscriber] in - guard let subscriber = subscriber else { return } - guard !subscribers.contains(where: { $0.value === subscriber }) else { + func addSubscriber(_ subscriber: HistoryHandlerSubscriber) { + subscribersQueue.async { [weak self, weak subscriber] in + guard let self = self, let subscriber = subscriber else { return } + guard !self.subscribers.contains(where: { $0.value === subscriber }) else { return } let ref = WeakRef(value: subscriber as AnyObject) - subscribers.append(ref) + self.subscribers.append(ref) } } - static func removeSubscriber(_ subscriber: HistoryHandlerSubscriber) { - subscribersQueue.async { [weak subscriber] in - guard let subscriber = subscriber else { return } - subscribers = subscribers.filter { $0.value != nil && $0.value !== subscriber } + func removeSubscriber(_ subscriber: HistoryHandlerSubscriber) { + subscribersQueue.async { [weak self, weak subscriber] in + guard let self = self, let subscriber = subscriber else { return } + self.subscribers = self.subscribers.filter { $0.value != nil && $0.value !== subscriber } } } // MARK: - Dependencies - private static var storageService: StorageService { - return StorageService.sharedInstance - } - - private static var messageEditService: MessageEditServiceProtocol { - return MessageEditService(dependencies: .init(storageService: storageService)) - } + private let storageService = StorageService.sharedInstance - private static let stickersDownloadingService: StickersDownloadingService = { - return StickersDownloadingService() + private let messageEditService: MessageEditServiceProtocol = { + return MessageEditService(dependencies: .init(storageService: StorageService.sharedInstance)) }() + private let stickersDownloadingService = StickersDownloadingService() + // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let history = get_History().parse(bert: data) as? History else { return } @@ -99,7 +102,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]() @@ -175,7 +178,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, @@ -211,14 +214,14 @@ final class HistoryHandler: BaseHandler { } /// Mark messages with 'serverId' <= id as trusted - private static func markHistoryAsTrusted(before id: MessageServerId, in feed: Feed) { + private func markHistoryAsTrusted(before id: MessageServerId, in feed: Feed) { try? MessageDAO.trustMessages(before: id, in: feed) } // 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"] @@ -233,7 +236,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 7caa63230d30736c32c046ba62ea82ddc23007ae..c34ce570e779daab2271a5ac99fd2f86cf3ec9f1 100644 --- a/Nynja/Services/HandleServices/MessageHandler.swift +++ b/Nynja/Services/HandleServices/MessageHandler.swift @@ -10,46 +10,54 @@ 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 - private static let subscribersQueue = DispatchQueue(label: String.label(withSuffix: "message-handler.subscribers-queue")) + + private let subscribersQueue = DispatchQueue(label: String.label(withSuffix: "message-handler.subscribers-queue")) - static var subscribers: [MessageHandlerSubscriberReference] = [] + private var subscribers: [MessageHandlerSubscriberReference] = [] - static func addSubscriber(_ subscriber: MessageHandlerSubscriber) { - subscribersQueue.async { [weak subscriber] in - guard let subscriber = subscriber else { return } - guard !subscribers.contains(where: { $0.subscriber === subscriber }) else { + func addSubscriber(_ subscriber: MessageHandlerSubscriber) { + subscribersQueue.async { [weak self, weak subscriber] in + guard let self = self, let subscriber = subscriber else { return } + guard !self.subscribers.contains(where: { $0.subscriber === subscriber }) else { return } let ref = MessageHandlerSubscriberReference(subscriber) - subscribers.append(ref) + self.subscribers.append(ref) } } - static func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { - subscribersQueue.async { [weak subscriber] in - guard let subscriber = subscriber else { return } - subscribers = subscribers.filter { $0.subscriber != nil && $0.subscriber !== subscriber } + func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { + subscribersQueue.async { [weak self, weak subscriber] in + guard let self = self, let subscriber = subscriber else { return } + self.subscribers = self.subscribers.filter { $0.subscriber != nil && $0.subscriber !== subscriber } } } - private static func notify(block: @escaping (MessageHandlerSubscriber) -> Void) { + private func notify(block: @escaping (MessageHandlerSubscriber) -> Void) { subscribersQueue.sync { subscribers.forEach { ref in guard let subscriber = ref.subscriber else { @@ -66,7 +74,7 @@ final class MessageHandler: BaseHandler { // 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 @@ -88,11 +96,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 @@ -105,11 +113,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) @@ -118,7 +126,7 @@ final class MessageHandler: BaseHandler { } } - private static func editMessage(_ message: Message) { + private func editMessage(_ message: Message) { do { try save(message) try ChatService.editMessage(message) @@ -127,7 +135,7 @@ final class MessageHandler: BaseHandler { } } - private static func updateMessage(_ message: Message) { + private func updateMessage(_ message: Message) { do { try save(message) try ChatService.updateMessage(message) @@ -137,7 +145,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 } @@ -181,7 +189,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, @@ -194,7 +202,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) } @@ -207,7 +215,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 88d1508715738346662fded765d1f189190be8f1..bb63ccfd6bbc0cbbe6808a96da5d681e4716d6d7 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -7,34 +7,33 @@ // final class ProfileHandler: BaseHandler, StaticDelegating { + + weak var delegate: ProfileHandlerDelegate? + + + // MARK: - Singleton - static weak var delegate: ProfileHandlerDelegate? + static let shared = ProfileHandler() + private init() {} + + // MARK: - Dependencies - private static var mqttService: MQTTService { - return .sharedInstance - } + private let mqttService = MQTTService.sharedInstance - private static var historyFactory: HistoryRequestModelFactoryProtocol { - return HistoryRequestModelFactory() - } + private let historyFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() - private static var storageService: StorageService { - return .sharedInstance - } + private let storageService = StorageService.sharedInstance + + private let messageBackgroundTaskHandler = MessageBackgroundTaskHandler() + + private let communicatorService = NynjaCommunicatorService.sharedInstance - private static var messageBackgroundTaskHandler: BackgroundTaskHandler { - return MessageBackgroundTaskHandler() - } - - private static var communicatorService: NynjaCommunicatorService { - return .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 @@ -42,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) } @@ -61,7 +60,7 @@ final class ProfileHandler: BaseHandler, StaticDelegating { // 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 @@ -83,7 +82,7 @@ final class ProfileHandler: BaseHandler, StaticDelegating { } catch { } } - private static func prepareForReceived(_ newRoster: Roster) { + private func prepareForReceived(_ newRoster: Roster) { let currentRoster = RosterDAO.currentRoster func shouldSave(_ message: Message?) -> Bool { @@ -147,22 +146,21 @@ final class ProfileHandler: BaseHandler, StaticDelegating { } } - private static func configureTestFairy(with roster: Roster) { + private func configureTestFairy(with roster: Roster) { let phoneId = roster.phoneId ?? "" let fullName = roster.fullName ?? "" let userId = "\(phoneId)_\(fullName)" TestFairy.setUserId(userId) } - private static func configureNynjaCommunicatorService(with roster: Roster) { + private func configureNynjaCommunicatorService(with roster: Roster) { guard roster.phoneId != nil else { return } - communicatorService.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) @@ -171,7 +169,7 @@ final class ProfileHandler: BaseHandler, StaticDelegating { } } - 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) @@ -183,7 +181,7 @@ final class ProfileHandler: BaseHandler, StaticDelegating { // MARK: Remove - private static func handleRemove(_ profile: Profile) { + private func handleRemove(_ profile: Profile) { guard let phone = profile.phone else { return } diff --git a/Nynja/Services/HandleServices/RoomHandler.swift b/Nynja/Services/HandleServices/RoomHandler.swift index a29be4c96b26654494420b2fb87d193631c184b2..7a18d5cb2e77bcd07ea30dfad55e3df71b980fda 100644 --- a/Nynja/Services/HandleServices/RoomHandler.swift +++ b/Nynja/Services/HandleServices/RoomHandler.swift @@ -8,20 +8,23 @@ final class RoomHandler: BaseHandler { - // MARK: - Dependencies + // MARK: - Singleton - private static var storageService: StorageService { - return .sharedInstance - } + static let shared = RoomHandler() + + private init() {} - private static var notificationManager: NotificationManager { - return .shared - } + // MARK: - Dependencies + + private let storageService = StorageService.sharedInstance + + private let notificationManager = NotificationManager.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 +35,7 @@ final 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 +65,7 @@ final 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 +74,7 @@ final 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 +94,7 @@ final 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 +105,7 @@ final 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 +120,7 @@ final 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 +136,7 @@ final 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 +145,7 @@ final 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 +161,7 @@ final 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 +173,7 @@ final 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 +190,7 @@ final 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 +216,7 @@ final 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,7 +226,7 @@ final 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 2596cd69d26737090de3058ebcc3ed5c001090bd..6c53726711698dac1f11fb79cc1be7dc3bef5b88 100644 --- a/Nynja/Services/HandleServices/RosterHandler.swift +++ b/Nynja/Services/HandleServices/RosterHandler.swift @@ -6,13 +6,25 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // +import Foundation + final class RosterHandler: BaseHandler { + + // MARK: - Dependencies - private static var storageService: StorageService { - return .sharedInstance - } + private let storageService = StorageService.sharedInstance + + + // MARK: - Singleton + + static let shared = RosterHandler() + + private init() {} + + + // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let roster = get_Roster().parse(bert: data) as? Roster, let status = (roster.status as? StringAtom)?.string else { return diff --git a/Nynja/Services/HandleServices/SearchHandler.swift b/Nynja/Services/HandleServices/SearchHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..be743eb61d14d7fa9ca4e9f4406d11aa2269ef0a --- /dev/null +++ b/Nynja/Services/HandleServices/SearchHandler.swift @@ -0,0 +1,41 @@ +// +// SearchHandler.swift +// Nynja +// +// Created by Anton Makarov on 23.10.17. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class SearchHandler: BaseHandler { + + // 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 { + return + } + + switch refType { + case .PHONE: + break + case .PHONEBOOK: + break + case .QRCODE: + break + case .USERNAME: + break + } + } + +} diff --git a/Nynja/Services/HandleServices/StarHandler.swift b/Nynja/Services/HandleServices/StarHandler.swift index 1b830f2ad78939afaff056afeeb32639e4406562..9c289acf1f10c8c897d1ae04d34f9e3657b332f4 100644 --- a/Nynja/Services/HandleServices/StarHandler.swift +++ b/Nynja/Services/HandleServices/StarHandler.swift @@ -8,16 +8,24 @@ final class StarHandler: BaseHandler { - private static var storageService: StorageService { - return .sharedInstance - } + // MARK: - Dependencies + + private let storageService = StorageService.sharedInstance + + + // MARK: - Singleton + + static let shared = StarHandler() + + private init() {} + + + // MARK: - Handler - static func executeHandle(data: BertTuple) { - guard let star = get_Star().parse(bert: data) as? Star, - let status = star.starStatus else { - return + func executeHandle(data: BertTuple) { + guard let star = get_Star().parse(bert: data) as? Star, let status = star.starStatus else { + return } - do { switch status { case .add: diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index ce19b3bb3e1215e366ea4d7d43aa6c4707d7ce80..08714daaaafbd86491cb571691b0255a32dd9217 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -8,12 +8,26 @@ import Foundation -final class TypingHandler: BaseHandler, StaticDelegating { - static weak var delegate: TypingHandlerDelegate? +final class TypingHandler: BaseHandler, Observable { - static func executeHandle(data: BertTuple) { - if let typing = get_Typing().parse(bert: data) as? Typing { - delegate { $0.getTyping(typing: typing) } + // MARK: - Singleton + + 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/Extensions/MQTTService+Helper.swift b/Nynja/Services/MQTT/Extensions/MQTTService+Helper.swift index 5c75532f053af886b367e31d932f6a038f58b87f..f99eb0981ca5eced5dc0b0ee199eb898da2c28ea 100644 --- a/Nynja/Services/MQTT/Extensions/MQTTService+Helper.swift +++ b/Nynja/Services/MQTT/Extensions/MQTTService+Helper.swift @@ -16,7 +16,7 @@ extension MQTTService { func printMessage(msg: MQTTMessage, isSent: Bool) { queuePool.loggingQueue.async { [weak self] in - guard let `self` = self, + guard let self = self, let desc = try? self.prepareOutputMessage(msg: msg, isSent: isSent), let bert = desc.1 as? BertTuple else { return diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index 6d1f2bc33613a344e2fab8268b8ebb04ed4c7167..dd77b7a2a960c5b697187e8f3eed30f8354b5724 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -238,7 +238,8 @@ final class MQTTService: NSObject, MQTTServiceProtocol, MQTTSessionDelegate { 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/Member/MemberDAO.swift b/Nynja/Services/Member/MemberDAO.swift index f902615c3fac03e8c033b8f398e5ab976d8808a2..106f92a57d2626b3d204681957a07a19499b83f2 100644 --- a/Nynja/Services/Member/MemberDAO.swift +++ b/Nynja/Services/Member/MemberDAO.swift @@ -54,6 +54,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 6708aa04cc11eda69b95e73d129e560708b566f5..2cadf6f1a83406552e9e79f786a5cd42a9d563da 100644 --- a/Nynja/Services/Member/MemberDAOProtocol.swift +++ b/Nynja/Services/Member/MemberDAOProtocol.swift @@ -17,6 +17,8 @@ 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 diff --git a/Nynja/Services/Models/TypingModel.swift b/Nynja/Services/Models/TypingModel.swift index 2b60a9050130920c8a11646c3e09de6bec3f69bd..0720b2220f4f120aff138fd5667d6327de56dff7 100644 --- a/Nynja/Services/Models/TypingModel.swift +++ b/Nynja/Services/Models/TypingModel.swift @@ -85,7 +85,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/DAOFactoryProtocol.swift b/Nynja/Services/ServiceFactory/DAOFactoryProtocol.swift new file mode 100644 index 0000000000000000000000000000000000000000..5fcb0945d56c137bd0f046df182f73df8612fa29 --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..fd5a892a4792c115e9da88bad76b13a977bdc15c --- /dev/null +++ b/Nynja/Services/ServiceFactory/MQTTFactoryProtocol.swift @@ -0,0 +1,11 @@ +// +// MQTTFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +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 0000000000000000000000000000000000000000..b8e8472dcce98ab6c44bd0364cbee20804fb8274 --- /dev/null +++ b/Nynja/Services/ServiceFactory/MobileSDKFactoryProtocol.swift @@ -0,0 +1,14 @@ +// +// 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 + func makeAccountManager() -> AccountManager +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 40e647b31b274da538309f5f83f209796544b7c2..8407c4d676b1bffa251591e0b0a4422b0e9a04ef 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -7,70 +7,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 makeSyncFileManager(with kind: FileDownloaderKind) -> SyncFileManager - - func makeMuteChatService() -> MuteChatServiceProtocol - - func makeConnectionService() -> ConnectionService - - func makeAlertManager() -> AlertManager - - func makeStatusCodeManager() -> StatusCodeManager - - func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol - 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 makeDatabaseManager() -> DBManagerProtocol - - func makeValidatorFactory() -> ValidatorFactory - - func makeLocationService() -> LocationService - - func makeContactFinder() -> ContactFinder -} +import NynjaSDK final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { @@ -117,6 +54,40 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } + + // 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(), + storage: makeStorageService(), + appConfigurationProvider: makeAppConfigurationProvider()) + return AuthServiceImpl(dependencies: dependencies) + }() + + func makeAuthService() -> AuthService { + 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() + } + + func makeAppConfigurationProvider() -> AppConfigurationProvider { + return AppConfigurationProviderImpl() + } + + func makeTypingProvider() -> TypingProvider { + return TypingProviderImpl.shared + } func makeContactsProvider() -> ContactsProviding { return ContactsProvider() @@ -132,10 +103,18 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { dependencies: .init(storage: makeStorageService())) } + func makeCountriesProvider() -> CountriesProviding { + return CountriesProvider() + } + func makePermissionManager() -> PermissionManager { return PermissionManager() } + func makeContactManager() -> ContactManager { + return ContactManager.shared + } + func makeTextInputValidationService() -> TextInputValidationServiceProtocol { return TextInputValidationService() } @@ -160,6 +139,15 @@ 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 + } + func makeSyncFileManager() -> SyncFileManager { return SyncFileManager.sharedInstance } @@ -248,7 +236,44 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { } func makeContactFinder() -> ContactFinder { - return ContactFinder(dependencies: - .init(mqttService: makeMQTTService())) + return ContactFinder(dependencies: .init(mqttService: makeMQTTService())) + } + + func makePhoneNumberTextController() -> PhoneNumberTextController { + return PhoneNumberTextController(countryProvider: makeCountriesProvider()) + } +} + +// MARK: - MQTT Handlers + +extension ServiceFactory { + + func makeTypingHandler() -> TypingHandler { + return TypingHandler.shared + } +} + +// MARK: - SDK Services + +extension ServiceFactory { + + func makeCommunicator() -> NynjaCommunicator { + return NynjaCommunicator.sharedInstance() + } + + func makeLoginManager() -> LoginManager { + return makeCommunicator().getLoginManager() + } + + func makeAccountManager() -> AccountManager { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..8334b6e9949296941017a56a41c1ab87d90e0662 --- /dev/null +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -0,0 +1,87 @@ +// +// ServiceFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTFactoryProtocol, MobileSDKFactoryProtocol, DAOFactoryProtocol { + 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 makeAuthService() -> AuthService + func makeAccountService() -> AccountService + func makeGoogleAuthService() -> GoogleAuthService + func makeAppConfigurationProvider() -> AppConfigurationProvider + + 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 makeContactManager() -> ContactManager + + func makeWalletService() -> WalletService + + func makeImageUploader() -> ImageUploader + func makeTransferManager() -> TransferManager + func makeSyncFileManager() -> SyncFileManager + func makeSyncFileManager(with kind: FileDownloaderKind) -> SyncFileManager + + func makeMuteChatService() -> MuteChatServiceProtocol + + func makeConnectionService() -> ConnectionService + + func makeAlertManager() -> AlertManager + + func makeStatusCodeManager() -> StatusCodeManager + + func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol + 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 + + func makeValidatorFactory() -> ValidatorFactory + + func makeLocationService() -> LocationService + + func makeContactFinder() -> ContactFinder + + func makePhoneNumberTextController() -> PhoneNumberTextController +} diff --git a/Nynja/Services/Storage/DAO/Account/AccountDAO.swift b/Nynja/Services/Storage/DAO/Account/AccountDAO.swift new file mode 100644 index 0000000000000000000000000000000000000000..9695a82104537ffc374d0d43a2d29b7ca52dfe8a --- /dev/null +++ b/Nynja/Services/Storage/DAO/Account/AccountDAO.swift @@ -0,0 +1,82 @@ +// +// 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 + } + + + // 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 new file mode 100644 index 0000000000000000000000000000000000000000..28f53cfc99f4551c2195d3ac8a9e9a13136e869c --- /dev/null +++ b/Nynja/Services/Storage/DAO/Account/AccountDAOProtocol.swift @@ -0,0 +1,21 @@ +// +// 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(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/Nynja/Services/StorageService.swift b/Nynja/Services/StorageService.swift index a4188df4648c21fb845340759c5f8a800f8d38e3..56046694e670bb8cbd9288648192af1711fa09a1 100644 --- a/Nynja/Services/StorageService.swift +++ b/Nynja/Services/StorageService.swift @@ -10,21 +10,18 @@ import Foundation import GRDBCipher import CryptoSwift -// MARK: - The Game is began - /// Thread-safe -class StorageService { +final class StorageService { + + // MARK: - The Game is began 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 + private let databaseManager = DatabaseManager() var dbPool: DatabasePool? { @@ -34,12 +31,6 @@ class StorageService { private let isolationQueue = DispatchQueue(label: String.label(withSuffix: "storage-service.isolation-queue")) - // MARK: - Properties - - lazy var countries: [CountryModel] = { - return CountriesProvider().fetchCountries() - }() - /// 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 @@ -48,7 +39,10 @@ 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 @@ -170,9 +164,19 @@ extension StorageService: DBManagerProtocol { #endif -// MARK: - UserInfo +// MARK: - UserInfo + SessionStorage -extension StorageService: UserInfo { +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 } @@ -183,6 +187,15 @@ extension StorageService: UserInfo { get { return userInfo.tokenData } } + var refreshToken: String? { + get { return userInfo.refreshToken } + set { userInfo.refreshToken = newValue } + } + + var refreshTokenData: Data? { + get { return userInfo.refreshTokenData } + } + var tokenChanged: ((String?) -> Void)? { get { return userInfo.tokenChanged } set { userInfo.tokenChanged = newValue } @@ -223,8 +236,19 @@ extension StorageService: UserInfo { 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/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift new file mode 100644 index 0000000000000000000000000000000000000000..00924123ebebb03461840c37d440cd414e827b8d --- /dev/null +++ b/Nynja/Statuses/Observable.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/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..99ce063bb12f3ed892c678369c339457e19bb440 --- /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 0000000000000000000000000000000000000000..34a6b75e99cab9d2e8698e56e88155f989613f84 --- /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 0000000000000000000000000000000000000000..99d383c97fbb7efd85b1e33ae5a5b8b5bdc9d439 --- /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/Nynja/TransferManager.swift b/Nynja/TransferManager.swift index 7f0f77c9e2f13cdbfb097122bc5beb95aba7bef5..a3d42f57a44d57dd29cdaaf358d762c35a60f684 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/TranslationService/TranslationService.swift b/Nynja/TranslationService/TranslationService.swift index 6866db53b54ddf8417b14853afdba7306c86d850..369325665ee9e7ec82d5d6c12f44f0642a3833c8 100644 --- a/Nynja/TranslationService/TranslationService.swift +++ b/Nynja/TranslationService/TranslationService.swift @@ -10,7 +10,6 @@ import Foundation import SwiftyJSON protocol TranslationServiceInterface { - typealias Result = (Value?, ServiceError?) typealias Lang = String associatedtype ServiceError: Error @@ -62,7 +61,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) @@ -83,7 +82,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) @@ -118,7 +117,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) @@ -195,7 +194,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 -> Language? in @@ -205,7 +204,7 @@ private extension TranslationService { return Language(language: language, name: name) }).compactMap { $0 } - return (result, nil) + return .success(result) } func parseDetectedLanguages(data: Data?) -> Result { @@ -213,15 +212,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 { @@ -229,15 +228,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 ?? "") } } diff --git a/Nynja/TypingHandlerDelegate.swift b/Nynja/TypingHandlerDelegate.swift index de1844d5f079ea07d68d21f78f5c0327a805d40f..4b7822d53d35f2a4f2ca399d713a9ec47f326d04 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/Nynja/UserIdentifiers.swift b/Nynja/UserIdentifiers.swift index 76d175cf00d753a01820125f684d0da54bc31185..83cd0988bd07eb1633284cacf224e199f684031e 100644 --- a/Nynja/UserIdentifiers.swift +++ b/Nynja/UserIdentifiers.swift @@ -11,11 +11,15 @@ import Foundation enum UserIdentifiers: String { case token case pushToken + case refreshToken case phone case rosterId case clientId = "clientID" + case identityId + case accountId + case wasLogined case wasRun } diff --git a/Nynja/UserInfo.swift b/Nynja/UserInfo.swift index 59c7862f0bc59d9e9ede50cc52171bd21d143366..f292e1611863081c0bf210f578366030e69f121f 100644 --- a/Nynja/UserInfo.swift +++ b/Nynja/UserInfo.swift @@ -9,17 +9,22 @@ import Foundation protocol UserInfo: class { + var identityId: String? { get set } + var accountId: String? { get set } + var token: String? { get set } var tokenData: Data? { get } var tokenChanged: ((String?) -> Void)? { get set } + 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 } } @@ -27,18 +32,24 @@ 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 hasIdentity: Bool { + return identityId != nil + } + var hasPhone: Bool { return phone != nil } @@ -48,8 +59,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 529aa8c0737aa9e9e442b14829bd1903ccff8244..6189f2e7643ec8f631d37dee4fffbe107900b585 100644 --- a/Nynja/UserInfoImpl.swift +++ b/Nynja/UserInfoImpl.swift @@ -8,7 +8,7 @@ import Foundation -class UserInfoImpl: UserInfo, InitializeInjectable { +final class UserInfoImpl: UserInfo, InitializeInjectable { let userDefaults: UserDefaults? @@ -28,6 +28,16 @@ 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) } @@ -46,12 +56,29 @@ class UserInfoImpl: UserInfo, InitializeInjectable { userDefaults?.synchronize() return } - set(data as NSData, forId: .token) LogService.log(topic: .userDefaults) { return "Save token: \(token)" } } } + 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) + } + } + var tokenChanged: ((String?) -> Void)? var pushToken: String? { diff --git a/Nynja/ValidatorFactory/ValidatorFactoryImpl.swift b/Nynja/ValidatorFactory/ValidatorFactoryImpl.swift index 2a22b9b40d282bd1993e15edbaf7d89ddc87b399..8816b27b93e019f78bb57b9b18cf52546e54c7a8 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 == "" { diff --git a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift b/Nynja/Viper/BaseModule/Wireframe/NavigableWireframeProtocol.swift similarity index 64% rename from Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift rename to Nynja/Viper/BaseModule/Wireframe/NavigableWireframeProtocol.swift index a109004a12c7f127f0432078f65034dc4f5a8301..71d42f8cea3f2327793ee44c58ece7176167aa76 100644 --- a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift +++ b/Nynja/Viper/BaseModule/Wireframe/NavigableWireframeProtocol.swift @@ -1,22 +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 WireframeProtocol: 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 0000000000000000000000000000000000000000..a620bb88fba00c94304175dc34c5fd363af8bf26 --- /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: ()) + } +} diff --git a/NynjaUnitTests/InputsCachePolicy/InputsCachePolicyTest.swift b/NynjaUnitTests/InputsCachePolicy/InputsCachePolicyTest.swift index ca4679f7beca2cb012193fdbb2c35b837a869a85..1520288fa8b3d8cac69a2ae4e5406be4d0504963 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 6675a8b735eef20d1e5e7328194d456f44e45d55..0f6933918c023dc10bb3a7bbea028294436dff30 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 23b0bf82383b7e5c477412b799a1cc4f80809668..0d58bdfe78c3c3aff5f9930a3222c824d15af0a8 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 tokenChanged: ((String?) -> Void)? var pushToken: String? var phone: String? diff --git a/Podfile b/Podfile index ccb23b6234373cfffa964771734eb10369751357..9e54228b63529fed619f921012132774949176d7 100644 --- a/Podfile +++ b/Podfile @@ -30,16 +30,17 @@ 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' pod 'SwiftyJSON', '= 4.2.0' pod 'AutoScrollLabel', '= 0.4.3' - pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' pod 'JTAppleCalendar', '= 7.1.6' - pod 'NynjaSDK', '= 1.8.1' +# pod 'NynjaSDK', '= 1.8.1' + pod 'NynjaSDK-MultiAcc', '= 0.5.6.5' pod 'CryptoSwift', '= 0.13.0' @@ -61,9 +62,13 @@ 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' + pod 'JTAppleCalendar', '= 7.1.6' + + pod 'MaterialComponents/ActivityIndicator', '= 55.3.0' pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' end @@ -79,6 +84,8 @@ end def commonPodsForNynjaUIKit pod 'SnapKit', '= 4.2.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 d6dfcf8448a86e007986cfc6df445c38e2a200b6..a8b790f1cc8f4d9d684ad7acfcef082f64f6d231 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -13,25 +13,25 @@ PODS: - Fabric (~> 1.6.3) - CryptoSwift (0.13.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) @@ -40,29 +40,58 @@ 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.6) - 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) - MQTTClient/Min (0.15.2) - MQTTClient/Websocket (0.15.2): - MQTTClient/Min - SocketRocket - MulticastDelegateSwift (2.1.1) - - NynjaSDK (1.8.1) + - NynjaSDK-MultiAcc (0.5.6.5) - QRCode (2.0) - SDWebImage (4.4.2): - SDWebImage/Core (= 4.4.2) @@ -88,14 +117,16 @@ 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.6) - libPhoneNumber-iOS (= 0.9.13) + - MaterialComponents/ActivityIndicator (= 55.3.0) - MaterialComponents/FlexibleHeader (= 55.3.0) - MQTTClient/Websocket (= 0.15.2) - MulticastDelegateSwift (= 2.1.1) - - NynjaSDK (= 1.8.1) + - NynjaSDK-MultiAcc (= 0.5.6.5) - QRCode (= 2.0) - SDWebImage (= 4.4.2) - SnapKit (= 4.2.0) @@ -117,14 +148,20 @@ SPEC REPOS: - FirebaseStorage - GoogleMaps - GooglePlaces + - GoogleSignIn + - GoogleToolboxForMac - GoogleUtilities - GRDBCipher + - GTMOAuth2 - GTMSessionFetcher - Intercom - JTAppleCalendar - libPhoneNumber-iOS - MaterialComponents + - MDFInternationalization - MDFTextAccessibility + - MotionAnimator + - MotionInterchange - MQTTClient - MulticastDelegateSwift - QRCode @@ -135,7 +172,7 @@ SPEC REPOS: - SwiftyJSON - TestFairy https://nynjagroup.jfrog.io/nynjagroup/api/pods/cocoapods-local: - - NynjaSDK + - NynjaSDK-MultiAcc EXTERNAL SOURCES: CocoaLumberjack: @@ -155,24 +192,30 @@ SPEC CHECKSUMS: Crashlytics: 95d05f4e4c19a771250c4bd9ce344d996de32bbf CryptoSwift: 16e78bebf567bad1c87b2d58f6547f25b74c31aa 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: e8b6512c896235149df975c41d9a36c868ab7fba + GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e Intercom: 083a05bf222811b0b5e0a0b24c863544123397f0 JTAppleCalendar: abb30678f42a4ef8a340a932b1dcb8c85a33dac2 libPhoneNumber-iOS: e444379ac18bbfbdefad571da735b2cd7e096caa MaterialComponents: 915f4e844400a35db3ea4c710a9af40aa8bcb093 + MDFInternationalization: b5b8626628abf026e630e5ebaeee563037712cbb MDFTextAccessibility: 94098925e0853551c5a311ce7c1ecefbe297cdb6 + MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a + MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MQTTClient: 902c7bcac1501595f3d0b15178c7205b40331fb0 MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 - NynjaSDK: 5fdd248089a3bbd8eee950efb84124628344bda8 + NynjaSDK-MultiAcc: 67e8020651c12becf5ad35292537c173eaee38a3 QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a @@ -181,6 +224,6 @@ SPEC CHECKSUMS: SwiftyJSON: c4bcba26dd9ec7a027fc8eade48e2c911f229e96 TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 -PODFILE CHECKSUM: a26709f4f5bfd33de084ef65033a3017a3958d16 +PODFILE CHECKSUM: aed00751e5ab00bf518a70531bfe14818135d5de COCOAPODS: 1.5.3 diff --git a/Shared/Library/StaticDelegating.swift b/Shared/Library/StaticDelegating.swift index 39173973ea109dfad29d557c7f20831309d2b23c..13e5d29d644c7b143a7299bae087bb04a5d2c876 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/AuthHandler/AuthHandler.swift b/Shared/Services/Handlers/AuthHandler/AuthHandler.swift index 02c74e852fd88cd656896b2dda53af878cd336c6..7ea949565049bc3404bffb17379e96ddcdad8d0e 100644 --- a/Shared/Services/Handlers/AuthHandler/AuthHandler.swift +++ b/Shared/Services/Handlers/AuthHandler/AuthHandler.swift @@ -10,18 +10,20 @@ import Foundation final class AuthHandler: BaseHandler, StaticDelegating { - private static var storageService: StorageService { + static let shared = AuthHandler() + + private init() {} + + private var storageService: StorageService { return .sharedInstance } - static weak var delegate: AuthHandlerDelegate? + weak var delegate: AuthHandlerDelegate? - static func executeHandle(data: BertTuple) { - guard let auth = get_Auth().parse(bert: data) as? Auth, - let type = StringAtom.string(auth.type) else { - return - } - + 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" { delegate { $0.processDelete(auth: auth) } } @@ -34,7 +36,7 @@ final class AuthHandler: BaseHandler, StaticDelegating { } } - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { let auths = data.elements.compactMap { get_Auth().parse(bert: $0) as? Auth } delegate { $0.processGetAll(auths: auths) } } diff --git a/Shared/Services/Handlers/Base/BaseHandler.swift b/Shared/Services/Handlers/Base/BaseHandler.swift index cdf0cebeebe97cd2293dba0e59be9aef478cc4a9..3021b29cd732a4e4c7033a867ae2dfcd382ed96f 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 5465d157167da9920916064ac0282c45cbe45cfe..d85e43db27d94d3b7f5dc24b23289da0a2115ed3 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 { diff --git a/Shared/Services/Handlers/IoHandler/IoHandler.swift b/Shared/Services/Handlers/IoHandler/IoHandler.swift index afa4f61e52afed6d278b8f398a24000794efa28d..86556ce04d7d0377f7f2365e4b519d11b67910bc 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 { @@ -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": @@ -109,14 +101,4 @@ final class IoHandler: BaseHandler, StaticDelegating { } } } - - static func delegate(_ closure: @escaping (IoHandlerDelegate) -> Void) { - guard let delegate = self.delegate else { - return - } - - dispatchAsyncMain { - closure(delegate) - } - } } diff --git a/Shared/Services/Handlers/IoHandler/IoHandlerDelegate.swift b/Shared/Services/Handlers/IoHandler/IoHandlerDelegate.swift index 7620fa52bbbf27ad4288e4a40ed4af9f177cab94..e9fff4b8181497fc405606225cfa14e46fa2731b 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() {} diff --git a/Shared/Services/Messaging/TypingSenderService.swift b/Shared/Services/Messaging/TypingSenderService.swift index 5b6ac91d936caf460e36b787da391c082d65d4e3..7b32f46285353d94de2a7d6df11b9432a521dbd2 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 } }