diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AppCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AppCore.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..b5c156a91b7402c846b359ae58ee5a2841f263a7 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AppCore.xcscheme @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1410" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "AppCore" + BuildableName = "AppCore" + BlueprintName = "AppCore" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "AppCore" + BuildableName = "AppCore" + BlueprintName = "AppCore" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..c1eeaf920bb20f7f66ef27f50c0bdfc24728e417 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1410" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BackupFeature" + BuildableName = "BackupFeature" + BlueprintName = "BackupFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BackupFeature" + BuildableName = "BackupFeature" + BlueprintName = "BackupFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Countries.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Countries.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..4c7d001db37cb451452f23627813acfe6b17b4d1 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Countries.xcscheme @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1410" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "Countries" + BuildableName = "Countries" + BlueprintName = "Countries" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "Countries" + BuildableName = "Countries" + BlueprintName = "Countries" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature 1.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature 1.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..550e1ea22c75a628d02430c276f3fe8f14053252 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature 1.xcscheme @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1410" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "LaunchFeature" + BuildableName = "LaunchFeature" + BlueprintName = "LaunchFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "LaunchFeature" + BuildableName = "LaunchFeature" + BlueprintName = "LaunchFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/OnboardingFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/OnboardingFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..16cd9172dfb7808e9dfad0db2323fbeda4c764c3 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/OnboardingFeature.xcscheme @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1410" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "OnboardingFeature" + BuildableName = "OnboardingFeature" + BlueprintName = "OnboardingFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "OnboardingFeature" + BuildableName = "OnboardingFeature" + BlueprintName = "OnboardingFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/StatusBarFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/StatusBarFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..34f2d75bb6289d7283a321a3574f369a72da8605 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/StatusBarFeature.xcscheme @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1410" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "StatusBarFeature" + BuildableName = "StatusBarFeature" + BlueprintName = "StatusBarFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "StatusBarFeature" + BuildableName = "StatusBarFeature" + BlueprintName = "StatusBarFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index b173e088056b9a96da598954f6a23b4d0024d8d0..b13be6187f4691c5ee9d95e4db8864367a979327 100644 --- a/App/client-ios.xcodeproj/project.pbxproj +++ b/App/client-ios.xcodeproj/project.pbxproj @@ -13,10 +13,10 @@ 02FDD07021EDA39B000F1286 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 02FDD06E21EDA39B000F1286 /* LaunchScreen.storyboard */; }; 32179BA826410149008B26EC /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32179BA726410149008B26EC /* NotificationService.swift */; }; 32179BAC26410149008B26EC /* NotificationExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 32179BA526410149008B26EC /* NotificationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 3273327126C7391D0027D79D /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 3273327026C7391D0027D79D /* App */; }; + 3242BD412921DC950045E647 /* AppFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 3242BD402921DC950045E647 /* AppFeature */; }; + 3242BD432921DC9E0045E647 /* AppFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 3242BD422921DC9E0045E647 /* AppFeature */; }; 32C194E02808C65500876917 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 32C194DF2808C65500876917 /* GoogleService-Info.plist */; }; 32C194E12808C65500876917 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 32C194DF2808C65500876917 /* GoogleService-Info.plist */; }; - 32CAAFAE2845836100446BB9 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 32CAAFAD2845836100446BB9 /* App */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -64,8 +64,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3273327126C7391D0027D79D /* App in Frameworks */, 026135B321F2729900038B5E /* libsqlite3.tbd in Frameworks */, + 3242BD432921DC9E0045E647 /* AppFeature in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -73,7 +73,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 32CAAFAE2845836100446BB9 /* App in Frameworks */, + 3242BD412921DC950045E647 /* AppFeature in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -159,7 +159,7 @@ ); name = "client-ios"; packageProductDependencies = ( - 3273327026C7391D0027D79D /* App */, + 3242BD422921DC9E0045E647 /* AppFeature */, ); productName = "client-ios"; productReference = 02FDD06221EDA39A000F1286 /* client-ios.app */; @@ -179,7 +179,7 @@ ); name = NotificationExtension; packageProductDependencies = ( - 32CAAFAD2845836100446BB9 /* App */, + 3242BD402921DC950045E647 /* AppFeature */, ); productName = NotificationExtension; productReference = 32179BA526410149008B26EC /* NotificationExtension.appex */; @@ -610,13 +610,13 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 3273327026C7391D0027D79D /* App */ = { + 3242BD402921DC950045E647 /* AppFeature */ = { isa = XCSwiftPackageProductDependency; - productName = App; + productName = AppFeature; }; - 32CAAFAD2845836100446BB9 /* App */ = { + 3242BD422921DC9E0045E647 /* AppFeature */ = { isa = XCSwiftPackageProductDependency; - productName = App; + productName = AppFeature; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/App/client-ios/Resources/GoogleService-Info.plist b/App/client-ios/Resources/GoogleService-Info.plist index 676030ed57400f2bf5a72dc61d4ba5ed3e5263cb..10566b5ece72bfa2c34ad23c1d289d327d757327 100644 --- a/App/client-ios/Resources/GoogleService-Info.plist +++ b/App/client-ios/Resources/GoogleService-Info.plist @@ -1,36 +1,36 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> - <dict> - <key>CLIENT_ID</key> - <string>662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de.apps.googleusercontent.com</string> - <key>REVERSED_CLIENT_ID</key> - <string>com.googleusercontent.apps.662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de</string> - <key>ANDROID_CLIENT_ID</key> - <string>662236151640-2ughgo2dvc59dm4o39b45lbdungp2mct.apps.googleusercontent.com</string> - <key>API_KEY</key> - <string>AIzaSyCbI2yQ7pbuVSRvraqanjGcS9CDrjD7lNU</string> - <key>GCM_SENDER_ID</key> - <string>662236151640</string> - <key>PLIST_VERSION</key> - <string>1</string> - <key>BUNDLE_ID</key> - <string>io.xxlabs.messenger</string> - <key>PROJECT_ID</key> - <string>xx-messenger-6e03e</string> - <key>STORAGE_BUCKET</key> - <string>xx-messenger-6e03e.appspot.com</string> - <key>IS_ADS_ENABLED</key> - <false></false> - <key>IS_ANALYTICS_ENABLED</key> - <false></false> - <key>IS_APPINVITE_ENABLED</key> - <true></true> - <key>IS_GCM_ENABLED</key> - <true></true> - <key>IS_SIGNIN_ENABLED</key> - <true></true> - <key>GOOGLE_APP_ID</key> - <string>1:662236151640:ios:24badb58ab07515d8cef2d</string> - </dict> +<dict> + <key>CLIENT_ID</key> + <string>662236151640-r1lrlppqdcmhb4p8urq32fo7784cdoal.apps.googleusercontent.com</string> + <key>REVERSED_CLIENT_ID</key> + <string>com.googleusercontent.apps.662236151640-r1lrlppqdcmhb4p8urq32fo7784cdoal</string> + <key>ANDROID_CLIENT_ID</key> + <string>662236151640-2ughgo2dvc59dm4o39b45lbdungp2mct.apps.googleusercontent.com</string> + <key>API_KEY</key> + <string>AIzaSyCbI2yQ7pbuVSRvraqanjGcS9CDrjD7lNU</string> + <key>GCM_SENDER_ID</key> + <string>662236151640</string> + <key>PLIST_VERSION</key> + <string>1</string> + <key>BUNDLE_ID</key> + <string>io.xxlabs.messenger</string> + <key>PROJECT_ID</key> + <string>xx-messenger-6e03e</string> + <key>STORAGE_BUCKET</key> + <string>xx-messenger-6e03e.appspot.com</string> + <key>IS_ADS_ENABLED</key> + <false/> + <key>IS_ANALYTICS_ENABLED</key> + <false/> + <key>IS_APPINVITE_ENABLED</key> + <true/> + <key>IS_GCM_ENABLED</key> + <true/> + <key>IS_SIGNIN_ENABLED</key> + <true/> + <key>GOOGLE_APP_ID</key> + <string>1:662236151640:ios:24badb58ab07515d8cef2d</string> +</dict> </plist> diff --git a/App/client-ios/main.swift b/App/client-ios/main.swift index 00c22b5303b5134db6e20ca68475518f8105efc7..3d2972992b1778100134c827e9c22617f3837ad1 100644 --- a/App/client-ios/main.swift +++ b/App/client-ios/main.swift @@ -1,5 +1,5 @@ -import App import UIKit +import AppFeature let appDelegate: String? = NSClassFromString("XCTestCase") == nil diff --git a/Package.swift b/Package.swift index 2162bd759f6d261ecd4b2a85b2329e4add3cbe79..79eb6ae391a45399efb0dce157a0239cdfa40c29 100644 --- a/Package.swift +++ b/Package.swift @@ -8,40 +8,38 @@ let package = Package( .iOS(.v14), ], products: [ - .library(name: "App", targets: ["App"]), .library(name: "Shared", targets: ["Shared"]), - .library(name: "XXLogger", targets: ["XXLogger"]), + .library(name: "AppCore", targets: ["AppCore"]), .library(name: "Defaults", targets: ["Defaults"]), .library(name: "Keychain", targets: ["Keychain"]), - .library(name: "Voxophone", targets: ["Voxophone"]), - .library(name: "Countries", targets: ["Countries"]), + .library(name: "AppFeature", targets: ["AppFeature"]), .library(name: "InputField", targets: ["InputField"]), .library(name: "ScanFeature", targets: ["ScanFeature"]), - .library(name: "Permissions", targets: ["Permissions"]), .library(name: "MenuFeature", targets: ["MenuFeature"]), .library(name: "ChatFeature", targets: ["ChatFeature"]), .library(name: "PushFeature", targets: ["PushFeature"]), + .library(name: "AppResources", targets: ["AppResources"]), .library(name: "CrashService", targets: ["CrashService"]), .library(name: "TermsFeature", targets: ["TermsFeature"]), - .library(name: "Presentation", targets: ["Presentation"]), .library(name: "BackupFeature", targets: ["BackupFeature"]), .library(name: "LaunchFeature", targets: ["LaunchFeature"]), .library(name: "SearchFeature", targets: ["SearchFeature"]), .library(name: "DrawerFeature", targets: ["DrawerFeature"]), - .library(name: "CollectionView", targets: ["CollectionView"]), .library(name: "RestoreFeature", targets: ["RestoreFeature"]), .library(name: "CrashReporting", targets: ["CrashReporting"]), .library(name: "ProfileFeature", targets: ["ProfileFeature"]), .library(name: "ContactFeature", targets: ["ContactFeature"]), - .library(name: "NetworkMonitor", targets: ["NetworkMonitor"]), - .library(name: "VersionChecking", targets: ["VersionChecking"]), .library(name: "SettingsFeature", targets: ["SettingsFeature"]), .library(name: "ChatListFeature", targets: ["ChatListFeature"]), .library(name: "RequestsFeature", targets: ["RequestsFeature"]), + .library(name: "ReportingFeature", targets: ["ReportingFeature"]), + .library(name: "StatusBarFeature", targets: ["StatusBarFeature"]), .library(name: "ChatInputFeature", targets: ["ChatInputFeature"]), .library(name: "OnboardingFeature", targets: ["OnboardingFeature"]), + .library(name: "CountryListFeature", targets: ["CountryListFeature"]), + .library(name: "PermissionsFeature", targets: ["PermissionsFeature"]), .library(name: "ContactListFeature", targets: ["ContactListFeature"]), - .library(name: "ReportingFeature", targets: ["ReportingFeature"]) + .library(name: "RequestPermissionFeature", targets: ["RequestPermissionFeature"]), ], dependencies: [ .package( @@ -72,10 +70,6 @@ let package = Package( url: "https://github.com/apple/swift-protobuf", .upToNextMajor(from: "1.14.0") ), - .package( - url: "https://github.com/SwiftyBeaver/SwiftyBeaver.git", - .upToNextMajor(from: "1.9.5") - ), .package( url: "https://github.com/darrarski/ScrollViewController", .upToNextMajor(from: "1.2.0") @@ -106,7 +100,7 @@ let package = Package( ), .package( url: "https://github.com/pointfreeco/swift-composable-architecture.git", - .upToNextMajor(from: "0.32.0") + .upToNextMajor(from: "0.43.0") ), .package( url: "https://github.com/pointfreeco/swift-custom-dump.git", @@ -116,25 +110,28 @@ let package = Package( url: "https://github.com/swiftcsv/SwiftCSV.git", from: "0.8.0" ), + .package( + url: "https://github.com/apple/swift-log.git", + .upToNextMajor(from: "1.4.4") + ), .package( url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", .upToNextMajor(from: "0.3.3") ), .package( - url: "https://git.xx.network/elixxir/xxm-di.git", - .upToNextMajor(from: "1.0.0") + path: "../xxm-navigation" ), .package( - path: "../xxm-navigation" + url: "https://git.xx.network/elixxir/xxm-di", + .upToNextMajor(from: "1.0.0") ) ], targets: [ .target( - name: "App", + name: "AppFeature", dependencies: [ + .target(name: "AppCore"), .target(name: "Keychain"), - .target(name: "Voxophone"), - .target(name: "Permissions"), .target(name: "ScanFeature"), .target(name: "ChatFeature"), .target(name: "MenuFeature"), @@ -154,26 +151,66 @@ let package = Package( .target(name: "ReportingFeature"), .target(name: "OnboardingFeature"), .target(name: "ContactListFeature"), + .target(name: "RequestPermissionFeature"), .product(name: "DependencyInjection", package: "xxm-di"), ] ), .testTarget( - name: "AppTests", + name: "AppFeatureTests", + dependencies: [ + .target(name: "AppFeature"), + ] + ), + .target( + name: "AppCore", dependencies: [ - .target(name: "App"), + .target(name: "Shared"), + .target(name: "AppResources"), + .target(name: "StatusBarFeature"), + .product(name: "SnapKit", package: "SnapKit"), + .product(name: "Logging", package: "swift-log"), + .product(name: "XXModels", package: "client-ios-db"), + .product(name: "XXDatabase", package: "client-ios-db"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] ), .target( name: "CrashReporting" ), .target( - name: "NetworkMonitor", + name: "PermissionsFeature", dependencies: [ - .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product( + name: "XCTestDynamicOverlay", + package: "xctest-dynamic-overlay" + ), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), ] ), .target( - name: "VersionChecking" + name: "StatusBarFeature", + dependencies: [ + .product( + name: "XCTestDynamicOverlay", + package: "xctest-dynamic-overlay" + ), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "AppResources", + resources: [ + .process("Resources") + ] ), .target( name: "InputField", @@ -182,11 +219,15 @@ let package = Package( ] ), .target( - name: "Permissions", + name: "RequestPermissionFeature", dependencies: [ .target(name: "Shared"), - .product(name: "Navigation", package: "xxm-navigation"), - .product(name: "DependencyInjection", package: "xxm-di"), + .target(name: "AppResources"), + .target(name: "PermissionsFeature"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), ] ), .target( @@ -206,12 +247,6 @@ let package = Package( .product(name: "KeychainAccess", package: "KeychainAccess"), ] ), - .target( - name: "Voxophone", - dependencies: [ - .target(name: "Shared"), - ] - ), .target( name: "Defaults", dependencies: [ @@ -226,10 +261,10 @@ let package = Package( ] ), .target( - name: "Countries", + name: "CountryListFeature", dependencies: [ .target(name: "Shared"), - .product(name: "DependencyInjection", package: "xxm-di"), + .target(name: "StatusBarFeature") ] ), .target( @@ -240,12 +275,6 @@ let package = Package( .product(name: "ScrollViewController", package: "ScrollViewController"), ] ), - .target( - name: "XXLogger", - dependencies: [ - .product(name: "SwiftyBeaver", package: "SwiftyBeaver"), - ] - ), .target( name: "Shared", dependencies: [ @@ -261,33 +290,19 @@ let package = Package( .process("Resources"), ] ), - .target( - name: "Presentation", - dependencies: [ - .target(name: "Shared"), - .product(name: "SnapKit", package: "SnapKit"), - ] - ), - .testTarget( - name: "PresentationTests", - dependencies: [ - .target(name: "Presentation"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), .target( name: "ChatInputFeature", dependencies: [ - .target(name: "Voxophone"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), ] ), .target( name: "RestoreFeature", dependencies: [ .target(name: "Shared"), - .target(name: "Presentation"), .product(name: "XXDatabase", package: "client-ios-db"), .product(name: "Navigation", package: "xxm-navigation"), .product(name: "DependencyInjection", package: "xxm-di"), @@ -304,7 +319,6 @@ let package = Package( .target(name: "Shared"), .target(name: "InputField"), .target(name: "ChatFeature"), - .target(name: "Presentation"), .product(name: "CombineSchedulers", package: "combine-schedulers"), .product(name: "ScrollViewController", package: "ScrollViewController"), ] @@ -315,29 +329,26 @@ let package = Package( .target(name: "Shared"), .target(name: "Defaults"), .target(name: "Keychain"), - .target(name: "Voxophone"), - .target(name: "Permissions"), - .target(name: "Presentation"), .target(name: "DrawerFeature"), .target(name: "ChatInputFeature"), .target(name: "ReportingFeature"), + .target(name: "RequestPermissionFeature"), .product(name: "DependencyInjection", package: "xxm-di"), .product(name: "ChatLayout", package: "ChatLayout"), .product(name: "DifferenceKit", package: "DifferenceKit"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "ScrollViewController", package: "ScrollViewController"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), .target( name: "SearchFeature", dependencies: [ .target(name: "Shared"), - .target(name: "Countries"), .target(name: "PushFeature"), - .target(name: "Presentation"), .target(name: "ContactFeature"), - .target(name: "NetworkMonitor"), + .target(name: "CountryListFeature"), .product(name: "Retry", package: "Retry"), .product(name: "XXDatabase", package: "client-ios-db"), .product(name: "DependencyInjection", package: "xxm-di"), @@ -349,11 +360,9 @@ let package = Package( .target(name: "Shared"), .target(name: "Defaults"), .target(name: "PushFeature"), - .target(name: "Permissions"), .target(name: "BackupFeature"), - .target(name: "VersionChecking"), .target(name: "ReportingFeature"), - .product(name: "DependencyInjection", package: "xxm-di"), + .target(name: "RequestPermissionFeature"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "CloudFilesSFTP", package: "xxm-cloud-providers"), .product(name: "CombineSchedulers", package: "combine-schedulers"), @@ -367,7 +376,6 @@ let package = Package( dependencies: [ .target(name: "Shared"), .target(name: "Defaults"), - .target(name: "Presentation"), .product(name: "Navigation", package: "xxm-navigation"), ] ), @@ -386,13 +394,12 @@ let package = Package( .target(name: "Shared"), .target(name: "Keychain"), .target(name: "Defaults"), - .target(name: "Countries"), .target(name: "InputField"), .target(name: "MenuFeature"), - .target(name: "Permissions"), - .target(name: "Presentation"), .target(name: "DrawerFeature"), .target(name: "BackupFeature"), + .target(name: "CountryListFeature"), + .target(name: "RequestPermissionFeature"), .product(name: "Navigation", package: "xxm-navigation"), .product(name: "DependencyInjection", package: "xxm-di"), .product(name: "CombineSchedulers", package: "combine-schedulers"), @@ -419,18 +426,17 @@ let package = Package( name: "OnboardingFeature", dependencies: [ .target(name: "Shared"), + .target(name: "AppCore"), .target(name: "Defaults"), .target(name: "Keychain"), - .target(name: "Countries"), .target(name: "InputField"), - .target(name: "Permissions"), .target(name: "PushFeature"), - .target(name: "Presentation"), .target(name: "DrawerFeature"), - .target(name: "VersionChecking"), - .product(name: "DependencyInjection", package: "xxm-di"), + .target(name: "CountryListFeature"), + .target(name: "RequestPermissionFeature"), .product(name: "CombineSchedulers", package: "combine-schedulers"), .product(name: "ScrollViewController", package: "ScrollViewController"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] ), .target( @@ -438,7 +444,6 @@ let package = Package( dependencies: [ .target(name: "Shared"), .target(name: "Defaults"), - .target(name: "Presentation"), .target(name: "DrawerFeature"), .target(name: "ReportingFeature"), .product(name: "Navigation", package: "xxm-navigation"), @@ -450,29 +455,50 @@ let package = Package( name: "BackupFeature", dependencies: [ .target(name: "Shared"), + .target(name: "AppCore"), .target(name: "InputField"), - .target(name: "Presentation"), .target(name: "DrawerFeature"), - .target(name: "NetworkMonitor"), - .product(name: "Navigation", package: "xxm-navigation"), - .product(name: "DependencyInjection", package: "xxm-di"), - .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), - .product(name: "CloudFilesSFTP", package: "xxm-cloud-providers"), - .product(name: "CloudFilesDrive", package: "xxm-cloud-providers"), - .product(name: "CloudFilesICloud", package: "xxm-cloud-providers"), - .product(name: "CloudFilesDropbox", package: "xxm-cloud-providers"), - .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + .product( + name: "Navigation", + package: "xxm-navigation" + ), + .product( + name: "XXClient", + package: "elixxir-dapps-sdk-swift" + ), + .product( + name: "CloudFilesSFTP", + package: "xxm-cloud-providers" + ), + .product( + name: "CloudFilesDrive", + package: "xxm-cloud-providers" + ), + .product( + name: "CloudFilesICloud", + package: "xxm-cloud-providers" + ), + .product( + name: "CloudFilesDropbox", + package: "xxm-cloud-providers" + ), + .product( + name: "XXMessengerClient", + package: "elixxir-dapps-sdk-swift" + ), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), ] ), .target( name: "ScanFeature", dependencies: [ .target(name: "Shared"), - .target(name: "Countries"), - .target(name: "Permissions"), - .target(name: "Presentation"), .target(name: "ContactFeature"), - .target(name: "NetworkMonitor"), + .target(name: "CountryListFeature"), + .target(name: "RequestPermissionFeature"), .product(name: "DependencyInjection", package: "xxm-di"), .product(name: "SnapKit", package: "SnapKit"), ] @@ -481,7 +507,6 @@ let package = Package( name: "ContactListFeature", dependencies: [ .target(name: "Shared"), - .target(name: "Presentation"), .target(name: "ContactFeature"), .product(name: "DependencyInjection", package: "xxm-di"), .product(name: "DifferenceKit", package: "DifferenceKit"), @@ -493,32 +518,16 @@ let package = Package( .target(name: "Shared"), .target(name: "Defaults"), .target(name: "Keychain"), - .target(name: "XXLogger"), .target(name: "InputField"), .target(name: "PushFeature"), - .target(name: "Permissions"), .target(name: "MenuFeature"), - .target(name: "Presentation"), .target(name: "DrawerFeature"), + .target(name: "RequestPermissionFeature"), .product(name: "DependencyInjection", package: "xxm-di"), .product(name: "CombineSchedulers", package: "combine-schedulers"), .product(name: "ScrollViewController", package: "ScrollViewController"), ] ), - .target( - name: "CollectionView", - dependencies: [ - .product(name: "ChatLayout", package: "ChatLayout"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), - ] - ), - .testTarget( - name: "CollectionViewTests", - dependencies: [ - .target(name: "CollectionView"), - .product(name: "CustomDump", package: "swift-custom-dump"), - ] - ), .target( name: "ReportingFeature", dependencies: [ diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift deleted file mode 100644 index f562bd38e398632c5c5539762316d0ee69dfda58..0000000000000000000000000000000000000000 --- a/Sources/App/DependencyRegistrator.swift +++ /dev/null @@ -1,316 +0,0 @@ -// MARK: SDK - -import UIKit -import Network -import QuickLook -import MobileCoreServices - -// MARK: Isolated features - -import Bindings -import XXLogger -import Keychain -import Defaults -import Countries -import Voxophone -import Permissions -import PushFeature -import CrashService -import CrashReporting -import NetworkMonitor -import VersionChecking -import ReportingFeature -import DI - -// MARK: UI Features - -import ScanFeature -import ChatFeature -import MenuFeature -import TermsFeature -import BackupFeature -import DrawerFeature -import SearchFeature -import LaunchFeature -import RestoreFeature -import ContactFeature -import ProfileFeature -import ChatListFeature -import SettingsFeature -import RequestsFeature -import OnboardingFeature -import ContactListFeature - -import Shared -import XXClient -import Navigation -import KeychainAccess -import XXMessengerClient - -struct DependencyRegistrator { - static private let container = DI.Container.shared - - static func registerDependencies() { - #if DEBUG - DependencyRegistrator.registerForMock() - #else - DependencyRegistrator.registerForLive() - #endif - } - - // MARK: MOCK - - static func registerForMock() { - container.register(XXLogger.noop) - container.register(VersionCheck.mock) - container.register(CrashReporter.noop) - container.register(ReportingStatus.mock()) - container.register(SendReport.mock()) - container.register(MockNetworkMonitor() as NetworkMonitoring) - container.register(KeyObjectStore.userDefaults) - container.register(MockPushHandler() as PushHandling) - container.register(MockKeychainHandler() as KeychainHandling) - container.register(MockPermissionHandler() as PermissionHandling) - - registerCommonDependencies() - } - - // MARK: LIVE - - static func registerForLive() { - let cMixManager = CMixManager.live(passwordStorage: .keychain) - container.register(cMixManager) - - container.register(GetIdFromContact.live) - container.register(GetFactsFromContact.live) - - container.register(KeyObjectStore.userDefaults) - container.register(XXLogger.live()) - container.register(VersionCheck.live) - container.register(CrashReporter.live) - container.register(ReportingStatus.live()) - container.register(SendReport.live) - - container.register(NetworkMonitor() as NetworkMonitoring) - container.register(PushHandler() as PushHandling) - container.register(KeychainHandler() as KeychainHandling) - container.register(PermissionHandler() as PermissionHandling) - - registerCommonDependencies() - } - - // MARK: COMMON - - static public func registerNavigators(_ navController: UINavigationController) { - container.register(CombinedNavigator( - PresentModalNavigator(), - DismissModalNavigator(), - PushNavigator(), - PopToRootNavigator(), - PopToNavigator(), - SetStackNavigator(), - - OpenUpNavigator(), - OpenLeftNavigator(), - - PresentOnboardingStartNavigator( - screen: OnboardingStartController.init, - navigationController: { navController } - ), - PresentChatListNavigator( - screen: ChatListController.init, - navigationController: { navController } - ), - PresentTermsAndConditionsNavigator( - screen: TermsConditionsController.init, - navigationController: { navController } - ), - PresentSearchNavigator( - screen: SearchContainerController.init(_:), - navigationController: { navController } - ), - PresentRequestsNavigator( - screen: RequestsContainerController.init, - navigationController: { navController } - ), - PresentChatNavigator( - screen: SingleChatController.init(_:), - navigationController: { navController } - ), - PresentGroupChatNavigator( - screen: GroupChatController.init(_:), - navigationController: { navController } - ), - PresentOnboardingWelcomeNavigator( - screen: OnboardingWelcomeController.init, - navigationController: { navController } - ), - PresentOnboardingUsernameNavigator( - screen: OnboardingUsernameController.init, - navigationController: { navController } - ), - PresentRestoreListNavigator( - screen: RestoreListController.init, - navigationController: { navController } - ), - PresentOnboardingEmailNavigator( - screen: OnboardingEmailController.init, - navigationController: { navController } - ), - PresentOnboardingPhoneNavigator( - screen: OnboardingPhoneController.init, - navigationController: { navController } - ), - PresentOnboardingCodeNavigator( - screen: OnboardingCodeController.init(_:_:_:), - navigationController: { navController } - ), - PresentDrawerNavigator( - screen: DrawerController.init(_:), - navigationController: { navController } - ), - PresentContactListNavigator( - screen: ContactListController.init, - navigationController: { navController } - ), - PresentMenuNavigator( - screen: MenuController.init(_:), - navigationController: { navController } - ), - PresentScanNavigator( - screen: ScanContainerController.init, - navigationController: { navController } - ), - PresentNewGroupNavigator( - screen: CreateGroupController.init, - navigationController: { navController } - ), - PresentCountryListNavigator( - screen: CountryListController.init(_:), - navigationController: { navController } - ), - PresentProfileNavigator( - screen: ProfileController.init, - navigationController: { navController } - ), - PresentSettingsNavigator( - screen: SettingsController.init, - navigationController: { navController } - ), - PresentSettingsAdvancedNavigator( - screen: SettingsAdvancedController.init, - navigationController: { navController } - ), - PresentSettingsBackupNavigator( - screen: BackupController.init, - navigationController: { navController } - ), - PresentSettingsAccountDeleteNavigator( - screen: AccountDeleteController.init, - navigationController: { navController } - ), - PresentContactNavigator( - screen: ContactController.init(_:), - navigationController: { navController } - ), - PresentActivitySheetNavigator( - screen: { UIActivityViewController( - activityItems: $0, - applicationActivities: nil - )}, - navigationController: { navController } - ), - PresentProfileEmailNavigator( - screen: ProfileEmailController.init, - navigationController: { navController } - ), - PresentProfilePhoneNavigator( - screen: ProfilePhoneController.init, - navigationController: { navController } - ), - PresentPermissionRequestNavigator( - screen: RequestPermissionController.init, - navigationController: { navController } - ), - PresentPhotoLibraryNavigator( - screen: UIImagePickerController.init, - navigationController: { navController } - ), - PresentProfileCodeNavigator( - screen: ProfileCodeController.init(_:_:_:), - navigationController: { navController } - ) - ) as Navigator) - } - - static private func registerCommonDependencies() { - var environment: MessengerEnvironment = .live() - environment.ndfEnvironment = .mainnet - environment.serviceList = .userDefaults( - key: "preImage", - userDefaults: UserDefaults(suiteName: "group.elixxir.messenger")! - ) - environment.udEnvironment = .init( - address: AlternativeUDConstants.address, - cert: AlternativeUDConstants.cert.data(using: .utf8)!, - contact: AlternativeUDConstants.contact.data(using: .utf8)! - ) - container.register(Messenger.live(environment)) - - container.register(Voxophone()) - container.register(BackupService()) - container.register(MakeAppScreenshot.live) - container.register(FetchBannedList.live) - container.register(ProcessBannedList.live) - container.register(MakeReportDrawer.live) - - // MARK: Isolated - - container.register(HUDController()) - container.register(ToastController()) - container.register(StatusBarStylist()) - } -} - -extension PasswordStorage { - static let keychain: PasswordStorage = { - let keychain = KeychainAccess.Keychain( - service: "XXM" - ) - return PasswordStorage( - save: { password in keychain[data: "password"] = password}, - load: { try keychain[data: "password"] ?? { throw MissingPasswordError() }() }, - remove: { try keychain.remove("password") } - ) - }() -} - -private enum AlternativeUDConstants { - static let address = "46.101.98.49:18001" - static let cert = """ ------BEGIN CERTIFICATE----- -MIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV -BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx -GzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp -cDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT -MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV -BAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh -Dwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs -WYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE -tJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA -m3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9 -bJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA -AaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA -neUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf -U/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2 -qvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4 -cyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R -tgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5 -6m52PyzMNV+2N21IPppKwA== ------END CERTIFICATE----- -""" - static let contact = """ -<xxc(2)7mbKFLE201WzH4SGxAOpHjjehwztIV+KGifi5L/PYPcDkAZiB9kZo+Dl3Vc7dD2SdZCFMOJVgwqGzfYRDkjc8RGEllBqNxq2sRRX09iQVef0kJQUgJCHNCOcvm6Ki0JJwvjLceyFh36iwK8oLbhLgqEZY86UScdACTyBCzBIab3ob5mBthYc3mheV88yq5PGF2DQ+dEvueUm+QhOSfwzppAJA/rpW9Wq9xzYcQzaqc3ztAGYfm2BBAHS7HVmkCbvZ/K07Xrl4EBPGHJYq12tWAN/C3mcbbBYUOQXyEzbSl/mO7sL3ORr0B4FMuqCi8EdlD6RO52pVhY+Cg6roRH1t5Ng1JxPt8Mv1yyjbifPhZ5fLKwxBz8UiFORfk0/jnhwgm25LRHqtNRRUlYXLvhv0HhqyYTUt17WNtCLATSVbqLrFGdy2EGadn8mP+kQNHp93f27d/uHgBNNe7LpuYCJMdWpoG6bOqmHEftxt0/MIQA8fTtTm3jJzv+7/QjZJDvQIv0SNdp8HFogpuwde+GuS4BcY7v5xz+ArGWcRR63ct2z83MqQEn9ODr1/gAAAgA7szRpDDQIdFUQo9mkWg8xBA==xxc> -""" -} diff --git a/Sources/App/PushRouter.swift b/Sources/App/PushRouter.swift deleted file mode 100644 index acebc7b3677915766a6f302a46d2b65cbba3cf07..0000000000000000000000000000000000000000 --- a/Sources/App/PushRouter.swift +++ /dev/null @@ -1,57 +0,0 @@ -import UIKit -import PushFeature -import ChatFeature -import SearchFeature -import LaunchFeature -import ChatListFeature -import RequestsFeature -import DI -import XXModels -import XXMessengerClient - -extension PushRouter { - static func live(navigationController: UINavigationController) -> PushRouter { - PushRouter { route, completion in - if let launchController = navigationController.viewControllers.last as? LaunchController { - launchController.pendingPushRoute = route - } else { - switch route { - case .requests: - if !(navigationController.viewControllers.last is RequestsContainerController) { - navigationController.setViewControllers([RequestsContainerController()], animated: true) - } - case .search(username: let username): - if let messenger = try? DI.Container.shared.resolve() as Messenger, - let _ = try? messenger.ud.get()?.getContact() { - if !(navigationController.viewControllers.last is SearchContainerController) { - navigationController.setViewControllers([ - ChatListController(), - SearchContainerController(username) - ], animated: true) - } else { - (navigationController.viewControllers.last as? SearchContainerController)?.startSearchingFor(username) - } - } - case .contactChat(id: let id): - if let database: Database = try? DI.Container.shared.resolve(), - let contact = try? database.fetchContacts(.init(id: [id])).first { - navigationController.setViewControllers([ - ChatListController(), - SingleChatController(contact) - ], animated: true) - } - case .groupChat(id: let id): - if let database: Database = try? DI.Container.shared.resolve(), - let info = try? database.fetchGroupInfos(.init(groupId: id)).first { - navigationController.setViewControllers([ - ChatListController(), - GroupChatController(info) - ], animated: true) - } - } - } - - completion() - } - } -} diff --git a/Sources/AppCore/AppDependencies.swift b/Sources/AppCore/AppDependencies.swift new file mode 100644 index 0000000000000000000000000000000000000000..1f3ec64e5898e606f93ed1e8957a6f7a2279d3ba --- /dev/null +++ b/Sources/AppCore/AppDependencies.swift @@ -0,0 +1,144 @@ +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay +import ComposableArchitecture + +public struct AppDependencies { + public var networkMonitor: NetworkMonitorManager + public var toastManager: ToastManager + public var hudManager: HUDManager + public var dbManager: DBManager + public var messenger: Messenger + public var authHandler: AuthCallbackHandler + public var backupStorage: BackupStorage + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> + public var now: () -> Date + public var sendMessage: SendMessage + public var sendImage: SendImage + public var messageListener: MessageListenerHandler + public var receiveFileHandler: ReceiveFileHandler + public var log: Logger + public var loadData: URLDataLoader +} + +extension AppDependencies { + public static func live() -> AppDependencies { + let dbManager = DBManager.live() + var messengerEnv = MessengerEnvironment.live() + messengerEnv.udEnvironment = .init( + address: Constants.address, + cert: Constants.cert.data(using: .utf8)!, + contact: Constants.contact.data(using: .utf8)! + ) + messengerEnv.serviceList = .userDefaults( + key: "preImage", + userDefaults: UserDefaults(suiteName: "group.elixxir.messenger")! + ) + let messenger = Messenger.live(messengerEnv) + let now: () -> Date = Date.init + + return AppDependencies( + networkMonitor: .live(), + toastManager: .live(), + hudManager: .live(), + dbManager: dbManager, + messenger: messenger, + authHandler: .live( + messenger: messenger, + handleRequest: .live(db: dbManager.getDB, now: now), + handleConfirm: .live(db: dbManager.getDB), + handleReset: .live(db: dbManager.getDB) + ), + backupStorage: .onDisk(), + mainQueue: DispatchQueue.main.eraseToAnyScheduler(), + bgQueue: DispatchQueue.global(qos: .background).eraseToAnyScheduler(), + now: now, + sendMessage: .live( + messenger: messenger, + db: dbManager.getDB, + now: now + ), + sendImage: .live( + messenger: messenger, + db: dbManager.getDB, + now: now + ), + messageListener: .live( + messenger: messenger, + db: dbManager.getDB + ), + receiveFileHandler: .live( + messenger: messenger, + db: dbManager.getDB, + now: now + ), + log: .live(), + loadData: .live + ) + } + + public static let unimplemented = AppDependencies( + networkMonitor: .unimplemented, + toastManager: .unimplemented, + hudManager: .unimplemented, + dbManager: .unimplemented, + messenger: .unimplemented, + authHandler: .unimplemented, + backupStorage: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented, + now: XCTestDynamicOverlay.unimplemented( + "\(Self.self)", + placeholder: Date(timeIntervalSince1970: 0) + ), + sendMessage: .unimplemented, + sendImage: .unimplemented, + messageListener: .unimplemented, + receiveFileHandler: .unimplemented, + log: .unimplemented, + loadData: .unimplemented + ) +} + +private enum AppDependenciesKey: DependencyKey { + static let liveValue: AppDependencies = .live() + static let testValue: AppDependencies = .unimplemented +} + +extension DependencyValues { + public var app: AppDependencies { + get { self[AppDependenciesKey.self] } + set { self[AppDependenciesKey.self] = newValue } + } +} + +private enum Constants { + static let address = "46.101.98.49:18001" + static let cert = """ +-----BEGIN CERTIFICATE----- +MIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx +GzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp +cDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV +BAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh +Dwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs +WYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE +tJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA +m3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9 +bJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA +AaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA +neUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf +U/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2 +qvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4 +cyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R +tgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5 +6m52PyzMNV+2N21IPppKwA== +-----END CERTIFICATE----- +""" + static let contact = """ +<xxc(2)7mbKFLE201WzH4SGxAOpHjjehwztIV+KGifi5L/PYPcDkAZiB9kZo+Dl3Vc7dD2SdZCFMOJVgwqGzfYRDkjc8RGEllBqNxq2sRRX09iQVef0kJQUgJCHNCOcvm6Ki0JJwvjLceyFh36iwK8oLbhLgqEZY86UScdACTyBCzBIab3ob5mBthYc3mheV88yq5PGF2DQ+dEvueUm+QhOSfwzppAJA/rpW9Wq9xzYcQzaqc3ztAGYfm2BBAHS7HVmkCbvZ/K07Xrl4EBPGHJYq12tWAN/C3mcbbBYUOQXyEzbSl/mO7sL3ORr0B4FMuqCi8EdlD6RO52pVhY+Cg6roRH1t5Ng1JxPt8Mv1yyjbifPhZ5fLKwxBz8UiFORfk0/jnhwgm25LRHqtNRRUlYXLvhv0HhqyYTUt17WNtCLATSVbqLrFGdy2EGadn8mP+kQNHp93f27d/uHgBNNe7LpuYCJMdWpoG6bOqmHEftxt0/MIQA8fTtTm3jJzv+7/QjZJDvQIv0SNdp8HFogpuwde+GuS4BcY7v5xz+ArGWcRR63ct2z83MqQEn9ODr1/gAAAgA7szRpDDQIdFUQo9mkWg8xBA==xxc> +""" +} diff --git a/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..c6e03cbedb7f1d0020d11a94c6fc0172a5fc5247 --- /dev/null +++ b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift @@ -0,0 +1,49 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct AuthCallbackHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension AuthCallbackHandler { + public static func live( + messenger: Messenger, + handleRequest: AuthCallbackHandlerRequest, + handleConfirm: AuthCallbackHandlerConfirm, + handleReset: AuthCallbackHandlerReset + ) -> AuthCallbackHandler { + AuthCallbackHandler { onError in + messenger.registerAuthCallbacks(.init { callback in + do { + switch callback { + case .request(let contact, _, _, _): + try handleRequest(contact) + + case .confirm(let contact, _, _, _): + try handleConfirm(contact) + + case .reset(let contact, _, _, _): + try handleReset(contact) + } + } catch { + onError(error) + } + }) + } + } +} + +extension AuthCallbackHandler { + public static let unimplemented = AuthCallbackHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift new file mode 100644 index 0000000000000000000000000000000000000000..2aa6787fff741651d649fb8f88397669b448d92f --- /dev/null +++ b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift @@ -0,0 +1,31 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXModels + +public struct AuthCallbackHandlerConfirm { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerConfirm { + public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerConfirm { + AuthCallbackHandlerConfirm { xxContact in + let id = try xxContact.getId() + guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { + return + } + dbContact.authStatus = .friend + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerConfirm { + public static let unimplemented = AuthCallbackHandlerConfirm( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift new file mode 100644 index 0000000000000000000000000000000000000000..6d1943e923732bc6dbdafe053deeb962191b7d13 --- /dev/null +++ b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift @@ -0,0 +1,41 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct AuthCallbackHandlerRequest { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerRequest { + public static func live( + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> AuthCallbackHandlerRequest { + AuthCallbackHandlerRequest { xxContact in + let id = try xxContact.getId() + guard try db().fetchContacts(.init(id: [id])).isEmpty else { + return + } + var dbContact = XXModels.Contact(id: id) + dbContact.marshaled = xxContact.data + dbContact.username = try xxContact.getFact(.username)?.value + dbContact.email = try xxContact.getFact(.email)?.value + dbContact.phone = try xxContact.getFact(.phone)?.value + dbContact.authStatus = .stranger + dbContact.createdAt = now() + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerRequest { + public static let unimplemented = AuthCallbackHandlerRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift new file mode 100644 index 0000000000000000000000000000000000000000..a894b5e79200fb13b3986840410492050510e768 --- /dev/null +++ b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift @@ -0,0 +1,31 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXModels + +public struct AuthCallbackHandlerReset { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerReset { + public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerReset { + AuthCallbackHandlerReset { xxContact in + let id = try xxContact.getId() + guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { + return + } + dbContact.authStatus = .friend + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerReset { + public static let unimplemented = AuthCallbackHandlerReset( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/DBManager/DBManager.swift b/Sources/AppCore/DBManager/DBManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..b66fb3d731f3cb53cfb463ae84b516e98927430d --- /dev/null +++ b/Sources/AppCore/DBManager/DBManager.swift @@ -0,0 +1,40 @@ +import XXModels +import Foundation + +public struct DBManager { + public var hasDB: DBManagerHasDB + public var makeDB: DBManagerMakeDB + public var getDB: DBManagerGetDB + public var removeDB: DBManagerRemoveDB +} + +extension DBManager { + public static func live( + url: URL = FileManager.default + .urls(for: .applicationSupportDirectory, in: .userDomainMask) + .first! + .appendingPathComponent("database") + ) -> DBManager { + class Container { + var db: Database? + } + + let container = Container() + + return DBManager( + hasDB: .init { container.db != nil }, + makeDB: .live(url: url, setDB: { container.db = $0 }), + getDB: .live(getDB: { container.db }), + removeDB: .live(url: url, getDB: { container.db }, unsetDB: { container.db = nil }) + ) + } +} + +extension DBManager { + public static let unimplemented = DBManager( + hasDB: .unimplemented, + makeDB: .unimplemented, + getDB: .unimplemented, + removeDB: .unimplemented + ) +} diff --git a/Sources/AppCore/DBManager/DBManagerGetDB.swift b/Sources/AppCore/DBManager/DBManagerGetDB.swift new file mode 100644 index 0000000000000000000000000000000000000000..aae596a1b98cf5993181400242ab4ce11189b3e6 --- /dev/null +++ b/Sources/AppCore/DBManager/DBManagerGetDB.swift @@ -0,0 +1,33 @@ +import XXModels +import XCTestDynamicOverlay + +public struct DBManagerGetDB { + public enum Error: Swift.Error, Equatable { + case missingDB + } + + public var run: () throws -> Database + + public func callAsFunction() throws -> Database { + try run() + } +} + +extension DBManagerGetDB { + public static func live( + getDB: @escaping () -> Database? + ) -> DBManagerGetDB { + DBManagerGetDB { + guard let db = getDB() else { + throw Error.missingDB + } + return db + } + } +} + +extension DBManagerGetDB { + public static let unimplemented = DBManagerGetDB( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/DBManager/DBManagerHasDB.swift b/Sources/AppCore/DBManager/DBManagerHasDB.swift new file mode 100644 index 0000000000000000000000000000000000000000..12fb1bb34ca0882404bf765f640e61496cbaedd5 --- /dev/null +++ b/Sources/AppCore/DBManager/DBManagerHasDB.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct DBManagerHasDB { + init(run: @escaping () -> Bool) { + self.run = run + } + + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } +} + +extension DBManagerHasDB { + public static let unimplemented = DBManagerHasDB( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/DBManager/DBManagerMakeDB.swift b/Sources/AppCore/DBManager/DBManagerMakeDB.swift new file mode 100644 index 0000000000000000000000000000000000000000..4376f12fb3e5630ce0f9d05cd27474b9dd75515c --- /dev/null +++ b/Sources/AppCore/DBManager/DBManagerMakeDB.swift @@ -0,0 +1,37 @@ +import XXModels +import Foundation +import XXDatabase +import XCTestDynamicOverlay + +public struct DBManagerMakeDB { + public var run: () throws -> Void + + public func callAsFunction() throws -> Void { + try run() + } +} + +extension DBManagerMakeDB { + public static func live( + url: URL, + setDB: @escaping (Database) -> Void + ) -> DBManagerMakeDB { + DBManagerMakeDB { + try? FileManager.default + .createDirectory(at: url, withIntermediateDirectories: true) + + let dbFilePath = url + .appendingPathComponent("db") + .appendingPathExtension("sqlite") + .path + + setDB(try Database.onDisk(path: dbFilePath)) + } + } +} + +extension DBManagerMakeDB { + public static let unimplemented = DBManagerMakeDB( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/DBManager/DBManagerRemoveDB.swift b/Sources/AppCore/DBManager/DBManagerRemoveDB.swift new file mode 100644 index 0000000000000000000000000000000000000000..9b6c14d4830d63fd70f8e54ac977fa652df28495 --- /dev/null +++ b/Sources/AppCore/DBManager/DBManagerRemoveDB.swift @@ -0,0 +1,36 @@ +import XXModels +import Foundation +import XXDatabase +import XCTestDynamicOverlay + +public struct DBManagerRemoveDB { + public var run: () throws -> Void + + public func callAsFunction() throws -> Void { + try run() + } +} + +extension DBManagerRemoveDB { + public static func live( + url: URL, + getDB: @escaping () -> Database?, + unsetDB: @escaping () -> Void + ) -> DBManagerRemoveDB { + DBManagerRemoveDB { + let db = getDB() + unsetDB() + try db?.drop() + let fm = FileManager.default + if fm.fileExists(atPath: url.path) { + try fm.removeItem(atPath: url.path) + } + } + } +} + +extension DBManagerRemoveDB { + public static let unimplemented = DBManagerRemoveDB( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/HUDManager/HUDHide.swift b/Sources/AppCore/HUDManager/HUDHide.swift new file mode 100644 index 0000000000000000000000000000000000000000..fe96ef1faacb7a9c59d58b96407a1de8573bc77d --- /dev/null +++ b/Sources/AppCore/HUDManager/HUDHide.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct HUDHide { + init(run: @escaping () -> Void) { + self.run = run + } + + public var run: () -> Void + + public func callAsFunction() -> Void { + run() + } +} + +extension HUDHide { + public static let unimplemented = HUDHide( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/HUDManager/HUDManager.swift b/Sources/AppCore/HUDManager/HUDManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..2c402e3b6b55677fbb3066ddb32777fd20d8224b --- /dev/null +++ b/Sources/AppCore/HUDManager/HUDManager.swift @@ -0,0 +1,49 @@ +import Combine +import Foundation +import XCTestDynamicOverlay + +public struct HUDManager { + public var show: HUDShow + public var hide: HUDHide + public var observe: HUDObserve +} + +extension HUDManager { + public static func live() -> HUDManager { + class Context { + var timer: Timer? + let modelSubject = PassthroughSubject<HUDModel?, Never>() + } + + let context = Context() + + return .init( + show: .init { + guard let model = $0 else { + context.modelSubject.send(.init(hasDotAnimation: true)) + return + } + if model.isAutoDismissable { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + context.modelSubject.send(nil) + } + } + context.modelSubject.send(model) + }, + hide: .init { + context.modelSubject.send(nil) + }, + observe: .init { + context.modelSubject.eraseToAnyPublisher() + } + ) + } +} + +extension HUDManager { + public static let unimplemented = HUDManager( + show: .unimplemented, + hide: .unimplemented, + observe: .unimplemented + ) +} diff --git a/Sources/AppCore/HUDManager/HUDObserve.swift b/Sources/AppCore/HUDManager/HUDObserve.swift new file mode 100644 index 0000000000000000000000000000000000000000..c4257fdfc52b9d49bd199747a033cb51ccd18904 --- /dev/null +++ b/Sources/AppCore/HUDManager/HUDObserve.swift @@ -0,0 +1,16 @@ +import Combine +import XCTestDynamicOverlay + +public struct HUDObserve { + public var run: () -> AnyPublisher<HUDModel?, Never> + + public func callAsFunction() -> AnyPublisher<HUDModel?, Never> { + run() + } +} + +extension HUDObserve { + public static let unimplemented = HUDObserve( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/HUDManager/HUDShow.swift b/Sources/AppCore/HUDManager/HUDShow.swift new file mode 100644 index 0000000000000000000000000000000000000000..81038e4e2799a5681c0de3aa92ad0816ff77646b --- /dev/null +++ b/Sources/AppCore/HUDManager/HUDShow.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct HUDShow { + init(run: @escaping (HUDModel?) -> Void) { + self.run = run + } + + public var run: (HUDModel?) -> Void + + public func callAsFunction(_ model: HUDModel? = nil) -> Void { + run(model) + } +} + +extension HUDShow { + public static let unimplemented = HUDShow( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/Logger/Logger.swift b/Sources/AppCore/Logger/Logger.swift new file mode 100644 index 0000000000000000000000000000000000000000..fbb48f0d2dff23711420e5d7bf80bc3c8a160198 --- /dev/null +++ b/Sources/AppCore/Logger/Logger.swift @@ -0,0 +1,43 @@ +import Logging +import Foundation +import XCTestDynamicOverlay + +public struct Logger { + public enum Message: Equatable { + case error(NSError) + } + + public var run: (Message, String, String, UInt) -> Void + + public func callAsFunction( + _ msg: Message, + file: String = #file, + function: String = #function, + line: UInt = #line + ) { + run(msg, file, function, line) + } +} + +extension Logger { + public static func live() -> Logger { + let logger = Logging.Logger(label: "xx.messenger") + return Logger { msg, file, function, line in + switch msg { + case .error(let error): + logger.error( + .init(stringLiteral: error.localizedDescription), + file: file, + function: function, + line: line + ) + } + } + } +} + +extension Logger { + public static let unimplemented = Logger( + run: XCTUnimplemented("\(Self.self).error") + ) +} diff --git a/Sources/AppCore/MessageListenerHandler/MessageListenerHandler.swift b/Sources/AppCore/MessageListenerHandler/MessageListenerHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..70fbace772986d506de4ee2338445a7c656bae8e --- /dev/null +++ b/Sources/AppCore/MessageListenerHandler/MessageListenerHandler.swift @@ -0,0 +1,50 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct MessageListenerHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension MessageListenerHandler { + public static func live( + messenger: Messenger, + db: DBManagerGetDB + ) -> MessageListenerHandler { + MessageListenerHandler { onError in + let listener = Listener { message in + do { + let payload = try MessagePayload.decode(message.payload) + try db().saveMessage(.init( + networkId: message.id, + senderId: message.sender, + recipientId: message.recipientId, + groupId: nil, + date: Date(timeIntervalSince1970: TimeInterval(message.timestamp) / 1_000_000_000), + status: .received, + isUnread: true, + text: payload.text, + roundURL: message.roundURL + )) + } catch { + onError(error) + } + } + return messenger.registerMessageListener(listener) + } + } +} + +extension MessageListenerHandler { + public static let unimplemented = MessageListenerHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Sources/Shared/Models/HUDModel.swift b/Sources/AppCore/Models/HUDModel.swift similarity index 98% rename from Sources/Shared/Models/HUDModel.swift rename to Sources/AppCore/Models/HUDModel.swift index c77d5607aca8b33031373e17d47d087d36aedce8..8d2679e6e870fa7507d8593ae683ee3b76ab0fcb 100644 --- a/Sources/Shared/Models/HUDModel.swift +++ b/Sources/AppCore/Models/HUDModel.swift @@ -1,4 +1,5 @@ import UIKit +import AppResources public struct HUDModel { var title: String? diff --git a/Sources/AppCore/Models/MessagePayload.swift b/Sources/AppCore/Models/MessagePayload.swift new file mode 100644 index 0000000000000000000000000000000000000000..67fb94d905b06ad25816ca8c4d11081c72053f19 --- /dev/null +++ b/Sources/AppCore/Models/MessagePayload.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct MessagePayload: Equatable { + public init(text: String) { + self.text = text + } + + public var text: String +} + +extension MessagePayload: Codable { + enum CodingKeys: String, CodingKey { + case text + } + + public static func decode(_ data: Data) throws -> Self { + try JSONDecoder().decode(Self.self, from: data) + } + + public func encode() throws -> Data { + try JSONEncoder().encode(self) + } +} diff --git a/Sources/AppCore/Models/ToastModel.swift b/Sources/AppCore/Models/ToastModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..7758f11c162fa0931b0fe344c4319a934f5f29ed --- /dev/null +++ b/Sources/AppCore/Models/ToastModel.swift @@ -0,0 +1,36 @@ +import UIKit +import AppResources + +public struct ToastModel { + let id: UUID + let title: String + let color: UIColor + let subtitle: String? + let leftImage: UIImage + let timeToLive: Int + let buttonTitle: String? + let autodismissable: Bool + let onTapClosure: (() -> Void)? + + public init( + id: UUID = UUID(), + title: String, + color: UIColor = Asset.neutralOverlay.color, + subtitle: String? = nil, + leftImage: UIImage, + timeToLive: Int = 4, + buttonTitle: String? = nil, + onTapClosure: (() -> Void)? = nil, + autodismissable: Bool = true + ) { + self.id = id + self.title = title + self.color = color + self.subtitle = subtitle + self.leftImage = leftImage + self.timeToLive = timeToLive + self.buttonTitle = buttonTitle + self.onTapClosure = onTapClosure + self.autodismissable = autodismissable + } +} diff --git a/Sources/AppCore/NetworkMonitorManager/NetworkMonitorConnType.swift b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorConnType.swift new file mode 100644 index 0000000000000000000000000000000000000000..8340f33fd448d9dfd87d76c30dc736a452375c38 --- /dev/null +++ b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorConnType.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct NetworkMonitorConnType { + public init(run: @escaping () -> NetworkMonitorManager.ConnType) { + self.run = run + } + + public var run: () -> NetworkMonitorManager.ConnType + + public func callAsFunction() -> NetworkMonitorManager.ConnType { + run() + } +} + +extension NetworkMonitorConnType { + public static let unimplemented = NetworkMonitorConnType( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/NetworkMonitorManager/NetworkMonitorManager.swift b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..e77d4c63087711bff028f6cd78f1b896b399a888 --- /dev/null +++ b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorManager.swift @@ -0,0 +1,77 @@ +import Combine +import Network + +public struct NetworkMonitorManager { + public enum Status: Equatable { + case unknown + case available + case xxNotAvailable + case internetNotAvailable + } + public enum ConnType: Equatable { + case unknown + case wifi + case ethernet + case cellular + } + + public var start: NetworkMonitorStart + public var update: NetworkMonitorUpdate + public var status: NetworkMonitorStatus + public var connType: NetworkMonitorConnType +} + +extension NetworkMonitorManager { + public static func live() -> NetworkMonitorManager { + class Context { + var monitor = NWPathMonitor() + let xxAvailability = CurrentValueSubject<Bool?, Never>(nil) + let internetAvailability = CurrentValueSubject<Bool?, Never>(nil) + let currentConnType = CurrentValueSubject<ConnType, Never>(.unknown) + } + + let context = Context() + + return .init( + start: .init { + context.monitor.pathUpdateHandler = { + let currentInterface: ConnType + + if $0.usesInterfaceType(.wifi) { + currentInterface = .wifi + } else if $0.usesInterfaceType(.wiredEthernet) { + currentInterface = .ethernet + } else if $0.usesInterfaceType(.cellular) { + currentInterface = .cellular + } else { + currentInterface = .unknown + } + context.currentConnType.send(currentInterface) + context.internetAvailability.send($0.status == .satisfied) + } + context.monitor.start(queue: .global()) + }, + update: .init { + context.xxAvailability.send($0) + }, + status: .init { + guard let xxAvailability = context.xxAvailability.value else { + return .xxNotAvailable + } + return xxAvailability ? .available : .xxNotAvailable + }, + connType: .init { + context.currentConnType.value + } + ) + } +} + +extension NetworkMonitorManager { + public static let unimplemented = NetworkMonitorManager( + start: .unimplemented, + update: .unimplemented, + status: .unimplemented, + connType: .unimplemented + ) +} diff --git a/Sources/AppCore/NetworkMonitorManager/NetworkMonitorStart.swift b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorStart.swift new file mode 100644 index 0000000000000000000000000000000000000000..4bb08bdbdd23a28b04c3b7f1a13bfbf53995e0a1 --- /dev/null +++ b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorStart.swift @@ -0,0 +1,20 @@ +import XCTestDynamicOverlay + +public struct NetworkMonitorStart { + public init(run: @escaping () -> Void) { + self.run = run + } + + public var run: () -> Void + + public func callAsFunction() -> Void { + run() + } +} + +extension NetworkMonitorStart { + public static let unimplemented = NetworkMonitorStart( + run: XCTUnimplemented("\(Self.self)") + ) +} + diff --git a/Sources/AppCore/NetworkMonitorManager/NetworkMonitorStatus.swift b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorStatus.swift new file mode 100644 index 0000000000000000000000000000000000000000..afc3169ce97622e94ec6bf74259512cc3e378d44 --- /dev/null +++ b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorStatus.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct NetworkMonitorStatus { + public init(run: @escaping () -> NetworkMonitorManager.Status) { + self.run = run + } + + public var run: () -> NetworkMonitorManager.Status + + public func callAsFunction() -> NetworkMonitorManager.Status { + run() + } +} + +extension NetworkMonitorStatus { + public static let unimplemented = NetworkMonitorStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/NetworkMonitorManager/NetworkMonitorUpdate.swift b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorUpdate.swift new file mode 100644 index 0000000000000000000000000000000000000000..6659aad330ce06f45306cd484c24835d67d83e30 --- /dev/null +++ b/Sources/AppCore/NetworkMonitorManager/NetworkMonitorUpdate.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct NetworkMonitorUpdate { + public init(run: @escaping (Bool) -> Void) { + self.run = run + } + + public var run: (Bool) -> Void + + public func callAsFunction(_ status: Bool) -> Void { + run(status) + } +} + +extension NetworkMonitorUpdate { + public static let unimplemented = NetworkMonitorUpdate( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift b/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..187963707ea131e8f10de1e3dcf0ea0de4972620 --- /dev/null +++ b/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift @@ -0,0 +1,117 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +public struct ReceiveFileHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension ReceiveFileHandler { + public static func live( + messenger: Messenger, + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> ReceiveFileHandler { + ReceiveFileHandler { onError in + func receiveFile(_ file: ReceivedFile) { + do { + let date = now() + try db().saveFileTransfer(XXModels.FileTransfer( + id: file.transferId, + contactId: file.senderId, + name: file.name, + type: file.type, + data: nil, + progress: 0, + isIncoming: true, + createdAt: date + )) + try db().saveMessage(XXModels.Message( + senderId: file.senderId, + recipientId: try messenger.e2e.tryGet().getContact().getId(), + groupId: nil, + date: date, + status: .received, + isUnread: false, + text: "", + fileTransferId: file.transferId + )) + try messenger.receiveFile(.init( + transferId: file.transferId, + callbackIntervalMS: 500 + )) { info in + switch info { + case .progress(let transmitted, let total): + updateProgress( + transferId: file.transferId, + transmitted: transmitted, + total: total + ) + + case .finished(let data): + saveData( + transferId: file.transferId, + data: data + ) + + case .failed(.receive(let error)): + onError(error) + + case .failed(.callback(let error)): + onError(error) + } + } + } catch { + onError(error) + } + } + + func updateProgress(transferId: Data, transmitted: Int, total: Int) { + do { + if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first { + transfer.progress = total > 0 ? Float(transmitted) / Float(total) : 0 + try db().saveFileTransfer(transfer) + } + } catch { + onError(error) + } + } + + func saveData(transferId: Data, data: Data) { + do { + if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first { + transfer.progress = 1 + transfer.data = data + try db().saveFileTransfer(transfer) + } + } catch { + onError(error) + } + } + + return messenger.registerReceiveFileCallback(.init { result in + switch result { + case .success(let file): + receiveFile(file) + + case .failure(let error): + onError(error) + } + }) + } + } +} + +extension ReceiveFileHandler { + public static let unimplemented = ReceiveFileHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Sources/Shared/Controllers/RootViewController.swift b/Sources/AppCore/RootViewController.swift similarity index 92% rename from Sources/Shared/Controllers/RootViewController.swift rename to Sources/AppCore/RootViewController.swift index a0ca01e577f3a811e501414f0cf6ce3d21c169f6..eb7db899e63a9c1687ba4b9d6ab22f9abbf58a62 100644 --- a/Sources/Shared/Controllers/RootViewController.swift +++ b/Sources/AppCore/RootViewController.swift @@ -1,60 +1,61 @@ import UIKit import Combine -import DI +import StatusBarFeature +import ComposableArchitecture public final class RootViewController: UIViewController { - @Dependency var barStylist: StatusBarStylist - @Dependency var hudDispatcher: HUDController - @Dependency var toastDispatcher: ToastController - + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager + @Dependency(\.app.toastManager) var toastManager: ToastManager + var hud: HUDView? var cancellables = Set<AnyCancellable>() public let navController: UINavigationController - + var toastTimer: Timer? let toastTopPadding: CGFloat = 10 var topToastConstraint: NSLayoutConstraint? - + public init(_ content: UINavigationController) { self.navController = content super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { nil } - + public override var preferredStatusBarStyle: UIStatusBarStyle { - barStylist.styleSubject.value + statusBar.current() } - + public override func viewDidLoad() { super.viewDidLoad() - + addChild(navController) view.addSubview(navController.view) navController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] navController.view.frame = view.bounds navController.didMove(toParent: self) - - barStylist - .styleSubject + + statusBar + .observe() .receive(on: DispatchQueue.main) .sink { [weak self] _ in UIView.animate(withDuration: 0.2) { self?.setNeedsStatusBarAppearanceUpdate() } }.store(in: &cancellables) - - toastDispatcher - .currentToast + + toastManager + .observe() .receive(on: DispatchQueue.main) .sink { [unowned self] model in let toastView = ToastView(model: model) add(toastView: toastView) present(toastView: toastView) }.store(in: &cancellables) - - hudDispatcher - .modelPublisher + + hudManager + .observe() .receive(on: DispatchQueue.main) .sink { [unowned self] model in guard let model else { @@ -77,35 +78,35 @@ public final class RootViewController: UIViewController { extension RootViewController { @objc private func didPanToast(_ sender: UIPanGestureRecognizer) { guard let toastView = sender.view else { return } - + switch sender.state { case .began, .changed: toastTimer?.invalidate() let padding = toastTopPadding + min(0, sender.translation(in: view).y) topToastConstraint?.constant = padding - + case .cancelled, .ended, .failed: let halfFrameHeight = -0.5 * toastView.frame.height let verticalTranslation = sender.translation(in: toastView).y let didSwipeAboveHalf = verticalTranslation < halfFrameHeight - + if didSwipeAboveHalf { dismiss(toastView: toastView) } else { present(toastView: toastView) } - + case .possible: break @unknown default: break } } - + private func dismiss(toastView: UIView) { toastView.isUserInteractionEnabled = false topToastConstraint?.constant = -(toastView.frame.height + view.safeAreaLayoutGuide.layoutFrame.minY) - + topToastConstraint = nil UIView.animate(withDuration: 0.25) { self.view.setNeedsLayout() @@ -113,38 +114,38 @@ extension RootViewController { } completion: { _ in toastView.isUserInteractionEnabled = true toastView.removeFromSuperview() - self.toastDispatcher.dismissCurrentToast() + self.toastManager.dismiss() } } - + private func add(toastView: UIView) { let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanToast(_:))) toastView.addGestureRecognizer(gestureRecognizer) - + toastView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(toastView) - + NSLayoutConstraint.activate([ toastView.heightAnchor.constraint(equalToConstant: 78), toastView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 20), toastView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -20) ]) - + topToastConstraint = toastView.topAnchor.constraint( equalTo: view.safeAreaLayoutGuide.topAnchor, constant: -(toastView.frame.height + view.safeAreaLayoutGuide.layoutFrame.height) ) - + topToastConstraint?.isActive = true - + view.setNeedsLayout() view.layoutIfNeeded() } - + private func present(toastView: UIView) { toastView.isUserInteractionEnabled = false topToastConstraint?.constant = toastTopPadding - + UIView.animate( withDuration: 0.5, delay: 0, @@ -156,7 +157,7 @@ extension RootViewController { self.view.layoutIfNeeded() } completion: { _ in toastView.isUserInteractionEnabled = true - + self.toastTimer?.invalidate() self.toastTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in guard let self else { return } @@ -174,28 +175,28 @@ extension RootViewController { hud.removeFromSuperview() self.hud = nil } - + hudView.alpha = 0.0 hudView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(hudView) - + NSLayoutConstraint.activate([ hudView.topAnchor.constraint(equalTo: view.topAnchor), hudView.leftAnchor.constraint(equalTo: view.leftAnchor), hudView.rightAnchor.constraint(equalTo: view.rightAnchor), hudView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - + view.setNeedsLayout() view.layoutIfNeeded() - + UIView.animate(withDuration: 0.2) { hudView.alpha = 1.0 } - + hud = hudView } - + // if statusSubject.value.isPresented == true && status.isPresented == true { // self.errorView = nil // self.animation = nil @@ -225,7 +226,7 @@ extension RootViewController { // // showWindow() // } - + // if statusSubject.value.isPresented == false && status.isPresented == true { // switch status { // case .on: @@ -249,7 +250,7 @@ extension RootViewController { // // showWindow() // } - + // if statusSubject.value.isPresented == true && status.isPresented == false { // hideWindow() // } diff --git a/Sources/AppCore/SendImage/SendImage.swift b/Sources/AppCore/SendImage/SendImage.swift new file mode 100644 index 0000000000000000000000000000000000000000..04d7b69ee9dbaa6d845eaa944176b160f9117a55 --- /dev/null +++ b/Sources/AppCore/SendImage/SendImage.swift @@ -0,0 +1,108 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +public struct SendImage { + public typealias OnError = (Error) -> Void + public typealias Completion = () -> Void + + public var run: (Data, Data, @escaping OnError, @escaping Completion) -> Void + + public func callAsFunction( + _ image: Data, + to recipientId: Data, + onError: @escaping OnError, + completion: @escaping Completion + ) { + run(image, recipientId, onError, completion) + } +} + +extension SendImage { + public static func live( + messenger: Messenger, + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> SendImage { + SendImage { image, recipientId, onError, completion in + func updateProgress(transferId: Data, progress: Float) { + do { + if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first { + transfer.progress = progress + try db().saveFileTransfer(transfer) + } + } catch { + onError(error) + } + } + + let file = FileSend( + name: "image.jpg", + type: "image", + preview: nil, + contents: image + ) + let params = MessengerSendFile.Params( + file: file, + recipientId: recipientId, + retry: 2, + callbackIntervalMS: 500 + ) + do { + let date = now() + let myContactId = try messenger.e2e.tryGet().getContact().getId() + let transferId = try messenger.sendFile(params) { info in + switch info { + case .progress(let transferId, let transmitted, let total): + updateProgress( + transferId: transferId, + progress: total > 0 ? Float(transmitted) / Float(total) : 0 + ) + + case .finished(let transferId): + updateProgress( + transferId: transferId, + progress: 1 + ) + + case .failed(_, .callback(let error)): + onError(error) + + case .failed(_, .close(let error)): + onError(error) + } + } + try db().saveFileTransfer(XXModels.FileTransfer( + id: transferId, + contactId: myContactId, + name: file.name, + type: file.type, + data: image, + progress: 0, + isIncoming: false, + createdAt: date + )) + try db().saveMessage(XXModels.Message( + senderId: myContactId, + recipientId: recipientId, + groupId: nil, + date: date, + status: .sent, + isUnread: false, + text: "", + fileTransferId: transferId + )) + } catch { + onError(error) + } + } + } +} + +extension SendImage { + public static let unimplemented = SendImage( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/SendMessage/SendMessage.swift b/Sources/AppCore/SendMessage/SendMessage.swift new file mode 100644 index 0000000000000000000000000000000000000000..f675dc0d2322cbe3b7611d92823eece9b5420d42 --- /dev/null +++ b/Sources/AppCore/SendMessage/SendMessage.swift @@ -0,0 +1,84 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +public struct SendMessage { + public typealias OnError = (Error) -> Void + public typealias Completion = () -> Void + + public var run: (String, Data, @escaping OnError, @escaping Completion) -> Void + + public func callAsFunction( + text: String, + to recipientId: Data, + onError: @escaping OnError, + completion: @escaping Completion + ) { + run(text, recipientId, onError, completion) + } +} + +extension SendMessage { + public static func live( + messenger: Messenger, + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> SendMessage { + SendMessage { text, recipientId, onError, completion in + do { + let myContactId = try messenger.e2e.tryGet().getContact().getId() + let message = try db().saveMessage(.init( + senderId: myContactId, + recipientId: recipientId, + groupId: nil, + date: now(), + status: .sending, + isUnread: false, + text: text + )) + let payload = MessagePayload(text: message.text) + let report = try messenger.sendMessage( + recipientId: recipientId, + payload: try payload.encode(), + deliveryCallback: { deliveryReport in + let status: XXModels.Message.Status + switch deliveryReport.result { + case .delivered: + status = .sent + case .notDelivered(let timedOut): + status = timedOut ? .sendingTimedOut : .sendingFailed + case .failure(let error): + status = .sendingFailed + onError(error) + } + do { + try db().bulkUpdateMessages( + .init(id: [message.id]), + .init(status: status) + ) + } catch { + onError(error) + } + completion() + } + ) + if var message = try db().fetchMessages(.init(id: [message.id])).first { + message.networkId = report.messageId + message.roundURL = report.roundURL + _ = try db().saveMessage(message) + } + } catch { + onError(error) + completion() + } + } + } +} + +extension SendMessage { + public static let unimplemented = SendMessage( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/ToastManager/ToastDismiss.swift b/Sources/AppCore/ToastManager/ToastDismiss.swift new file mode 100644 index 0000000000000000000000000000000000000000..a328ca5757b1fc0c57a73ff6c7021d49f45242ea --- /dev/null +++ b/Sources/AppCore/ToastManager/ToastDismiss.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct ToastDismiss { + init(run: @escaping () -> Void) { + self.run = run + } + + public var run: () -> Void + + public func callAsFunction() -> Void { + run() + } +} + +extension ToastDismiss { + public static let unimplemented = ToastDismiss( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/ToastManager/ToastEnqueue.swift b/Sources/AppCore/ToastManager/ToastEnqueue.swift new file mode 100644 index 0000000000000000000000000000000000000000..ee0145b416e104cf445af6ba29a6216e64b9be36 --- /dev/null +++ b/Sources/AppCore/ToastManager/ToastEnqueue.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct ToastEnqueue { + init(run: @escaping (ToastModel) -> Void) { + self.run = run + } + + public var run: (ToastModel) -> Void + + public func callAsFunction(_ model: ToastModel) -> Void { + run(model) + } +} + +extension ToastEnqueue { + public static let unimplemented = ToastEnqueue( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/ToastManager/ToastManager.swift b/Sources/AppCore/ToastManager/ToastManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..ce14e820840d63894cab76e8b81d5ad4379dcc66 --- /dev/null +++ b/Sources/AppCore/ToastManager/ToastManager.swift @@ -0,0 +1,44 @@ +import Combine +import XCTestDynamicOverlay + +public struct ToastManager { + public var enqueue: ToastEnqueue + public var dismiss: ToastDismiss + public var observe: ToastObserve +} + +extension ToastManager { + public static func live() -> ToastManager { + class Context { + let queue = CurrentValueSubject<[ToastModel], Never>([]) + } + + let context = Context() + + return .init( + enqueue: .init { + context.queue.value.append($0) + }, + dismiss: .init { + guard context.queue.value.isEmpty == false else { + return + } + _ = context.queue.value.removeFirst() + }, + observe: .init { + context.queue + .compactMap(\.first) + .removeDuplicates(by: { $0.id == $1.id }) + .eraseToAnyPublisher() + } + ) + } +} + +extension ToastManager { + public static let unimplemented: ToastManager = .init( + enqueue: .unimplemented, + dismiss: .unimplemented, + observe: .unimplemented + ) +} diff --git a/Sources/AppCore/ToastManager/ToastObserve.swift b/Sources/AppCore/ToastManager/ToastObserve.swift new file mode 100644 index 0000000000000000000000000000000000000000..d1b5fb7d2ce6b4153bbc1f283b4825eb1dfd3214 --- /dev/null +++ b/Sources/AppCore/ToastManager/ToastObserve.swift @@ -0,0 +1,16 @@ +import Combine +import XCTestDynamicOverlay + +public struct ToastObserve { + public var run: () -> AnyPublisher<ToastModel, Never> + + public func callAsFunction() -> AnyPublisher<ToastModel, Never> { + run() + } +} + +extension ToastObserve { + public static let unimplemented = ToastObserve( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/Shared/Views/HUDView.swift b/Sources/AppCore/UI/HUDView.swift similarity index 96% rename from Sources/Shared/Views/HUDView.swift rename to Sources/AppCore/UI/HUDView.swift index 328c0b9f92308877cd627995a7289bc7cbc314a1..926e36420b97b084cfa35255c981885cbf240eb4 100644 --- a/Sources/Shared/Views/HUDView.swift +++ b/Sources/AppCore/UI/HUDView.swift @@ -1,5 +1,8 @@ import UIKit +import Shared import Combine +import SnapKit +import AppResources final class HUDView: UIView { let titleLabel = UILabel() @@ -47,7 +50,7 @@ final class HUDView: UIView { required init?(coder: NSCoder) { nil } - func setup(model: HUDModel) -> HUDView { + func setup(model: HUDModel) -> Self { if let title = model.title { titleLabel.text = title stackView.addArrangedSubview(titleLabel) diff --git a/Sources/Shared/Views/ToastView.swift b/Sources/AppCore/UI/ToastView.swift similarity index 96% rename from Sources/Shared/Views/ToastView.swift rename to Sources/AppCore/UI/ToastView.swift index bf1254429a07344f7ba7c20b9dee19e13ebeff03..a5202803704d20d44720f87a78c5149fd49c4291 100644 --- a/Sources/Shared/Views/ToastView.swift +++ b/Sources/AppCore/UI/ToastView.swift @@ -1,5 +1,6 @@ import UIKit import Combine +import AppResources final class ToastView: UIView { let titleLabel = UILabel() @@ -9,69 +10,69 @@ final class ToastView: UIView { let verticalStackView = UIStackView() let horizontalStackView = UIStackView() var cancellables = Set<AnyCancellable>() - + init(model: ToastModel) { super.init(frame: .zero) backgroundColor = model.color layer.cornerRadius = 18.0 - + titleLabel.textColor = .white subtitleLabel.textColor = .white leftImageView.contentMode = .center - + titleLabel.numberOfLines = 0 subtitleLabel.numberOfLines = 0 titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) subtitleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - + leftImageView.image = Asset.sharedSuccess.image leftImageView.setContentHuggingPriority(.required, for: .horizontal) - + rightButton.titleLabel?.numberOfLines = 0 rightButton.titleLabel?.textAlignment = .center rightButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 12.0) - + verticalStackView.axis = .vertical verticalStackView.distribution = .fill verticalStackView.addArrangedSubview(titleLabel) verticalStackView.addArrangedSubview(subtitleLabel) - + horizontalStackView.spacing = 12 horizontalStackView.addArrangedSubview(leftImageView) horizontalStackView.addArrangedSubview(verticalStackView) horizontalStackView.addArrangedSubview(rightButton) - + addSubview(horizontalStackView) - + horizontalStackView.snp.makeConstraints { $0.top.equalToSuperview().offset(17) $0.left.equalToSuperview().offset(20) $0.right.equalToSuperview().offset(-20) $0.bottom.equalToSuperview().offset(-17) } - + titleLabel.text = model.title leftImageView.image = model.leftImage - + if let subtitle = model.subtitle { subtitleLabel.text = subtitle subtitleLabel.numberOfLines = 0 } else { subtitleLabel.isHidden = true } - + if let buttonTitle = model.buttonTitle { rightButton.setTitle(buttonTitle, for: .normal) rightButton.setContentHuggingPriority(.required, for: .horizontal) } else { rightButton.isHidden = true } - + rightButton .publisher(for: .touchUpInside) .sink { model.onTapClosure?() } .store(in: &cancellables) } - + required init?(coder: NSCoder) { nil } } diff --git a/Sources/AppCore/URLDataLoader/URLDataLoader.swift b/Sources/AppCore/URLDataLoader/URLDataLoader.swift new file mode 100644 index 0000000000000000000000000000000000000000..bca96e58f59d9880aca1f38d100192fdf9c97f47 --- /dev/null +++ b/Sources/AppCore/URLDataLoader/URLDataLoader.swift @@ -0,0 +1,22 @@ +import Foundation +import XCTestDynamicOverlay + +public struct URLDataLoader { + public var load: (URL) throws -> Data + + public func callAsFunction(_ url: URL) throws -> Data { + try load(url) + } +} + +extension URLDataLoader { + public static let live = URLDataLoader { url in + try Data(contentsOf: url) + } +} + +extension URLDataLoader { + public static let unimplemented = URLDataLoader( + load: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/App/AppDelegate.swift b/Sources/AppFeature/AppDelegate.swift similarity index 99% rename from Sources/App/AppDelegate.swift rename to Sources/AppFeature/AppDelegate.swift index fd23e19c9ad2a4846e099f0c7023c2c1ab236bbf..c4dc80dab5df9e71b580a6e7525fd90858c0d414 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/AppFeature/AppDelegate.swift @@ -41,7 +41,6 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { UNUserNotificationCenter.current().delegate = self - DependencyRegistrator.registerDependencies() setupCloudFilesManagers() setupCrashReporting() setupLogging() diff --git a/Sources/AppFeature/DependencyRegistrator.swift b/Sources/AppFeature/DependencyRegistrator.swift new file mode 100644 index 0000000000000000000000000000000000000000..cd4baf2baf6b1af0cb2946df7c72f45a41437c96 --- /dev/null +++ b/Sources/AppFeature/DependencyRegistrator.swift @@ -0,0 +1,278 @@ +// MARK: SDK + +import UIKit +import Network +import QuickLook +import MobileCoreServices + +// MARK: Isolated features + +import Bindings +import XXLogger +import Keychain +import Defaults +import Voxophone +import Permissions +import PushFeature +import CrashService +import CrashReporting +import VersionChecking +import ReportingFeature +import CountryListFeature +import DI + +// MARK: UI Features + +import ScanFeature +import ChatFeature +import MenuFeature +import TermsFeature +import BackupFeature +import DrawerFeature +import SearchFeature +import LaunchFeature +import RestoreFeature +import ContactFeature +import ProfileFeature +import ChatListFeature +import SettingsFeature +import RequestsFeature +import OnboardingFeature +import ContactListFeature + +import Shared +import XXClient +import Navigation +import KeychainAccess +import XXMessengerClient + +import ComposableArchitecture + +struct DependencyRegistrator { + static public func registerNavigators(_ navController: UINavigationController) { +// container.register(CombinedNavigator( +// PresentModalNavigator(), +// DismissModalNavigator(), +// PushNavigator(), +// PopToRootNavigator(), +// PopToNavigator(), +// SetStackNavigator(), +// +// OpenUpNavigator(), +// OpenLeftNavigator(), +// +// PresentOnboardingStartNavigator( +// screen: OnboardingStartController.init, +// navigationController: { navController } +// ), +// PresentChatListNavigator( +// screen: ChatListController.init, +// navigationController: { navController } +// ), +// PresentTermsAndConditionsNavigator( +// screen: TermsConditionsController.init, +// navigationController: { navController } +// ), +// PresentSearchNavigator( +// screen: SearchContainerController.init(_:), +// navigationController: { navController } +// ), +// PresentRequestsNavigator( +// screen: RequestsContainerController.init, +// navigationController: { navController } +// ), +// PresentChatNavigator( +// screen: SingleChatController.init(_:), +// navigationController: { navController } +// ), +// PresentGroupChatNavigator( +// screen: GroupChatController.init(_:), +// navigationController: { navController } +// ), +// PresentOnboardingWelcomeNavigator( +// screen: OnboardingWelcomeController.init, +// navigationController: { navController } +// ), +// PresentOnboardingUsernameNavigator( +// screen: OnboardingUsernameController.init, +// navigationController: { navController } +// ), +// PresentRestoreListNavigator( +// screen: RestoreListController.init, +// navigationController: { navController } +// ), +// PresentOnboardingEmailNavigator( +// screen: OnboardingEmailController.init, +// navigationController: { navController } +// ), +// PresentOnboardingPhoneNavigator( +// screen: OnboardingPhoneController.init, +// navigationController: { navController } +// ), +// PresentOnboardingCodeNavigator( +// screen: OnboardingCodeController.init(_:_:_:), +// navigationController: { navController } +// ), +// PresentDrawerNavigator( +// screen: DrawerController.init(_:), +// navigationController: { navController } +// ), +// PresentContactListNavigator( +// screen: ContactListController.init, +// navigationController: { navController } +// ), +// PresentMenuNavigator( +// screen: MenuController.init(_:), +// navigationController: { navController } +// ), +// PresentScanNavigator( +// screen: ScanContainerController.init, +// navigationController: { navController } +// ), +// PresentNewGroupNavigator( +// screen: CreateGroupController.init, +// navigationController: { navController } +// ), +// PresentCountryListNavigator( +// screen: CountryListController.init(_:), +// navigationController: { navController } +// ), +// PresentProfileNavigator( +// screen: ProfileController.init, +// navigationController: { navController } +// ), +// PresentSettingsNavigator( +// screen: SettingsController.init, +// navigationController: { navController } +// ), +// PresentSettingsAdvancedNavigator( +// screen: SettingsAdvancedController.init, +// navigationController: { navController } +// ), +// PresentSettingsBackupNavigator( +// screen: BackupController.init, +// navigationController: { navController } +// ), +// PresentSettingsAccountDeleteNavigator( +// screen: AccountDeleteController.init, +// navigationController: { navController } +// ), +// PresentContactNavigator( +// screen: ContactController.init(_:), +// navigationController: { navController } +// ), +// PresentActivitySheetNavigator( +// screen: { UIActivityViewController( +// activityItems: $0, +// applicationActivities: nil +// )}, +// navigationController: { navController } +// ), +// PresentProfileEmailNavigator( +// screen: ProfileEmailController.init, +// navigationController: { navController } +// ), +// PresentProfilePhoneNavigator( +// screen: ProfilePhoneController.init, +// navigationController: { navController } +// ), +// PresentPermissionRequestNavigator( +// screen: RequestPermissionController.init, +// navigationController: { navController } +// ), +// PresentPhotoLibraryNavigator( +// screen: UIImagePickerController.init, +// navigationController: { navController } +// ), +// PresentProfileCodeNavigator( +// screen: ProfileCodeController.init(_:_:_:), +// navigationController: { navController } +// ) +// ) as Navigator) + } +} + +public struct OtherDependencies { + public var voxophone: Voxophone + public var sendReport: SendReport + public var pushHandler: PushHandler + public var versionCheck: VersionCheck + public var backupService: BackupService + public var hudController: HUDController + public var crashReporter: CrashReporter + public var networkMonitor: NetworkMonitor + public var keyObjectStore: KeyObjectStore + public var fetchBannedList: FetchBannedList + public var toastController: ToastController + public var reportingStatus: ReportingStatus + public var keychainHandler: KeychainHandler + public var makeReportDrawer: MakeReportDrawer + public var statusBarStylist: StatusBarStylist + public var getIdFromContact: GetIdFromContact + public var permissionHandler: PermissionHandler + public var processBannedList: ProcessBannedList + public var makeAppScreenshot: MakeAppScreenshot + public var getFactsFromContact: GetFactsFromContact +} + +extension OtherDependencies { + public static func live() -> OtherDependencies { + .init( + voxophone: .init(), + sendReport: .live, + pushHandler: .init(), + versionCheck: .live, + backupService: .init(), + hudController: .init(), + crashReporter: .live, + networkMonitor: .init(), + keyObjectStore: .userDefaults, + fetchBannedList: .live, + toastController: .init(), + reportingStatus: .live(), + keychainHandler: .init(), + makeReportDrawer: .live, + statusBarStylist: .init(), + getIdFromContact: .live, + permissionHandler: .init(), + processBannedList: .live, + makeAppScreenshot: .live, + getFactsFromContact: .live + ) + } + + public static let unimplemented = OtherDependencies( + voxophone: .init(), + sendReport: .unimplemented, + pushHandler: .init(), + versionCheck: .unimplemented, + backupService: .init(), + hudController: .init(), + crashReporter: .noop, + networkMonitor: .init(), + keyObjectStore: .mock(dictionary: [:]), + fetchBannedList: .unimplemented, + toastController: .init(), + reportingStatus: .mock(), + keychainHandler: .init(), + makeReportDrawer: .unimplemented, + statusBarStylist: .init(), + getIdFromContact: .live, + permissionHandler: .init(), + processBannedList: .unimplemented, + makeAppScreenshot: .unimplemented, + getFactsFromContact: .live + ) +} + +private enum OtherDependenciesKey: DependencyKey { + static let liveValue: OtherDependencies = .live() + static let testValue: OtherDependencies = .unimplemented +} + +extension DependencyValues { + public var others: OtherDependencies { + get { self[OtherDependenciesKey.self] } + set { self[OtherDependenciesKey.self] = newValue } + } +} diff --git a/Sources/AppFeature/PushRouter.swift b/Sources/AppFeature/PushRouter.swift new file mode 100644 index 0000000000000000000000000000000000000000..dc790bd8d3af5923dd99ea87e3b051adcf840bad --- /dev/null +++ b/Sources/AppFeature/PushRouter.swift @@ -0,0 +1,57 @@ +import DI +import UIKit +import XXModels +import PushFeature +import ChatFeature +import SearchFeature +import LaunchFeature +import ChatListFeature +import RequestsFeature +import XXMessengerClient + +extension PushRouter { + static func live(navigationController: UINavigationController) -> PushRouter { + PushRouter { route, completion in + if let launchController = navigationController.viewControllers.last as? LaunchController { + launchController.pendingPushRoute = route + } else { + switch route { + case .requests: + if !(navigationController.viewControllers.last is RequestsContainerController) { + navigationController.setViewControllers([RequestsContainerController()], animated: true) + } + case .search(username: let username): + if let messenger = try? DI.Container.shared.resolve() as Messenger, + let _ = try? messenger.ud.get()?.getContact() { + if !(navigationController.viewControllers.last is SearchContainerController) { + navigationController.setViewControllers([ + ChatListController(), + SearchContainerController(username) + ], animated: true) + } else { + (navigationController.viewControllers.last as? SearchContainerController)?.startSearchingFor(username) + } + } + case .contactChat(id: let id): + if let database: Database = try? DI.Container.shared.resolve(), + let contact = try? database.fetchContacts(.init(id: [id])).first { + navigationController.setViewControllers([ + ChatListController(), + SingleChatController(contact) + ], animated: true) + } + case .groupChat(id: let id): + if let database: Database = try? DI.Container.shared.resolve(), + let info = try? database.fetchGroupInfos(.init(groupId: id)).first { + navigationController.setViewControllers([ + ChatListController(), + GroupChatController(info) + ], animated: true) + } + } + } + + completion() + } + } +} diff --git a/Sources/Shared/AutoGenerated/Assets.swift b/Sources/AppResources/Assets.swift similarity index 100% rename from Sources/Shared/AutoGenerated/Assets.swift rename to Sources/AppResources/Assets.swift diff --git a/Sources/Shared/AutoGenerated/Fonts.swift b/Sources/AppResources/Fonts.swift similarity index 100% rename from Sources/Shared/AutoGenerated/Fonts.swift rename to Sources/AppResources/Fonts.swift diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsBackup/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsBackup/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsBackup/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Group 512227.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Group 512227.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Group 512227.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsBackup/backup_success.imageset/Group 512227.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Icon (9).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Icon (9).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Icon (9).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_close_speaker.imageset/Icon (9).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Icon (7).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Icon (7).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Icon (7).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_open_speaker.imageset/Icon (7).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Icon (8).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Icon (8).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Icon (8).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_pause.imageset/Icon (8).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Icon (10).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Icon (10).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Icon (10).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_play.imageset/Icon (10).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/sound.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/sound.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/sound.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_audio_spectrum.imageset/sound.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Icon (2).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Icon (2).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Icon (2).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_camera.imageset/Icon (2).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Icon (5).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Icon (5).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Icon (5).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_close.imageset/Icon (5).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Icon (4).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Icon (4).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Icon (4).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_files.imageset/Icon (4).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Icon (3).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Icon (3).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Icon (3).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_gallery.imageset/Icon (3).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Icon-5.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Icon-5.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Icon-5.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_action_open.imageset/Icon-5.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/pause_brand_light.png b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/pause_brand_light.png similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/pause_brand_light.png rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_pause.imageset/pause_brand_light.png diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Icon (6).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Icon (6).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Icon (6).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_play.imageset/Icon (6).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Icon (1).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Icon (1).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Icon (1).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_start.imageset/Icon (1).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Group 2.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Group 2.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Group 2.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_input_voice_stop.imageset/Group 2.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Vector-26.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Vector-26.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Vector-26.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_locker.imageset/Vector-26.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Icon-6.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Icon-6.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Icon-6.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_more.imageset/Icon-6.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Ellipse 1-2.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Ellipse 1-2.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Ellipse 1-2.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_placeholder_image.imageset/Ellipse 1-2.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Icon-34.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Icon-34.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Icon-34.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChat/chat_send.imageset/Icon-34.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Icon-7.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Icon-7.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Icon-7.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_delete_swipe.imageset/Icon-7.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Vector-19.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Vector-19.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Vector-19.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu.imageset/Vector-19.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Icon-11.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Icon-11.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Icon-11.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_delete.imageset/Icon-11.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Vector-27.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Vector-27.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Vector-27.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_menu_pin.imageset/Vector-27.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Icon-19.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Icon-19.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Icon-19.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new.imageset/Icon-19.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Icon-9.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Icon-9.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Icon-9.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_pin_swipe.imageset/Icon-9.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Ellipse 1-3.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Ellipse 1-3.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Ellipse 1-3.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_placeholder.imageset/Ellipse 1-3.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsCode/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsCode/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsCode/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsCode/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsCode/code.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsCode/code.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsCode/code.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsCode/code.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsCode/code.imageset/circle-bg 2-3.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsCode/code.imageset/circle-bg 2-3.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsCode/code.imageset/circle-bg 2-3.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsCode/code.imageset/circle-bg 2-3.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Group 512213.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Group 512213.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Group 512213.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_add_placeholder.imageset/Group 512213.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Vector-14.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Vector-14.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Vector-14.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_details_padlock.imageset/Vector-14.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Group 512219.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Group 512219.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Group 512219.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_nickname_edit.imageset/Group 512219.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Icon-12.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Icon-12.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Icon-12.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_exclamation.imageset/Icon-12.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Group 512213.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Group 512213.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Group 512213.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_request_placeholder.imageset/Group 512213.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContact/contact_send_message.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Icon-17.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Icon-17.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Icon-17.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_avatar_remove.imageset/Icon-17.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Icon-23.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Icon-23.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Icon-23.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_new_group.imageset/Icon-23.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Ellipse 1-3.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Ellipse 1-3.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Ellipse 1-3.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_placeholder.imageset/Ellipse 1-3.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Icon-24.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Icon-24.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Icon-24.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_requests.imageset/Icon-24.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Union-6.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Union-6.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Union-6.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_search.imageset/Union-6.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Icon-16.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Icon-16.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Icon-16.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsContactList/contactList_user_search.imageset/Icon-16.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsDrawer/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsDrawer/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsDrawer/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsDrawer/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/circle-bg 2-9.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/circle-bg 2-9.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/circle-bg 2-9.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsDrawer/drawer_negative.imageset/circle-bg 2-9.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Vector-9.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Vector-9.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Vector-9.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_chats.imageset/Vector-9.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Vector-10.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Vector-10.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Vector-10.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_contacts.imageset/Vector-10.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/dashboardIconMd.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/dashboardIconMd.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/dashboardIconMd.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_dashboard.imageset/dashboardIconMd.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Vector-11.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Vector-11.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Vector-11.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_profile.imageset/Vector-11.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Icon-33.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Icon-33.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Icon-33.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_requests.imageset/Icon-33.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Union-3.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Union-3.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Union-3.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_scan.imageset/Union-3.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Vector-12.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Vector-12.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Vector-12.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_settings.imageset/Vector-12.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/photo-1599149535927-36b3fc68a1d6 1-2.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/photo-1599149535927-36b3fc68a1d6 1-2.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/photo-1599149535927-36b3fc68a1d6 1-2.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_background.imageset/photo-1599149535927-36b3fc68a1d6 1-2.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Powered by xx network.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Powered by xx network.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Powered by xx network.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_bottom_logo_start.imageset/Powered by xx network.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/circle-bg 2-6.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/circle-bg 2-6.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/circle-bg 2-6.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_email.imageset/circle-bg 2-6.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Logo.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Logo.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Logo.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo.imageset/Logo.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Group-2.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Group-2.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Group-2.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_logo_start.imageset/Group-2.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/circle-bg 2-7.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/circle-bg 2-7.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/circle-bg 2-7.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_phone.imageset/circle-bg 2-7.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/circle-bg 3.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/circle-bg 3.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/circle-bg 3.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsOnboarding/onboarding_success.imageset/circle-bg 3.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/circle-bg 2 (2).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/circle-bg 2 (2).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/circle-bg 2 (2).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_camera.imageset/circle-bg 2 (2).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/circle-bg 2 (3).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/circle-bg 2 (3).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/circle-bg 2 (3).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_library.imageset/circle-bg 2 (3).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Logo (1).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Logo (1).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Logo (1).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_logo.imageset/Logo (1).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/circle-bg 2 (1).pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/circle-bg 2 (1).pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/circle-bg 2 (1).pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsPermissions/permission_microphone.imageset/circle-bg 2 (1).pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Icon-21.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Icon-21.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Icon-21.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_add.imageset/Icon-21.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Icon-22.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Icon-22.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Icon-22.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_delete.imageset/Icon-22.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Group 512237.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Group 512237.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Group 512237.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_email.imageset/Group 512237.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Group 512242.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Group 512242.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Group 512242.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_button.imageset/Group 512242.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Icon-20.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Icon-20.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Icon-20.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_image_placeholder.imageset/Icon-20.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Group 512237-2.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Group 512237-2.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Group 512237-2.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsProfile/profile_phone.imageset/Group 512237-2.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_accepted.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_failed_toaster.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/request_sent_toaster.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/circle-bg 2-11.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/circle-bg 2-11.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/circle-bg 2-11.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_received_placeholder.imageset/circle-bg 2-11.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resend.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_resent.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_failed.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_received.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_tab_sent.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRequests/requests_verification_failed.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_drive.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Icon-3.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Icon-3.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Icon-3.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_dropbox.imageset/Icon-3.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Icon-2.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Icon-2.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Icon-2.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_icloud.imageset/Icon-2.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Group 512227.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Group 512227.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Group 512227.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsRestore/restore_success.imageset/Group 512227.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Vector-23.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Vector-23.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Vector-23.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_email.imageset/Vector-23.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Icon-36.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Icon-36.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Icon-36.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_error.imageset/Icon-36.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Vector-24.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Vector-24.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Vector-24.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_phone.imageset/Vector-24.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Vector-18.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Vector-18.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Vector-18.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_email.imageset/Vector-18.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Vector-25.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Vector-25.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Vector-25.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_lens.imageset/Vector-25.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Vector-17.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Vector-17.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Vector-17.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_phone.imageset/Vector-17.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Ellipse 1.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Ellipse 1.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Ellipse 1.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_placeholder_image.imageset/Ellipse 1.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Vector-16.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Vector-16.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Vector-16.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSearch/search_username.imageset/Vector-16.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Icon-32.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Icon-32.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Icon-32.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/Icon-32.imageset/Icon-32.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Icon-31.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Icon-31.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Icon-31.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_advanced.imageset/Icon-31.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Icon-25.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Icon-25.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Icon-25.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_biometrics.imageset/Icon-25.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Group.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Group.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Group.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_crash.imageset/Group.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Icon-35.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Icon-35.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Icon-35.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete.imageset/Icon-35.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Group 512235.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Group 512235.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Group 512235.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_delete_large.imageset/Group 512235.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Vector-5.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Vector-5.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Vector-5.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_disclosure.imageset/Vector-5.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/variant=download.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/variant=download.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/variant=download.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_download.imageset/variant=download.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Icon-27.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Icon-27.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Icon-27.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_enter.imageset/Icon-27.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Icon-29.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Icon-29.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Icon-29.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_folder.imageset/Icon-29.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_hide.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Icon-26.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Icon-26.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Icon-26.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_keyboard.imageset/Icon-26.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Vector-21.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Vector-21.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Vector-21.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_logs.imageset/Vector-21.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_notifications.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Icon-28.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Icon-28.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Icon-28.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsSettings/settings_privacy.imageset/Icon-28.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Icon-18.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Icon-18.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Icon-18.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/balloon.imageset/Icon-18.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/notVisibleIcon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/notVisibleIcon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/notVisibleIcon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_closed.imageset/notVisibleIcon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/visibleIconLg.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/visibleIconLg.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/visibleIconLg.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/eye_open.imageset/visibleIconLg.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/info_icon_grey.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/lens.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/lens.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/lens.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/lens.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/lens.imageset/searchIcon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/lens.imageset/searchIcon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/lens.imageset/searchIcon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/lens.imageset/searchIcon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Vector.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Vector.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Vector.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/navigation_bar_back.imageset/Vector.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Icon-13.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Icon-13.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Icon-13.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_gray.imageset/Icon-13.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/circle-bg 2-11.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/circle-bg 2-11.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/circle-bg 2-11.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/person_placeholder.imageset/circle-bg 2-11.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512.png b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512.png similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512.png rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512.png diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@2x.png b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@2x.png similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@2x.png rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@2x.png diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@3x.png b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@3x.png similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@3x.png rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/reply_abort.imageset/closeIconDark512@3x.png diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Icon-8.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Icon-8.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Icon-8.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_cross.imageset/Icon-8.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_error.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Icon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Icon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Icon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_group.imageset/Icon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Union-7.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Union-7.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Union-7.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_scan.imageset/Union-7.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Icon-2.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Icon-2.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Icon-2.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_success.imageset/Icon-2.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/alertDidntsendIcon.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/alertDidntsendIcon.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/alertDidntsendIcon.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/shared_white_exclamation.imageset/alertDidntsendIcon.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/splash.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/splash.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/splash.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/splash.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsShared/splash.imageset/group.pdf b/Sources/AppResources/Resources/Assets.xcassets/AssetsShared/splash.imageset/group.pdf similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/AssetsShared/splash.imageset/group.pdf rename to Sources/AppResources/Resources/Assets.xcassets/AssetsShared/splash.imageset/group.pdf diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsAccent/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsAccent/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_danger.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_danger.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_danger.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_danger.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_safe.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_safe.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_safe.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_safe.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_success.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_success.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_success.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_success.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_warning.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_warning.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsAccent/accent_warning.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsAccent/accent_warning.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsBrand/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsBrand/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_background.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_background.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_background.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_background.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_bubble.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_bubble.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_bubble.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_bubble.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_default.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_default.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_default.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_default.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_light.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_light.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_light.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_light.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_primary.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_primary.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsBrand/brand_primary.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsBrand/brand_primary.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_active.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_active.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_active.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_active.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_body.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_body.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_body.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_body.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_dark.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_dark.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_dark.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_dark.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_disabled.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_disabled.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_disabled.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_disabled.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_line.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_line.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_line.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_line.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_overlay.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_overlay.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_overlay.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_overlay.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary_alternative.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary_alternative.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary_alternative.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_secondary_alternative.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_weak.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_weak.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_weak.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_weak.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_white.colorset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_white.colorset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/ColorsNeutral/neutral_white.colorset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/ColorsNeutral/neutral_white.colorset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/transfer_image_placeholder.imageset/Contents.json b/Sources/AppResources/Resources/Assets.xcassets/transfer_image_placeholder.imageset/Contents.json similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/transfer_image_placeholder.imageset/Contents.json rename to Sources/AppResources/Resources/Assets.xcassets/transfer_image_placeholder.imageset/Contents.json diff --git a/Sources/Shared/Resources/Assets.xcassets/transfer_image_placeholder.imageset/placeholder-images-image_large.png b/Sources/AppResources/Resources/Assets.xcassets/transfer_image_placeholder.imageset/placeholder-images-image_large.png similarity index 100% rename from Sources/Shared/Resources/Assets.xcassets/transfer_image_placeholder.imageset/placeholder-images-image_large.png rename to Sources/AppResources/Resources/Assets.xcassets/transfer_image_placeholder.imageset/placeholder-images-image_large.png diff --git a/Sources/Shared/Resources/Fonts/Mulish-Black.ttf b/Sources/AppResources/Resources/Fonts/Mulish-Black.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-Black.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-Black.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-BlackItalic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-BlackItalic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-BlackItalic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-BlackItalic.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-Bold.ttf b/Sources/AppResources/Resources/Fonts/Mulish-Bold.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-Bold.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-Bold.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-BoldItalic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-BoldItalic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-BoldItalic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-BoldItalic.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-ExtraBold.ttf b/Sources/AppResources/Resources/Fonts/Mulish-ExtraBold.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-ExtraBold.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-ExtraBold.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-ExtraBoldItalic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-ExtraBoldItalic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-ExtraBoldItalic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-ExtraBoldItalic.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-ExtraLight.ttf b/Sources/AppResources/Resources/Fonts/Mulish-ExtraLight.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-ExtraLight.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-ExtraLight.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-ExtraLightItalic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-ExtraLightItalic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-ExtraLightItalic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-ExtraLightItalic.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-Italic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-Italic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-Italic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-Italic.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-Light.ttf b/Sources/AppResources/Resources/Fonts/Mulish-Light.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-Light.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-Light.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-LightItalic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-LightItalic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-LightItalic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-LightItalic.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-Medium.ttf b/Sources/AppResources/Resources/Fonts/Mulish-Medium.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-Medium.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-Medium.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-MediumItalic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-MediumItalic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-MediumItalic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-MediumItalic.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-Regular.ttf b/Sources/AppResources/Resources/Fonts/Mulish-Regular.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-Regular.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-Regular.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-SemiBold.ttf b/Sources/AppResources/Resources/Fonts/Mulish-SemiBold.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-SemiBold.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-SemiBold.ttf diff --git a/Sources/Shared/Resources/Fonts/Mulish-SemiBoldItalic.ttf b/Sources/AppResources/Resources/Fonts/Mulish-SemiBoldItalic.ttf similarity index 100% rename from Sources/Shared/Resources/Fonts/Mulish-SemiBoldItalic.ttf rename to Sources/AppResources/Resources/Fonts/Mulish-SemiBoldItalic.ttf diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/AppResources/Resources/en.lproj/Localizable.strings similarity index 100% rename from Sources/Shared/Resources/en.lproj/Localizable.strings rename to Sources/AppResources/Resources/en.lproj/Localizable.strings diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/AppResources/Strings.swift similarity index 100% rename from Sources/Shared/AutoGenerated/Strings.swift rename to Sources/AppResources/Strings.swift diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift index 5c75751ef6bbe2a7c8149d1f36cc65407d83ee57..25772de76f0a5a0bc13a45ddc2789088b3307982 100644 --- a/Sources/BackupFeature/Controllers/BackupConfigController.swift +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -1,10 +1,11 @@ +import DI import UIKit import Shared import Combine import CloudFiles import Navigation import DrawerFeature -import DI +import AppResources final class BackupConfigController: UIViewController { @Dependency var navigator: Navigator @@ -271,7 +272,7 @@ final class BackupConfigController: UIViewController { wifiOnlyButton, wifiAndCellularButton, cancelButton - ])) + ], isDismissable: true, from: self)) } private func presentFrequencyDrawer(manual: Bool) { @@ -329,6 +330,6 @@ final class BackupConfigController: UIViewController { manualButton, automaticButton, cancelButton - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/BackupFeature/Controllers/BackupController.swift b/Sources/BackupFeature/Controllers/BackupController.swift index aa5b7e8db6f20d08baebc313b4ea459fc093532c..b01e3865f6aee5765f0cdb18423b553c266de428 100644 --- a/Sources/BackupFeature/Controllers/BackupController.swift +++ b/Sources/BackupFeature/Controllers/BackupController.swift @@ -1,65 +1,66 @@ +import DI import UIKit import Shared import Combine -import DI +import AppResources public final class BackupController: UIViewController { - private let viewModel = BackupViewModel.live() - private var cancellables = Set<AnyCancellable>() + private let viewModel = BackupViewModel.live() + private var cancellables = Set<AnyCancellable>() - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } - public override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = Asset.neutralWhite.color - setupNavigationBar() - setupBindings() - } + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = Asset.neutralWhite.color + setupNavigationBar() + setupBindings() + } - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.Backup.header - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } + private func setupNavigationBar() { + let title = UILabel() + title.text = Localized.Backup.header + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) + navigationItem.leftItemsSupplementBackButton = true + } - private func setupBindings() { - viewModel.state() - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [unowned self] in - switch $0 { - case .setup: - contentViewController = BackupSetupController(viewModel.setupViewModel()) - case .config: - contentViewController = BackupConfigController(viewModel.configViewModel()) - } - }.store(in: &cancellables) - } + private func setupBindings() { + viewModel.state() + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + switch $0 { + case .setup: + contentViewController = BackupSetupController(viewModel.setupViewModel()) + case .config: + contentViewController = BackupConfigController(viewModel.configViewModel()) + } + }.store(in: &cancellables) + } - private var contentViewController: UIViewController? { - didSet { - guard contentViewController != oldValue else { return } + private var contentViewController: UIViewController? { + didSet { + guard contentViewController != oldValue else { return } - if let oldValue = oldValue { - oldValue.willMove(toParent: nil) - oldValue.view.removeFromSuperview() - oldValue.removeFromParent() - } + if let oldValue = oldValue { + oldValue.willMove(toParent: nil) + oldValue.view.removeFromSuperview() + oldValue.removeFromParent() + } - if let newValue = contentViewController { - addChild(newValue) - view.addSubview(newValue.view) - newValue.view.snp.makeConstraints { $0.edges.equalToSuperview() } - newValue.didMove(toParent: self) - } - } + if let newValue = contentViewController { + addChild(newValue) + view.addSubview(newValue.view) + newValue.view.snp.makeConstraints { $0.edges.equalToSuperview() } + newValue.didMove(toParent: self) + } } + } } diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift index 2d5e9989434f1f46a380597bfcc51e1716a29369..c471287bad37f16d3cda7d81dfb7ff4f1fcc0f07 100644 --- a/Sources/BackupFeature/Service/BackupService.swift +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -1,17 +1,17 @@ +import AppCore import UIKit import Combine import XXClient import Defaults import CloudFiles import CloudFilesSFTP -import NetworkMonitor import KeychainAccess import XXMessengerClient -import DI +import ComposableArchitecture public final class BackupService { - @Dependency var messenger: Messenger - @Dependency var networkManager: NetworkMonitoring + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.networkMonitor) var networkMonitor: NetworkMonitorManager @KeyObject(.email, defaultValue: nil) var email: String? @KeyObject(.phone, defaultValue: nil) var phone: String? @@ -34,7 +34,6 @@ public final class BackupService { }).eraseToAnyPublisher() } - private var connType: ConnectionType = .wifi private var cancellables = Set<AnyCancellable>() private let connectedServicesSubject = CurrentValueSubject<Set<CloudService>, Never>([]) private let backupSubject = CurrentValueSubject<[CloudService: Fetch.Metadata], Never>([:]) @@ -44,13 +43,9 @@ public final class BackupService { settings .dropFirst() .removeDuplicates() - .sink { [unowned self] in storedSettings = $0.toData() } - .store(in: &cancellables) - - networkManager.connType - .receive(on: DispatchQueue.main) - .sink { [unowned self] in connType = $0 } - .store(in: &cancellables) + .sink { [unowned self] in + storedSettings = $0.toData() + }.store(in: &cancellables) } func didSetWiFiOnly(enabled: Bool) { @@ -87,26 +82,21 @@ public final class BackupService { private func shouldBackupIfSetAutomatic() { guard let lastBackup = try? Data(contentsOf: getBackupURL()) else { - print(">>> No stored backup so won't upload anything.") - return + return // No stored backup so won't upload anything } guard settings.value.automaticBackups else { - print(">>> Backups are not set to automatic") - return + return // Backups are not set to automatic } guard settings.value.enabledService != nil else { - print(">>> No service enabled to upload") - return + return // No service enabled to upload } if settings.value.wifiOnlyBackup { - guard connType == .wifi else { - print(">>> WiFi only backups, and connType != Wifi") - return + guard networkMonitor.connType() == .wifi else { + return // WiFi only backups, and connType != Wifi } } else { - guard connType != .unknown else { - print(">>> Connectivity is unknown") - return + guard networkMonitor.connType() != .unknown else { + return // Connectivity is unknown } } performUpload(of: lastBackup) @@ -155,8 +145,6 @@ public final class BackupService { backupManager.addJSON(string) } - // MARK: - CloudProviders - func setupSFTP(host: String, username: String, password: String) { let sftpManager = CloudFilesManager.sftp( host: host, @@ -233,8 +221,7 @@ public final class BackupService { ) } guard let manager = CloudFilesManager.all[enabledService] else { - print(">>> Tried to upload but the enabled service is not set") - return + return // Tried to upload but the enabled service is not set } do { try manager.upload(data) { [weak self] in diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index 4ae6414e89a48f752080c7775fb33d4376265519..5318952dcadddd6a33bf7c257a4d51ae96851523 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -1,14 +1,12 @@ import UIKit import Shared +import AppCore import Combine import XXClient import Defaults -import Foundation - -import DI - import CloudFiles import Navigation +import ComposableArchitecture enum BackupActionState { case backupFinished @@ -34,9 +32,9 @@ struct BackupConfigViewModel { extension BackupConfigViewModel { static func live() -> Self { class Context { - @Dependency var navigator: Navigator - @Dependency var service: BackupService - @Dependency var hudController: HUDController + //@Dependency var service: BackupService + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.hudManager) var hudManager: HUDManager } let context = Context() @@ -70,9 +68,9 @@ extension BackupConfigViewModel { }, didTapService: { service, controller in if service == .sftp { - context.navigator.perform(PresentSFTP { host, username, password in + context.navigator.perform(PresentSFTP(completion: { host, username, password in context.service.setupSFTP(host: host, username: username, password: password) - }) + }, on: controller.navigationController!)) return } diff --git a/Sources/BackupFeature/ViewModels/BackupViewModel.swift b/Sources/BackupFeature/ViewModels/BackupViewModel.swift index 17fca00a15143c3fd16752e780bb49ce15a48bc8..d25ddc5ebfbd3778131f15b111ad57e89f4c6733 100644 --- a/Sources/BackupFeature/ViewModels/BackupViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupViewModel.swift @@ -1,34 +1,34 @@ -import Combine import DI +import Combine enum BackupViewState: Equatable { - case setup - case config + case setup + case config } struct BackupViewModel { - var setupViewModel: () -> BackupSetupViewModel - var configViewModel: () -> BackupConfigViewModel + var setupViewModel: () -> BackupSetupViewModel + var configViewModel: () -> BackupConfigViewModel - var state: () -> AnyPublisher<BackupViewState, Never> + var state: () -> AnyPublisher<BackupViewState, Never> } extension BackupViewModel { - static func live() -> Self { - class Context { - @Dependency var service: BackupService - } + static func live() -> Self { + class Context { + @Dependency var service: BackupService + } - let context = Context() + let context = Context() - return .init( - setupViewModel: { BackupSetupViewModel.live() }, - configViewModel: { BackupConfigViewModel.live() }, - state: { - context.service.connectedServicesPublisher - .map { $0.isEmpty ? BackupViewState.setup : .config } - .eraseToAnyPublisher() - } - ) - } + return .init( + setupViewModel: { BackupSetupViewModel.live() }, + configViewModel: { BackupConfigViewModel.live() }, + state: { + context.service.connectedServicesPublisher + .map { $0.isEmpty ? BackupViewState.setup : .config } + .eraseToAnyPublisher() + } + ) + } } diff --git a/Sources/BackupFeature/Views/BackupActionView.swift b/Sources/BackupFeature/Views/BackupActionView.swift index 705263e0ef815ae9fab0ff5657cc536f02c98101..ae30fe5575fef2d271afb848c3720af29cd6b75b 100644 --- a/Sources/BackupFeature/Views/BackupActionView.swift +++ b/Sources/BackupFeature/Views/BackupActionView.swift @@ -1,125 +1,126 @@ import UIKit import Shared +import AppResources final class BackupActionView: UIView { - let stackView = UIStackView() - let backupNowButton = CapsuleButton() + let stackView = UIStackView() + let backupNowButton = CapsuleButton() - let progressView = UIView() - let progressLabel = UILabel() - let progressBarPartial = UIView() - let progressBarFull = UIView() + let progressView = UIView() + let progressLabel = UILabel() + let progressBarPartial = UIView() + let progressBarFull = UIView() - let finishedView = UIView() - let finishedLabel = UILabel() - let finishedImage = UIImageView() + let finishedView = UIView() + let finishedLabel = UILabel() + let finishedImage = UIImageView() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - setupProgressView() - setupFinishedView() + setupProgressView() + setupFinishedView() - backupNowButton.set(style: .brandColored, title: Localized.Backup.Config.backupNow) + backupNowButton.set(style: .brandColored, title: Localized.Backup.Config.backupNow) - stackView.spacing = 15 - stackView.axis = .vertical - stackView.addArrangedSubview(backupNowButton) - stackView.addArrangedSubview(progressView) - stackView.addArrangedSubview(finishedView) + stackView.spacing = 15 + stackView.axis = .vertical + stackView.addArrangedSubview(backupNowButton) + stackView.addArrangedSubview(progressView) + stackView.addArrangedSubview(finishedView) - addSubview(stackView) + addSubview(stackView) - stackView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } + stackView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.equalToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - private func setupFinishedView() { - finishedImage.contentMode = .center - finishedImage.image = Asset.restoreSuccess.image + private func setupFinishedView() { + finishedImage.contentMode = .center + finishedImage.image = Asset.restoreSuccess.image - finishedLabel.text = "Backup completed!" - finishedLabel.textColor = Asset.neutralBody.color - finishedLabel.font = Fonts.Mulish.regular.font(size: 16.0) + finishedLabel.text = "Backup completed!" + finishedLabel.textColor = Asset.neutralBody.color + finishedLabel.font = Fonts.Mulish.regular.font(size: 16.0) - finishedView.addSubview(finishedImage) - finishedView.addSubview(finishedLabel) + finishedView.addSubview(finishedImage) + finishedView.addSubview(finishedLabel) - finishedImage.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.bottom.equalToSuperview() - } + finishedImage.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.bottom.equalToSuperview() + } - finishedLabel.snp.makeConstraints { make in - make.left.equalTo(finishedImage.snp.right).offset(10) - make.centerY.equalTo(finishedImage) - make.right.lessThanOrEqualToSuperview() - } + finishedLabel.snp.makeConstraints { make in + make.left.equalTo(finishedImage.snp.right).offset(10) + make.centerY.equalTo(finishedImage) + make.right.lessThanOrEqualToSuperview() + } + } + + private func setupProgressView() { + progressLabel.textColor = Asset.neutralDisabled.color + progressLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + progressBarFull.backgroundColor = Asset.neutralLine.color + progressBarPartial.backgroundColor = Asset.brandPrimary.color + progressBarFull.layer.masksToBounds = true + progressBarFull.layer.cornerRadius = 4 + + progressBarFull.addSubview(progressBarPartial) + progressView.addSubview(progressLabel) + progressView.addSubview(progressBarFull) + + progressBarFull.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + make.height.equalTo(8) } - private func setupProgressView() { - progressLabel.textColor = Asset.neutralDisabled.color - progressLabel.font = Fonts.Mulish.regular.font(size: 14.0) - - progressBarFull.backgroundColor = Asset.neutralLine.color - progressBarPartial.backgroundColor = Asset.brandPrimary.color - progressBarFull.layer.masksToBounds = true - progressBarFull.layer.cornerRadius = 4 - - progressBarFull.addSubview(progressBarPartial) - progressView.addSubview(progressLabel) - progressView.addSubview(progressBarFull) - - progressBarFull.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.height.equalTo(8) - } - - progressLabel.snp.makeConstraints { make in - make.top.equalTo(progressBarFull.snp.bottom).offset(10) - make.left.equalToSuperview() - make.bottom.equalToSuperview() - } - - progressBarPartial.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.width.equalToSuperview().multipliedBy(0.5) - make.bottom.equalToSuperview() - } + progressLabel.snp.makeConstraints { make in + make.top.equalTo(progressBarFull.snp.bottom).offset(10) + make.left.equalToSuperview() + make.bottom.equalToSuperview() } - func setState(_ state: BackupActionState) { - switch state { - case .backupFinished: - backupNowButton.isHidden = true - progressView.isHidden = true - finishedView.isHidden = false - - case .backupAllowed(let bool): - backupNowButton.isHidden = false - progressView.isHidden = true - finishedView.isHidden = true - backupNowButton.isEnabled = bool - - case .backupInProgress(let uploaded, let total): - backupNowButton.isHidden = true - progressView.isHidden = false - finishedView.isHidden = true - - let uploadedKb = String(format: "%.1f kb", uploaded/1000) - let totalkb = String(format: "%.1f kb", total/1000) - - progressLabel.text = "Uploaded \(uploadedKb) of \(totalkb) (\(total/uploaded)%)" - } + progressBarPartial.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.width.equalToSuperview().multipliedBy(0.5) + make.bottom.equalToSuperview() + } + } + + func setState(_ state: BackupActionState) { + switch state { + case .backupFinished: + backupNowButton.isHidden = true + progressView.isHidden = true + finishedView.isHidden = false + + case .backupAllowed(let bool): + backupNowButton.isHidden = false + progressView.isHidden = true + finishedView.isHidden = true + backupNowButton.isEnabled = bool + + case .backupInProgress(let uploaded, let total): + backupNowButton.isHidden = true + progressView.isHidden = false + finishedView.isHidden = true + + let uploadedKb = String(format: "%.1f kb", uploaded/1000) + let totalkb = String(format: "%.1f kb", total/1000) + + progressLabel.text = "Uploaded \(uploadedKb) of \(totalkb) (\(total/uploaded)%)" } + } } diff --git a/Sources/BackupFeature/Views/BackupConfigView.swift b/Sources/BackupFeature/Views/BackupConfigView.swift index 1c65f58f7222f1c069284c717518904dc2e596ca..60555ce672c01384fd2037af58a3803b465560ef 100644 --- a/Sources/BackupFeature/Views/BackupConfigView.swift +++ b/Sources/BackupFeature/Views/BackupConfigView.swift @@ -1,117 +1,118 @@ import UIKit import Shared +import AppResources final class BackupConfigView: UIView { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let actionView = BackupActionView() - - let stackView = UIStackView() - let sftpButton = BackupSwitcherButton() - let iCloudButton = BackupSwitcherButton() - let dropboxButton = BackupSwitcherButton() - let googleDriveButton = BackupSwitcherButton() - - let enabledSubtitleView = UIView() - let enabledSubtitleLabel = UILabel() - let frequencyDetailView = BackupDetailView() - let latestBackupDetailView = BackupDetailView() - let infrastructureDetailView = BackupDetailView() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - titleLabel.textColor = Asset.neutralDark.color - titleLabel.text = Localized.Backup.Config.title - titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) - - enabledSubtitleLabel.numberOfLines = 0 - enabledSubtitleLabel.textColor = Asset.neutralWeak.color - enabledSubtitleLabel.font = Fonts.Mulish.regular.font(size: 14.0) - - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - let attString = NSAttributedString( - string: Localized.Backup.subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ]) - - subtitleLabel.numberOfLines = 0 - subtitleLabel.attributedText = attString - - sftpButton.titleLabel.text = Localized.Backup.sftp - sftpButton.logoImageView.image = Asset.restoreSFTP.image - - iCloudButton.titleLabel.text = Localized.Backup.iCloud - iCloudButton.logoImageView.image = Asset.restoreIcloud.image - - dropboxButton.titleLabel.text = Localized.Backup.dropbox - dropboxButton.logoImageView.image = Asset.restoreDropbox.image - - googleDriveButton.titleLabel.text = Localized.Backup.googleDrive - googleDriveButton.logoImageView.image = Asset.restoreDrive.image - - latestBackupDetailView.titleLabel.text = Localized.Backup.Config.latestBackup - frequencyDetailView.accessoryImageView.image = Asset.settingsDisclosure.image - - infrastructureDetailView.titleLabel.text = Localized.Backup.Config.infrastructure.uppercased() - infrastructureDetailView.accessoryImageView.image = Asset.settingsDisclosure.image - - enabledSubtitleView.addSubview(enabledSubtitleLabel) - - stackView.axis = .vertical - stackView.addArrangedSubview(googleDriveButton) - stackView.addArrangedSubview(iCloudButton) - stackView.addArrangedSubview(dropboxButton) - stackView.addArrangedSubview(sftpButton) - stackView.addArrangedSubview(enabledSubtitleView) - stackView.addArrangedSubview(latestBackupDetailView) - stackView.addArrangedSubview(frequencyDetailView) - stackView.addArrangedSubview(infrastructureDetailView) - - addSubview(titleLabel) - addSubview(subtitleLabel) - addSubview(actionView) - addSubview(stackView) - - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-41) - } - - enabledSubtitleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(-10) - $0.left.equalToSuperview().offset(92) - $0.right.equalToSuperview().offset(-48) - $0.bottom.equalToSuperview() - } - - subtitleLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(8) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-41) - } - - actionView.snp.makeConstraints { - $0.top.equalTo(subtitleLabel.snp.bottom).offset(15) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-38) - } - - stackView.snp.makeConstraints { - $0.top.equalTo(actionView.snp.bottom).offset(28) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let actionView = BackupActionView() + + let stackView = UIStackView() + let sftpButton = BackupSwitcherButton() + let iCloudButton = BackupSwitcherButton() + let dropboxButton = BackupSwitcherButton() + let googleDriveButton = BackupSwitcherButton() + + let enabledSubtitleView = UIView() + let enabledSubtitleLabel = UILabel() + let frequencyDetailView = BackupDetailView() + let latestBackupDetailView = BackupDetailView() + let infrastructureDetailView = BackupDetailView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.neutralDark.color + titleLabel.text = Localized.Backup.Config.title + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + enabledSubtitleLabel.numberOfLines = 0 + enabledSubtitleLabel.textColor = Asset.neutralWeak.color + enabledSubtitleLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSAttributedString( + string: Localized.Backup.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = attString + + sftpButton.titleLabel.text = Localized.Backup.sftp + sftpButton.logoImageView.image = Asset.restoreSFTP.image + + iCloudButton.titleLabel.text = Localized.Backup.iCloud + iCloudButton.logoImageView.image = Asset.restoreIcloud.image + + dropboxButton.titleLabel.text = Localized.Backup.dropbox + dropboxButton.logoImageView.image = Asset.restoreDropbox.image + + googleDriveButton.titleLabel.text = Localized.Backup.googleDrive + googleDriveButton.logoImageView.image = Asset.restoreDrive.image + + latestBackupDetailView.titleLabel.text = Localized.Backup.Config.latestBackup + frequencyDetailView.accessoryImageView.image = Asset.settingsDisclosure.image + + infrastructureDetailView.titleLabel.text = Localized.Backup.Config.infrastructure.uppercased() + infrastructureDetailView.accessoryImageView.image = Asset.settingsDisclosure.image + + enabledSubtitleView.addSubview(enabledSubtitleLabel) + + stackView.axis = .vertical + stackView.addArrangedSubview(googleDriveButton) + stackView.addArrangedSubview(iCloudButton) + stackView.addArrangedSubview(dropboxButton) + stackView.addArrangedSubview(sftpButton) + stackView.addArrangedSubview(enabledSubtitleView) + stackView.addArrangedSubview(latestBackupDetailView) + stackView.addArrangedSubview(frequencyDetailView) + stackView.addArrangedSubview(infrastructureDetailView) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(actionView) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - required init?(coder: NSCoder) { nil } + enabledSubtitleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(-10) + $0.left.equalToSuperview().offset(92) + $0.right.equalToSuperview().offset(-48) + $0.bottom.equalToSuperview() + } + + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + + actionView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + } + + stackView.snp.makeConstraints { + $0.top.equalTo(actionView.snp.bottom).offset(28) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/BackupFeature/Views/BackupDetailView.swift b/Sources/BackupFeature/Views/BackupDetailView.swift index d28647330d7cd7b81d516ad7d4b506f712d94d0c..28b75bb1418c0a2e7ae4d9f16920cdea019db646 100644 --- a/Sources/BackupFeature/Views/BackupDetailView.swift +++ b/Sources/BackupFeature/Views/BackupDetailView.swift @@ -1,40 +1,41 @@ import UIKit import Shared +import AppResources final class BackupDetailView: UIControl { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let accessoryImageView = UIImageView() - - init() { - super.init(frame: .zero) - - titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) - subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) - - titleLabel.textColor = Asset.neutralWeak.color - subtitleLabel.textColor = Asset.neutralActive.color - - addSubview(titleLabel) - addSubview(subtitleLabel) - addSubview(accessoryImageView) - - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview().offset(92) - } - - subtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(4) - make.left.equalTo(titleLabel) - make.bottom.equalToSuperview().offset(-2) - } - - accessoryImageView.snp.makeConstraints { make in - make.right.equalToSuperview().offset(-48) - make.centerY.equalTo(titleLabel.snp.bottom) - } + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let accessoryImageView = UIImageView() + + init() { + super.init(frame: .zero) + + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + titleLabel.textColor = Asset.neutralWeak.color + subtitleLabel.textColor = Asset.neutralActive.color + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(accessoryImageView) + + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.left.equalToSuperview().offset(92) + } + + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(4) + make.left.equalTo(titleLabel) + make.bottom.equalToSuperview().offset(-2) + } + + accessoryImageView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-48) + make.centerY.equalTo(titleLabel.snp.bottom) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/BackupFeature/Views/BackupPassphraseView.swift b/Sources/BackupFeature/Views/BackupPassphraseView.swift index f2ff27b4d01652f72abab2482a70db9f031d5e55..ea0630520b803a717c5b8d8ed949867b74a52279 100644 --- a/Sources/BackupFeature/Views/BackupPassphraseView.swift +++ b/Sources/BackupFeature/Views/BackupPassphraseView.swift @@ -1,80 +1,81 @@ import UIKit import Shared import InputField +import AppResources final class BackupPassphraseView: UIView { - let titleLabel = UILabel() - let stackView = UIStackView() - let inputField = InputField() - let subtitleLabel = UILabel() - let cancelButton = CapsuleButton() - let continueButton = CapsuleButton() + let titleLabel = UILabel() + let stackView = UIStackView() + let inputField = InputField() + let subtitleLabel = UILabel() + let cancelButton = CapsuleButton() + let continueButton = CapsuleButton() - init() { - super.init(frame: .zero) - layer.cornerRadius = 40 - backgroundColor = Asset.neutralWhite.color - layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + init() { + super.init(frame: .zero) + layer.cornerRadius = 40 + backgroundColor = Asset.neutralWhite.color + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - setupInput() - setupLabels() - setupButtons() - setupStackView() - } + setupInput() + setupLabels() + setupButtons() + setupStackView() + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - private func setupInput() { - inputField.setup( - style: .regular, - title: Localized.Backup.Passphrase.Input.title, - placeholder: Localized.Backup.Passphrase.Input.placeholder, - rightView: .toggleSecureEntry, - subtitleColor: Asset.neutralDisabled.color, - allowsEmptySpace: false, - autocapitalization: .none, - contentType: .newPassword - ) - } + private func setupInput() { + inputField.setup( + style: .regular, + title: Localized.Backup.Passphrase.Input.title, + placeholder: Localized.Backup.Passphrase.Input.placeholder, + rightView: .toggleSecureEntry, + subtitleColor: Asset.neutralDisabled.color, + allowsEmptySpace: false, + autocapitalization: .none, + contentType: .newPassword + ) + } - private func setupLabels() { - titleLabel.textAlignment = .left - titleLabel.text = Localized.Backup.Passphrase.title - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + private func setupLabels() { + titleLabel.textAlignment = .left + titleLabel.text = Localized.Backup.Passphrase.title + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) - subtitleLabel.numberOfLines = 0 - subtitleLabel.textAlignment = .left - subtitleLabel.textColor = Asset.neutralActive.color - subtitleLabel.text = Localized.Backup.Passphrase.subtitle - subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) - } + subtitleLabel.numberOfLines = 0 + subtitleLabel.textAlignment = .left + subtitleLabel.textColor = Asset.neutralActive.color + subtitleLabel.text = Localized.Backup.Passphrase.subtitle + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + } - private func setupButtons() { - cancelButton.setStyle(.seeThrough) - cancelButton.setTitle(Localized.Backup.Passphrase.cancel, for: .normal) + private func setupButtons() { + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Backup.Passphrase.cancel, for: .normal) - continueButton.isEnabled = false - continueButton.setStyle(.brandColored) - continueButton.setTitle(Localized.Backup.Passphrase.continue, for: .normal) - } + continueButton.isEnabled = false + continueButton.setStyle(.brandColored) + continueButton.setTitle(Localized.Backup.Passphrase.continue, for: .normal) + } - private func setupStackView() { - stackView.spacing = 20 - stackView.axis = .vertical - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(subtitleLabel) - stackView.addArrangedSubview(inputField) - stackView.addArrangedSubview(continueButton) - stackView.addArrangedSubview(cancelButton) + private func setupStackView() { + stackView.spacing = 20 + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + stackView.addArrangedSubview(inputField) + stackView.addArrangedSubview(continueButton) + stackView.addArrangedSubview(cancelButton) - addSubview(stackView) + addSubview(stackView) - stackView.snp.makeConstraints { - $0.top.equalToSuperview().offset(60) - $0.left.equalToSuperview().offset(50) - $0.right.equalToSuperview().offset(-50) - $0.bottom.equalToSuperview().offset(-70) - } + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(60) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + $0.bottom.equalToSuperview().offset(-70) } + } } diff --git a/Sources/BackupFeature/Views/BackupSetupView.swift b/Sources/BackupFeature/Views/BackupSetupView.swift index 3e19d50034f4d03ba62d3e4d038d82d24196af89..30ae722dd7b2c630a861cc50dd9f808311446f43 100644 --- a/Sources/BackupFeature/Views/BackupSetupView.swift +++ b/Sources/BackupFeature/Views/BackupSetupView.swift @@ -1,99 +1,100 @@ import UIKit import Shared +import AppResources final class BackupSetupView: UIView { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - - let stackView = UIStackView() - let sftpButton = BackupSwitcherButton() - let iCloudButton = BackupSwitcherButton() - let dropboxButton = BackupSwitcherButton() - let googleDriveButton = BackupSwitcherButton() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - let title = Localized.Backup.Setup.title - - let attString = NSMutableAttributedString(string: title) - let firstParagraph = NSMutableParagraphStyle() - firstParagraph.alignment = .left - firstParagraph.lineHeightMultiple = 1 - - attString.addAttribute(.paragraphStyle, value: firstParagraph) - attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) - attString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 34.0) as Any) - - attString.addAttributes(attributes: [ - .font: Fonts.Mulish.bold.font(size: 34.0) as Any, - .foregroundColor: Asset.brandPrimary.color - ], betweenCharacters: "#") - - titleLabel.numberOfLines = 0 - titleLabel.attributedText = attString - - let secondParagraph = NSMutableParagraphStyle() - secondParagraph.alignment = .left - secondParagraph.lineHeightMultiple = 1.15 - - let secondAttString = NSAttributedString( - string: Localized.Backup.subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: secondParagraph - ]) - - subtitleLabel.numberOfLines = 0 - subtitleLabel.attributedText = secondAttString - - iCloudButton.titleLabel.text = Localized.Backup.iCloud - iCloudButton.logoImageView.image = Asset.restoreIcloud.image - iCloudButton.showChevron() - - dropboxButton.titleLabel.text = Localized.Backup.dropbox - dropboxButton.logoImageView.image = Asset.restoreDropbox.image - dropboxButton.showChevron() - - googleDriveButton.titleLabel.text = Localized.Backup.googleDrive - googleDriveButton.logoImageView.image = Asset.restoreDrive.image - googleDriveButton.showChevron() - - sftpButton.titleLabel.text = Localized.Backup.sftp - sftpButton.logoImageView.image = Asset.restoreSFTP.image - sftpButton.showChevron() - - stackView.axis = .vertical - stackView.addArrangedSubview(googleDriveButton) - stackView.addArrangedSubview(iCloudButton) - stackView.addArrangedSubview(dropboxButton) - stackView.addArrangedSubview(sftpButton) - - addSubview(titleLabel) - addSubview(subtitleLabel) - addSubview(stackView) - - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-41) - } - - subtitleLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(8) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-41) - } - - stackView.snp.makeConstraints { - $0.top.equalTo(subtitleLabel.snp.bottom).offset(28) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let subtitleLabel = UILabel() + + let stackView = UIStackView() + let sftpButton = BackupSwitcherButton() + let iCloudButton = BackupSwitcherButton() + let dropboxButton = BackupSwitcherButton() + let googleDriveButton = BackupSwitcherButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + let title = Localized.Backup.Setup.title + + let attString = NSMutableAttributedString(string: title) + let firstParagraph = NSMutableParagraphStyle() + firstParagraph.alignment = .left + firstParagraph.lineHeightMultiple = 1 + + attString.addAttribute(.paragraphStyle, value: firstParagraph) + attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) + attString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 34.0) as Any) + + attString.addAttributes(attributes: [ + .font: Fonts.Mulish.bold.font(size: 34.0) as Any, + .foregroundColor: Asset.brandPrimary.color + ], betweenCharacters: "#") + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = attString + + let secondParagraph = NSMutableParagraphStyle() + secondParagraph.alignment = .left + secondParagraph.lineHeightMultiple = 1.15 + + let secondAttString = NSAttributedString( + string: Localized.Backup.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: secondParagraph + ]) + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = secondAttString + + iCloudButton.titleLabel.text = Localized.Backup.iCloud + iCloudButton.logoImageView.image = Asset.restoreIcloud.image + iCloudButton.showChevron() + + dropboxButton.titleLabel.text = Localized.Backup.dropbox + dropboxButton.logoImageView.image = Asset.restoreDropbox.image + dropboxButton.showChevron() + + googleDriveButton.titleLabel.text = Localized.Backup.googleDrive + googleDriveButton.logoImageView.image = Asset.restoreDrive.image + googleDriveButton.showChevron() + + sftpButton.titleLabel.text = Localized.Backup.sftp + sftpButton.logoImageView.image = Asset.restoreSFTP.image + sftpButton.showChevron() + + stackView.axis = .vertical + stackView.addArrangedSubview(googleDriveButton) + stackView.addArrangedSubview(iCloudButton) + stackView.addArrangedSubview(dropboxButton) + stackView.addArrangedSubview(sftpButton) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - required init?(coder: NSCoder) { nil } + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + + stackView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(28) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/BackupFeature/Views/BackupSwitcherButton.swift b/Sources/BackupFeature/Views/BackupSwitcherButton.swift index 2c115cdc9b723cc7c2ece188e17ef5e3a7ea7533..dfd3a32ea1b66967f9df5da2ae4b62553beab12b 100644 --- a/Sources/BackupFeature/Views/BackupSwitcherButton.swift +++ b/Sources/BackupFeature/Views/BackupSwitcherButton.swift @@ -1,69 +1,70 @@ import UIKit import Shared +import AppResources final class BackupSwitcherButton: UIControl { - let titleLabel = UILabel() - let separatorView = UIView() - let switcherView = UISwitch() - let logoImageView = UIImageView() - let chevronImageView = UIImageView() + let titleLabel = UILabel() + let separatorView = UIView() + let switcherView = UISwitch() + let logoImageView = UIImageView() + let chevronImageView = UIImageView() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) - switcherView.onTintColor = Asset.brandLight.color - chevronImageView.image = Asset.settingsDisclosure.image - separatorView.backgroundColor = Asset.neutralLine.color + switcherView.onTintColor = Asset.brandLight.color + chevronImageView.image = Asset.settingsDisclosure.image + separatorView.backgroundColor = Asset.neutralLine.color - addSubview(separatorView) - addSubview(logoImageView) - addSubview(titleLabel) - addSubview(switcherView) - addSubview(chevronImageView) + addSubview(separatorView) + addSubview(logoImageView) + addSubview(titleLabel) + addSubview(switcherView) + addSubview(chevronImageView) - logoImageView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview().offset(36) - make.bottom.equalToSuperview().offset(-20) - } + logoImageView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.left.equalToSuperview().offset(36) + make.bottom.equalToSuperview().offset(-20) + } - titleLabel.snp.makeConstraints { make in - make.left.equalTo(logoImageView.snp.right).offset(15) - make.centerY.equalTo(logoImageView) - } + titleLabel.snp.makeConstraints { make in + make.left.equalTo(logoImageView.snp.right).offset(15) + make.centerY.equalTo(logoImageView) + } - chevronImageView.snp.makeConstraints { make in - make.centerY.equalTo(logoImageView) - make.right.equalToSuperview().offset(-48) - } + chevronImageView.snp.makeConstraints { make in + make.centerY.equalTo(logoImageView) + make.right.equalToSuperview().offset(-48) + } - switcherView.snp.makeConstraints { make in - make.right.equalToSuperview().offset(-25) - make.centerY.equalTo(logoImageView) - } + switcherView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-25) + make.centerY.equalTo(logoImageView) + } - separatorView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.height.equalTo(1) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } + separatorView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.height.equalTo(1) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func showSwitcher(enabled: Bool) { - switcherView.isOn = enabled - switcherView.isHidden = false - chevronImageView.isHidden = true - } + func showSwitcher(enabled: Bool) { + switcherView.isOn = enabled + switcherView.isHidden = false + chevronImageView.isHidden = true + } - func showChevron() { - switcherView.isOn = false - switcherView.isHidden = true - chevronImageView.isHidden = false - } + func showChevron() { + switcherView.isOn = false + switcherView.isHidden = true + chevronImageView.isHidden = false + } } diff --git a/Sources/BackupFeature/Views/RestoreSFTPView.swift b/Sources/BackupFeature/Views/RestoreSFTPView.swift index dfb4d73a12c784ac51c25b27702ac2d592744f01..3d12ccbcf180961a0bdc989404b2571aa944b841 100644 --- a/Sources/BackupFeature/Views/RestoreSFTPView.swift +++ b/Sources/BackupFeature/Views/RestoreSFTPView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class BackupSFTPView: UIView { let titleLabel = UILabel() diff --git a/Sources/CollectionView/CellFactory.swift b/Sources/ChatFeature/CellFactory.swift similarity index 100% rename from Sources/CollectionView/CellFactory.swift rename to Sources/ChatFeature/CellFactory.swift diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift index 97a65ca8cdfb906866a0bd90d8ffff7c3c66bdd4..7a5090ef483860e517d5f128dfcc8e758d86d4b8 100644 --- a/Sources/ChatFeature/Controllers/GroupChatController.swift +++ b/Sources/ChatFeature/Controllers/GroupChatController.swift @@ -196,7 +196,7 @@ public final class GroupChatController: UIViewController { lineHeightMultiple: 1.35, spacingAfter: 25 ), button - ])) + ], isDismissable: false, from: self)) case .webview(let urlString): navigator.perform(PresentWebsite(url: URL(string: urlString)!)) @@ -260,7 +260,7 @@ public final class GroupChatController: UIViewController { spacing: 20.0, views: [reportButton, cancelButton] ) - ])) + ], isDismissable: true, from: self)) }.store(in: &cancellables) viewModel diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index c4a890898d8ffe657812c7af427ceaf3708df6a2..a8052cb16ae611a98ff44c37c29102e1fc442701 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -213,49 +213,49 @@ public final class SingleChatController: UIViewController { viewModel.navigation .receive(on: DispatchQueue.main) .removeDuplicates() - .sink { [unowned self] in - switch $0 { - case .library: - navigator.perform(PresentPhotoLibrary()) - case .camera: - navigator.perform(PresentCamera()) - case .cameraPermission: - navigator.perform(PresentPermissionRequest(type: .camera)) - case .microphonePermission: - navigator.perform(PresentPermissionRequest(type: .microphone)) - case .libraryPermission: - navigator.perform(PresentPermissionRequest(type: .library)) - case .webview(let urlString): - navigator.perform(PresentWebsite(url: URL(string: urlString)!)) - case .waitingRound: - let button = DrawerCapsuleButton(model: .init( - title: Localized.Chat.RoundDrawer.action, - style: .brandColored - )) - - button - .action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - navigator.perform(DismissModal(from: self)) { [weak self] in - guard let self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - navigator.perform(PresentDrawer(items: [ - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Chat.RoundDrawer.title, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ), - button - ])) - case .none: - break - } + .sink { [unowned self] _ in +// switch $0 { +// case .library: +// navigator.perform(PresentPhotoLibrary()) +// case .camera: +// navigator.perform(PresentCamera()) +// case .cameraPermission: +// navigator.perform(PresentPermissionRequest(type: .camera)) +// case .microphonePermission: +// navigator.perform(PresentPermissionRequest(type: .microphone)) +// case .libraryPermission: +// navigator.perform(PresentPermissionRequest(type: .library)) +// case .webview(let urlString): +// navigator.perform(PresentWebsite(url: URL(string: urlString)!)) +// case .waitingRound: +// let button = DrawerCapsuleButton(model: .init( +// title: Localized.Chat.RoundDrawer.action, +// style: .brandColored +// )) +// +// button +// .action +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in +// navigator.perform(DismissModal(from: self)) { [weak self] in +// guard let self else { return } +// self.drawerCancellables.removeAll() +// } +// }.store(in: &drawerCancellables) +// +// navigator.perform(PresentDrawer(items: [ +// DrawerText( +// font: Fonts.Mulish.semiBold.font(size: 14.0), +// text: Localized.Chat.RoundDrawer.title, +// color: Asset.neutralWeak.color, +// lineHeightMultiple: 1.35, +// spacingAfter: 25 +// ), +// button +// ])) +// case .none: +// break +// } viewModel.didNavigateSomewhere() }.store(in: &cancellables) @@ -270,7 +270,10 @@ public final class SingleChatController: UIViewController { case .clear: presentDeleteAllDrawer() case .details: - navigator.perform(PresentContact(contact: viewModel.contact)) + navigator.perform(PresentContact( + contact: viewModel.contact, + on: navigationController! + )) case .report: presentReportDrawer() } @@ -444,7 +447,7 @@ public final class SingleChatController: UIViewController { spacing: 20.0, views: [reportButton, cancelButton] ) - ])) + ], isDismissable: true, from: self)) } private func presentDeleteAllDrawer() { @@ -497,7 +500,7 @@ public final class SingleChatController: UIViewController { spacing: 20.0, views: [clearButton, cancelButton] ) - ])) + ], isDismissable: true, from: self)) } private func previewItemAt(_ indexPath: IndexPath) { @@ -516,7 +519,10 @@ public final class SingleChatController: UIViewController { } @objc private func didTapInfo() { - navigator.perform(PresentContact(contact: viewModel.contact)) + navigator.perform(PresentContact( + contact: viewModel.contact, + on: navigationController! + )) } } diff --git a/Sources/CollectionView/ViewConfigurator.swift b/Sources/ChatFeature/ViewConfigurator.swift similarity index 100% rename from Sources/CollectionView/ViewConfigurator.swift rename to Sources/ChatFeature/ViewConfigurator.swift diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 6f0b3f96767fe92d97cca76bc472ffafb4f665f7..adc8743e058ea95d384a5a60bb522f2312f139c2 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -15,7 +15,6 @@ import XXMessengerClient import struct XXModels.Message import struct XXModels.FileTransfer -import NetworkMonitor enum SingleChatNavigationRoutes: Equatable { case none diff --git a/Sources/ChatInputFeature/ActionButton.swift b/Sources/ChatInputFeature/ActionButton.swift index 492ba1657c56ffb68bafb5c85b4f0cc94d87c4e3..8ffd5b84b508afe79f71203de25bef1bb9ce8e61 100644 --- a/Sources/ChatInputFeature/ActionButton.swift +++ b/Sources/ChatInputFeature/ActionButton.swift @@ -1,51 +1,48 @@ import UIKit import Shared +import AppResources final class ActionButton: UIControl { + let titleLabel = UILabel() + let imageView = UIImageView() + let imageBackgroundView = UIView() - let titleLabel = UILabel() - let imageView = UIImageView() - let imageBackgroundView = UIView() + init() { + super.init(frame: .zero) - init() { - super.init(frame: .zero) - setup() - } + imageBackgroundView.layer.cornerRadius = 4 + titleLabel.textColor = Asset.neutralDark.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 10.0) + imageBackgroundView.backgroundColor = Asset.neutralSecondary.color + + addSubview(titleLabel) + addSubview(imageBackgroundView) + imageBackgroundView.addSubview(imageView) + + imageView.isUserInteractionEnabled = false + imageBackgroundView.isUserInteractionEnabled = false - required init?(coder: NSCoder) { nil } + imageView.snp.makeConstraints { $0.center.equalToSuperview() } - func setup(title: String, image: UIImage) { - titleLabel.text = title - imageView.image = image + imageBackgroundView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.width.equalTo(imageBackgroundView.snp.height) } - private func setup() { - imageBackgroundView.layer.cornerRadius = 4 - titleLabel.textColor = Asset.neutralDark.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 10.0) - imageBackgroundView.backgroundColor = Asset.neutralSecondary.color - - addSubview(titleLabel) - addSubview(imageBackgroundView) - imageBackgroundView.addSubview(imageView) - - imageView.isUserInteractionEnabled = false - imageBackgroundView.isUserInteractionEnabled = false - - imageView.snp.makeConstraints { $0.center.equalToSuperview() } - - imageBackgroundView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.width.equalTo(imageBackgroundView.snp.height) - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(imageBackgroundView.snp.bottom).offset(4) - make.centerX.equalToSuperview() - make.left.greaterThanOrEqualToSuperview() - make.bottom.equalToSuperview() - } + titleLabel.snp.makeConstraints { + $0.top.equalTo(imageBackgroundView.snp.bottom).offset(4) + $0.centerX.equalToSuperview() + $0.left.greaterThanOrEqualToSuperview() + $0.bottom.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + func setup(title: String, image: UIImage) { + titleLabel.text = title + imageView.image = image + } } diff --git a/Sources/ChatInputFeature/ActionsView.swift b/Sources/ChatInputFeature/ActionsView.swift index f6cd9123754a31e7f70512a507cb31a965fe10bd..0668f7b94162a9d31c2dd818c892886a186ef79a 100644 --- a/Sources/ChatInputFeature/ActionsView.swift +++ b/Sources/ChatInputFeature/ActionsView.swift @@ -1,43 +1,39 @@ import UIKit import Shared +import AppResources final class ActionsView: UIView { - - let stack = UIStackView() - let cameraButton = ActionButton() - let libraryButton = ActionButton() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - private func setup() { - cameraButton.setup( - title: Localized.Chat.Actions.camera, - image: Asset.chatInputActionCamera.image - ) - - libraryButton.setup( - title: Localized.Chat.Actions.gallery, - image: Asset.chatInputActionGallery.image - ) - - stack.spacing = 33 - stack.axis = .horizontal - stack.distribution = .fillEqually - stack.addArrangedSubview(cameraButton) - stack.addArrangedSubview(libraryButton) - - addSubview(stack) - stack.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: topAnchor), - stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), - stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), - stack.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } + let stack = UIStackView() + let cameraButton = ActionButton() + let libraryButton = ActionButton() + + init() { + super.init(frame: .zero) + cameraButton.setup( + title: Localized.Chat.Actions.camera, + image: Asset.chatInputActionCamera.image + ) + + libraryButton.setup( + title: Localized.Chat.Actions.gallery, + image: Asset.chatInputActionGallery.image + ) + + stack.spacing = 33 + stack.axis = .horizontal + stack.distribution = .fillEqually + stack.addArrangedSubview(cameraButton) + stack.addArrangedSubview(libraryButton) + + addSubview(stack) + stack.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: topAnchor), + stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), + stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), + stack.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/ChatInputFeature/AudioView.swift b/Sources/ChatInputFeature/AudioView.swift index cd43082ac940a9038edd6e8506237b5be141715a..b7fb0d354800cfd719d28df6db56e2ad0cc7b0b4 100644 --- a/Sources/ChatInputFeature/AudioView.swift +++ b/Sources/ChatInputFeature/AudioView.swift @@ -1,56 +1,53 @@ import UIKit import Shared +import AppResources final class AudioView: UIView { - - let stack = UIStackView() - let timeLabel = UILabel() - let playButton = UIButton() - let sendButton = UIButton() - let cancelButton = UIButton() - let stopPlaybackButton = UIButton() - let stopRecordingButton = UIButton() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - private func setup() { - timeLabel.textAlignment = .center - timeLabel.textColor = Asset.neutralDark.color - timeLabel.font = Fonts.Mulish.semiBold.font(size: 13) - - sendButton.setImage(Asset.chatSend.image, for: .normal) - playButton.setImage(Asset.chatInputVoicePlay.image, for: .normal) - cancelButton.setImage(Asset.chatInputActionClose.image, for: .normal) - stopPlaybackButton.setImage(Asset.chatInputVoicePause.image, for: .normal) - stopRecordingButton.setImage(Asset.chatInputVoiceStop.image, for: .normal) - - stack.spacing = 8 - stack.axis = .horizontal - stack.addArrangedSubview(cancelButton) - stack.addArrangedSubview(playButton) - stack.addArrangedSubview(stopPlaybackButton) - stack.addArrangedSubview(timeLabel) - stack.addArrangedSubview(stopRecordingButton) - stack.addArrangedSubview(sendButton) - - cancelButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - playButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - stopPlaybackButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - sendButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - timeLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) - - addSubview(stack) - stack.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: topAnchor), - stack.leadingAnchor.constraint(equalTo: leadingAnchor), - stack.trailingAnchor.constraint(equalTo: trailingAnchor), - stack.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } + let stack = UIStackView() + let timeLabel = UILabel() + let playButton = UIButton() + let sendButton = UIButton() + let cancelButton = UIButton() + let stopPlaybackButton = UIButton() + let stopRecordingButton = UIButton() + + init() { + super.init(frame: .zero) + + timeLabel.textAlignment = .center + timeLabel.textColor = Asset.neutralDark.color + timeLabel.font = Fonts.Mulish.semiBold.font(size: 13) + + sendButton.setImage(Asset.chatSend.image, for: .normal) + playButton.setImage(Asset.chatInputVoicePlay.image, for: .normal) + cancelButton.setImage(Asset.chatInputActionClose.image, for: .normal) + stopPlaybackButton.setImage(Asset.chatInputVoicePause.image, for: .normal) + stopRecordingButton.setImage(Asset.chatInputVoiceStop.image, for: .normal) + + stack.spacing = 8 + stack.axis = .horizontal + stack.addArrangedSubview(cancelButton) + stack.addArrangedSubview(playButton) + stack.addArrangedSubview(stopPlaybackButton) + stack.addArrangedSubview(timeLabel) + stack.addArrangedSubview(stopRecordingButton) + stack.addArrangedSubview(sendButton) + + cancelButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + playButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + stopPlaybackButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + sendButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + timeLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + addSubview(stack) + stack.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: topAnchor), + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/ChatInputFeature/ChatInputReply.swift b/Sources/ChatInputFeature/ChatInputReply.swift index d5353e34ab95481fc899189efa84c6e7ad4ca86f..ff3d9f958cc5f133d4d5189c9d57305b16ecb7fd 100644 --- a/Sources/ChatInputFeature/ChatInputReply.swift +++ b/Sources/ChatInputFeature/ChatInputReply.swift @@ -1,78 +1,71 @@ import UIKit import Shared +import AppResources final class ChatInputReply: UIView { + let nameLabel = UILabel() + let titleLabel = UILabel() + let abortButton = UIButton() + let messageLabel = UILabel() - let nameLabel = UILabel() - let titleLabel = UILabel() - let abortButton = UIButton() - let messageLabel = UILabel() + init() { + super.init(frame: .zero) - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - func setup(message: String?, sender: String?) { - guard let message = message else { - isHidden = true - return - } - - isHidden = false - messageLabel.text = message - nameLabel.text = sender ?? "You" - } + titleLabel.text = "Replying to" + messageLabel.numberOfLines = 2 + abortButton.setImage(Asset.replyAbort.image, for: .normal) - private func setup() { - titleLabel.text = "Replying to" - messageLabel.numberOfLines = 2 - abortButton.setImage(Asset.replyAbort.image, for: .normal) + nameLabel.font = Fonts.Mulish.bold.font(size: 11.0) + titleLabel.font = Fonts.Mulish.regular.font(size: 12.0) + messageLabel.font = Fonts.Mulish.regular.font(size: 11.0) - nameLabel.font = Fonts.Mulish.bold.font(size: 11.0) - titleLabel.font = Fonts.Mulish.regular.font(size: 12.0) - messageLabel.font = Fonts.Mulish.regular.font(size: 11.0) + nameLabel.textColor = Asset.neutralBody.color + titleLabel.textColor = Asset.neutralBody.color + messageLabel.textColor = Asset.neutralBody.color - nameLabel.textColor = Asset.neutralBody.color - titleLabel.textColor = Asset.neutralBody.color - messageLabel.textColor = Asset.neutralBody.color + addSubview(nameLabel) + addSubview(titleLabel) + addSubview(abortButton) + addSubview(messageLabel) - addSubview(nameLabel) - addSubview(titleLabel) - addSubview(abortButton) - addSubview(messageLabel) + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.left.equalToSuperview().offset(19) + $0.right.lessThanOrEqualToSuperview() + $0.height.equalTo(15) + } - setupConstraints() + nameLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(11) + $0.left.equalTo(titleLabel) + $0.right.lessThanOrEqualToSuperview().offset(-30) + $0.height.equalTo(10) } - private func setupConstraints() { - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(10) - make.left.equalToSuperview().offset(19) - make.right.lessThanOrEqualToSuperview() - make.height.equalTo(15) - } + messageLabel.snp.makeConstraints { + $0.left.equalToSuperview().offset(28) + $0.top.equalTo(nameLabel.snp.bottom).offset(4) + $0.right.equalToSuperview().offset(-41) + $0.bottom.equalToSuperview().offset(-10) + $0.height.equalTo(30) + } - nameLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(11) - make.left.equalTo(titleLabel) - make.right.lessThanOrEqualToSuperview().offset(-30) - make.height.equalTo(10) - } + abortButton.snp.makeConstraints { + $0.top.equalToSuperview().offset(12) + $0.right.equalToSuperview().offset(-12) + } + } - messageLabel.snp.makeConstraints { make in - make.left.equalToSuperview().offset(28) - make.top.equalTo(nameLabel.snp.bottom).offset(4) - make.right.equalToSuperview().offset(-41) - make.bottom.equalToSuperview().offset(-10) - make.height.equalTo(30) - } + required init?(coder: NSCoder) { nil } - abortButton.snp.makeConstraints { make in - make.top.equalToSuperview().offset(12) - make.right.equalToSuperview().offset(-12) - } + func setup(message: String?, sender: String?) { + guard let message = message else { + isHidden = true + return } + + isHidden = false + messageLabel.text = message + nameLabel.text = sender ?? "You" + } } diff --git a/Sources/ChatInputFeature/ChatInputView.swift b/Sources/ChatInputFeature/ChatInputView.swift index 31e5a8ae93cb5f49b64765280130dd89efe87bd1..eef904e58e8f57277a57a3789bd3db15f7ac7adc 100644 --- a/Sources/ChatInputFeature/ChatInputView.swift +++ b/Sources/ChatInputFeature/ChatInputView.swift @@ -3,228 +3,228 @@ import Shared import Combine import CasePaths import Voxophone +import AppResources import ComposableArchitecture public final class ChatInputView: UIToolbar { - - public init(store: Store<ChatInputState, ChatInputAction>) { - self.store = store - self.viewStore = ViewStore(store) - super.init(frame: .zero) - - setup() - observeStore() - setupUIActions() - viewStore.send(.setup) - } - - required init?(coder: NSCoder) { nil } - - deinit { - viewStore.send(.destroy) - } - - public func setMaxHeight(_ function: @escaping () -> CGFloat) { - text.maxHeight = function - } - - public func setupReply(message: String, sender: String) { - viewStore.send(.text(.didTriggerReply(message, sender))) - } - - let store: Store<ChatInputState, ChatInputAction> - let viewStore: ViewStore<ChatInputState, ChatInputAction> - private var cancellables: Set<AnyCancellable> = [] - - let stack = UIStackView() - let text = TextInputView() - let audio = AudioView() - let actions = ActionsView() - - private func setup() { - isTranslucent = false - translatesAutoresizingMaskIntoConstraints = false - barTintColor = Asset.neutralWhite.color - - stack.axis = .vertical - stack.spacing = 8 - stack.addArrangedSubview(text) - stack.addArrangedSubview(audio) - stack.addArrangedSubview(actions) - - addSubview(stack) - stack.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 8), - stack.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 8), - stack.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -8), - stack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8), - ]) - } - - private func observeStore() { - viewStore.publisher - .map(\.isPresentingActions) - .combineLatest(viewStore.publisher.map(\.canAddAttachments)) - .sink { [unowned self] isPresentingActions, canAddAttachments in - if canAddAttachments { - text.showActionsButton.isHidden = isPresentingActions - text.hideActionsButton.isHidden = !isPresentingActions - actions.isHidden = !isPresentingActions - } else { - text.showActionsButton.isHidden = true - text.hideActionsButton.isHidden = true - actions.isHidden = true - } - } - .store(in: &cancellables) - - viewStore.publisher - .map(\.reply) - .sink { [unowned self] reply in - guard let reply = reply else { - text.replyView.isHidden = true - return - } - - text.replyView.isHidden = false - text.replyView.messageLabel.text = reply.text - text.replyView.nameLabel.text = reply.name - }.store(in: &cancellables) - - viewStore.publisher - .map(\.audio) - .map { $0 != nil } - .sink { [unowned self] in - text.isHidden = $0 - audio.isHidden = !$0 - } - .store(in: &cancellables) - - viewStore.publisher - .map(\.text.isEmpty) - .combineLatest(viewStore.publisher.map(\.canAddAttachments)) - .sink { [unowned self] textIsEmpty, canAddAttachments in - if canAddAttachments { - text.sendButton.isHidden = textIsEmpty - text.audioButton.isHidden = !textIsEmpty - } else { - text.sendButton.isHidden = false - text.audioButton.isHidden = true - } - - text.sendButton.isEnabled = !textIsEmpty - text.placeholderView.isHidden = !textIsEmpty - } - .store(in: &cancellables) - - viewStore.publisher - .map(\.text) - .sink { [unowned self] in - if text.textView.markedTextRange == nil { - let range = text.textView.selectedTextRange - text.textView.text = $0 - - if let range = range { - text.textView.selectedTextRange = range - } - } else if $0 == "" { - text.textView.text = $0 - } - - text.updateHeight() - }.store(in: &cancellables) - - let timeFormatter = DateComponentsFormatter() - timeFormatter.unitsStyle = .positional - timeFormatter.allowedUnits = [.minute, .second] - timeFormatter.zeroFormattingBehavior = .pad - - viewStore.publisher - .map(\.audio) - .sink { [unowned self] in - switch $0 { - case let .idle(_, duration): - audio.playButton.isHidden = false - audio.stopPlaybackButton.isHidden = true - audio.stopRecordingButton.isHidden = true - audio.sendButton.isHidden = false - audio.timeLabel.text = timeFormatter.string(from: duration) - - case let .recording(_, time): - audio.playButton.isHidden = true - audio.stopPlaybackButton.isHidden = true - audio.stopRecordingButton.isHidden = false - audio.sendButton.isHidden = true - audio.timeLabel.text = timeFormatter.string(from: time) - - case let .playing(_, _, time): - audio.playButton.isHidden = true - audio.stopPlaybackButton.isHidden = false - audio.stopRecordingButton.isHidden = true - audio.sendButton.isHidden = false - audio.timeLabel.text = timeFormatter.string(from: time) - - case .none: - audio.playButton.isHidden = true - audio.stopPlaybackButton.isHidden = true - audio.stopRecordingButton.isHidden = true - audio.sendButton.isHidden = true - audio.timeLabel.text = "" - } - } - .store(in: &cancellables) - } - - private func setupUIActions() { - text.textDidChange = { [unowned self] text in viewStore.send(.text(.didUpdate(text))) } - - text.replyView.abortButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.text(.didTapAbortReply)) } - .store(in: &cancellables) - - text.showActionsButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.text(.didTapShowActions)) } - .store(in: &cancellables) - - text.hideActionsButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.text(.didTapHideActions)) } - .store(in: &cancellables) - - text.sendButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.text(.didTapSend)) } - .store(in: &cancellables) - - text.audioButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.text(.didTapAudio)) } - .store(in: &cancellables) - - audio.cancelButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.audio(.didTapCancel)) } - .store(in: &cancellables) - - audio.playButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.audio(.didTapPlay)) } - .store(in: &cancellables) - - audio.stopPlaybackButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.audio(.didTapStopPlayback)) } - .store(in: &cancellables) - - audio.stopRecordingButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.audio(.didTapStopRecording)) } - .store(in: &cancellables) - - audio.sendButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.audio(.didTapSend)) } - .store(in: &cancellables) - - actions.libraryButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.actions(.didTapLibrary)) } - .store(in: &cancellables) - - actions.cameraButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewStore.send(.actions(.didTapCamera)) } - .store(in: &cancellables) - } + public init(store: Store<ChatInputState, ChatInputAction>) { + self.store = store + self.viewStore = ViewStore(store) + super.init(frame: .zero) + + setup() + observeStore() + setupUIActions() + viewStore.send(.setup) + } + + required init?(coder: NSCoder) { nil } + + deinit { + viewStore.send(.destroy) + } + + public func setMaxHeight(_ function: @escaping () -> CGFloat) { + text.maxHeight = function + } + + public func setupReply(message: String, sender: String) { + viewStore.send(.text(.didTriggerReply(message, sender))) + } + + let store: Store<ChatInputState, ChatInputAction> + let viewStore: ViewStore<ChatInputState, ChatInputAction> + private var cancellables: Set<AnyCancellable> = [] + + let stack = UIStackView() + let text = TextInputView() + let audio = AudioView() + let actions = ActionsView() + + private func setup() { + isTranslucent = false + translatesAutoresizingMaskIntoConstraints = false + barTintColor = Asset.neutralWhite.color + + stack.axis = .vertical + stack.spacing = 8 + stack.addArrangedSubview(text) + stack.addArrangedSubview(audio) + stack.addArrangedSubview(actions) + + addSubview(stack) + stack.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 8), + stack.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 8), + stack.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -8), + stack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8), + ]) + } + + private func observeStore() { + viewStore.publisher + .map(\.isPresentingActions) + .combineLatest(viewStore.publisher.map(\.canAddAttachments)) + .sink { [unowned self] isPresentingActions, canAddAttachments in + if canAddAttachments { + text.showActionsButton.isHidden = isPresentingActions + text.hideActionsButton.isHidden = !isPresentingActions + actions.isHidden = !isPresentingActions + } else { + text.showActionsButton.isHidden = true + text.hideActionsButton.isHidden = true + actions.isHidden = true + } + } + .store(in: &cancellables) + + viewStore.publisher + .map(\.reply) + .sink { [unowned self] reply in + guard let reply = reply else { + text.replyView.isHidden = true + return + } + + text.replyView.isHidden = false + text.replyView.messageLabel.text = reply.text + text.replyView.nameLabel.text = reply.name + }.store(in: &cancellables) + + viewStore.publisher + .map(\.audio) + .map { $0 != nil } + .sink { [unowned self] in + text.isHidden = $0 + audio.isHidden = !$0 + } + .store(in: &cancellables) + + viewStore.publisher + .map(\.text.isEmpty) + .combineLatest(viewStore.publisher.map(\.canAddAttachments)) + .sink { [unowned self] textIsEmpty, canAddAttachments in + if canAddAttachments { + text.sendButton.isHidden = textIsEmpty + text.audioButton.isHidden = !textIsEmpty + } else { + text.sendButton.isHidden = false + text.audioButton.isHidden = true + } + + text.sendButton.isEnabled = !textIsEmpty + text.placeholderView.isHidden = !textIsEmpty + } + .store(in: &cancellables) + + viewStore.publisher + .map(\.text) + .sink { [unowned self] in + if text.textView.markedTextRange == nil { + let range = text.textView.selectedTextRange + text.textView.text = $0 + + if let range = range { + text.textView.selectedTextRange = range + } + } else if $0 == "" { + text.textView.text = $0 + } + + text.updateHeight() + }.store(in: &cancellables) + + let timeFormatter = DateComponentsFormatter() + timeFormatter.unitsStyle = .positional + timeFormatter.allowedUnits = [.minute, .second] + timeFormatter.zeroFormattingBehavior = .pad + + viewStore.publisher + .map(\.audio) + .sink { [unowned self] in + switch $0 { + case let .idle(_, duration): + audio.playButton.isHidden = false + audio.stopPlaybackButton.isHidden = true + audio.stopRecordingButton.isHidden = true + audio.sendButton.isHidden = false + audio.timeLabel.text = timeFormatter.string(from: duration) + + case let .recording(_, time): + audio.playButton.isHidden = true + audio.stopPlaybackButton.isHidden = true + audio.stopRecordingButton.isHidden = false + audio.sendButton.isHidden = true + audio.timeLabel.text = timeFormatter.string(from: time) + + case let .playing(_, _, time): + audio.playButton.isHidden = true + audio.stopPlaybackButton.isHidden = false + audio.stopRecordingButton.isHidden = true + audio.sendButton.isHidden = false + audio.timeLabel.text = timeFormatter.string(from: time) + + case .none: + audio.playButton.isHidden = true + audio.stopPlaybackButton.isHidden = true + audio.stopRecordingButton.isHidden = true + audio.sendButton.isHidden = true + audio.timeLabel.text = "" + } + } + .store(in: &cancellables) + } + + private func setupUIActions() { + text.textDidChange = { [unowned self] text in viewStore.send(.text(.didUpdate(text))) } + + text.replyView.abortButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.text(.didTapAbortReply)) } + .store(in: &cancellables) + + text.showActionsButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.text(.didTapShowActions)) } + .store(in: &cancellables) + + text.hideActionsButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.text(.didTapHideActions)) } + .store(in: &cancellables) + + text.sendButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.text(.didTapSend)) } + .store(in: &cancellables) + + text.audioButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.text(.didTapAudio)) } + .store(in: &cancellables) + + audio.cancelButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.audio(.didTapCancel)) } + .store(in: &cancellables) + + audio.playButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.audio(.didTapPlay)) } + .store(in: &cancellables) + + audio.stopPlaybackButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.audio(.didTapStopPlayback)) } + .store(in: &cancellables) + + audio.stopRecordingButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.audio(.didTapStopRecording)) } + .store(in: &cancellables) + + audio.sendButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.audio(.didTapSend)) } + .store(in: &cancellables) + + actions.libraryButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.actions(.didTapLibrary)) } + .store(in: &cancellables) + + actions.cameraButton.publisher(for: .touchUpInside) + .sink { [unowned self] in viewStore.send(.actions(.didTapCamera)) } + .store(in: &cancellables) + } } diff --git a/Sources/ChatInputFeature/TextInputView.swift b/Sources/ChatInputFeature/TextInputView.swift index 960d534fe9757710edb7cb157dd6fe6852f77d00..9a074c8cf78f058a97ff59e5f49475f5da85c1dd 100644 --- a/Sources/ChatInputFeature/TextInputView.swift +++ b/Sources/ChatInputFeature/TextInputView.swift @@ -1,121 +1,122 @@ import UIKit import Shared +import AppResources final class TextInputView: UIView, UITextViewDelegate { - let internalStack = UIStackView() - var replyView = ChatInputReply() - var placeholderView = UITextView() - lazy var bubble = BubbleView(internalStack, padding: 4) - - let stack = UIStackView() - let textView = UITextView() - let showActionsButton = UIButton() - let hideActionsButton = UIButton() - let sendButton = UIButton() - let audioButton = UIButton() - - var maxHeight: () -> CGFloat = { 150 } - var textDidChange: (String) -> Void = { _ in } - - private var computedTextHeight: CGFloat { - let textWidth = textView.frame.size.width - let size = CGSize(width: textWidth, height: .greatestFiniteMagnitude) - return textView.sizeThatFits(size).height + let internalStack = UIStackView() + var replyView = ChatInputReply() + var placeholderView = UITextView() + lazy var bubble = BubbleView(internalStack, padding: 4) + + let stack = UIStackView() + let textView = UITextView() + let showActionsButton = UIButton() + let hideActionsButton = UIButton() + let sendButton = UIButton() + let audioButton = UIButton() + + var maxHeight: () -> CGFloat = { 150 } + var textDidChange: (String) -> Void = { _ in } + + private var computedTextHeight: CGFloat { + let textWidth = textView.frame.size.width + let size = CGSize(width: textWidth, height: .greatestFiniteMagnitude) + return textView.sizeThatFits(size).height + } + + init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + override func layoutSubviews() { + super.layoutSubviews() + updateHeight() + } + + func updateHeight() { + let replyHeight = replyView.isHidden ? 0 : replyView.bounds.height + let computedTextHeight = self.computedTextHeight + let computedHeight = computedTextHeight + replyHeight + let maxHeight = self.maxHeight() + + if computedHeight < maxHeight { + textView.snp.updateConstraints { $0.height.equalTo(computedTextHeight) } + textView.isScrollEnabled = false + } else { + textView.snp.updateConstraints { $0.height.equalTo(maxHeight - replyHeight) } + textView.isScrollEnabled = true } - - init() { - super.init(frame: .zero) - setup() + } + + private func setup() { + replyView.isHidden = true + textView.autocorrectionType = .default + placeholderView.isUserInteractionEnabled = false + textView.font = Fonts.Mulish.semiBold.font(size: 14.0) + placeholderView.text = Localized.Chat.placeholder + placeholderView.font = Fonts.Mulish.semiBold.font(size: 14.0) + + textView.backgroundColor = .clear + placeholderView.backgroundColor = .clear + textView.textColor = Asset.neutralActive.color + bubble.backgroundColor = Asset.neutralSecondary.color + placeholderView.textColor = Asset.neutralDisabled.color + + showActionsButton.setImage(Asset.chatInputActionOpen.image, for: .normal) + hideActionsButton.setImage(Asset.chatInputActionClose.image, for: .normal) + audioButton.setImage(Asset.chatInputVoiceStart.image, for: .normal) + sendButton.setImage(Asset.chatSend.image, for: .normal) + + showActionsButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + showActionsButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + hideActionsButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + hideActionsButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + sendButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + sendButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + audioButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + audioButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + internalStack.axis = .vertical + internalStack.addArrangedSubview(replyView) + internalStack.addArrangedSubview(textView) + + textView.addSubview(placeholderView) + textView.setContentHuggingPriority(.defaultLow, for: .horizontal) + + placeholderView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.height.equalToSuperview() + make.width.equalToSuperview() } - required init?(coder: NSCoder) { nil } - - override func layoutSubviews() { - super.layoutSubviews() - updateHeight() - } - - func updateHeight() { - let replyHeight = replyView.isHidden ? 0 : replyView.bounds.height - let computedTextHeight = self.computedTextHeight - let computedHeight = computedTextHeight + replyHeight - let maxHeight = self.maxHeight() - - if computedHeight < maxHeight { - textView.snp.updateConstraints { $0.height.equalTo(computedTextHeight) } - textView.isScrollEnabled = false - } else { - textView.snp.updateConstraints { $0.height.equalTo(maxHeight - replyHeight) } - textView.isScrollEnabled = true - } - } - - private func setup() { - replyView.isHidden = true - textView.autocorrectionType = .default - placeholderView.isUserInteractionEnabled = false - textView.font = Fonts.Mulish.semiBold.font(size: 14.0) - placeholderView.text = Localized.Chat.placeholder - placeholderView.font = Fonts.Mulish.semiBold.font(size: 14.0) - - textView.backgroundColor = .clear - placeholderView.backgroundColor = .clear - textView.textColor = Asset.neutralActive.color - bubble.backgroundColor = Asset.neutralSecondary.color - placeholderView.textColor = Asset.neutralDisabled.color - - showActionsButton.setImage(Asset.chatInputActionOpen.image, for: .normal) - hideActionsButton.setImage(Asset.chatInputActionClose.image, for: .normal) - audioButton.setImage(Asset.chatInputVoiceStart.image, for: .normal) - sendButton.setImage(Asset.chatSend.image, for: .normal) - - showActionsButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - showActionsButton.setContentCompressionResistancePriority(.required, for: .horizontal) - - hideActionsButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - hideActionsButton.setContentCompressionResistancePriority(.required, for: .horizontal) - - sendButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - sendButton.setContentCompressionResistancePriority(.required, for: .horizontal) - - audioButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - audioButton.setContentCompressionResistancePriority(.required, for: .horizontal) - - internalStack.axis = .vertical - internalStack.addArrangedSubview(replyView) - internalStack.addArrangedSubview(textView) - - textView.addSubview(placeholderView) - textView.setContentHuggingPriority(.defaultLow, for: .horizontal) - - placeholderView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.height.equalToSuperview() - make.width.equalToSuperview() - } - - stack.axis = .horizontal - stack.spacing = 8 - stack.addArrangedSubview(showActionsButton) - stack.addArrangedSubview(hideActionsButton) - stack.addArrangedSubview(bubble) - stack.addArrangedSubview(sendButton) - stack.addArrangedSubview(audioButton) - - addSubview(stack) - stack.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: topAnchor), - stack.leadingAnchor.constraint(equalTo: leadingAnchor), - stack.trailingAnchor.constraint(equalTo: trailingAnchor), - stack.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - textView.delegate = self - } - - func textViewDidChange(_ textView: UITextView) { - textDidChange(textView.text) - } + stack.axis = .horizontal + stack.spacing = 8 + stack.addArrangedSubview(showActionsButton) + stack.addArrangedSubview(hideActionsButton) + stack.addArrangedSubview(bubble) + stack.addArrangedSubview(sendButton) + stack.addArrangedSubview(audioButton) + + addSubview(stack) + stack.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: topAnchor), + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + textView.delegate = self + } + + func textViewDidChange(_ textView: UITextView) { + textDidChange(textView.text) + } } diff --git a/Sources/ChatInputFeature/Voxophone.swift b/Sources/ChatInputFeature/Voxophone.swift new file mode 100644 index 0000000000000000000000000000000000000000..61141ffd36ff068e3eb0a8385dd38d76699c9aa1 --- /dev/null +++ b/Sources/ChatInputFeature/Voxophone.swift @@ -0,0 +1,227 @@ +import Shared +import Combine +import AVFoundation + +public final class Voxophone: NSObject, AVAudioRecorderDelegate, AVAudioPlayerDelegate { + public enum State: Equatable { + case empty(isLoudspeaker: Bool) + case idle(URL, duration: TimeInterval, isLoudspeaker: Bool) + case recording(URL, time: TimeInterval, isLoudspeaker: Bool) + case playing(URL, duration: TimeInterval, time: TimeInterval, isLoudspeaker: Bool) + } + + public override init() { + super.init() + } + + deinit { + destroyPlayer() + destroyRecorder() + stopTimer() + } + + @Published public private(set) var state: State = .empty(isLoudspeaker: false) + + private let session: AVAudioSession = .sharedInstance() + private var recorder: AVAudioRecorder? + private var player: AVAudioPlayer? + private var timer: Timer? + + public func reset() { + destroyPlayer() + destroyRecorder() + state = .empty(isLoudspeaker: false) + } + + public func toggleLoudspeaker() { + state.isLoudspeaker.toggle() + setupSessionCategory() + } + + public func load(_ url: URL) { + destroyPlayer() + destroyRecorder() + let player = setupPlayer(url: url) + state = .idle(url, duration: player.duration, isLoudspeaker: state.isLoudspeaker) + } + + public func play() { + guard let player = player, let url = player.url else { return } + destroyRecorder() + state = .playing(url, duration: player.duration, time: player.currentTime, isLoudspeaker: state.isLoudspeaker) + startPlayback() + } + + public func record() { + let url = URL(fileURLWithPath: FileManager.xxPath + "/recording_\(Date.asTimestamp).m4a") + + destroyPlayer() + destroyRecorder() + let recorder = setupRecorder(url: url) + state = .recording(url, time: recorder.currentTime, isLoudspeaker: state.isLoudspeaker) + startRecording() + } + + public func stop() { + switch state { + case .empty, .idle: + return + + case .recording: + finishRecording() + + case .playing(let url, let duration, _, let isLoudspeaker): + stopPlayback() + state = .idle(url, duration: duration, isLoudspeaker: isLoudspeaker) + } + } + + private func setupPlayer(url: URL) -> AVAudioPlayer { + let player = try! AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.m4a.rawValue) + self.player = player + return player + } + + private func setupSessionCategory() { + switch state { + case .playing(_, _, _, let isLoud): + if isLoud, session.category != .playback { + try! session.setCategory(.playback, options: .duckOthers) + } + + if !isLoud, session.category != .playAndRecord { + try! session.setCategory(.playAndRecord, options: .duckOthers) + } + case .recording(_, _, _): + if session.category != .playAndRecord { + try! session.setCategory(.playAndRecord, options: .duckOthers) + } + default: + break + } + } + + private func startPlayback() { + guard let player = player else { return } + try! session.setActive(true) + setupSessionCategory() + player.delegate = self + player.prepareToPlay() + player.play() + startTimer() + } + + private func stopPlayback() { + guard let player = player else { return } + player.stop() + } + + private func destroyPlayer() { + player?.delegate = nil + player?.stop() + player = nil + } + + // MARK: - Recorder + + private func setupRecorder(url: URL) -> AVAudioRecorder { + let recorder = try! AVAudioRecorder(url: url, settings: [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVSampleRateKey: 12000, + AVNumberOfChannelsKey: 1 + ]) + self.recorder = recorder + return recorder + } + + private func startRecording() { + guard let recorder = recorder else { return } + try! session.setActive(true) + setupSessionCategory() + recorder.delegate = self + recorder.record() + startTimer() + } + + private func finishRecording() { + guard let recorder = recorder else { return } + recorder.stop() + } + + private func destroyRecorder() { + recorder?.delegate = nil + recorder?.stop() + recorder = nil + } + + // MARK: - Timer + + private func startTimer() { + stopTimer() + timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in + self.timerTick() + } + } + + private func timerTick() { + switch state { + case .empty, .idle: + stopTimer() + + case .recording(_, _, let isLoud): + guard let recorder = recorder else { return } + state = .recording(recorder.url, time: recorder.currentTime, isLoudspeaker: isLoud) + + case .playing(_, _, _, let isLoud): + guard let player = player, let url = player.url else { return } + state = .playing(url, duration: player.duration, time: player.currentTime, isLoudspeaker: isLoud) + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + + // MARK: - AVAudioRecorderDelegate + + public func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + guard flag else { + state = .empty(isLoudspeaker: state.isLoudspeaker) + return + } + load(recorder.url) + } + + // MARK: - AVAudioPlayerDelegate + + public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + guard flag, let url = player.url else { + state = .empty(isLoudspeaker: state.isLoudspeaker) + return + } + load(url) + } +} + +public extension Voxophone.State { + var isLoudspeaker: Bool { + get { + switch self { + case .playing(_, _, _, let isLoud), .idle(_, _, let isLoud), .empty(let isLoud), .recording(_, _, let isLoud): + return isLoud + } + } set { + switch self { + case .empty(_): + self = .empty(isLoudspeaker: newValue) + case let .idle(url, duration, _): + self = .idle(url, duration: duration, isLoudspeaker: newValue) + case let .playing(url, duration, time, _): + self = .playing(url, duration: duration, time: time, isLoudspeaker: newValue) + case let .recording(url, time, _): + self = .recording(url, time: time, isLoudspeaker: newValue) + } + } + } +} diff --git a/Sources/ChatListFeature/Controller/ChatListController.swift b/Sources/ChatListFeature/Controller/ChatListController.swift index dfa484b08116958931d12a93f98a54f53a7db096..c141c1ae99af73758f9acb538e98b4e4d7ae94f2 100644 --- a/Sources/ChatListFeature/Controller/ChatListController.swift +++ b/Sources/ChatListFeature/Controller/ChatListController.swift @@ -2,7 +2,6 @@ import UIKit import Shared import Combine import XXModels -import MenuFeature import Navigation import DI @@ -70,9 +69,9 @@ public final class ChatListController: UIViewController { .sink { [unowned self] in switch $0 { case .didTapSearch: - navigator.perform(PresentSearch(replacing: false)) + navigator.perform(PresentSearch(searching: nil, replacing: false, on: navigationController!)) case .didTapNewGroup: - navigator.perform(PresentNewGroup()) + navigator.perform(PresentNewGroup(on: navigationController!)) } }.store(in: &cancellables) @@ -87,7 +86,7 @@ public final class ChatListController: UIViewController { .actionPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentMenu(currentItem: .chats)) + navigator.perform(PresentMenu(currentItem: .chats, from: self)) }.store(in: &cancellables) } @@ -146,7 +145,7 @@ public final class ChatListController: UIViewController { .rightPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentScan()) + navigator.perform(PresentScan(on: navigationController!)) }.store(in: &cancellables) screenView @@ -211,7 +210,11 @@ public final class ChatListController: UIViewController { .searchButton .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentSearch(replacing: false)) + navigator.perform(PresentSearch( + searching: nil, + replacing: false, + on: navigationController! + )) }.store(in: &cancellables) screenView @@ -221,7 +224,7 @@ public final class ChatListController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentContactList()) + navigator.perform(PresentContactList(on: navigationController!)) }.store(in: &cancellables) viewModel @@ -240,7 +243,7 @@ extension ChatListController: UICollectionViewDelegate { didSelectItemAt indexPath: IndexPath ) { if let contact = collectionDataSource.itemIdentifier(for: indexPath) { - navigator.perform(PresentChat(contact: contact)) + navigator.perform(PresentChat(contact: contact, on: navigationController!)) } } } diff --git a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift index d2949c9c73eb457990b5caf69563e809585d8626..b9a877cbf6f4942ba12d547a1fdef94aa8cf502d 100644 --- a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift @@ -108,18 +108,18 @@ extension ChatSearchTableController { switch chatInfo { case .group(let group): if let groupInfo = viewModel.groupInfo(from: group) { - navigator.perform(PresentGroupChat(model: groupInfo)) + navigator.perform(PresentGroupChat(groupInfo: groupInfo, on: navigationController!)) } case .groupChat(let info): if let groupInfo = viewModel.groupInfo(from: info.group) { - navigator.perform(PresentGroupChat(model: groupInfo)) + navigator.perform(PresentGroupChat(groupInfo: groupInfo, on: navigationController!)) } case .contactChat(let info): guard info.contact.authStatus == .friend else { return } - navigator.perform(PresentChat(contact: info.contact)) + navigator.perform(PresentChat(contact: info.contact, on: navigationController!)) } case .connection(let contact): - navigator.perform(PresentContact(contact: contact)) + navigator.perform(PresentContact(contact: contact, on: navigationController!)) } } } diff --git a/Sources/ChatListFeature/Controller/ChatListTableController.swift b/Sources/ChatListFeature/Controller/ChatListTableController.swift index 7129b97c4318bfb0f9569e9bc7f0b57f36ac0822..8db077fa8c454239b6d1b5553c9c25632ed7e38d 100644 --- a/Sources/ChatListFeature/Controller/ChatListTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListTableController.swift @@ -96,15 +96,21 @@ extension ChatListTableController { switch rows[indexPath.row] { case .group(let group): if let groupInfo = viewModel.groupInfo(from: group) { - navigator.perform(PresentGroupChat(model: groupInfo)) + navigator.perform(PresentGroupChat( + groupInfo: groupInfo, + on: navigationController! + )) } case .groupChat(let info): if let groupInfo = viewModel.groupInfo(from: info.group) { - navigator.perform(PresentGroupChat(model: groupInfo)) + navigator.perform(PresentGroupChat( + groupInfo: groupInfo, + on: navigationController! + )) } case .contactChat(let info): guard info.contact.authStatus == .friend else { return } - navigator.perform(PresentChat(contact: info.contact)) + navigator.perform(PresentChat(contact: info.contact, on: navigationController!)) } } @@ -202,6 +208,6 @@ extension ChatListTableController { spacingAfter: 39 ), actionButton - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift index 145318147b35a7d79992bfca2e4e93e9c3643eb1..b1e667cecb38dc02c386fcc875317c961674f786 100644 --- a/Sources/ContactFeature/Controllers/ContactController.swift +++ b/Sources/ContactFeature/Controllers/ContactController.swift @@ -50,7 +50,8 @@ public final class ContactController: UIViewController { screenView.didTapSend = { [weak self] in guard let self else { return } self.navigator.perform(PresentChat( - contact: self.viewModel.contact + contact: self.viewModel.contact, + on: self.navigationController! )) } screenView.didTapInfo = { [weak self] in @@ -83,7 +84,9 @@ public final class ContactController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentPhotoLibrary()) + navigator.perform( + PresentPhotoLibrary(from: self) + ) }.store(in: &cancellables) viewModel @@ -146,7 +149,7 @@ public final class ContactController: UIViewController { .sentRequests .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentRequests()) + navigator.perform(PresentRequests(on: navigationController!)) }.store(in: &cancellables) viewModel @@ -178,10 +181,10 @@ public final class ContactController: UIViewController { .publisher(for: .touchUpInside) .sink { [unowned self] in let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" - navigator.perform(PresentNickname(prefilled: nickname) { [weak self] in + navigator.perform(PresentNickname(prefilled: nickname, completion: { [weak self] in guard let self else { return } self.viewModel.didTapRequest(with: $0) - }) + }, from: self)) }.store(in: &cancellables) } @@ -191,10 +194,10 @@ public final class ContactController: UIViewController { .publisher(for: .touchUpInside) .sink { [unowned self] in let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" - navigator.perform(PresentNickname(prefilled: nickname) { [weak self] in + navigator.perform(PresentNickname(prefilled: nickname, completion: { [weak self] in guard let self else { return } self.viewModel.didTapAccept($0) - }) + }, from: self)) }.store(in: &cancellables) screenView @@ -263,7 +266,7 @@ public final class ContactController: UIViewController { navigator.perform(PresentNickname(prefilled: nickname, completion: { [weak self] in guard let self else { return } self.viewModel.didUpdateNickname($0) - })) + }, from: self)) }.store(in: &cancellables) let usernameAttribute = AttributeComponent() @@ -352,7 +355,7 @@ public final class ContactController: UIViewController { spacing: 20.0, views: [clearButton, cancelButton] ) - ])) + ], isDismissable: true, from: self)) } } @@ -422,7 +425,7 @@ extension ContactController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } private func presentDeleteInfo() { @@ -456,6 +459,6 @@ extension ContactController { customAttributes: [.font: Fonts.Mulish.bold.font(size: 16.0)] ), actionButton - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/ContactListFeature/Controllers/ContactListController.swift b/Sources/ContactListFeature/Controllers/ContactListController.swift index 2c45e7d914ac37c39257f6cf9c606e92b91f046b..c5a2dbb059953c0a8ed6c5b680ec98a6851261ee 100644 --- a/Sources/ContactListFeature/Controllers/ContactListController.swift +++ b/Sources/ContactListFeature/Controllers/ContactListController.swift @@ -86,7 +86,10 @@ public final class ContactListController: UIViewController { .didTap .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentChat(contact: $0)) + navigator.perform(PresentChat( + contact: $0, + on: navigationController! + )) }.store(in: &cancellables) screenView @@ -94,7 +97,7 @@ public final class ContactListController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentRequests()) + navigator.perform(PresentRequests(on: navigationController!)) }.store(in: &cancellables) screenView @@ -102,7 +105,7 @@ public final class ContactListController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentNewGroup()) + navigator.perform(PresentNewGroup(on: navigationController!)) }.store(in: &cancellables) screenView @@ -110,7 +113,11 @@ public final class ContactListController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentSearch(replacing: false)) + navigator.perform(PresentSearch( + searching: nil, + replacing: false, + on: navigationController! + )) }.store(in: &cancellables) viewModel @@ -132,14 +139,18 @@ public final class ContactListController: UIViewController { } @objc private func didTapSearch() { - navigator.perform(PresentSearch(replacing: false)) + navigator.perform(PresentSearch( + searching: nil, + replacing: false, + on: navigationController! + )) } @objc private func didTapScan() { - navigator.perform(PresentScan()) + navigator.perform(PresentScan(on: navigationController!)) } @objc private func didTapMenu() { - navigator.perform(PresentMenu(currentItem: .contacts)) + navigator.perform(PresentMenu(currentItem: .contacts, from: self)) } } diff --git a/Sources/ContactListFeature/Controllers/CreateGroupController.swift b/Sources/ContactListFeature/Controllers/CreateGroupController.swift index 4397c8cd53a8f58d94df05348ea2149e1be6cd6e..34539b857c33cec6bbf1ba0a074db9f228493e48 100644 --- a/Sources/ContactListFeature/Controllers/CreateGroupController.swift +++ b/Sources/ContactListFeature/Controllers/CreateGroupController.swift @@ -157,7 +157,10 @@ public final class CreateGroupController: UIViewController { .info .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentGroupChat(model: $0)) + navigator.perform(PresentGroupChat( + groupInfo: $0, + on: navigationController! + )) }.store(in: &cancellables) createButton diff --git a/Sources/Countries/CountryListCell.swift b/Sources/Countries/CountryListCell.swift deleted file mode 100644 index b3b650e5ae8daae80cc8b026fc3d242bac1b9920..0000000000000000000000000000000000000000 --- a/Sources/Countries/CountryListCell.swift +++ /dev/null @@ -1,62 +0,0 @@ -import UIKit -import Shared - -final class CountryListCell: UITableViewCell { - let nameLabel = UILabel() - let flagLabel = UILabel() - let prefixLabel = UILabel() - let separatorView = UIView() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = Asset.neutralWhite.color - - nameLabel.textColor = Asset.neutralDark.color - prefixLabel.textColor = Asset.neutralWeak.color - nameLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - prefixLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - separatorView.backgroundColor = Asset.brandBackground.color - prefixLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - - contentView.addSubview(nameLabel) - contentView.addSubview(flagLabel) - contentView.addSubview(prefixLabel) - contentView.addSubview(separatorView) - - flagLabel.snp.makeConstraints { - $0.left.top.equalToSuperview().inset(18) - $0.bottom.equalToSuperview().offset(-16) - } - - nameLabel.snp.makeConstraints { - $0.left.equalToSuperview().offset(55) - $0.centerY.equalToSuperview() - $0.right.lessThanOrEqualTo(prefixLabel.snp.left).offset(-10) - } - - prefixLabel.snp.makeConstraints { - $0.right.equalToSuperview().offset(-18) - $0.centerY.equalToSuperview() - } - - separatorView.snp.makeConstraints { - $0.bottom.equalToSuperview() - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - $0.height.equalTo(1) - } - } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - - nameLabel.text = nil - flagLabel.text = nil - prefixLabel.text = nil - } -} diff --git a/Sources/Countries/CountryListView.swift b/Sources/Countries/CountryListView.swift deleted file mode 100644 index cf743823ace4b8b1d45fefeb1dedc0360374799a..0000000000000000000000000000000000000000 --- a/Sources/Countries/CountryListView.swift +++ /dev/null @@ -1,42 +0,0 @@ -import UIKit -import Shared - -final class CountryListView: UIView { - let tableView = UITableView() - let searchComponent = SearchComponent() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - private func setup() { - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - backgroundColor = Asset.neutralWhite.color - - searchComponent.set( - imageAtRight: UIImage.color(.clear), - inputAccessibility: Localized.Accessibility.Countries.Search.field, - rightAccessibility: Localized.Accessibility.Countries.Search.right - ) - - addSubview(tableView) - addSubview(searchComponent) - - searchComponent.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview().offset(20) - make.right.equalToSuperview().offset(-20) - } - - tableView.snp.makeConstraints { make in - make.top.equalTo(searchComponent.snp.bottom).offset(20) - make.left.equalToSuperview() - make.bottom.equalToSuperview() - make.right.equalToSuperview() - } - } -} diff --git a/Sources/Countries/CountryListViewModel.swift b/Sources/Countries/CountryListViewModel.swift deleted file mode 100644 index e4157a16e1938cc506f0549113d393eb6f5b6202..0000000000000000000000000000000000000000 --- a/Sources/Countries/CountryListViewModel.swift +++ /dev/null @@ -1,49 +0,0 @@ -import os -import UIKit -import Shared -import Combine -import Foundation - -private let logger = Logger(subsystem: "logs_xxmessenger", category: "Countries.CountryListViewModel.swift") - -final class CountryListViewModel { - var countries: AnyPublisher<NSDiffableDataSourceSnapshot<SectionId, Country>, Never> { - countriesRelay.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let searchQueryRelay = CurrentValueSubject<String, Never>("") - private let countriesRelay = CurrentValueSubject<NSDiffableDataSourceSnapshot<SectionId, Country>, Never>(.init()) - - func fetchCountryList() { - logger.log("fetchCountryList()") - - Publishers.CombineLatest(Just(Country.all()), searchQueryRelay) - .map { countryList, query -> NSDiffableDataSourceSnapshot<SectionId, Country> in - var snapshot = NSDiffableDataSourceSnapshot<SectionId, Country>() - let section = SectionId() - snapshot.appendSections([section]) - - guard !query.isEmpty else { - logger.log("query.isEmpty, returning all countries") - snapshot.appendItems(countryList, toSection: section) - return snapshot - } - - let filtered = countryList.filter { - $0.name.lowercased().contains(query.lowercased()) || - $0.prefix.lowercased().contains(query.lowercased()) - } - - snapshot.appendItems(filtered, toSection: section) - return snapshot - - }.sink { [weak countriesRelay] in countriesRelay?.send($0) } - .store(in: &cancellables) - } - - func didSearchFor(_ string: String) { - logger.log("didSearchFor \(string, privacy: .public)()") - searchQueryRelay.send(string) - } -} diff --git a/Sources/CountryListFeature/CountryListCell.swift b/Sources/CountryListFeature/CountryListCell.swift new file mode 100644 index 0000000000000000000000000000000000000000..61dc02fcab863e5a965f6c5e589e880d3617ba12 --- /dev/null +++ b/Sources/CountryListFeature/CountryListCell.swift @@ -0,0 +1,63 @@ +import UIKit +import Shared +import AppResources + +final class CountryListCell: UITableViewCell { + let nameLabel = UILabel() + let flagLabel = UILabel() + let prefixLabel = UILabel() + let separatorView = UIView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + selectionStyle = .none + backgroundColor = Asset.neutralWhite.color + + nameLabel.textColor = Asset.neutralDark.color + prefixLabel.textColor = Asset.neutralWeak.color + nameLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + prefixLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + separatorView.backgroundColor = Asset.brandBackground.color + prefixLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + + contentView.addSubview(nameLabel) + contentView.addSubview(flagLabel) + contentView.addSubview(prefixLabel) + contentView.addSubview(separatorView) + + flagLabel.snp.makeConstraints { + $0.left.top.equalToSuperview().inset(18) + $0.bottom.equalToSuperview().offset(-16) + } + + nameLabel.snp.makeConstraints { + $0.left.equalToSuperview().offset(55) + $0.centerY.equalToSuperview() + $0.right.lessThanOrEqualTo(prefixLabel.snp.left).offset(-10) + } + + prefixLabel.snp.makeConstraints { + $0.right.equalToSuperview().offset(-18) + $0.centerY.equalToSuperview() + } + + separatorView.snp.makeConstraints { + $0.bottom.equalToSuperview() + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + $0.height.equalTo(1) + } + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + + nameLabel.text = nil + flagLabel.text = nil + prefixLabel.text = nil + } +} diff --git a/Sources/Countries/CountryListController.swift b/Sources/CountryListFeature/CountryListController.swift similarity index 82% rename from Sources/Countries/CountryListController.swift rename to Sources/CountryListFeature/CountryListController.swift index 5a6181315eb8446afbac9a82beee507244cd9d9a..f7c11b93853371b1ccf191b32abfe367ca59b237 100644 --- a/Sources/Countries/CountryListController.swift +++ b/Sources/CountryListFeature/CountryListController.swift @@ -1,12 +1,12 @@ import UIKit import Shared import Combine -import Navigation -import DI +import AppResources +import StatusBarFeature +import ComposableArchitecture public final class CountryListController: UIViewController, UITableViewDelegate { - @Dependency var navigator: Navigator - @Dependency var barStylist: StatusBarStylist + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager private lazy var screenView = CountryListView() @@ -24,12 +24,7 @@ public final class CountryListController: UIViewController, UITableViewDelegate public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - barStylist.styleSubject.send(.darkContent) - navigationController?.navigationBar.customize( - backgroundColor: Asset.neutralWhite.color, - shadowColor: Asset.neutralDisabled.color - ) + statusBar.update(.darkContent) } public override func loadView() { @@ -75,7 +70,7 @@ public final class CountryListController: UIViewController, UITableViewDelegate public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let country = dataSource.itemIdentifier(for: indexPath) { completion(country) - navigator.perform(DismissModal(from: self)) + dismiss(animated: true) } } } diff --git a/Sources/CountryListFeature/CountryListView.swift b/Sources/CountryListFeature/CountryListView.swift new file mode 100644 index 0000000000000000000000000000000000000000..9d5f65f297a66aa47c3db75cc97782845600a3f4 --- /dev/null +++ b/Sources/CountryListFeature/CountryListView.swift @@ -0,0 +1,39 @@ +import UIKit +import Shared +import AppResources + +final class CountryListView: UIView { + let tableView = UITableView() + let searchComponent = SearchComponent() + + init() { + super.init(frame: .zero) + + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + backgroundColor = Asset.neutralWhite.color + + searchComponent.set( + imageAtRight: UIImage.color(.clear), + inputAccessibility: Localized.Accessibility.Countries.Search.field, + rightAccessibility: Localized.Accessibility.Countries.Search.right + ) + + addSubview(tableView) + addSubview(searchComponent) + + searchComponent.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + } + tableView.snp.makeConstraints { + $0.top.equalTo(searchComponent.snp.bottom).offset(20) + $0.left.equalToSuperview() + $0.bottom.equalToSuperview() + $0.right.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/CountryListFeature/CountryListViewModel.swift b/Sources/CountryListFeature/CountryListViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..4144345c6118e05b0f2743abe8b4a92c68d53527 --- /dev/null +++ b/Sources/CountryListFeature/CountryListViewModel.swift @@ -0,0 +1,43 @@ +import UIKit +import Shared +import Combine + +final class CountryListViewModel { + var countries: AnyPublisher<NSDiffableDataSourceSnapshot<SectionId, Country>, Never> { + countriesRelay.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let searchQueryRelay = CurrentValueSubject<String, Never>("") + private let countriesRelay = CurrentValueSubject<NSDiffableDataSourceSnapshot<SectionId, Country>, Never>(.init()) + + func fetchCountryList() { + Publishers + .CombineLatest(Just(Country.all()), searchQueryRelay) + .map { countryList, query -> NSDiffableDataSourceSnapshot<SectionId, Country> in + var snapshot = NSDiffableDataSourceSnapshot<SectionId, Country>() + let section = SectionId() + snapshot.appendSections([section]) + + guard !query.isEmpty else { + snapshot.appendItems(countryList, toSection: section) + return snapshot + } + + let filtered = countryList.filter { + $0.name.lowercased().contains(query.lowercased()) || + $0.prefix.lowercased().contains(query.lowercased()) + } + + snapshot.appendItems(filtered, toSection: section) + return snapshot + + }.sink { [weak countriesRelay] in + countriesRelay?.send($0) + }.store(in: &cancellables) + } + + func didSearchFor(_ string: String) { + searchQueryRelay.send(string) + } +} diff --git a/Sources/CrashReporting/CrashReporter.swift b/Sources/CrashReporting/CrashReporter.swift index f249d741c7baa42218d77487a26e717866ea2f9a..e83c1c54b0f765442b9f31c5c779c86096a92db7 100644 --- a/Sources/CrashReporting/CrashReporter.swift +++ b/Sources/CrashReporting/CrashReporter.swift @@ -1,25 +1,25 @@ import Foundation public struct CrashReporter { - public var configure: () -> Void - public var sendError: (NSError) -> Void - public var setEnabled: (Bool) -> Void + public var configure: () -> Void + public var sendError: (NSError) -> Void + public var setEnabled: (Bool) -> Void - public init( - configure: @escaping () -> Void, - sendError: @escaping (NSError) -> Void, - setEnabled: @escaping (Bool) -> Void - ) { - self.configure = configure - self.sendError = sendError - self.setEnabled = setEnabled - } + public init( + configure: @escaping () -> Void, + sendError: @escaping (NSError) -> Void, + setEnabled: @escaping (Bool) -> Void + ) { + self.configure = configure + self.sendError = sendError + self.setEnabled = setEnabled + } } public extension CrashReporter { - static let noop = Self( - configure: {}, - sendError: { _ in }, - setEnabled: { _ in } - ) + static let noop = Self( + configure: {}, + sendError: { _ in }, + setEnabled: { _ in } + ) } diff --git a/Sources/CrashService/CrashService.swift b/Sources/CrashService/CrashService.swift index 4815ed483b3177262b90089ecc2af62973208a5d..4226369deeb3db598fee81058dbd139ff0dc1c2b 100644 --- a/Sources/CrashService/CrashService.swift +++ b/Sources/CrashService/CrashService.swift @@ -3,9 +3,9 @@ import CrashReporting import FirebaseCrashlytics public extension CrashReporter { - static let live = Self( - configure: { FirebaseApp.configure() }, - sendError: { Crashlytics.crashlytics().record(error: $0) }, - setEnabled: { Crashlytics.crashlytics().setCrashlyticsCollectionEnabled($0) } - ) + static let live = Self( + configure: { FirebaseApp.configure() }, + sendError: { Crashlytics.crashlytics().record(error: $0) }, + setEnabled: { Crashlytics.crashlytics().setCrashlyticsCollectionEnabled($0) } + ) } diff --git a/Sources/Defaults/KeyObject.swift b/Sources/Defaults/KeyObject.swift index fc350c6090a25ec3eda750b1c3db128b1e27fb86..1072e3c1cca9e9e195e99f892f489d52b4d6150f 100644 --- a/Sources/Defaults/KeyObject.swift +++ b/Sources/Defaults/KeyObject.swift @@ -1,109 +1,90 @@ -import Foundation import DI +import Foundation public enum Key: String { - // MARK: Profile - - case email - case phone - case avatar - case username - - case sharingEmail - case sharingPhone - - // MARK: Notifications - - case requestCounter - case pushNotifications - case inappnotifications - - // MARK: General - - case acceptedTerms - - // MARK: Requests - - case isShowingHiddenRequests - - // MARK: Backup - - case backupSettings - - // MARK: Settings - - case biometrics - case hideAppList - case recordingLogs - case crashReporting - case icognitoKeyboard - - case dummyTrafficOn - case askedDummyTrafficOnce + case email + case phone + case avatar + case username + case sharingEmail + case sharingPhone + case requestCounter + case pushNotifications + case inappnotifications + case acceptedTerms + case isShowingHiddenRequests + case backupSettings + case biometrics + case hideAppList + case recordingLogs + case crashReporting + case icognitoKeyboard + case dummyTrafficOn + case askedDummyTrafficOnce } public struct KeyObjectStore { - var objectForKey: (String) -> Any? - var setObjectForKey: (Any?, String) -> Void - var removeObjectForKey: (String) -> Void - - public init( - objectForKey: @escaping (String) -> Any?, - setObjectForKey: @escaping (Any?, String) -> Void, - removeObjectForKey: @escaping (String) -> Void - ) { - self.objectForKey = objectForKey - self.setObjectForKey = setObjectForKey - self.removeObjectForKey = removeObjectForKey - } + var objectForKey: (String) -> Any? + var setObjectForKey: (Any?, String) -> Void + var removeObjectForKey: (String) -> Void + + public init( + objectForKey: @escaping (String) -> Any?, + setObjectForKey: @escaping (Any?, String) -> Void, + removeObjectForKey: @escaping (String) -> Void + ) { + self.objectForKey = objectForKey + self.setObjectForKey = setObjectForKey + self.removeObjectForKey = removeObjectForKey + } } public extension KeyObjectStore { - static func mock(dictionary: NSMutableDictionary) -> Self { - Self(objectForKey: { dictionary[$0] }, - setObjectForKey: { dictionary[$1] = $0 }, - removeObjectForKey: { dictionary[$0] = nil }) - } - - static let userDefaults = Self( - objectForKey: UserDefaults.standard.object(forKey:), - setObjectForKey: UserDefaults.standard.set(_:forKey:), - removeObjectForKey: UserDefaults.standard.removeObject(forKey:) - ) + static func mock(dictionary: NSMutableDictionary) -> Self { + Self(objectForKey: { dictionary[$0] }, + setObjectForKey: { dictionary[$1] = $0 }, + removeObjectForKey: { dictionary[$0] = nil }) + } + + static let userDefaults = Self( + objectForKey: UserDefaults.standard.object(forKey:), + setObjectForKey: UserDefaults.standard.set(_:forKey:), + removeObjectForKey: UserDefaults.standard.removeObject(forKey:) + ) } @propertyWrapper public struct KeyObject<T> { - let key: String - let defaultValue: T + let key: String + let defaultValue: T - @Dependency var store: KeyObjectStore + @Dependency var store: KeyObjectStore - public init(_ key: Key, defaultValue: T) { - self.key = key.rawValue - self.defaultValue = defaultValue - } + public init(_ key: Key, defaultValue: T) { + self.key = key.rawValue + self.defaultValue = defaultValue + } - public var wrappedValue: T { - get { - store.objectForKey(key) as? T ?? defaultValue - } - set { - if let value = newValue as? OptionalProtocol, value.isNil() { - store.removeObjectForKey(key) - } else { - store.setObjectForKey(newValue, key) - } - } + public var wrappedValue: T { + get { + store.objectForKey(key) as? T ?? defaultValue } + set { + if let value = newValue as? OptionalProtocol, value.isNil() { + store.removeObjectForKey(key) + } else { + store.setObjectForKey(newValue, key) + } + } + } } fileprivate protocol OptionalProtocol { - func isNil() -> Bool + func isNil() -> Bool } extension Optional : OptionalProtocol { - func isNil() -> Bool { - return self == nil - } + func isNil() -> Bool { + return self == nil + } } diff --git a/Sources/DrawerFeature/DrawerView.swift b/Sources/DrawerFeature/DrawerView.swift index 4c157e0df68dd9b5eade8cbb1569d7018499f354..95b711c3280ccdd40502d6cc4421eb98f058c672 100644 --- a/Sources/DrawerFeature/DrawerView.swift +++ b/Sources/DrawerFeature/DrawerView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class DrawerView: UIView { let stackView = UIStackView() diff --git a/Sources/DrawerFeature/Items/DrawerLinkText.swift b/Sources/DrawerFeature/Items/DrawerLinkText.swift index 428acbaa89e295fb8f95f4bc3a131f0955e571fc..313e98d08ae67f511bc0d6c9ae34d527a2a12f8a 100644 --- a/Sources/DrawerFeature/Items/DrawerLinkText.swift +++ b/Sources/DrawerFeature/Items/DrawerLinkText.swift @@ -1,63 +1,64 @@ import UIKit import Shared +import AppResources public final class DrawerLinkText: NSObject, DrawerItem { - let text: String - let urlString: String - - public var spacingAfter: CGFloat? = 0 - - public init( - text: String, - urlString: String, - spacingAfter: CGFloat = 10 - ) { - self.text = text - self.urlString = urlString - self.spacingAfter = spacingAfter - } + let text: String + let urlString: String + + public var spacingAfter: CGFloat? = 0 + + public init( + text: String, + urlString: String, + spacingAfter: CGFloat = 10 + ) { + self.text = text + self.urlString = urlString + self.spacingAfter = spacingAfter + } + + public func makeView() -> UIView { + let textView = UnselectableTextView() + textView.delegate = self + textView.isEditable = false + textView.isSelectable = true + textView.isScrollEnabled = false + textView.backgroundColor = .clear + textView.isUserInteractionEnabled = true + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + paragraphStyle.lineHeightMultiple = 1.1 - public func makeView() -> UIView { - let textView = UnselectableTextView() - textView.delegate = self - textView.isEditable = false - textView.isSelectable = true - textView.isScrollEnabled = false - textView.backgroundColor = .clear - textView.isUserInteractionEnabled = true - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .left - paragraphStyle.lineHeightMultiple = 1.1 - - let attrString = NSMutableAttributedString(string: text) - attrString.addAttributes([ - .paragraphStyle: paragraphStyle, - .foregroundColor: Asset.neutralDark.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any - ]) - - if let url = URL(string: urlString) { - attrString.addAttribute(name: .link, value: url, betweenCharacters: "#") - - textView.linkTextAttributes = [ - .paragraphStyle: paragraphStyle, - .foregroundColor: Asset.brandPrimary.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any - ] - } - - textView.attributedText = attrString - - return textView + let attrString = NSMutableAttributedString(string: text) + attrString.addAttributes([ + .paragraphStyle: paragraphStyle, + .foregroundColor: Asset.neutralDark.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any + ]) + + if let url = URL(string: urlString) { + attrString.addAttribute(name: .link, value: url, betweenCharacters: "#") + + textView.linkTextAttributes = [ + .paragraphStyle: paragraphStyle, + .foregroundColor: Asset.brandPrimary.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any + ] } - public func textView( - _: UITextView, - shouldInteractWith: URL, - in: NSRange, - interaction: UITextItemInteraction - ) -> Bool { true } + textView.attributedText = attrString + + return textView + } + + public func textView( + _: UITextView, + shouldInteractWith: URL, + in: NSRange, + interaction: UITextItemInteraction + ) -> Bool { true } } extension DrawerLinkText: UITextViewDelegate {} diff --git a/Sources/DrawerFeature/Items/DrawerLoadingRetry.swift b/Sources/DrawerFeature/Items/DrawerLoadingRetry.swift index dcf46a4c812ef1698972c1f5337c1963279a7122..552cb10f94f06161c7a6a6eb6f84aef0c1cb88d0 100644 --- a/Sources/DrawerFeature/Items/DrawerLoadingRetry.swift +++ b/Sources/DrawerFeature/Items/DrawerLoadingRetry.swift @@ -1,57 +1,58 @@ import UIKit import Shared import Combine +import AppResources public final class DrawerLoadingRetry: DrawerItem { - public var retryPublisher: AnyPublisher<Void, Never> { - retrySubject.eraseToAnyPublisher() - } - - private let view = UIView() - private let retryButton = UIButton() - private let stackView = UIStackView() - private var cancellables = Set<AnyCancellable>() - private let activityIndicator = UIActivityIndicatorView() - private let retrySubject = PassthroughSubject<Void, Never>() - - public var spacingAfter: CGFloat? = 0 - - public init(spacingAfter: CGFloat? = 10) { - self.spacingAfter = spacingAfter - self.activityIndicator.style = .large - self.activityIndicator.hidesWhenStopped = true - } - - public func startSpinning() { - activityIndicator.startAnimating() - retryButton.isHidden = true - } - - public func stopSpinning(withRetry retry: Bool) { - guard retry else { view.isHidden = true; return } - - retryButton.isHidden = false - activityIndicator.stopAnimating() - retryButton.setTitle("Retry", for: .normal) - retryButton.setTitleColor(.red, for: .normal) - - retryButton.titleLabel?.numberOfLines = 0 - retryButton.titleLabel?.textAlignment = .center - retryButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 16.0) - } - - public func makeView() -> UIView { - stackView.axis = .vertical - stackView.addArrangedSubview(activityIndicator) - stackView.addArrangedSubview(retryButton) - - retryButton - .publisher(for: .touchUpInside) - .sink { [weak retrySubject] in retrySubject?.send() } - .store(in: &cancellables) - - view.addSubview(stackView) - stackView.snp.makeConstraints { $0.edges.equalToSuperview() } - return view - } + public var retryPublisher: AnyPublisher<Void, Never> { + retrySubject.eraseToAnyPublisher() + } + + private let view = UIView() + private let retryButton = UIButton() + private let stackView = UIStackView() + private var cancellables = Set<AnyCancellable>() + private let activityIndicator = UIActivityIndicatorView() + private let retrySubject = PassthroughSubject<Void, Never>() + + public var spacingAfter: CGFloat? = 0 + + public init(spacingAfter: CGFloat? = 10) { + self.spacingAfter = spacingAfter + self.activityIndicator.style = .large + self.activityIndicator.hidesWhenStopped = true + } + + public func startSpinning() { + activityIndicator.startAnimating() + retryButton.isHidden = true + } + + public func stopSpinning(withRetry retry: Bool) { + guard retry else { view.isHidden = true; return } + + retryButton.isHidden = false + activityIndicator.stopAnimating() + retryButton.setTitle("Retry", for: .normal) + retryButton.setTitleColor(.red, for: .normal) + + retryButton.titleLabel?.numberOfLines = 0 + retryButton.titleLabel?.textAlignment = .center + retryButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 16.0) + } + + public func makeView() -> UIView { + stackView.axis = .vertical + stackView.addArrangedSubview(activityIndicator) + stackView.addArrangedSubview(retryButton) + + retryButton + .publisher(for: .touchUpInside) + .sink { [weak retrySubject] in retrySubject?.send() } + .store(in: &cancellables) + + view.addSubview(stackView) + stackView.snp.makeConstraints { $0.edges.equalToSuperview() } + return view + } } diff --git a/Sources/DrawerFeature/Items/DrawerRadio.swift b/Sources/DrawerFeature/Items/DrawerRadio.swift index de3b764fe403ef3f2c6d80c0e1f91d57ac905382..a3251cf81e9f164ebd4ab6a6ae0317dca0f81872 100644 --- a/Sources/DrawerFeature/Items/DrawerRadio.swift +++ b/Sources/DrawerFeature/Items/DrawerRadio.swift @@ -1,79 +1,80 @@ import UIKit import Shared import Combine +import AppResources public final class DrawerRadio: DrawerItem { - private let title: String - private let isSelected: Bool - private var cancellables = Set<AnyCancellable>() - private let actionSubject = PassthroughSubject<Void, Never>() + private let title: String + private let isSelected: Bool + private var cancellables = Set<AnyCancellable>() + private let actionSubject = PassthroughSubject<Void, Never>() - public var spacingAfter: CGFloat? = 0 - public var action: AnyPublisher<Void, Never> { actionSubject.eraseToAnyPublisher() } + public var spacingAfter: CGFloat? = 0 + public var action: AnyPublisher<Void, Never> { actionSubject.eraseToAnyPublisher() } - public init( - title: String, - isSelected: Bool, - spacingAfter: CGFloat = 10 - ) { - self.title = title - self.isSelected = isSelected - self.spacingAfter = spacingAfter - } + public init( + title: String, + isSelected: Bool, + spacingAfter: CGFloat = 10 + ) { + self.title = title + self.isSelected = isSelected + self.spacingAfter = spacingAfter + } - public func makeView() -> UIView { - cancellables.removeAll() + public func makeView() -> UIView { + cancellables.removeAll() - let radioView = UIView() - let titleLabel = UILabel() - let radioInnerView = UIView() + let radioView = UIView() + let titleLabel = UILabel() + let radioInnerView = UIView() - let view = UIControl() - view.addSubview(titleLabel) - view.addSubview(radioView) - radioView.addSubview(radioInnerView) + let view = UIControl() + view.addSubview(titleLabel) + view.addSubview(radioView) + radioView.addSubview(radioInnerView) - titleLabel.text = title - titleLabel.textColor = Asset.neutralDark.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + titleLabel.text = title + titleLabel.textColor = Asset.neutralDark.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - radioView.layer.cornerRadius = 11.0 - radioInnerView.layer.cornerRadius = 3 - radioView.isUserInteractionEnabled = false + radioView.layer.cornerRadius = 11.0 + radioInnerView.layer.cornerRadius = 3 + radioView.isUserInteractionEnabled = false - if isSelected { - radioView.layer.borderWidth = 0.0 - radioView.backgroundColor = Asset.brandLight.color - radioView.layer.borderColor = Asset.brandLight.color.cgColor - radioInnerView.backgroundColor = Asset.neutralWhite.color - } else { - radioView.layer.borderWidth = 1.0 - radioView.backgroundColor = Asset.neutralSecondary.color - radioView.layer.borderColor = Asset.neutralLine.color.cgColor - radioInnerView.backgroundColor = .clear - } + if isSelected { + radioView.layer.borderWidth = 0.0 + radioView.backgroundColor = Asset.brandLight.color + radioView.layer.borderColor = Asset.brandLight.color.cgColor + radioInnerView.backgroundColor = Asset.neutralWhite.color + } else { + radioView.layer.borderWidth = 1.0 + radioView.backgroundColor = Asset.neutralSecondary.color + radioView.layer.borderColor = Asset.neutralLine.color.cgColor + radioInnerView.backgroundColor = .clear + } - titleLabel.snp.makeConstraints { - $0.left.equalToSuperview().offset(42) - $0.centerY.equalToSuperview() - } + titleLabel.snp.makeConstraints { + $0.left.equalToSuperview().offset(42) + $0.centerY.equalToSuperview() + } - radioView.snp.makeConstraints { - $0.right.equalTo(titleLabel.snp.left).offset(-12) - $0.width.height.equalTo(20) - $0.centerY.equalToSuperview() - $0.bottom.equalToSuperview().offset(-5) - } + radioView.snp.makeConstraints { + $0.right.equalTo(titleLabel.snp.left).offset(-12) + $0.width.height.equalTo(20) + $0.centerY.equalToSuperview() + $0.bottom.equalToSuperview().offset(-5) + } - radioInnerView.snp.makeConstraints { - $0.width.height.equalTo(6) - $0.center.equalToSuperview() - } + radioInnerView.snp.makeConstraints { + $0.width.height.equalTo(6) + $0.center.equalToSuperview() + } - view.publisher(for: .touchUpInside) - .sink { [weak self] in self?.actionSubject.send() } - .store(in: &cancellables) + view.publisher(for: .touchUpInside) + .sink { [weak self] in self?.actionSubject.send() } + .store(in: &cancellables) - return view - } + return view + } } diff --git a/Sources/DrawerFeature/Items/DrawerSwitch.swift b/Sources/DrawerFeature/Items/DrawerSwitch.swift index 449261487789f2685bc43b861ce313b8cf8aaa88..5db2555ed0a36ed68efc9042cdc9635030af4d3d 100644 --- a/Sources/DrawerFeature/Items/DrawerSwitch.swift +++ b/Sources/DrawerFeature/Items/DrawerSwitch.swift @@ -1,80 +1,81 @@ import UIKit import Shared import Combine +import AppResources public final class DrawerSwitch: DrawerItem { - public var isOnPublisher: AnyPublisher<Bool, Never> { - isOnSubject.eraseToAnyPublisher() - } + public var isOnPublisher: AnyPublisher<Bool, Never> { + isOnSubject.eraseToAnyPublisher() + } - private let title: String - private let content: String - private let isEnabled: Bool - private let isInitiallyOn: Bool - private var cancellables = Set<AnyCancellable>() - private let isOnSubject: CurrentValueSubject<Bool, Never> + private let title: String + private let content: String + private let isEnabled: Bool + private let isInitiallyOn: Bool + private var cancellables = Set<AnyCancellable>() + private let isOnSubject: CurrentValueSubject<Bool, Never> - public var spacingAfter: CGFloat? = 0 + public var spacingAfter: CGFloat? = 0 - public init( - title: String, - content: String, - isEnabled: Bool = true, - spacingAfter: CGFloat = 10, - isInitiallyOn: Bool = false - ) { - self.title = title - self.content = content - self.isEnabled = isEnabled - self.spacingAfter = spacingAfter - self.isInitiallyOn = isInitiallyOn - self.isOnSubject = .init(isInitiallyOn) - } + public init( + title: String, + content: String, + isEnabled: Bool = true, + spacingAfter: CGFloat = 10, + isInitiallyOn: Bool = false + ) { + self.title = title + self.content = content + self.isEnabled = isEnabled + self.spacingAfter = spacingAfter + self.isInitiallyOn = isInitiallyOn + self.isOnSubject = .init(isInitiallyOn) + } - public func makeView() -> UIView { - let view = UIView() - let titleLabel = UILabel() - let contentLabel = UILabel() - let switcherView = UISwitch() + public func makeView() -> UIView { + let view = UIView() + let titleLabel = UILabel() + let contentLabel = UILabel() + let switcherView = UISwitch() - titleLabel.text = title - contentLabel.text = content + titleLabel.text = title + contentLabel.text = content - switcherView.isOn = isInitiallyOn - switcherView.isEnabled = isEnabled - switcherView.onTintColor = Asset.brandPrimary.color + switcherView.isOn = isInitiallyOn + switcherView.isEnabled = isEnabled + switcherView.onTintColor = Asset.brandPrimary.color - titleLabel.textColor = Asset.neutralWeak.color - contentLabel.textColor = Asset.neutralActive.color + titleLabel.textColor = Asset.neutralWeak.color + contentLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) - contentLabel.font = Fonts.Mulish.regular.font(size: 16.0) + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + contentLabel.font = Fonts.Mulish.regular.font(size: 16.0) - view.addSubview(titleLabel) - view.addSubview(contentLabel) - view.addSubview(switcherView) + view.addSubview(titleLabel) + view.addSubview(contentLabel) + view.addSubview(switcherView) - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - } + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + } - contentLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(5) - $0.left.equalToSuperview() - $0.bottom.equalToSuperview() - } + contentLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(5) + $0.left.equalToSuperview() + $0.bottom.equalToSuperview() + } - switcherView.snp.makeConstraints { - $0.right.equalToSuperview() - $0.centerY.equalToSuperview() - } + switcherView.snp.makeConstraints { + $0.right.equalToSuperview() + $0.centerY.equalToSuperview() + } - switcherView.publisher(for: .valueChanged) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in isOnSubject.send(switcherView.isOn) } - .store(in: &cancellables) + switcherView.publisher(for: .valueChanged) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in isOnSubject.send(switcherView.isOn) } + .store(in: &cancellables) - return view - } + return view + } } diff --git a/Sources/DrawerFeature/Items/DrawerTable.swift b/Sources/DrawerFeature/Items/DrawerTable.swift index 665b660ea6656c3ad1ddb5a786cdbe7e508be61d..78f2b9299ca078c20fdbf426159d732a1af3366e 100644 --- a/Sources/DrawerFeature/Items/DrawerTable.swift +++ b/Sources/DrawerFeature/Items/DrawerTable.swift @@ -1,147 +1,148 @@ import UIKit import Shared import SnapKit +import AppResources enum DrawerTableSection { - case main + case main } public final class DrawerTable: DrawerItem { - private let view = UIView() - private let tableView = UITableView() - private var heightConstraint: Constraint? - private let dataSource: UITableViewDiffableDataSource<DrawerTableSection, DrawerTableCellModel> - - public var spacingAfter: CGFloat? = 0 - - public init(spacingAfter: CGFloat? = 10) { - self.dataSource = .init( - tableView: tableView, - cellProvider: { tableView, indexPath, model in - let cell: DrawerTableCell = tableView.dequeueReusableCell(forIndexPath: indexPath) - - cell.titleLabel.text = model.title - cell.avatarView.setupProfile( - title: model.title, - image: model.image, - size: .medium - ) - - if model.isCreator { - cell.subtitleLabel.text = "Creator" - cell.subtitleLabel.isHidden = false - cell.subtitleLabel.textColor = Asset.accentSafe.color - } else if !model.isConnection { - cell.subtitleLabel.text = "Not a connection" - cell.subtitleLabel.isHidden = false - cell.subtitleLabel.textColor = Asset.neutralSecondaryAlternative.color - } else { - cell.subtitleLabel.isHidden = true - } - - return cell - }) - - self.spacingAfter = spacingAfter - } + private let view = UIView() + private let tableView = UITableView() + private var heightConstraint: Constraint? + private let dataSource: UITableViewDiffableDataSource<DrawerTableSection, DrawerTableCellModel> + + public var spacingAfter: CGFloat? = 0 + + public init(spacingAfter: CGFloat? = 10) { + self.dataSource = .init( + tableView: tableView, + cellProvider: { tableView, indexPath, model in + let cell: DrawerTableCell = tableView.dequeueReusableCell(forIndexPath: indexPath) + + cell.titleLabel.text = model.title + cell.avatarView.setupProfile( + title: model.title, + image: model.image, + size: .medium + ) + + if model.isCreator { + cell.subtitleLabel.text = "Creator" + cell.subtitleLabel.isHidden = false + cell.subtitleLabel.textColor = Asset.accentSafe.color + } else if !model.isConnection { + cell.subtitleLabel.text = "Not a connection" + cell.subtitleLabel.isHidden = false + cell.subtitleLabel.textColor = Asset.neutralSecondaryAlternative.color + } else { + cell.subtitleLabel.isHidden = true + } - public func makeView() -> UIView { - tableView.register(DrawerTableCell.self) - tableView.dataSource = dataSource - tableView.separatorStyle = .none - tableView.backgroundColor = UIColor.white + return cell + }) - view.addSubview(tableView) + self.spacingAfter = spacingAfter + } - tableView.snp.makeConstraints { - $0.edges.equalToSuperview() - heightConstraint = $0.height.equalTo(1).priority(.low).constraint - } + public func makeView() -> UIView { + tableView.register(DrawerTableCell.self) + tableView.dataSource = dataSource + tableView.separatorStyle = .none + tableView.backgroundColor = UIColor.white - return view + view.addSubview(tableView) + + tableView.snp.makeConstraints { + $0.edges.equalToSuperview() + heightConstraint = $0.height.equalTo(1).priority(.low).constraint } - public func update(models: [DrawerTableCellModel]) { - let cellHeight = 56 - self.heightConstraint?.update(offset: cellHeight * models.count) + return view + } - var snapshot = NSDiffableDataSourceSnapshot<DrawerTableSection, DrawerTableCellModel>() - snapshot.appendSections([.main]) - snapshot.appendItems(models, toSection: .main) - dataSource.apply(snapshot, animatingDifferences: false) { [self] in - tableView.isScrollEnabled = tableView.contentSize.height > tableView.frame.height - } + public func update(models: [DrawerTableCellModel]) { + let cellHeight = 56 + self.heightConstraint?.update(offset: cellHeight * models.count) + + var snapshot = NSDiffableDataSourceSnapshot<DrawerTableSection, DrawerTableCellModel>() + snapshot.appendSections([.main]) + snapshot.appendItems(models, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: false) { [self] in + tableView.isScrollEnabled = tableView.contentSize.height > tableView.frame.height } + } } public struct DrawerTableCellModel: Hashable { - let id: Data - let title: String - let image: Data? - let isCreator: Bool - let isConnection: Bool - - public init( - id: Data, - title: String, - image: Data? = nil, - isCreator: Bool = false, - isConnection: Bool = true - ) { - self.id = id - self.title = title - self.image = image - self.isCreator = isCreator - self.isConnection = isConnection - } + let id: Data + let title: String + let image: Data? + let isCreator: Bool + let isConnection: Bool + + public init( + id: Data, + title: String, + image: Data? = nil, + isCreator: Bool = false, + isConnection: Bool = true + ) { + self.id = id + self.title = title + self.image = image + self.isCreator = isCreator + self.isConnection = isConnection + } } final class DrawerTableCell: UITableViewCell { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let avatarView = AvatarView() - let stackView = UIStackView() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = Asset.neutralWhite.color - - titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) - subtitleLabel.font = Fonts.Mulish.regular.font(size: 14.0) - titleLabel.textColor = Asset.neutralActive.color - - stackView.axis = .vertical - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(subtitleLabel) - - contentView.addSubview(avatarView) - contentView.addSubview(stackView) - - avatarView.snp.makeConstraints { - $0.width.equalTo(36) - $0.height.equalTo(36) - $0.top.equalToSuperview().offset(10) - $0.left.equalToSuperview() - $0.bottom.equalToSuperview().offset(-10) - } + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let avatarView = AvatarView() + let stackView = UIStackView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + selectionStyle = .none + backgroundColor = Asset.neutralWhite.color + + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + subtitleLabel.font = Fonts.Mulish.regular.font(size: 14.0) + titleLabel.textColor = Asset.neutralActive.color + + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + + contentView.addSubview(avatarView) + contentView.addSubview(stackView) + + avatarView.snp.makeConstraints { + $0.width.equalTo(36) + $0.height.equalTo(36) + $0.top.equalToSuperview().offset(10) + $0.left.equalToSuperview() + $0.bottom.equalToSuperview().offset(-10) + } - stackView.snp.makeConstraints { - $0.left.equalTo(avatarView.snp.right).offset(15) - $0.top.equalTo(avatarView) - $0.bottom.equalTo(avatarView) - $0.right.equalToSuperview() - } + stackView.snp.makeConstraints { + $0.left.equalTo(avatarView.snp.right).offset(15) + $0.top.equalTo(avatarView) + $0.bottom.equalTo(avatarView) + $0.right.equalToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - override func prepareForReuse() { - super.prepareForReuse() + override func prepareForReuse() { + super.prepareForReuse() - titleLabel.text = nil - subtitleLabel.text = nil - avatarView.prepareForReuse() - } + titleLabel.text = nil + subtitleLabel.text = nil + avatarView.prepareForReuse() + } } diff --git a/Sources/DrawerFeature/Items/DrawerText.swift b/Sources/DrawerFeature/Items/DrawerText.swift index 8cfeaffa7487d0fe644b7538fd245648ef4cd21c..beac93e6951e417a42e5f2844f730c228af11dbf 100644 --- a/Sources/DrawerFeature/Items/DrawerText.swift +++ b/Sources/DrawerFeature/Items/DrawerText.swift @@ -1,70 +1,71 @@ import UIKit import Shared +import AppResources public final class DrawerText: DrawerItem { - private let font: UIFont - private let text: String - private let color: UIColor - private let leftImage: UIImage? - private let alignment: NSTextAlignment - private let lineHeightMultiple: CGFloat - private let customAttributes: [NSAttributedString.Key: Any]? - private let stackView = UIStackView() + private let font: UIFont + private let text: String + private let color: UIColor + private let leftImage: UIImage? + private let alignment: NSTextAlignment + private let lineHeightMultiple: CGFloat + private let customAttributes: [NSAttributedString.Key: Any]? + private let stackView = UIStackView() - public var spacingAfter: CGFloat? = 0 + public var spacingAfter: CGFloat? = 0 - public init( - font: UIFont = Fonts.Mulish.regular.font(size: 16.0), - text: String, - color: UIColor = Asset.neutralActive.color, - alignment: NSTextAlignment = .left, - lineHeightMultiple: CGFloat = 1.1, - spacingAfter: CGFloat = 10, - customAttributes: [NSAttributedString.Key: Any]? = nil, - leftImage: UIImage? = nil - ) { - self.font = font - self.text = text - self.color = color - self.leftImage = leftImage - self.alignment = alignment - self.spacingAfter = spacingAfter - self.customAttributes = customAttributes - self.lineHeightMultiple = lineHeightMultiple - } - - public func makeView() -> UIView { - let label = UILabel() - label.numberOfLines = 0 + public init( + font: UIFont = Fonts.Mulish.regular.font(size: 16.0), + text: String, + color: UIColor = Asset.neutralActive.color, + alignment: NSTextAlignment = .left, + lineHeightMultiple: CGFloat = 1.1, + spacingAfter: CGFloat = 10, + customAttributes: [NSAttributedString.Key: Any]? = nil, + leftImage: UIImage? = nil + ) { + self.font = font + self.text = text + self.color = color + self.leftImage = leftImage + self.alignment = alignment + self.spacingAfter = spacingAfter + self.customAttributes = customAttributes + self.lineHeightMultiple = lineHeightMultiple + } - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = alignment - paragraphStyle.lineHeightMultiple = lineHeightMultiple + public func makeView() -> UIView { + let label = UILabel() + label.numberOfLines = 0 - let attrString = NSMutableAttributedString(string: text) - attrString.addAttributes([ - .paragraphStyle: paragraphStyle, - .foregroundColor: color, - .font: font as Any - ]) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = alignment + paragraphStyle.lineHeightMultiple = lineHeightMultiple - if let customAttributes = customAttributes { - attrString.addAttributes( - attributes: customAttributes, - betweenCharacters: "#" - ) - } + let attrString = NSMutableAttributedString(string: text) + attrString.addAttributes([ + .paragraphStyle: paragraphStyle, + .foregroundColor: color, + .font: font as Any + ]) - label.attributedText = attrString + if let customAttributes = customAttributes { + attrString.addAttributes( + attributes: customAttributes, + betweenCharacters: "#" + ) + } - if let image = leftImage { - let imageView = UIImageView() - imageView.image = image - stackView.addArrangedSubview(imageView) - } + label.attributedText = attrString - stackView.addArrangedSubview(label) - stackView.spacing = 5 - return stackView + if let image = leftImage { + let imageView = UIImageView() + imageView.image = image + stackView.addArrangedSubview(imageView) } + + stackView.addArrangedSubview(label) + stackView.spacing = 5 + return stackView + } } diff --git a/Sources/InputField/InputField.swift b/Sources/InputField/InputField.swift index e041db906a504c4a1e6295c84e14a456a18b357c..f0be951d47dbeeecebeb8abd5a3d56eb13c6e31c 100644 --- a/Sources/InputField/InputField.swift +++ b/Sources/InputField/InputField.swift @@ -1,351 +1,350 @@ import UIKit import Shared import Combine +import AppResources public final class InputField: UIView { - public enum Style { - case phone - case regular + public enum Style { + case phone + case regular + } + + public enum LeftView { + case image(UIImage) + } + + public enum RightView { + case image(UIImage) + case toggleSecureEntry + } + + public enum ValidationStatus: Equatable { + case valid(String?) + case invalid(String) + case unknown(String?) + } + + let title = UILabel() + let hide = UIButton() + let clear = UIButton() + let subtitle = UILabel() + + let outerStack = UIStackView() + let codeContainer = UIView() + let code = PhoneCodeField() + + let container = UIView() + let innerStack = UIStackView() + let left = UIImageView() + let field = UITextField() + + let toolbar = UIToolbar() + let toolbarButton = UIButton() + + var isPhone: Bool = false + + // MARK: Properties + + private var rightView: RightView? = .none { + didSet { set(rightView: rightView) } + } + + private var clearable: Bool = false + private var allowsEmptySpace: Bool = true + private var cancellables = Set<AnyCancellable>() + + private let codeSubject = PassthroughSubject<Void, Never>() + private let returnSubject = PassthroughSubject<Void, Never>() + private let textSubject = PassthroughSubject<String, Never>() + + public var codePublisher: AnyPublisher<Void, Never> { codeSubject.eraseToAnyPublisher() } + public var textPublisher: AnyPublisher<String, Never> { textSubject.eraseToAnyPublisher() } + public var returnPublisher: AnyPublisher<Void, Never> { returnSubject.eraseToAnyPublisher() } + + public init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + public func makeFirstResponder() { + field.becomeFirstResponder() + } + + public func setup( + style: Style = .regular, + title: String? = nil, + placeholder: String? = nil, + leftView: LeftView? = nil, + rightView: RightView? = nil, + accessibility: String? = nil, + subtitleAccessibility: String? = nil, + subtitleColor: UIColor = Asset.neutralWhite.color, + allowsEmptySpace: Bool = true, + keyboardType: UIKeyboardType = .default, + autocapitalization: UITextAutocapitalizationType = .sentences, + autoCorrect: UITextAutocorrectionType = .no, + contentType: UITextContentType? = nil, + returnKeyType: UIReturnKeyType = .done, + toolbarButtonTitle: String = Localized.Shared.done, + codeAccessibility: String? = nil, + clearable: Bool = false + ) { + self.title.text = title + self.set(leftView: leftView) + + self.rightView = rightView + self.field.attributedPlaceholder = NSAttributedString( + string: placeholder ?? "", + attributes: [ + .font: Fonts.Mulish.semiBold.font(size: 14.0), + .foregroundColor: Asset.neutralDisabled.color + ]) + + if contentType == .telephoneNumber { + isPhone = true + } else { + self.field.textContentType = contentType } - public enum LeftView { - case image(UIImage) + self.field.returnKeyType = returnKeyType + self.field.keyboardType = keyboardType + self.subtitle.textColor = subtitleColor + self.allowsEmptySpace = allowsEmptySpace + self.field.autocorrectionType = autoCorrect + self.field.accessibilityIdentifier = accessibility + self.field.autocapitalizationType = autocapitalization + self.subtitle.accessibilityIdentifier = subtitleAccessibility + self.clearable = clearable + + if style == .phone { + codeContainer.addSubview(code) + code.accessibilityIdentifier = codeAccessibility + code.snp.makeConstraints { $0.edges.equalToSuperview() } + outerStack.insertArrangedSubview(codeContainer, at: 0) + + code.publisher(for: .touchUpInside) + .sink { [weak codeSubject] in codeSubject?.send() } + .store(in: &cancellables) + + self.field.keyboardType = .numberPad + self.allowsEmptySpace = false + + toolbar.barTintColor = Asset.neutralWhite.color + toolbarButton.setTitle(toolbarButtonTitle, for: .normal) + toolbarButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + toolbarButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 17.0) + toolbar.setShadowImage(.color(Asset.neutralLine.color), forToolbarPosition: .any) + toolbarButton.addTarget(self, action: #selector(didTapDone), for: .touchUpInside) + toolbar.items = [UIBarButtonItem(customView: toolbarButton.pinning(at: .right(0)))] + + toolbar.sizeToFit() + self.field.inputAccessoryView = toolbar } - - public enum RightView { - case image(UIImage) - case toggleSecureEntry - } - - public enum ValidationStatus: Equatable { - case valid(String?) - case invalid(String) - case unknown(String?) - } - - let title = UILabel() - let hide = UIButton() - let clear = UIButton() - let subtitle = UILabel() - - let outerStack = UIStackView() - let codeContainer = UIView() - let code = PhoneCodeField() - - let container = UIView() - let innerStack = UIStackView() - let left = UIImageView() - let field = UITextField() - - let toolbar = UIToolbar() - let toolbarButton = UIButton() - - var isPhone: Bool = false - - // MARK: Properties - - private var rightView: RightView? = .none { - didSet { set(rightView: rightView) } + } + + public func set(prefix: String) { + code.content.text = prefix + } + + public func update(content: String) { + field.text = content + } + + public func update(placeholder: String) { + field.placeholder = placeholder + } + + public func update(status: ValidationStatus) { + switch status { + case .unknown(let text): + set(rightView: nil) + subtitle.text = text ?? " " + case .invalid(let text): + set(rightView: .image(Asset.sharedError.image)) + subtitle.text = text + case .valid(let text): + set(rightView: .image(Asset.sharedSuccess.image)) + subtitle.text = text ?? " " } + } - private var clearable: Bool = false - private var allowsEmptySpace: Bool = true - private var cancellables = Set<AnyCancellable>() - - private let codeSubject = PassthroughSubject<Void, Never>() - private let returnSubject = PassthroughSubject<Void, Never>() - private let textSubject = PassthroughSubject<String, Never>() + // MARK: Private - public var codePublisher: AnyPublisher<Void, Never> { codeSubject.eraseToAnyPublisher() } - public var textPublisher: AnyPublisher<String, Never> { textSubject.eraseToAnyPublisher() } - public var returnPublisher: AnyPublisher<Void, Never> { returnSubject.eraseToAnyPublisher() } - - public init() { - super.init(frame: .zero) - setup() + private func set(leftView: LeftView?) { + switch leftView { + case .image(let image): + left.image = image + left.tintColor = Asset.neutralDisabled.color + case .none: + innerStack.removeArrangedSubview(left) } - - required init?(coder: NSCoder) { nil } - - // MARK: Public - - public func makeFirstResponder() { - field.becomeFirstResponder() + } + + public func set(rightView: RightView?) { + switch rightView { + case.image(let image): + field.rightView = UIImageView(image: image) + case .toggleSecureEntry: + field.rightView = hide + field.isSecureTextEntry = true + hide.setImage(hideButtonImage(isSecureEntry: field.isSecureTextEntry), for: .normal) + case .none: + field.rightView = nil } - - public func setup( - style: Style = .regular, - title: String? = nil, - placeholder: String? = nil, - leftView: LeftView? = nil, - rightView: RightView? = nil, - accessibility: String? = nil, - subtitleAccessibility: String? = nil, - subtitleColor: UIColor = Asset.neutralWhite.color, - allowsEmptySpace: Bool = true, - keyboardType: UIKeyboardType = .default, - autocapitalization: UITextAutocapitalizationType = .sentences, - autoCorrect: UITextAutocorrectionType = .no, - contentType: UITextContentType? = nil, - returnKeyType: UIReturnKeyType = .done, - toolbarButtonTitle: String = Localized.Shared.done, - codeAccessibility: String? = nil, - clearable: Bool = false - ) { - self.title.text = title - self.set(leftView: leftView) - - self.rightView = rightView - self.field.attributedPlaceholder = NSAttributedString( - string: placeholder ?? "", - attributes: [ - .font: Fonts.Mulish.semiBold.font(size: 14.0), - .foregroundColor: Asset.neutralDisabled.color - ]) - - if contentType == .telephoneNumber { - isPhone = true - } else { - self.field.textContentType = contentType - } - - self.field.returnKeyType = returnKeyType - self.field.keyboardType = keyboardType - self.subtitle.textColor = subtitleColor - self.allowsEmptySpace = allowsEmptySpace - self.field.autocorrectionType = autoCorrect - self.field.accessibilityIdentifier = accessibility - self.field.autocapitalizationType = autocapitalization - self.subtitle.accessibilityIdentifier = subtitleAccessibility - self.clearable = clearable - - if style == .phone { - codeContainer.addSubview(code) - code.accessibilityIdentifier = codeAccessibility - code.snp.makeConstraints { $0.edges.equalToSuperview() } - outerStack.insertArrangedSubview(codeContainer, at: 0) - - code.publisher(for: .touchUpInside) - .sink { [weak codeSubject] in codeSubject?.send() } - .store(in: &cancellables) - - self.field.keyboardType = .numberPad - self.allowsEmptySpace = false - - toolbar.barTintColor = Asset.neutralWhite.color - toolbarButton.setTitle(toolbarButtonTitle, for: .normal) - toolbarButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - toolbarButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 17.0) - toolbar.setShadowImage(.color(Asset.neutralLine.color), forToolbarPosition: .any) - toolbarButton.addTarget(self, action: #selector(didTapDone), for: .touchUpInside) - toolbar.items = [UIBarButtonItem(customView: toolbarButton.pinning(at: .right(0)))] - - toolbar.sizeToFit() - self.field.inputAccessoryView = toolbar - } - } - - public func set(prefix: String) { - code.content.text = prefix - } - - public func update(content: String) { - field.text = content - } - - public func update(placeholder: String) { - field.placeholder = placeholder - } - - public func update(status: ValidationStatus) { - switch status { - case .unknown(let text): - set(rightView: nil) - subtitle.text = text ?? " " - case .invalid(let text): - set(rightView: .image(Asset.sharedError.image)) - subtitle.text = text - case .valid(let text): - set(rightView: .image(Asset.sharedSuccess.image)) - subtitle.text = text ?? " " - } - } - - // MARK: Private - - private func set(leftView: LeftView?) { - switch leftView { - case .image(let image): - left.image = image - left.tintColor = Asset.neutralDisabled.color - case .none: - innerStack.removeArrangedSubview(left) - } + } + + private func hideButtonImage(isSecureEntry: Bool) -> UIImage? { + let openImage = Asset.eyeOpen.image.withTintColor(Asset.neutralWeak.color) + let closedImage = Asset.eyeClosed.image.withTintColor(Asset.neutralWeak.color) + return isSecureEntry ? closedImage : openImage + } + + private func setup() { + subtitle.textAlignment = .right + subtitle.numberOfLines = 0 + container.layer.cornerRadius = 4 + container.backgroundColor = Asset.neutralSecondary.color + + codeContainer.layer.cornerRadius = 4 + codeContainer.backgroundColor = Asset.neutralSecondary.color + + title.textColor = Asset.neutralWeak.color + field.textColor = Asset.neutralActive.color + subtitle.textColor = Asset.neutralWhite.color + + title.font = Fonts.Mulish.regular.font(size: 12.0) + field.font = Fonts.Mulish.semiBold.font(size: 14.0) + subtitle.font = Fonts.Mulish.regular.font(size: 12.0) + + clear.setImage(Asset.sharedCross.image, for: .normal) + + field.textPublisher + .sink { [weak textSubject] in textSubject?.send($0) } + .store(in: &cancellables) + + hide.publisher(for: .touchUpInside) + .sink { [unowned self] _ in + field.isSecureTextEntry.toggle() + hide.setImage(hideButtonImage(isSecureEntry: field.isSecureTextEntry), for: .normal) + }.store(in: &cancellables) + + clear.publisher(for: .touchUpInside) + .sink { [unowned self] in + field.text = "" + textSubject.send("") + field.resignFirstResponder() + }.store(in: &cancellables) + + field.delegate = self + field.rightViewMode = .always + + left.contentMode = .center + left.setContentHuggingPriority(.required, for: .horizontal) + + innerStack.spacing = 12 + innerStack.addArrangedSubview(left) + innerStack.addArrangedSubview(field) + + outerStack.spacing = 8 + container.addSubview(innerStack) + outerStack.addArrangedSubview(container) + + addSubview(title) + addSubview(outerStack) + addSubview(subtitle) + + setupConstraints() + } + + private func setupConstraints() { + title.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(8) } - public func set(rightView: RightView?) { - switch rightView { - case.image(let image): - field.rightView = UIImageView(image: image) - case .toggleSecureEntry: - field.rightView = hide - field.isSecureTextEntry = true - hide.setImage(hideButtonImage(isSecureEntry: field.isSecureTextEntry), for: .normal) - case .none: - field.rightView = nil - } + outerStack.snp.makeConstraints { + $0.top.equalTo(title.snp.bottom).offset(10) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.height.equalTo(36) } - private func hideButtonImage(isSecureEntry: Bool) -> UIImage? { - let openImage = Asset.eyeOpen.image.withTintColor(Asset.neutralWeak.color) - let closedImage = Asset.eyeClosed.image.withTintColor(Asset.neutralWeak.color) - return isSecureEntry ? closedImage : openImage + innerStack.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(11) + $0.right.equalToSuperview().offset(-11) + $0.bottom.equalToSuperview() } - private func setup() { - subtitle.textAlignment = .right - subtitle.numberOfLines = 0 - container.layer.cornerRadius = 4 - container.backgroundColor = Asset.neutralSecondary.color - - codeContainer.layer.cornerRadius = 4 - codeContainer.backgroundColor = Asset.neutralSecondary.color - - title.textColor = Asset.neutralWeak.color - field.textColor = Asset.neutralActive.color - subtitle.textColor = Asset.neutralWhite.color - - title.font = Fonts.Mulish.regular.font(size: 12.0) - field.font = Fonts.Mulish.semiBold.font(size: 14.0) - subtitle.font = Fonts.Mulish.regular.font(size: 12.0) - - clear.setImage(Asset.sharedCross.image, for: .normal) - - field.textPublisher - .sink { [weak textSubject] in textSubject?.send($0) } - .store(in: &cancellables) - - hide.publisher(for: .touchUpInside) - .sink { [unowned self] _ in - field.isSecureTextEntry.toggle() - hide.setImage(hideButtonImage(isSecureEntry: field.isSecureTextEntry), for: .normal) - }.store(in: &cancellables) - - clear.publisher(for: .touchUpInside) - .sink { [unowned self] in - field.text = "" - textSubject.send("") - field.resignFirstResponder() - }.store(in: &cancellables) - - field.delegate = self - field.rightViewMode = .always - - left.contentMode = .center - left.setContentHuggingPriority(.required, for: .horizontal) - - innerStack.spacing = 12 - innerStack.addArrangedSubview(left) - innerStack.addArrangedSubview(field) - - outerStack.spacing = 8 - container.addSubview(innerStack) - outerStack.addArrangedSubview(container) - - addSubview(title) - addSubview(outerStack) - addSubview(subtitle) - - setupConstraints() + subtitle.snp.makeConstraints { + $0.top.equalTo(outerStack.snp.bottom).offset(8) + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + $0.left.greaterThanOrEqualToSuperview() } + } - private func setupConstraints() { - title.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview().offset(8) - } - - outerStack.snp.makeConstraints { make in - make.top.equalTo(title.snp.bottom).offset(10) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.height.equalTo(36) - } - - innerStack.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview().offset(11) - make.right.equalToSuperview().offset(-11) - make.bottom.equalToSuperview() - } + @objc private func didTapDone() { + returnSubject.send() + } - subtitle.snp.makeConstraints { make in - make.top.equalTo(outerStack.snp.bottom).offset(8) - make.right.equalToSuperview() - make.bottom.equalToSuperview() - make.left.greaterThanOrEqualToSuperview() - } + public func textFieldDidBeginEditing(_ textField: UITextField) { + if clearable { + field.rightView = clear } + } - @objc private func didTapDone() { - returnSubject.send() + public func textFieldDidEndEditing(_ textField: UITextField) { + if clearable { + set(rightView: rightView) } - - public func textFieldDidBeginEditing(_ textField: UITextField) { - if clearable { - field.rightView = clear - } + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + returnSubject.send() + return true + } + + public func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + if isPhone { + if string.count > 1 { + textField.text = string.replaceCharactersFromSet(characterSet: .decimalDigits.inverted) + textSubject.send(textField.text ?? "") + return false + } else { + return string.rangeOfCharacter(from: .decimalDigits) != nil || string == "" + } } - public func textFieldDidEndEditing(_ textField: UITextField) { - if clearable { - set(rightView: rightView) + if !allowsEmptySpace { + if string.count > 1 { + if textField.textContentType == .emailAddress && [".us", ".net", ".edu", ".org", ".com"].contains(string) { + textSubject.send(textField.text ?? "") + return true } - } - public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - returnSubject.send() - return true + textField.text = string.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) + textSubject.send(textField.text ?? "") + return false + } else { + return string != " " + } } - public func textField( - _ textField: UITextField, - shouldChangeCharactersIn range: NSRange, - replacementString string: String - ) -> Bool { - if isPhone { - if string.count > 1 { - textField.text = string.replaceCharactersFromSet(characterSet: .decimalDigits.inverted) - textSubject.send(textField.text ?? "") - return false - } else { - return string.rangeOfCharacter(from: .decimalDigits) != nil || string == "" - } - } - - if !allowsEmptySpace { - if string.count > 1 { - if textField.textContentType == .emailAddress && [".us", ".net", ".edu", ".org", ".com"].contains(string) { - textSubject.send(textField.text ?? "") - return true - } - - textField.text = string.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) - textSubject.send(textField.text ?? "") - return false - } else { - return string != " " - } - } - - return true - } + return true + } } extension InputField: UITextFieldDelegate {} private extension String { - func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String { - return components(separatedBy: characterSet).joined(separator: replacementString) - } + func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String { + return components(separatedBy: characterSet).joined(separator: replacementString) + } } diff --git a/Sources/InputField/OutlinedInputField.swift b/Sources/InputField/OutlinedInputField.swift index 3bb8ad3005167b9e9e99511243da912f7082d434..9ae881bbf855cbff43b463e0e0cf2425568b9c2a 100644 --- a/Sources/InputField/OutlinedInputField.swift +++ b/Sources/InputField/OutlinedInputField.swift @@ -1,85 +1,86 @@ import UIKit import Shared import Combine +import AppResources public final class OutlinedInputField: UIView { - private let stackView = UIStackView() - private let textField = UITextField() - private let placeholderLabel = UILabel() - private let inputContainerView = UIView() + private let stackView = UIStackView() + private let textField = UITextField() + private let placeholderLabel = UILabel() + private let inputContainerView = UIView() - private let secureInputButton = SecureInputButton() + private let secureInputButton = SecureInputButton() - public var textPublisher: AnyPublisher<String, Never> { - textField.textPublisher - } + public var textPublisher: AnyPublisher<String, Never> { + textField.textPublisher + } - public init() { - super.init(frame: .zero) + public init() { + super.init(frame: .zero) - layer.borderWidth = 1.0 - layer.cornerRadius = 4.0 - layer.masksToBounds = true - layer.borderColor = Asset.neutralWeak.color.cgColor + layer.borderWidth = 1.0 + layer.cornerRadius = 4.0 + layer.masksToBounds = true + layer.borderColor = Asset.neutralWeak.color.cgColor - textField.delegate = self - textField.backgroundColor = .clear - textField.textColor = Asset.neutralDark.color - placeholderLabel.textColor = Asset.neutralWeak.color - placeholderLabel.font = Fonts.Mulish.regular.font(size: 16.0) + textField.delegate = self + textField.backgroundColor = .clear + textField.textColor = Asset.neutralDark.color + placeholderLabel.textColor = Asset.neutralWeak.color + placeholderLabel.font = Fonts.Mulish.regular.font(size: 16.0) - secureInputButton.button.addTarget(self, action: #selector(didTapRight), for: .touchUpInside) + secureInputButton.button.addTarget(self, action: #selector(didTapRight), for: .touchUpInside) - inputContainerView.addSubview(placeholderLabel) - inputContainerView.addSubview(textField) + inputContainerView.addSubview(placeholderLabel) + inputContainerView.addSubview(textField) - stackView.addArrangedSubview(inputContainerView) - stackView.addArrangedSubview(secureInputButton) + stackView.addArrangedSubview(inputContainerView) + stackView.addArrangedSubview(secureInputButton) - addSubview(stackView) + addSubview(stackView) - placeholderLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(15) - $0.right.lessThanOrEqualToSuperview().offset(-15) - $0.bottom.equalToSuperview().offset(-18) - } + placeholderLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.lessThanOrEqualToSuperview().offset(-15) + $0.bottom.equalToSuperview().offset(-18) + } - textField.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(15) - $0.right.equalToSuperview().offset(-15) - $0.bottom.equalToSuperview().offset(-18) - } + textField.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + $0.bottom.equalToSuperview().offset(-18) + } - stackView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - public func setup(title: String, sensitive: Bool = false) { - placeholderLabel.text = title - textField.isSecureTextEntry = sensitive - secureInputButton.isHidden = !sensitive - } + public func setup(title: String, sensitive: Bool = false) { + placeholderLabel.text = title + textField.isSecureTextEntry = sensitive + secureInputButton.isHidden = !sensitive + } - @objc private func didTapRight() { - textField.isSecureTextEntry.toggle() - secureInputButton.setSecure(textField.isSecureTextEntry) - } + @objc private func didTapRight() { + textField.isSecureTextEntry.toggle() + secureInputButton.setSecure(textField.isSecureTextEntry) + } } extension OutlinedInputField: UITextFieldDelegate { - public func textField( - _ textField: UITextField, - shouldChangeCharactersIn range: NSRange, - replacementString string: String - ) -> Bool { - placeholderLabel.alpha = (textField.text! as NSString) - .replacingCharacters(in: range, with: string) - .count > 0 ? 0.0 : 1.0 - return true - } + public func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + placeholderLabel.alpha = (textField.text! as NSString) + .replacingCharacters(in: range, with: string) + .count > 0 ? 0.0 : 1.0 + return true + } } diff --git a/Sources/InputField/PhoneCodeField.swift b/Sources/InputField/PhoneCodeField.swift index 2bb6b4343a94f2f93736df04bfcbe7883cbdd88d..e0504384c1873517e43ddaa43db422894846b7e3 100644 --- a/Sources/InputField/PhoneCodeField.swift +++ b/Sources/InputField/PhoneCodeField.swift @@ -1,34 +1,26 @@ import UIKit import Shared +import AppResources final class PhoneCodeField: UIButton { - // MARK: UI + public let content = UILabel() - public let content = UILabel() + public init() { + super.init(frame: .zero) - // MARK: Lifecycle + content.textColor = Asset.neutralActive.color + content.font = Fonts.Mulish.semiBold.font(size: 14.0) - public init() { - super.init(frame: .zero) - setup() - } - - public required init?(coder: NSCoder) { nil } - - // MARK: Private + addSubview(content) - private func setup() { - content.textColor = Asset.neutralActive.color - content.font = Fonts.Mulish.semiBold.font(size: 14.0) - - addSubview(content) - - content.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview().offset(11) - make.right.equalToSuperview().offset(-11) - make.width.equalTo(60) - make.bottom.equalToSuperview() - } + content.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(11) + $0.right.equalToSuperview().offset(-11) + $0.width.equalTo(60) + $0.bottom.equalToSuperview() } + } + + public required init?(coder: NSCoder) { nil } } diff --git a/Sources/InputField/SecureInputButton.swift b/Sources/InputField/SecureInputButton.swift index 1f2e6b20751370755ec23b966c153ca5440c9f32..d40aa67be4143483570abb59750c528b9172d500 100644 --- a/Sources/InputField/SecureInputButton.swift +++ b/Sources/InputField/SecureInputButton.swift @@ -1,31 +1,32 @@ import UIKit import Shared +import AppResources final class SecureInputButton: UIView { - private(set) var button = UIButton() - private let color = Asset.neutralSecondaryAlternative.color - private lazy var openedImage = Asset.eyeOpen.image.withTintColor(color) - private lazy var closedImage = Asset.eyeClosed.image.withTintColor(color) + private(set) var button = UIButton() + private let color = Asset.neutralSecondaryAlternative.color + private lazy var openedImage = Asset.eyeOpen.image.withTintColor(color) + private lazy var closedImage = Asset.eyeClosed.image.withTintColor(color) - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - button.setContentCompressionResistancePriority(.required, for: .horizontal) - button.setImage(Asset.eyeClosed.image.withTintColor(color), for: .normal) + button.setContentCompressionResistancePriority(.required, for: .horizontal) + button.setImage(Asset.eyeClosed.image.withTintColor(color), for: .normal) - addSubview(button) + addSubview(button) - button.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview().offset(10) - $0.right.equalToSuperview().offset(-10) - $0.bottom.equalToSuperview() - } + button.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(10) + $0.right.equalToSuperview().offset(-10) + $0.bottom.equalToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func setSecure(_ bool: Bool) { - button.setImage(bool ? closedImage : openedImage, for: .normal) - } + func setSecure(_ bool: Bool) { + button.setImage(bool ? closedImage : openedImage, for: .normal) + } } diff --git a/Sources/InputField/Validator.swift b/Sources/InputField/Validator.swift index b649bd5df0e51e8e0361f12570794236c4374848..8d77034eec63a0178f0bccdfd4737cd062ed93e0 100644 --- a/Sources/InputField/Validator.swift +++ b/Sources/InputField/Validator.swift @@ -1,5 +1,6 @@ import Shared import Foundation +import AppResources private enum Constants { static let codeMinimum = Localized.Validator.Code.minimum diff --git a/Sources/Keychain/KeychainHandler.swift b/Sources/Keychain/KeychainHandler.swift index 26df90bdb133dffef5dae41cc126be53fe242606..3ed93be94118e9ec41f2ab86d80cf177d7fe0225 100644 --- a/Sources/Keychain/KeychainHandler.swift +++ b/Sources/Keychain/KeychainHandler.swift @@ -2,50 +2,50 @@ import Foundation import KeychainAccess public enum KeychainSFTP: String { - case pwd - case host - case username + case pwd + case host + case username } public protocol KeychainHandling { - func clear() throws - func getPassword() throws -> Data? - func remove(_ key: String) throws - func store(password pwd: Data) throws + func clear() throws + func getPassword() throws -> Data? + func remove(_ key: String) throws + func store(password pwd: Data) throws - func get(key: KeychainSFTP) throws -> String? - func store(key: KeychainSFTP, value: String) throws + func get(key: KeychainSFTP) throws -> String? + func store(key: KeychainSFTP, value: String) throws } public struct KeychainHandler: KeychainHandling { - private let keychain: Keychain - private let password = "password" + private let keychain: Keychain + private let password = "password" - public init() { - self.keychain = Keychain(service: "XXM") - } + public init() { + self.keychain = Keychain(service: "XXM") + } - public func remove(_ key: String) throws { - try keychain.remove(key) - } + public func remove(_ key: String) throws { + try keychain.remove(key) + } - public func clear() throws { - try keychain.removeAll() - } + public func clear() throws { + try keychain.removeAll() + } - public func store(password pwd: Data) throws { - try keychain.set(pwd, key: password) - } + public func store(password pwd: Data) throws { + try keychain.set(pwd, key: password) + } - public func getPassword() throws -> Data? { - try keychain.getData(password) - } + public func getPassword() throws -> Data? { + try keychain.getData(password) + } - public func get(key: KeychainSFTP) throws -> String? { - try keychain.get(key.rawValue) - } + public func get(key: KeychainSFTP) throws -> String? { + try keychain.get(key.rawValue) + } - public func store(key: KeychainSFTP, value: String) throws { - try keychain.set(value, key: key.rawValue) - } + public func store(key: KeychainSFTP, value: String) throws { + try keychain.set(value, key: key.rawValue) + } } diff --git a/Sources/VersionChecking/BackendVersionInformation.swift b/Sources/LaunchFeature/BackendVersionInformation.swift similarity index 100% rename from Sources/VersionChecking/BackendVersionInformation.swift rename to Sources/LaunchFeature/BackendVersionInformation.swift diff --git a/Sources/VersionChecking/DappVersionInformation.swift b/Sources/LaunchFeature/DappVersionInformation.swift similarity index 100% rename from Sources/VersionChecking/DappVersionInformation.swift rename to Sources/LaunchFeature/DappVersionInformation.swift diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index 48a8e1fe8b08cb9bad37448dabe77bbe35464e93..dbc4067bbfbdfeb388cf52a84b05eff5c24ff01e 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -1,13 +1,14 @@ import UIKit import Shared import Combine -import PushFeature import Navigation +import PushFeature import DrawerFeature -import DI +import AppResources +import ComposableArchitecture public final class LaunchController: UIViewController { - @Dependency var navigator: Navigator + @Dependency(\.navigator) var navigator: Navigator private lazy var screenView = LaunchView() @@ -54,18 +55,18 @@ public final class LaunchController: UIViewController { .sink { [unowned self] in guard $0.shouldPushChats == false else { guard $0.shouldShowTerms == false else { - navigator.perform(PresentTermsAndConditions(popAllowed: false)) + navigator.perform(PresentTermsAndConditions(replacing: true, on: navigationController!)) return } if let route = pendingPushRoute { hasPendingPushRoute(route) return } - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) return } guard $0.shouldPushOnboarding == false else { - navigator.perform(PresentOnboardingStart()) + navigator.perform(PresentOnboardingStart(on: navigationController!)) return } if let update = $0.shouldOfferUpdate { @@ -77,21 +78,24 @@ public final class LaunchController: UIViewController { private func hasPendingPushRoute(_ route: PushRouter.Route) { switch route { case .requests: - navigator.perform(PresentRequests()) + navigator.perform(PresentRequests(on: navigationController!)) case .search(username: let username): - navigator.perform(PresentSearch(searching: username)) + navigator.perform(PresentSearch( + searching: username, + replacing: true, + on: navigationController!)) case .groupChat(id: let groupId): if let info = viewModel.getGroupInfoWith(groupId: groupId) { - navigator.perform(PresentGroupChat(model: info)) + navigator.perform(PresentGroupChat(groupInfo: info, on: navigationController!)) return } - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) case .contactChat(id: let userId): if let model = viewModel.getContactWith(userId: userId) { - navigator.perform(PresentChat(contact: model)) + navigator.perform(PresentChat(contact: model, on: navigationController!)) return } - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) } } @@ -152,6 +156,6 @@ public final class LaunchController: UIViewController { axis: .vertical, views: actions ) - ], dismissable: false)) + ], isDismissable: false, from: self)) } } diff --git a/Sources/LaunchFeature/LaunchView.swift b/Sources/LaunchFeature/LaunchView.swift index 8a9fc7a43cfdf3b3b498b365c57763b9c2d7d0d0..b1c3982f558dc0b79c2e369c68f1e5b79f442723 100644 --- a/Sources/LaunchFeature/LaunchView.swift +++ b/Sources/LaunchFeature/LaunchView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class LaunchView: UIView { let imageView = UIImageView() diff --git a/Sources/LaunchFeature/LaunchViewModel+Database.swift b/Sources/LaunchFeature/LaunchViewModel+Database.swift index e2c7558b42680eed2d26722dc6e67f23c686c565..15ae6c42c88abf3f095c05d666720761707ffd22 100644 --- a/Sources/LaunchFeature/LaunchViewModel+Database.swift +++ b/Sources/LaunchFeature/LaunchViewModel+Database.swift @@ -1,7 +1,6 @@ import XXModels import Foundation import XXDatabase -import DI import XXLegacyDatabaseMigrator extension LaunchViewModel { diff --git a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift index 9871a24d1b8ffd21b98385b3abc7a896c46b72c3..abf90591ceb280b045fb00babcbc65ee111337a9 100644 --- a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift +++ b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift @@ -1,8 +1,6 @@ -import DI import Shared import XXClient import XXModels -import XXLogger import Foundation import XXMessengerClient @@ -93,61 +91,6 @@ extension LaunchViewModel { })) } - func handleIncomingTransfer(_ receivedFile: ReceivedFile) { - // if var model = try? database.saveFileTransfer(.init( - // id: receivedFile.transferId, - // contactId: receivedFile.senderId, - // name: receivedFile.name, - // type: receivedFile.type, - // data: nil, - // progress: 0.0, - // isIncoming: true, - // createdAt: Date() - // )) { - // try! database.saveMessage(.init( - // networkId: nil, - // senderId: receivedFile.senderId, - // recipientId: messenger.e2e.get()!.getContact().getId(), - // groupId: nil, - // date: Date(), - // status: .receiving, - // isUnread: false, - // text: "", - // replyMessageId: nil, - // roundURL: nil, - // fileTransferId: model.id - // )) - // - // if let manager: XXClient.FileTransfer = try? DI.Container.shared.resolve() { - // print(">>> registerReceivedProgressCallback") - // - // try! manager.registerReceivedProgressCallback( - // transferId: receivedFile.transferId, - // period: 1_000, - // callback: .init(handle: { [weak self] in - // guard let self else { return } - // switch $0 { - // case .success(let cb): - // if cb.progress.completed { - // model.progress = 100 - // model.data = try! manager.receive(transferId: receivedFile.transferId) - // } else { - // model.progress = Float(cb.progress.transmitted/cb.progress.total) - // } - // - // model = try! self.database.saveFileTransfer(model) - // - // case .failure(let error): - // print(error.localizedDescription) - // } - // }) - // ) - // } else { - // //print(DI.Container.shared.dependencies) - // } - // } - } - func handleDirectRequest(from contact: XXClient.Contact) { guard let id = try? contact.getId() else { fatalError("Couldn't extract ID from contact request arrived.") @@ -370,24 +313,6 @@ extension LaunchViewModel { DI.Container.shared.register(manager) } - func generateTransferManager() throws { - // let manager = try InitFileTransfer.live( - // e2eId: messenger.e2e()!.getId(), - // callback: .init(handle: { [weak self] in - // guard let self else { return } - // - // switch $0 { - // case .success(let receivedFile): - // self.handleIncomingTransfer(receivedFile, messenger: messenger) - // case .failure(let error): - // print(error.localizedDescription) - // } - // }) - // ) - // - // DI.Container.shared.register(manager) - } - func generateTrafficManager() throws { let manager = try NewDummyTrafficManager.live( cMixId: messenger.e2e()!.getId() @@ -420,7 +345,6 @@ extension LaunchViewModel { try generateGroupManager() try generateTrafficManager() - try generateTransferManager() listenToNetworkUpdates() if messenger.isLoggedIn() == false { @@ -449,3 +373,59 @@ extension LaunchViewModel { // TODO: Biometric auth } } + + +//func handleIncomingTransfer(_ receivedFile: ReceivedFile) { +// if var model = try? database.saveFileTransfer(.init( +// id: receivedFile.transferId, +// contactId: receivedFile.senderId, +// name: receivedFile.name, +// type: receivedFile.type, +// data: nil, +// progress: 0.0, +// isIncoming: true, +// createdAt: Date() +// )) { +// try! database.saveMessage(.init( +// networkId: nil, +// senderId: receivedFile.senderId, +// recipientId: messenger.e2e.get()!.getContact().getId(), +// groupId: nil, +// date: Date(), +// status: .receiving, +// isUnread: false, +// text: "", +// replyMessageId: nil, +// roundURL: nil, +// fileTransferId: model.id +// )) +// +// if let manager: XXClient.FileTransfer = try? DI.Container.shared.resolve() { +// print(">>> registerReceivedProgressCallback") +// +// try! manager.registerReceivedProgressCallback( +// transferId: receivedFile.transferId, +// period: 1_000, +// callback: .init(handle: { [weak self] in +// guard let self else { return } +// switch $0 { +// case .success(let cb): +// if cb.progress.completed { +// model.progress = 100 +// model.data = try! manager.receive(transferId: receivedFile.transferId) +// } else { +// model.progress = Float(cb.progress.transmitted/cb.progress.total) +// } +// +// model = try! self.database.saveFileTransfer(model) +// +// case .failure(let error): +// print(error.localizedDescription) +// } +// }) +// ) +// } else { +// //print(DI.Container.shared.dependencies) +// } +// } +//} diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 2097750bd4de11937e831653e4c56efdecc0ca4a..3bd27c5e7669f6f9bf98e6c11ab25cddfb870b85 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -8,13 +8,11 @@ import CloudFiles import Foundation import Permissions import BackupFeature -import NetworkMonitor import VersionChecking import ReportingFeature import CombineSchedulers import CloudFilesDropbox import XXMessengerClient -import DI import class XXClient.Cancellable diff --git a/Sources/VersionChecking/VersionChecking.swift b/Sources/LaunchFeature/VersionChecking.swift similarity index 95% rename from Sources/VersionChecking/VersionChecking.swift rename to Sources/LaunchFeature/VersionChecking.swift index 5acc10b5a6def59c701fa794cd2fe720b532d5ce..5dce368728baf174cf24c92b88189cc4b2b7ee0f 100644 --- a/Sources/VersionChecking/VersionChecking.swift +++ b/Sources/LaunchFeature/VersionChecking.swift @@ -14,6 +14,8 @@ public struct VersionCheck { } public extension VersionCheck { + static let unimplemented: Self = .init(verify: { _ in fatalError() }) + static let mock: Self = .init { $0(.upToDate) } static let live: Self = .init { completion in diff --git a/Sources/MenuFeature/Controllers/MenuController.swift b/Sources/MenuFeature/Controllers/MenuController.swift index 6b2cd52cd380421ab923b4fb994fbad1a02c01af..1c7623d54d4f74dd0fb30d7c51c4876c067f2db9 100644 --- a/Sources/MenuFeature/Controllers/MenuController.swift +++ b/Sources/MenuFeature/Controllers/MenuController.swift @@ -74,7 +74,7 @@ public final class MenuController: UIViewController { .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { [weak self] in guard let self, self.currentItem != .scan else { return } - self.navigator.perform(PresentScan()) + self.navigator.perform(PresentScan(on: self.navigationController!)) } }.store(in: &cancellables) @@ -86,7 +86,7 @@ public final class MenuController: UIViewController { .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { [weak self] in guard let self, self.currentItem != .profile else { return } - self.navigator.perform(PresentProfile()) + self.navigator.perform(PresentProfile(on: self.navigationController!)) } }.store(in: &cancellables) @@ -97,7 +97,7 @@ public final class MenuController: UIViewController { .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { [weak self] in guard let self, self.currentItem != .scan else { return } - self.navigator.perform(PresentScan()) + self.navigator.perform(PresentScan(on: self.navigationController!)) } }.store(in: &cancellables) @@ -108,7 +108,7 @@ public final class MenuController: UIViewController { .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { [weak self] in guard let self, self.currentItem != .chats else { return } - self.navigator.perform(PresentChatList()) + self.navigator.perform(PresentChatList(on: self.navigationController!)) } }.store(in: &cancellables) @@ -119,7 +119,7 @@ public final class MenuController: UIViewController { .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { [weak self] in guard let self, self.currentItem != .contacts else { return } - self.navigator.perform(PresentContactList()) + self.navigator.perform(PresentContactList(on: self.navigationController!)) } }.store(in: &cancellables) @@ -130,7 +130,7 @@ public final class MenuController: UIViewController { .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { [weak self] in guard let self, self.currentItem != .settings else { return } - self.navigator.perform(PresentSettings()) + self.navigator.perform(PresentSettings(on: self.navigationController!)) } }.store(in: &cancellables) @@ -158,7 +158,7 @@ public final class MenuController: UIViewController { .sink { [unowned self] in navigator.perform(DismissModal(from: self)) { [weak self] in guard let self, self.currentItem != .requests else { return } - self.navigator.perform(PresentRequests()) + self.navigator.perform(PresentRequests(on: self.navigationController!)) } }.store(in: &cancellables) @@ -188,7 +188,7 @@ public final class MenuController: UIViewController { guard let self, self.currentItem != .share else { return } self.navigator.perform(PresentActivitySheet(items: [ Localized.Menu.shareContent(self.viewModel.referralDeeplink) - ])) + ], from: self)) } }.store(in: &cancellables) @@ -239,6 +239,6 @@ public final class MenuController: UIViewController { spacingAfter: 39 ), actionButton - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/NetworkMonitor/MockNetworkMonitor.swift b/Sources/NetworkMonitor/MockNetworkMonitor.swift deleted file mode 100644 index 30bf846df9e36359ed80a7572a966e56091dce4d..0000000000000000000000000000000000000000 --- a/Sources/NetworkMonitor/MockNetworkMonitor.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Combine -import Foundation - -public struct MockNetworkMonitor: NetworkMonitoring { - private let statusRelay = PassthroughSubject<NetworkStatus, Never>() - - public var connType: AnyPublisher<ConnectionType, Never> { - Just(.wifi).eraseToAnyPublisher() - } - - public var statusPublisher: AnyPublisher<NetworkStatus, Never> { - statusRelay.eraseToAnyPublisher() - } - - public var xxStatus: NetworkStatus { - .available - } - - public init() { - // TODO - } - - public func start() { - simulateOscilation(.available) - } - - public func update(_ status: Bool) { - // TODO - } - - private func simulateOscilation(_ status: NetworkStatus) { - statusRelay.send(status) - - if status == .available { - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { - simulateOscilation(.internetNotAvailable) - } - } else if status == .internetNotAvailable { - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - simulateOscilation(.available) - } - } - } -} diff --git a/Sources/NetworkMonitor/NetworkMonitor.swift b/Sources/NetworkMonitor/NetworkMonitor.swift deleted file mode 100644 index 84899d7eb063acaab0f862fbb1b0031fa0a32d37..0000000000000000000000000000000000000000 --- a/Sources/NetworkMonitor/NetworkMonitor.swift +++ /dev/null @@ -1,91 +0,0 @@ -// https://www.reddit.com/r/swift/comments/ir8wn5/network_connectivity_is_always_unsatisfied_when/ - -import Network -import Combine -import XXClient -import Foundation - -public enum NetworkStatus: Equatable { - case unknown - case available - case xxNotAvailable - case internetNotAvailable -} - -public enum ConnectionType { - case wifi - case ethernet - case cellular - case unknown -} - -public protocol NetworkMonitoring { - func start() - func update(_ status: Bool) - - var xxStatus: NetworkStatus { get } - var connType: AnyPublisher<ConnectionType, Never> { get } - var statusPublisher: AnyPublisher<NetworkStatus, Never> { get } -} - -public struct NetworkMonitor: NetworkMonitoring { - public init() {} - - private var monitor = NWPathMonitor() - private let isXXAvailableRelay = CurrentValueSubject<Bool?, Never>(nil) - private let isInternetAvailableRelay = CurrentValueSubject<Bool?, Never>(nil) - private let connTypeSubject = PassthroughSubject<ConnectionType, Never>() - - public var xxStatus: NetworkStatus { - isXXAvailableRelay.value == true ? .available : .xxNotAvailable - } - - public var connType: AnyPublisher<ConnectionType, Never> { - connTypeSubject.eraseToAnyPublisher() - } - - public var statusPublisher: AnyPublisher<NetworkStatus, Never> { - isInternetAvailableRelay.combineLatest(isXXAvailableRelay) - .map { (isInternetAvailable, isXXAvailable) -> NetworkStatus in - - guard let isInternetAvailable = isInternetAvailable, - let isXXAvailable = isXXAvailable else { return .unknown } - - switch (isInternetAvailable, isXXAvailable) { - case (true, true): - return .available - case (true, false): - return .xxNotAvailable - case (false, _): - return .internetNotAvailable - } - } - .removeDuplicates() - .eraseToAnyPublisher() - } - - public func start() { - monitor.pathUpdateHandler = { [weak isInternetAvailableRelay, weak connTypeSubject] in - connTypeSubject?.send(checkConnectionTypeForPath($0)) - isInternetAvailableRelay?.send($0.status == .satisfied) - } - - monitor.start(queue: .global()) - } - - public func update(_ status: Bool) { - isXXAvailableRelay.send(status) - } - - private func checkConnectionTypeForPath(_ path: NWPath) -> ConnectionType { - if path.usesInterfaceType(.wifi) { - return .wifi - } else if path.usesInterfaceType(.wiredEthernet) { - return .ethernet - } else if path.usesInterfaceType(.cellular) { - return .cellular - } - - return .unknown - } -} diff --git a/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift index c9b91738175c76536e88cf78b2b572ba54bcd837..05f0be5bbc98ea56a1e57ada2328382b754cb712 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift @@ -2,13 +2,15 @@ import UIKit import Shared import Combine import Navigation +import AppResources import DrawerFeature -import DI +import StatusBarFeature import ScrollViewController +import ComposableArchitecture public final class OnboardingCodeController: UIViewController { - @Dependency var navigator: Navigator - @Dependency var barStylist: StatusBarStylist + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager private lazy var screenView = OnboardingCodeView() private lazy var scrollViewController = ScrollViewController() @@ -39,7 +41,7 @@ public final class OnboardingCodeController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.backButtonTitle = "" - barStylist.styleSubject.send(.darkContent) + statusBar.update(.darkContent) navigationController?.navigationBar.customize(translucent: true) } @@ -103,9 +105,9 @@ public final class OnboardingCodeController: UIViewController { .sink { [unowned self] in guard $0 == true else { return } if isEmail { - navigator.perform(PresentOnboardingPhone()) + navigator.perform(PresentOnboardingPhone(on: navigationController!)) } else { - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) } }.store(in: &cancellables) @@ -174,6 +176,6 @@ public final class OnboardingCodeController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift index ab9a453eaef6e3e0ce3274fc0108cfdb96b9f5f3..df1fc7bfebd56330f8e76f518d9ffde6769f74e7 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift @@ -2,13 +2,15 @@ import UIKit import Shared import Combine import Navigation +import AppResources import DrawerFeature -import DI +import StatusBarFeature import ScrollViewController +import ComposableArchitecture public final class OnboardingEmailController: UIViewController { - @Dependency var navigator: Navigator - @Dependency var barStylist: StatusBarStylist + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager private lazy var screenView = OnboardingEmailView() private lazy var scrollViewController = ScrollViewController() @@ -20,7 +22,7 @@ public final class OnboardingEmailController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.backButtonTitle = " " - barStylist.styleSubject.send(.darkContent) + statusBar.update(.darkContent) navigationController?.navigationBar.customize(translucent: true) } @@ -74,7 +76,8 @@ public final class OnboardingEmailController: UIViewController { PresentOnboardingCode( isEmail: true, content: $0.input, - confirmationId: id + confirmationId: id, + on: navigationController! ) ) }.store(in: &cancellables) @@ -99,7 +102,7 @@ public final class OnboardingEmailController: UIViewController { .skipButton .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentOnboardingPhone()) + navigator.perform(PresentOnboardingPhone(on: navigationController!)) }.store(in: &cancellables) } @@ -139,6 +142,6 @@ public final class OnboardingEmailController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift index c2d4aeba4f30aa7bd74aca903b0093f99790330f..0f4eebf87044c777e9af241523077fd1634f1d82 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift @@ -2,13 +2,15 @@ import UIKit import Shared import Combine import Navigation +import AppResources import DrawerFeature -import DI +import StatusBarFeature import ScrollViewController +import ComposableArchitecture public final class OnboardingPhoneController: UIViewController { - @Dependency var navigator: Navigator - @Dependency var barStylist: StatusBarStylist + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager private lazy var screenView = OnboardingPhoneView() private lazy var scrollViewController = ScrollViewController() @@ -20,7 +22,7 @@ public final class OnboardingPhoneController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.backButtonTitle = "" - barStylist.styleSubject.send(.darkContent) + statusBar.update(.darkContent) navigationController?.navigationBar.customize(translucent: true) } @@ -84,7 +86,7 @@ public final class OnboardingPhoneController: UIViewController { .skipButton .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) }.store(in: &cancellables) screenView @@ -96,7 +98,7 @@ public final class OnboardingPhoneController: UIViewController { guard let self else { return } self.navigator.perform(DismissModal(from: self)) self.viewModel.didChooseCountry($0 as! Country) - })) + }, from: self)) }.store(in: &cancellables) viewModel @@ -109,7 +111,8 @@ public final class OnboardingPhoneController: UIViewController { PresentOnboardingCode( isEmail: false, content: content, - confirmationId: id + confirmationId: id, + on: navigationController! ) ) }.store(in: &cancellables) @@ -161,6 +164,6 @@ public final class OnboardingPhoneController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift index 7cb3faf0f4c1b305f6c203ece1d58cad36be9b0f..4e09d6b6609e27454822912e2b844bf8b3e4aa1b 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift @@ -1,10 +1,10 @@ import UIKit import Combine import Navigation -import DI +import ComposableArchitecture public final class OnboardingStartController: UIViewController { - @Dependency var navigator: Navigator + @Dependency(\.navigator) var navigator: Navigator private lazy var screenView = OnboardingStartView() @@ -45,7 +45,7 @@ public final class OnboardingStartController: UIViewController { .startButton .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentTermsAndConditions()) + navigator.perform(PresentTermsAndConditions(replacing: false, on: navigationController!)) }.store(in: &cancellables) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index 0f192fbb0228682593621a3935acb3b41abd227b..42f4f18981fabb12a44713d22f2904a4e831af54 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -2,13 +2,15 @@ import UIKit import Shared import Combine import Navigation +import AppResources import DrawerFeature -import DI +import StatusBarFeature import ScrollViewController +import ComposableArchitecture public final class OnboardingUsernameController: UIViewController { - @Dependency var navigator: Navigator - @Dependency var barStylist: StatusBarStylist + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager private lazy var screenView = OnboardingUsernameView() private lazy var scrollViewController = ScrollViewController() @@ -20,7 +22,7 @@ public final class OnboardingUsernameController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.backButtonTitle = "" - barStylist.styleSubject.send(.darkContent) + statusBar.update(.darkContent) navigationController?.navigationBar.customize(translucent: true) } @@ -65,7 +67,7 @@ public final class OnboardingUsernameController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentRestoreList()) + navigator.perform(PresentRestoreList(on: navigationController!)) }.store(in: &cancellables) screenView @@ -91,7 +93,7 @@ public final class OnboardingUsernameController: UIViewController { .receive(on: DispatchQueue.main) .sink { [unowned self] in guard $0.didConfirm == true else { return } - navigator.perform(PresentOnboardingWelcome()) + navigator.perform(PresentOnboardingWelcome(on: navigationController!)) }.store(in: &cancellables) viewModel @@ -140,6 +142,6 @@ public final class OnboardingUsernameController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift index 937524257df145b571957b51972c297b4ab679a1..09a9221ba5765e85d92a9626b41377566a0977c2 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift @@ -3,12 +3,15 @@ import Shared import Combine import Defaults import Navigation +import AppResources import DrawerFeature -import DI +import StatusBarFeature +import ComposableArchitecture public final class OnboardingWelcomeController: UIViewController { - @Dependency var navigator: Navigator - @Dependency var barStylist: StatusBarStylist + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager + @KeyObject(.username, defaultValue: "") var username: String private lazy var screenView = OnboardingWelcomeView() @@ -22,7 +25,7 @@ public final class OnboardingWelcomeController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - barStylist.styleSubject.send(.darkContent) + statusBar.update(.darkContent) navigationController?.navigationBar.customize(translucent: true) } @@ -35,14 +38,14 @@ public final class OnboardingWelcomeController: UIViewController { .continueButton .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentOnboardingEmail()) + navigator.perform(PresentOnboardingEmail(on: navigationController!)) }.store(in: &cancellables) screenView .skipButton .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) }.store(in: &cancellables) screenView.didTapInfo = { [weak self] in @@ -91,6 +94,6 @@ public final class OnboardingWelcomeController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/OnboardingFeature/OnboardingDependencies.swift b/Sources/OnboardingFeature/OnboardingDependencies.swift new file mode 100644 index 0000000000000000000000000000000000000000..fe3dce7f61997d7fc4b4d80d2d4b20caf358bcf4 --- /dev/null +++ b/Sources/OnboardingFeature/OnboardingDependencies.swift @@ -0,0 +1,91 @@ +import Navigation +import Dependencies + +private enum NavigatorKey: DependencyKey { + static let liveValue: Navigator = CombinedNavigator.core + static let testValue: Navigator = UnimplementedNavigator() +} + +extension DependencyValues { + var navigator: Navigator { + get { self[NavigatorKey.self] } + set { self[NavigatorKey.self] = newValue } + } +} + +import UIKit +import XCTestDynamicOverlay +import ComposableArchitecture + +public struct PresentStep: Navigation.Action, Equatable { + public init(viewController: UIViewController, from: UIViewController) { + self.viewController = viewController + self.from = from + } + + public var viewController: UIViewController + public var from: UIViewController +} + +struct PresentStepNavigator: Navigation.TypedNavigator { + @Dependency(\.navigator) var navigator + + func perform(_ action: PresentStep, completion: @escaping () -> Void) { + guard let navigationController = action.from.navigationController else { + completion() + return + } + navigator.perform( + SetStack( + navigationController.viewControllers + [action.viewController], + on: navigationController + ), + completion: completion + ) + } +} + +public struct DismissToStep: Navigation.Action, Equatable { + public init(viewController: UIViewController) { + self.viewController = viewController + } + + public var viewController: UIViewController +} + +struct DismissToStepNavigator: Navigation.TypedNavigator { + @Dependency(\.navigator) var navigator + + func perform(_ action: DismissToStep, completion: @escaping () -> Void) { + guard let navigationController = action.viewController.navigationController else { + completion() + return + } + navigator.perform( + PopTo(action.viewController, on: navigationController), + completion: completion + ) + } +} + +extension CombinedNavigator { + public static let core = CombinedNavigator( + SetStackNavigator(), + PopToNavigator(), + PresentStepNavigator(), + DismissToStepNavigator() + ) +} + +public struct UnimplementedNavigator: Navigator { + public init() {} + + public func perform(_ action: Navigation.Action, completion: @escaping () -> Void) { + XCTestDynamicOverlay.XCTFail("UnimplementedNavigator.perform not implemented") + } + + public func canPerform(_ action: Action) -> Bool { + XCTestDynamicOverlay.XCTFail("UnimplementedNavigator.canPerform not implemented") + return false + } +} diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift index 35dc2c9934294e5e245e77627e0257da4f1b8420..994b83b211a75eeb4bafb4e95ec683e263325881 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift @@ -1,3 +1,4 @@ +import AppCore import Shared import Combine import Defaults @@ -6,7 +7,7 @@ import InputField import Foundation import CombineSchedulers import XXMessengerClient -import DI +import ComposableArchitecture final class OnboardingCodeViewModel { struct ViewState: Equatable { @@ -20,8 +21,10 @@ final class OnboardingCodeViewModel { stateSubject.eraseToAnyPublisher() } - @Dependency var messenger: Messenger - @Dependency var hudController: HUDController + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + @KeyObject(.email, defaultValue: nil) var email: String? @KeyObject(.phone, defaultValue: nil) var phone: String? @@ -30,7 +33,6 @@ final class OnboardingCodeViewModel { private let content: String private let confirmationId: String private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) - private var scheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() init( isEmail: Bool, @@ -61,8 +63,8 @@ final class OnboardingCodeViewModel { } func didTapNext() { - hudController.show() - scheduler.schedule { [weak self] in + hudManager.show() + bgQueue.schedule { [weak self] in guard let self else { return } do { try self.messenger.ud.get()!.confirmFact( @@ -75,10 +77,10 @@ final class OnboardingCodeViewModel { self.phone = self.content } self.timer?.invalidate() - self.hudController.dismiss() + self.hudManager.hide() self.stateSubject.value.didConfirm = true } catch { - self.hudController.dismiss() + self.hudManager.hide() let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) self.stateSubject.value.status = .invalid(xxError) } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift index 6d3295db39a175da6be05464ee573fed622c8ec8..a175a674fa981f7d48cf2409c8d84041f60da5dc 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift @@ -1,3 +1,4 @@ +import AppCore import Shared import Combine import XXClient @@ -5,7 +6,7 @@ import InputField import Foundation import CombineSchedulers import XXMessengerClient -import DI +import ComposableArchitecture final class OnboardingEmailViewModel { struct ViewState: Equatable { @@ -14,15 +15,15 @@ final class OnboardingEmailViewModel { var status: InputField.ValidationStatus = .unknown(nil) } - @Dependency var messenger: Messenger - @Dependency var hudController: HUDController + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> var statePublisher: AnyPublisher<ViewState, Never> { stateSubject.eraseToAnyPublisher() } private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) - private var scheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() func clearUp() { stateSubject.value.confirmationId = nil @@ -34,17 +35,17 @@ final class OnboardingEmailViewModel { } func didTapNext() { - hudController.show() - scheduler.schedule { [weak self] in + hudManager.show() + bgQueue.schedule { [weak self] in guard let self else { return } do { let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( .init(type: .email, value: self.stateSubject.value.input) ) - self.hudController.dismiss() + self.hudManager.hide() self.stateSubject.value.confirmationId = confirmationId } catch { - self.hudController.dismiss() + self.hudManager.hide() let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) self.stateSubject.value.status = .invalid(xxError) } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift index bc6550f01c10bab35c4928faa2b5aa4e4835e343..39d9f282be5757d19bc7ef47ff450dcfd2d1f48e 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift @@ -1,12 +1,13 @@ +import AppCore import Shared import Combine import XXClient -import Countries import InputField import Foundation import CombineSchedulers import XXMessengerClient -import DI +import CountryListFeature +import ComposableArchitecture final class OnboardingPhoneViewModel { struct ViewState: Equatable { @@ -17,15 +18,15 @@ final class OnboardingPhoneViewModel { var country: Country = .fromMyPhone() } - @Dependency var messenger: Messenger - @Dependency var hudController: HUDController + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> var statePublisher: AnyPublisher<ViewState, Never> { stateSubject.eraseToAnyPublisher() } private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) - private var scheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() func clearUp() { stateSubject.value.confirmationId = nil @@ -42,19 +43,19 @@ final class OnboardingPhoneViewModel { } func didTapNext() { - hudController.show() - scheduler.schedule { [weak self] in + hudManager.show() + bgQueue.schedule { [weak self] in guard let self else { return } let content = "\(self.stateSubject.value.input)\(self.stateSubject.value.country.code)" do { let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( .init(type: .phone, value: content) ) - self.hudController.dismiss() + self.hudManager.hide() self.stateSubject.value.content = content self.stateSubject.value.confirmationId = confirmationId } catch { - self.hudController.dismiss() + self.hudManager.hide() let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) self.stateSubject.value.status = .invalid(xxError) } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift index 051e97838eda2756e8772f6600b2936ae6389995..917e9fea3bcd5f21227f5a285ceafe620b271e9f 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift @@ -1,3 +1,4 @@ +import AppCore import Shared import Combine import Defaults @@ -6,8 +7,7 @@ import XXClient import InputField import Foundation import XXMessengerClient -import CombineSchedulers -import DI +import ComposableArchitecture final class OnboardingUsernameViewModel { struct ViewState: Equatable { @@ -16,9 +16,11 @@ final class OnboardingUsernameViewModel { var didConfirm: Bool = false } - @Dependency var database: Database - @Dependency var messenger: Messenger - @Dependency var hudController: HUDController + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + @KeyObject(.username, defaultValue: "") var username: String var statePublisher: AnyPublisher<ViewState, Never> { @@ -26,7 +28,6 @@ final class OnboardingUsernameViewModel { } private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) - private var scheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() func didInput(_ string: String) { stateSubject.value.input = string.trimmingCharacters(in: .whitespacesAndNewlines) @@ -39,14 +40,14 @@ final class OnboardingUsernameViewModel { } func didTapRegister() { - hudController.show() - scheduler.schedule { [weak self] in + hudManager.show() + bgQueue.schedule { [weak self] in guard let self else { return } do { try self.messenger.register( username: self.stateSubject.value.input ) - try self.database.saveContact(.init( + try self.dbManager.getDB().saveContact(.init( id: self.messenger.e2e.get()!.getContact().getId(), marshaled: self.messenger.e2e.get()!.getContact().data, username: self.stateSubject.value.input, @@ -61,10 +62,10 @@ final class OnboardingUsernameViewModel { createdAt: Date() )) self.username = self.stateSubject.value.input - self.hudController.dismiss() + self.hudManager.hide() self.stateSubject.value.didConfirm = true } catch { - self.hudController.dismiss() + self.hudManager.hide() let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) self.stateSubject.value.status = .invalid(xxError) } diff --git a/Sources/OnboardingFeature/Views/OnboardingCodeView.swift b/Sources/OnboardingFeature/Views/OnboardingCodeView.swift index 9f4b48d5385f69a7de78f0be828494829dbd2751..63a33f1bdcfe95b33f9335f32f3358bdbe3d354c 100644 --- a/Sources/OnboardingFeature/Views/OnboardingCodeView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingCodeView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingCodeView: UIView { let titleLabel = UILabel() diff --git a/Sources/OnboardingFeature/Views/OnboardingEmailView.swift b/Sources/OnboardingFeature/Views/OnboardingEmailView.swift index c681215e691626cdb694b16f229f6c4065e134a2..e98117828c9de253fe169b3cb4e9a8fff67e92d1 100644 --- a/Sources/OnboardingFeature/Views/OnboardingEmailView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingEmailView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingEmailView: UIView { let titleLabel = UILabel() diff --git a/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift b/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift index 8caa27679273d4ab0485e6d8254723e78fbb31c7..45584a61af46433c46833141b1cae00a00333275 100644 --- a/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingPhoneConfirmationView: UIView { let titleLabel = UILabel() diff --git a/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift b/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift index a8df7c864d75041a34c23863595150dcda094ecc..f36c635ef4e0706297a6b40d328e9a387d3e556f 100644 --- a/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingPhoneView: UIView { let titleLabel = UILabel() diff --git a/Sources/OnboardingFeature/Views/OnboardingStartView.swift b/Sources/OnboardingFeature/Views/OnboardingStartView.swift index 2a83c1b6aa004c3ca068ff27b419212ea9353480..b0864bb32bb8bdd4557b0a677df73068e5c747d5 100644 --- a/Sources/OnboardingFeature/Views/OnboardingStartView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingStartView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class OnboardingStartView: UIView { let titleLabel = UILabel() diff --git a/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift b/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift index 170d0a6344239781af13acd2835c5fa4519d9569..7a418f71686b90456b03f24af3f4165d416bd7a8 100644 --- a/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class OnboardingUsernameRestoreView: UIView { let titleLabel = UILabel() diff --git a/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift b/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift index 7bd6e1026563d5a7de6fb17dce7e6a872b5c473d..8f5bf92b6a34377960f449c2d0a77aab28b3225c 100644 --- a/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingUsernameView: UIView { let titleLabel = UILabel() diff --git a/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift b/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift index ae4b5a609864ffdeed59ddc164ca303eeb474a7a..2daddf4b716a515abe8d17c61e9f6e2ee464aa6f 100644 --- a/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class OnboardingWelcomeView: UIView { let titleLabel = UILabel() diff --git a/Sources/Permissions/MockPermissionHandler.swift b/Sources/Permissions/MockPermissionHandler.swift deleted file mode 100644 index d43dd9f1907f0faa671980f3ae0290828c0fc10d..0000000000000000000000000000000000000000 --- a/Sources/Permissions/MockPermissionHandler.swift +++ /dev/null @@ -1,38 +0,0 @@ -import AVFoundation - -public class MockPermissionHandler: PermissionHandling { - private var cameraStatus = false - private var photosStatus = false - private var biometricsStatus = false - private var microphoneStatus = false - - public init() {} - - public var isCameraAllowed: Bool { cameraStatus } - - public var isPhotosAllowed: Bool { photosStatus } - - public var isMicrophoneAllowed: Bool { microphoneStatus } - - public var isBiometricsAvailable: Bool { biometricsStatus } - - public func requestBiometrics(_ completion: @escaping (Result<Bool, Error>) -> Void) { - biometricsStatus = true - completion(.success(true)) - } - - public func requestCamera(_ completion: @escaping (Bool) -> Void) { - cameraStatus = true - completion(true) - } - - public func requestMicrophone(_ completion: @escaping (Bool) -> Void) { - microphoneStatus = true - completion(true) - } - - public func requestPhotos(_ completion: @escaping (Bool) -> Void) { - photosStatus = true - completion(true) - } -} diff --git a/Sources/Permissions/PermissionHandler.swift b/Sources/Permissions/PermissionHandler.swift deleted file mode 100644 index 762a217f121daf85b61a172782ebef12dc8d6c21..0000000000000000000000000000000000000000 --- a/Sources/Permissions/PermissionHandler.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Photos -import AVFoundation -import LocalAuthentication - -public protocol PermissionHandling { - var isCameraAllowed: Bool { get } - var isPhotosAllowed: Bool { get } - var isMicrophoneAllowed: Bool { get } - var isBiometricsAvailable: Bool { get } - - func requestPhotos(_: @escaping (Bool) -> Void) - func requestCamera(_: @escaping (Bool) -> Void) - func requestMicrophone(_: @escaping (Bool) -> Void) - func requestBiometrics(_: @escaping (Result<Bool, Error>) -> Void) -} - -public struct PermissionHandler: PermissionHandling { - public init() {} - - public var isMicrophoneAllowed: Bool { - AVAudioSession.sharedInstance().recordPermission == .granted - } - - public var isCameraAllowed: Bool { - AVCaptureDevice.authorizationStatus(for: .video) == .authorized - } - - public var isPhotosAllowed: Bool { - PHPhotoLibrary.authorizationStatus() == .authorized - } - - public var isBiometricsAvailable: Bool { - var error: NSError? - let context = LAContext() - - if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) == true { - return true - } else { - let tooManyAttempts = LAError.Code.biometryLockout.rawValue - guard let error = error, error.code == tooManyAttempts else { return true } - return false - } - } - - public func requestBiometrics(_ completion: @escaping (Result<Bool, Error>) -> Void) { - let reason = "Authentication is required to use xx messenger" - LAContext().evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason, reply: { success, error in - guard let error = error else { - completion(.success(success)) - return - } - - completion(.failure(error)) - }) - } - - public func requestCamera(_ completion: @escaping (Bool) -> Void) { - AVCaptureDevice.requestAccess(for: .video, completionHandler: completion) - } - - public func requestMicrophone(_ completion: @escaping (Bool) -> Void) { - AVAudioSession.sharedInstance().requestRecordPermission(completion) - } - - public func requestPhotos(_ completion: @escaping (Bool) -> Void) { - PHPhotoLibrary.requestAuthorization { completion($0 == .authorized) } - } -} diff --git a/Sources/Permissions/RequestPermissionView.swift b/Sources/Permissions/RequestPermissionView.swift deleted file mode 100644 index 52fd14da4f3d250ee5e8be9c9dd1efce9cd21e3a..0000000000000000000000000000000000000000 --- a/Sources/Permissions/RequestPermissionView.swift +++ /dev/null @@ -1,106 +0,0 @@ -import UIKit -import Shared - -final class RequestPermissionView: UIView { - let titleLabel = UILabel() - let iconImage = UIImageView() - let subtitleLabel = UILabel() - let littleLogo = UIImageView() - private(set) var notNowButton = UIButton() - private(set) var continueButton = CapsuleButton() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - func setup(title: String, subtitle: String, image: UIImage) { - iconImage.image = image - titleLabel.text = title - - let paragraph = NSMutableParagraphStyle() - paragraph.lineHeightMultiple = 1.5 - paragraph.alignment = .center - - subtitleLabel.attributedText = NSAttributedString( - string: subtitle, - attributes: [ - .paragraphStyle: paragraph, - .font: Fonts.Mulish.regular.font(size: 14.0), - .foregroundColor: Asset.neutralBody.color, - ] - ) - } - - private func setup() { - littleLogo.image = Asset.permissionLogo.image - notNowButton.setTitle(Localized.Chat.Actions.Permission.notnow, for: .normal) - continueButton.set(style: .brandColored, title: Localized.Chat.Actions.Permission.continue) - - titleLabel.textAlignment = .center - - backgroundColor = Asset.neutralWhite.color - titleLabel.textColor = Asset.neutralActive.color - notNowButton.setTitleColor(Asset.neutralWeak.color, for: .normal) - - subtitleLabel.numberOfLines = 0 - - titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) - notNowButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 16) - - let actionsContainer = UIView() - actionsContainer.addSubview(continueButton) - actionsContainer.addSubview(notNowButton) - - addSubview(iconImage) - addSubview(titleLabel) - addSubview(littleLogo) - addSubview(subtitleLabel) - addSubview(actionsContainer) - - iconImage.snp.makeConstraints { make in - make.centerX.equalToSuperview() - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(iconImage.snp.bottom).offset(34) - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - subtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(32) - make.right.equalToSuperview().offset(-32) - make.bottom.equalTo(snp.centerY) - } - - littleLogo.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalTo(safeAreaLayoutGuide).offset(-15) - } - - actionsContainer.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(subtitleLabel.snp.bottom) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.lessThanOrEqualTo(littleLogo.snp.top) - } - - continueButton.snp.makeConstraints { make in - make.top.greaterThanOrEqualToSuperview() - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalTo(actionsContainer.snp.centerY).offset(-5) - } - - notNowButton.snp.makeConstraints { make in - make.top.equalTo(actionsContainer.snp.centerY).offset(5) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.lessThanOrEqualToSuperview() - } - } -} diff --git a/Sources/PermissionsFeature/PermissionBiometrics.swift b/Sources/PermissionsFeature/PermissionBiometrics.swift new file mode 100644 index 0000000000000000000000000000000000000000..066d835af9b179233a89bc9bcebf57d8ce5a3fe4 --- /dev/null +++ b/Sources/PermissionsFeature/PermissionBiometrics.swift @@ -0,0 +1,65 @@ +import LocalAuthentication +import XCTestDynamicOverlay + +public struct PermissionBiometrics { + public var status: PermissionBiometricsStatus + public var request: PermissionBiometricsRequest + + public static let live = PermissionBiometrics( + status: .live, + request: .live + ) + public static let unimplemented = PermissionBiometrics( + status: .unimplemented, + request: .unimplemented + ) +} + +public struct PermissionBiometricsStatus { + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } + + public static let live = PermissionBiometricsStatus { + var error: NSError? + let context = LAContext() + + if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) == true { + return true + } else { + let tooManyAttempts = LAError.Code.biometryLockout.rawValue + guard let error = error, error.code == tooManyAttempts else { return true } + return false + } + } + + public static let unimplemented = PermissionBiometricsStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} + +public struct PermissionBiometricsRequest { + public var run: (@escaping (Bool) -> Void) -> Void + + public func callAsFunction(_ completion: @escaping (Bool) -> Void) -> Void { + run(completion) + } + + public static let live = PermissionBiometricsRequest { completion in + let reason = "Authentication is required to use xx messenger" + LAContext().evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, error in + if let error { + completion(false) + return + } + + completion(success) + } + } + + public static let unimplemented = PermissionBiometricsRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/PermissionsFeature/PermissionCamera.swift b/Sources/PermissionsFeature/PermissionCamera.swift new file mode 100644 index 0000000000000000000000000000000000000000..1c4e263087b5428af0f7d85a54b00f83a072f71c --- /dev/null +++ b/Sources/PermissionsFeature/PermissionCamera.swift @@ -0,0 +1,48 @@ +import AVFoundation +import XCTestDynamicOverlay + +public struct PermissionCamera { + public var status: PermissionCameraStatus + public var request: PermissionCameraRequest + + public static let live = PermissionCamera( + status: .live, + request: .live + ) + public static let unimplemented = PermissionCamera( + status: .unimplemented, + request: .unimplemented + ) +} + +public struct PermissionCameraStatus { + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } + + public static let live = PermissionCameraStatus { + AVCaptureDevice.authorizationStatus(for: .video) == .authorized + } + + public static let unimplemented = PermissionCameraStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} + +public struct PermissionCameraRequest { + public var run: (@escaping (Bool) -> Void) -> Void + + public func callAsFunction(_ completion: @escaping (Bool) -> Void) -> Void { + run(completion) + } + + public static let live = PermissionCameraRequest { + AVCaptureDevice.requestAccess(for: .video, completionHandler: $0) + } + + public static let unimplemented = PermissionCameraRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/PermissionsFeature/PermissionLibrary.swift b/Sources/PermissionsFeature/PermissionLibrary.swift new file mode 100644 index 0000000000000000000000000000000000000000..485a666f58a7a6c04ff3cd425454798c6c711eda --- /dev/null +++ b/Sources/PermissionsFeature/PermissionLibrary.swift @@ -0,0 +1,48 @@ +import Photos +import XCTestDynamicOverlay + +public struct PermissionLibrary { + public var status: PermissionLibraryStatus + public var request: PermissionLibraryRequest + + public static let live = PermissionLibrary( + status: .live, + request: .live + ) + public static let unimplemented = PermissionLibrary( + status: .unimplemented, + request: .unimplemented + ) +} + +public struct PermissionLibraryStatus { + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } + + public static let live = PermissionLibraryStatus { + PHPhotoLibrary.authorizationStatus() == .authorized + } + + public static let unimplemented = PermissionLibraryStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} + +public struct PermissionLibraryRequest { + public var run: (@escaping (Bool) -> Void) -> Void + + public func callAsFunction(_ completion: @escaping (Bool) -> Void) -> Void { + run(completion) + } + + public static let live = PermissionLibraryRequest { completion in + PHPhotoLibrary.requestAuthorization { completion($0 == .authorized) } + } + + public static let unimplemented = PermissionLibraryRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/PermissionsFeature/PermissionMicrophone.swift b/Sources/PermissionsFeature/PermissionMicrophone.swift new file mode 100644 index 0000000000000000000000000000000000000000..0f5727837cbea30f05d920156d1dfd155f2f6bd4 --- /dev/null +++ b/Sources/PermissionsFeature/PermissionMicrophone.swift @@ -0,0 +1,56 @@ +import AVFoundation +import XCTestDynamicOverlay + +public struct PermissionMicrophone { + public var status: PermissionMicrophoneStatus + public var request: PermissionMicrophoneRequest + + public static let live = PermissionMicrophone( + status: .live, + request: .live + ) + public static let unimplemented = PermissionMicrophone( + status: .unimplemented, + request: .unimplemented + ) +} + +public struct PermissionMicrophoneRequest { + public var run: (@escaping (Bool) -> Void) -> Void + + public func callAsFunction(_ completion: @escaping (Bool) -> Void) -> Void { + run(completion) + } +} + +extension PermissionMicrophoneRequest { + public static let live = PermissionMicrophoneRequest { + AVAudioSession.sharedInstance().requestRecordPermission($0) + } +} + +extension PermissionMicrophoneRequest { + public static let unimplemented = PermissionMicrophoneRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} + +public struct PermissionMicrophoneStatus { + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } +} + +extension PermissionMicrophoneStatus { + public static let live = PermissionMicrophoneStatus { + AVAudioSession.sharedInstance().recordPermission == .granted + } +} + +extension PermissionMicrophoneStatus { + public static let unimplemented = PermissionMicrophoneStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/PermissionsFeature/PermissionType.swift b/Sources/PermissionsFeature/PermissionType.swift new file mode 100644 index 0000000000000000000000000000000000000000..93b8ba3906708b240dc45fb39bbca98173ae0495 --- /dev/null +++ b/Sources/PermissionsFeature/PermissionType.swift @@ -0,0 +1,5 @@ +public enum PermissionType: Int { + case camera + case library + case microphone +} diff --git a/Sources/PermissionsFeature/PermissionsManager.swift b/Sources/PermissionsFeature/PermissionsManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..89fd4b377e122725bd112bc4a1fdc3c760688a61 --- /dev/null +++ b/Sources/PermissionsFeature/PermissionsManager.swift @@ -0,0 +1,33 @@ +public struct PermissionsManager { + public var camera: PermissionCamera + public var library: PermissionLibrary + public var microphone: PermissionMicrophone + public var biometrics: PermissionBiometrics + + public static let live = PermissionsManager( + camera: .live, + library: .live, + microphone: .live, + biometrics: .live + ) + public static let unimplemented = PermissionsManager( + camera: .unimplemented, + library: .unimplemented, + microphone: .unimplemented, + biometrics: .unimplemented + ) +} + +import Dependencies + +private enum PermissionsDependencyKey: DependencyKey { + static let liveValue: PermissionsManager = .live + static let testValue: PermissionsManager = .unimplemented +} + +extension DependencyValues { + public var permissions: PermissionsManager { + get { self[PermissionsDependencyKey.self] } + set { self[PermissionsDependencyKey.self] = newValue } + } +} diff --git a/Sources/Presentation/BottomPresenter.swift b/Sources/Presentation/BottomPresenter.swift deleted file mode 100644 index d4d5778b09e00ffaedb3357b1d621f6017da339a..0000000000000000000000000000000000000000 --- a/Sources/Presentation/BottomPresenter.swift +++ /dev/null @@ -1,33 +0,0 @@ -import UIKit - -public final class BottomPresenter: NSObject, Presenting { - private var transition: BottomTransition? - - public func present(_ viewControllers: UIViewController..., from parent: UIViewController) { - guard let screen = viewControllers.first else { - fatalError("Tried to present empty list of view controllers") - } - - screen.modalPresentationStyle = .overFullScreen - screen.transitioningDelegate = self - parent.present(screen, animated: true) - } -} - -// MARK: UIViewControllerTransitioningDelegate -extension BottomPresenter: UIViewControllerTransitioningDelegate { - public func animationController(forPresented presented: UIViewController, - presenting: UIViewController, - source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition = BottomTransition(onDismissal: { [weak self] in - self?.transition = nil - }) - - return transition - } - - public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition?.direction = .dismiss - return transition - } -} diff --git a/Sources/Presentation/BottomTransition.swift b/Sources/Presentation/BottomTransition.swift deleted file mode 100644 index 28f41a490b2607e813ecd9123b373b673e579da2..0000000000000000000000000000000000000000 --- a/Sources/Presentation/BottomTransition.swift +++ /dev/null @@ -1,122 +0,0 @@ -import UIKit -import Combine -import SnapKit -import Shared - -final class BottomTransition: NSObject, UIViewControllerAnimatedTransitioning { - enum Direction { - case present - case dismiss - } - - var direction: Direction = .present - private let onDismissal: EmptyClosure - private weak var darkOverlayView: UIControl? - private weak var topConstraint: Constraint? - private weak var bottomConstraint: Constraint? - private var cancellables = Set<AnyCancellable>() - - private var presentedConstraints: [NSLayoutConstraint] = [] - private var dismissedConstraints: [NSLayoutConstraint] = [] - - init(onDismissal: @escaping EmptyClosure) { - self.onDismissal = onDismissal - super.init() - } - - func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.5 } - - func animateTransition(using context: UIViewControllerContextTransitioning) { - switch direction { - case .present: - present(using: context) - case .dismiss: - dismiss(using: context) - } - } - - private func present(using context: UIViewControllerContextTransitioning) { - guard let presentingController = context.viewController(forKey: .from), - let presentedView = context.view(forKey: .to) else { - context.completeTransition(false) - return - } - - let darkOverlayView = UIControl() - self.darkOverlayView = darkOverlayView - - darkOverlayView.alpha = 0.0 - darkOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) - context.containerView.addSubview(darkOverlayView) - darkOverlayView.frame = context.containerView.bounds - - darkOverlayView - .publisher(for: .touchUpInside) - .sink { [weak presentingController] _ in - presentingController?.dismiss(animated: true) - }.store(in: &cancellables) - - context.containerView.addSubview(presentedView) - presentedView.translatesAutoresizingMaskIntoConstraints = false - - presentedConstraints = [ - presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), - presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), - presentedView.bottomAnchor.constraint(equalTo: context.containerView.bottomAnchor), - presentedView.topAnchor.constraint( - greaterThanOrEqualTo: context.containerView.safeAreaLayoutGuide.topAnchor, - constant: 60 - ) - ] - - dismissedConstraints = [ - presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), - presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), - presentedView.topAnchor.constraint(equalTo: context.containerView.bottomAnchor) - ] - - NSLayoutConstraint.activate(dismissedConstraints) - - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - - NSLayoutConstraint.deactivate(dismissedConstraints) - NSLayoutConstraint.activate(presentedConstraints) - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { - darkOverlayView.alpha = 1.0 - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - }, - completion: { _ in - context.completeTransition(true) - }) - } - - private func dismiss(using context: UIViewControllerContextTransitioning) { - NSLayoutConstraint.deactivate(presentedConstraints) - NSLayoutConstraint.activate(dismissedConstraints) - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { [weak darkOverlayView] in - darkOverlayView?.alpha = 0.0 - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - }, - completion: { [weak self] _ in - context.completeTransition(true) - self?.onDismissal() - }) - } -} diff --git a/Sources/Presentation/CenterPresenter.swift b/Sources/Presentation/CenterPresenter.swift deleted file mode 100644 index 99277dc5708680bcd74482edc5cc88d542072381..0000000000000000000000000000000000000000 --- a/Sources/Presentation/CenterPresenter.swift +++ /dev/null @@ -1,35 +0,0 @@ -import UIKit - -public protocol CenterPresenterNonDismissingTarget: UIViewController {} - -public final class CenterPresenter: NSObject, Presenting { - private var transition: CenterTransition? - - public func present(_ viewControllers: UIViewController..., from parent: UIViewController) { - guard let screen = viewControllers.first else { - fatalError("Tried to present empty list of view controllers") - } - - screen.modalPresentationStyle = .overFullScreen - screen.transitioningDelegate = self - parent.present(screen, animated: true) - } -} - -// MARK: UIViewControllerTransitioningDelegate -extension CenterPresenter: UIViewControllerTransitioningDelegate { - public func animationController(forPresented presented: UIViewController, - presenting: UIViewController, - source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition = CenterTransition( - onDismissal: { [weak self] in self?.transition = nil }, - dismissable: (presented is CenterPresenterNonDismissingTarget) == false) - - return transition - } - - public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition?.direction = .dismiss - return transition - } -} diff --git a/Sources/Presentation/CenterTransition.swift b/Sources/Presentation/CenterTransition.swift deleted file mode 100644 index ad94401d4e69f15d116fe5a98966e7d268914985..0000000000000000000000000000000000000000 --- a/Sources/Presentation/CenterTransition.swift +++ /dev/null @@ -1,136 +0,0 @@ -import UIKit -import Combine -import SnapKit -import Shared - -final class CenterTransition: NSObject, UIViewControllerAnimatedTransitioning { - enum Direction { - case present - case dismiss - } - - let dismissable: Bool - var direction: Direction = .present - private let onDismissal: EmptyClosure - private weak var darkOverlayView: UIControl? - private weak var topConstraint: Constraint? - private weak var bottomConstraint: Constraint? - private var cancellables = Set<AnyCancellable>() - - private var presentedConstraints: [NSLayoutConstraint] = [] - private var dismissedConstraints: [NSLayoutConstraint] = [] - - init(onDismissal: @escaping EmptyClosure, - dismissable: Bool = true) { - self.dismissable = dismissable - self.onDismissal = onDismissal - super.init() - } - - func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.25 } - - func animateTransition(using context: UIViewControllerContextTransitioning) { - switch direction { - case .present: - present(using: context) - case .dismiss: - dismiss(using: context) - } - } - - private func present(using context: UIViewControllerContextTransitioning) { - guard let presentingController = context.viewController(forKey: .from), - let presentedView = context.view(forKey: .to) else { - context.completeTransition(false) - return - } - - let darkOverlayView = UIControl() - self.darkOverlayView = darkOverlayView - - darkOverlayView.alpha = 0.0 - darkOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) - context.containerView.addSubview(darkOverlayView) - darkOverlayView.frame = context.containerView.bounds - - if dismissable { - darkOverlayView - .publisher(for: .touchUpInside) - .sink { [weak presentingController] _ in - presentingController?.dismiss(animated: true) - } - .store(in: &cancellables) - } - - context.containerView.addSubview(presentedView) - presentedView.translatesAutoresizingMaskIntoConstraints = false - presentedView.alpha = 0.0 - - presentedView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - - presentedConstraints = [ - presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor, constant: 40), - presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor, constant: -40), - presentedView.centerYAnchor.constraint(equalTo: context.containerView.centerYAnchor) - ] - - dismissedConstraints = [ - presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor, constant: 40), - presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor, constant: -40), - presentedView.centerYAnchor.constraint(equalTo: context.containerView.centerYAnchor) - ] - - NSLayoutConstraint.activate(dismissedConstraints) - - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - - NSLayoutConstraint.deactivate(dismissedConstraints) - NSLayoutConstraint.activate(presentedConstraints) - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { - darkOverlayView.alpha = 1.0 - presentedView.alpha = 1.0 - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - presentedView.transform = .identity - }, - completion: { _ in - context.completeTransition(true) - }) - } - - private func dismiss(using context: UIViewControllerContextTransitioning) { - NSLayoutConstraint.deactivate(presentedConstraints) - NSLayoutConstraint.activate(dismissedConstraints) - - guard let presentedView = context.view(forKey: .from) else { - context.completeTransition(false) - return - } - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { [weak darkOverlayView] in - darkOverlayView?.alpha = 0.0 - presentedView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - presentedView.alpha = 0.0 - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - }, - completion: { [weak self] _ in - context.completeTransition(true) - self?.onDismissal() - }) - } -} diff --git a/Sources/Presentation/FadePresenter.swift b/Sources/Presentation/FadePresenter.swift deleted file mode 100644 index d4d27c85ec5af65355d97487b06fb39f4c4fa2e3..0000000000000000000000000000000000000000 --- a/Sources/Presentation/FadePresenter.swift +++ /dev/null @@ -1,29 +0,0 @@ -import UIKit - -public final class FadePresenter: NSObject, Presenting { - private var transition: FadeTransition? - - public func present(_ viewControllers: UIViewController..., from parent: UIViewController) { - guard let screen = viewControllers.first else { - fatalError("Tried to present empty list of view controllers") - } - - screen.modalPresentationStyle = .overFullScreen - screen.transitioningDelegate = self - parent.present(screen, animated: true) - } -} - -extension FadePresenter: UIViewControllerTransitioningDelegate { - public func animationController(forPresented presented: UIViewController, - presenting: UIViewController, - source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition = FadeTransition(didDismiss: { [weak self] in self?.transition = nil }) - return transition - } - - public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition?.direction = .dismiss - return transition - } -} diff --git a/Sources/Presentation/FadeTransition.swift b/Sources/Presentation/FadeTransition.swift deleted file mode 100644 index 71841ccd9b4433c8d95066be3e835bffe4f49c41..0000000000000000000000000000000000000000 --- a/Sources/Presentation/FadeTransition.swift +++ /dev/null @@ -1,90 +0,0 @@ -import UIKit -import Combine -import SnapKit -import Shared - -final class FadeTransition: NSObject, UIViewControllerAnimatedTransitioning { - enum Direction { - case present - case dismiss - } - - var direction: Direction = .present - private let didDismiss: EmptyClosure - private weak var darkOverlayView: UIControl? - - init(didDismiss: @escaping EmptyClosure) { - self.didDismiss = didDismiss - super.init() - } - - func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.25 } - - func animateTransition(using context: UIViewControllerContextTransitioning) { - switch direction { - case .present: - present(using: context) - case .dismiss: - dismiss(using: context) - } - } - - private func present(using context: UIViewControllerContextTransitioning) { - guard let presentedView = context.view(forKey: .to) else { - context.completeTransition(false) - return - } - - let darkOverlayView = UIControl() - self.darkOverlayView = darkOverlayView - - darkOverlayView.alpha = 0.0 - darkOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) - context.containerView.addSubview(darkOverlayView) - darkOverlayView.frame = context.containerView.bounds - - context.containerView.addSubview(presentedView) - presentedView.alpha = 0.0 - - presentedView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - presentedView.snp.makeConstraints { $0.edges.equalToSuperview() } - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { - darkOverlayView.alpha = 1.0 - presentedView.alpha = 1.0 - presentedView.transform = .identity - }, - completion: { _ in - context.completeTransition(true) - }) - } - - private func dismiss(using context: UIViewControllerContextTransitioning) { - guard let presentedView = context.view(forKey: .from) else { - context.completeTransition(false) - return - } - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { [weak darkOverlayView] in - darkOverlayView?.alpha = 0.0 - presentedView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - presentedView.alpha = 0.0 - }, - completion: { [weak self] _ in - context.completeTransition(true) - self?.didDismiss() - }) - } -} diff --git a/Sources/Presentation/FullscreenPresenter.swift b/Sources/Presentation/FullscreenPresenter.swift deleted file mode 100644 index 207c77e66aabce11e6682b32701fc6a2e483befc..0000000000000000000000000000000000000000 --- a/Sources/Presentation/FullscreenPresenter.swift +++ /dev/null @@ -1,33 +0,0 @@ -import UIKit - -public final class FullscreenPresenter: NSObject, Presenting { - private var transition: FullscreenTransition? - - public func present(_ viewControllers: UIViewController..., from parent: UIViewController) { - guard let screen = viewControllers.first else { - fatalError("Tried to present empty list of view controllers") - } - - screen.modalPresentationStyle = .overFullScreen - screen.transitioningDelegate = self - parent.present(screen, animated: true) - } -} - -// MARK: UIViewControllerTransitioningDelegate -extension FullscreenPresenter: UIViewControllerTransitioningDelegate { - public func animationController(forPresented presented: UIViewController, - presenting: UIViewController, - source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition = FullscreenTransition(onDismissal: { [weak self] in - self?.transition = nil - }) - - return transition - } - - public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition?.direction = .dismiss - return transition - } -} diff --git a/Sources/Presentation/FullscreenTransition.swift b/Sources/Presentation/FullscreenTransition.swift deleted file mode 100644 index 7a36e5beb64cbe2227890c37cd7215b87088193e..0000000000000000000000000000000000000000 --- a/Sources/Presentation/FullscreenTransition.swift +++ /dev/null @@ -1,120 +0,0 @@ -import UIKit -import Combine -import SnapKit -import Shared - -final class FullscreenTransition: NSObject, UIViewControllerAnimatedTransitioning { - enum Direction { - case present - case dismiss - } - - var direction: Direction = .present - private let onDismissal: EmptyClosure - private weak var darkOverlayView: UIControl? - private weak var topConstraint: Constraint? - private weak var bottomConstraint: Constraint? - private var cancellables = Set<AnyCancellable>() - - private var presentedConstraints: [NSLayoutConstraint] = [] - private var dismissedConstraints: [NSLayoutConstraint] = [] - - init(onDismissal: @escaping EmptyClosure) { - self.onDismissal = onDismissal - super.init() - } - - func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.5 } - - func animateTransition(using context: UIViewControllerContextTransitioning) { - switch direction { - case .present: - present(using: context) - case .dismiss: - dismiss(using: context) - } - } - - private func present(using context: UIViewControllerContextTransitioning) { - guard let presentingController = context.viewController(forKey: .from), - let presentedView = context.view(forKey: .to) else { - context.completeTransition(false) - return - } - - let darkOverlayView = UIControl() - self.darkOverlayView = darkOverlayView - - darkOverlayView.alpha = 0.0 - darkOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) - context.containerView.addSubview(darkOverlayView) - darkOverlayView.frame = context.containerView.bounds - - darkOverlayView - .publisher(for: .touchUpInside) - .sink { [weak presentingController] _ in - presentingController?.dismiss(animated: true) - }.store(in: &cancellables) - - context.containerView.addSubview(presentedView) - presentedView.translatesAutoresizingMaskIntoConstraints = false - - presentedConstraints = [ - presentedView.topAnchor.constraint(equalTo: context.containerView.topAnchor), - presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), - presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), - presentedView.bottomAnchor.constraint(equalTo: context.containerView.bottomAnchor) - ] - - dismissedConstraints = [ - presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), - presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), - presentedView.topAnchor.constraint(equalTo: context.containerView.bottomAnchor), - presentedView.heightAnchor.constraint(equalTo: context.containerView.heightAnchor) - ] - - NSLayoutConstraint.activate(dismissedConstraints) - - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - - NSLayoutConstraint.deactivate(dismissedConstraints) - NSLayoutConstraint.activate(presentedConstraints) - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { - darkOverlayView.alpha = 1.0 - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - }, - completion: { _ in - context.completeTransition(true) - }) - } - - private func dismiss(using context: UIViewControllerContextTransitioning) { - NSLayoutConstraint.deactivate(presentedConstraints) - NSLayoutConstraint.activate(dismissedConstraints) - - UIView.animate( - withDuration: transitionDuration(using: context), - delay: 0, - usingSpringWithDamping: 1, - initialSpringVelocity: 0, - options: .curveEaseInOut, - animations: { [weak darkOverlayView] in - darkOverlayView?.alpha = 0.0 - context.containerView.setNeedsLayout() - context.containerView.layoutIfNeeded() - }, - completion: { [weak self] _ in - context.completeTransition(true) - self?.onDismissal() - }) - } -} diff --git a/Sources/Presentation/Presenting.swift b/Sources/Presentation/Presenting.swift deleted file mode 100644 index b79da38e915bca7cefe9a82997ade14eae846135..0000000000000000000000000000000000000000 --- a/Sources/Presentation/Presenting.swift +++ /dev/null @@ -1,100 +0,0 @@ -import UIKit -import Shared - -public protocol Presenting { - func present(_ target: UIViewController..., from parent: UIViewController) - func dismiss(from parent: UIViewController) -} - -public extension Presenting { - func dismiss(from parent: UIViewController) { - parent.dismiss(animated: true) - } -} - -public struct PushPresenter: Presenting { - public init() {} - - public func present(_ target: UIViewController..., from parent: UIViewController) { - parent.navigationController?.pushViewController(target.first!, animated: true) - } -} - -public struct ModalPresenter: Presenting { - public init() {} - - public func present(_ target: UIViewController..., from parent: UIViewController) { - let statusBarVC = RootViewController(UINavigationController(rootViewController: target.first!)) - statusBarVC.modalPresentationStyle = .fullScreen - parent.present(statusBarVC, animated: true) - } -} - -public struct ReplacePresenter: Presenting { - public enum Mode { - case replaceAll - case replaceLast - case replaceBackwards(AnyObject.Type) - } - - var mode: Mode - - public init(mode: Mode = .replaceAll) { - self.mode = mode - } - - public func present(_ target: UIViewController..., from parent: UIViewController) { - guard let navigationController = parent.navigationController else { return } - - switch mode { - case .replaceAll: - navigationController.setViewControllers(target, animated: true) - - case .replaceBackwards(let OlderInStack): - if let oldScreen = navigationController.viewControllers.filter({ $0.isKind(of: OlderInStack.self) }).first, - let index = navigationController.viewControllers.firstIndex(of: oldScreen) { - - let viewControllersBefore = - navigationController.viewControllers.dropLast( - navigationController.viewControllers.count - index - ) - - if let coordinator = navigationController.transitionCoordinator { - coordinator.animate(alongsideTransition: nil) { _ in - navigationController.setViewControllers(viewControllersBefore + target , animated: true) - } - } else { - navigationController.setViewControllers(viewControllersBefore + target , animated: true) - } - - } else { - navigationController.pushViewController(target.first!, animated: true) - } - case .replaceLast: - let viewControllersBefore = navigationController.viewControllers.dropLast() - - func replace() { - navigationController.setViewControllers(viewControllersBefore + target , animated: true) - } - - if let coordinator = navigationController.transitionCoordinator { - coordinator.animate(alongsideTransition: nil) { _ in - replace() - } - } else { - replace() - } - } - } -} - -public struct PopReplacePresenter: Presenting { - public init() {} - - public func present(_ target: UIViewController..., from parent: UIViewController) { - if let lastViewController = parent.navigationController?.viewControllers.last { - parent.navigationController?.setViewControllers([target.first!, lastViewController], animated: false) - parent.navigationController?.setViewControllers([target.first!], animated: true) - } - } -} diff --git a/Sources/Presentation/SideMenuAnimator.swift b/Sources/Presentation/SideMenuAnimator.swift deleted file mode 100644 index 02f302616920022d137f73b8bc704fac75883382..0000000000000000000000000000000000000000 --- a/Sources/Presentation/SideMenuAnimator.swift +++ /dev/null @@ -1,26 +0,0 @@ -import UIKit - -public protocol SideMenuAnimating { - func animate(in containerView: UIView, to progress: CGFloat) -} - -public struct SideMenuAnimator: SideMenuAnimating { - public init() {} - - public func animate(in containerView: UIView, to progress: CGFloat) { - guard let fromView = containerView.viewWithTag(SideMenuPresentTransition.fromViewTag) - else { return } - - let cornerRadius = progress * 24 - let shadowOpacity = Float(progress) - let offsetX = containerView.bounds.size.width * 0.5 * progress - let offsetY = containerView.bounds.size.height * 0.08 * progress - let scale = 1 - (0.25 * progress) - - fromView.subviews.first?.layer.cornerRadius = cornerRadius - fromView.layer.shadowOpacity = shadowOpacity - fromView.transform = CGAffineTransform.identity - .translatedBy(x: offsetX, y: offsetY) - .scaledBy(x: scale, y: scale) - } -} diff --git a/Sources/Presentation/SideMenuDismissInteractor.swift b/Sources/Presentation/SideMenuDismissInteractor.swift deleted file mode 100644 index d170ebce1aff83b1e75641ee6970a51497ccb44c..0000000000000000000000000000000000000000 --- a/Sources/Presentation/SideMenuDismissInteractor.swift +++ /dev/null @@ -1,73 +0,0 @@ -import UIKit -import Shared - -public protocol SideMenuDismissInteracting: UIViewControllerInteractiveTransitioning { - var interactionInProgress: Bool { get } - - func setup(view: UIView, action: @escaping EmptyClosure) -} - -public final class SideMenuDismissInteractor: UIPercentDrivenInteractiveTransition, SideMenuDismissInteracting { - private var action: EmptyClosure? - private var shouldFinishTransition = false - - // MARK: SideMenuDismissInteracting - - public var interactionInProgress = false - - public func setup(view: UIView, action: @escaping EmptyClosure) { - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - view.addGestureRecognizer(panRecognizer) - - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) - view.addGestureRecognizer(tapRecognizer) - - self.action = action - } - - // MARK: Gesture handling - - @objc - private func handleTapGesture(_ recognizer: UITapGestureRecognizer) { - action?() - } - - @objc - private func handlePanGesture(_ recognizer: UIPanGestureRecognizer) { - guard let view = recognizer.view, - let containerView = view.superview - else { return } - - let viewWidth = containerView.bounds.size.width - guard viewWidth > 0 else { return } - - let translation = recognizer.translation(in: view) - let progress = min(1, max(0, -translation.x / (viewWidth * 0.8))) - - switch recognizer.state { - case .possible, .failed: - interactionInProgress = false - - case .began: - interactionInProgress = true - shouldFinishTransition = false - action?() - - case .changed: - shouldFinishTransition = progress >= 0.5 - update(progress) - - case .cancelled: - interactionInProgress = false - cancel() - - case .ended: - interactionInProgress = false - shouldFinishTransition ? finish() : cancel() - - @unknown default: - interactionInProgress = false - cancel() - } - } -} diff --git a/Sources/Presentation/SideMenuDismissTransition.swift b/Sources/Presentation/SideMenuDismissTransition.swift deleted file mode 100644 index 829367fec8d564210b9f018eba1a0c6a4ac9a04b..0000000000000000000000000000000000000000 --- a/Sources/Presentation/SideMenuDismissTransition.swift +++ /dev/null @@ -1,31 +0,0 @@ -import UIKit - -final class SideMenuDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { - - init(menuAnimator: SideMenuAnimating, - viewAnimator: UIViewAnimating.Type) { - self.menuAnimator = menuAnimator - self.viewAnimator = viewAnimator - super.init() - } - - let menuAnimator: SideMenuAnimating - let viewAnimator: UIViewAnimating.Type - - // MARK: UIViewControllerAnimatedTransitioning - - func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.25 } - - func animateTransition(using context: UIViewControllerContextTransitioning) { - viewAnimator.animate( - withDuration: transitionDuration(using: context), - animations: { - self.menuAnimator.animate(in: context.containerView, to: 0) - }, - completion: { _ in - let isCancelled = context.transitionWasCancelled - context.completeTransition(isCancelled == false) - } - ) - } -} diff --git a/Sources/Presentation/SideMenuPresentTransition.swift b/Sources/Presentation/SideMenuPresentTransition.swift deleted file mode 100644 index a29d7aea09e7e6414116bfb4ebd38da78b7d07c8..0000000000000000000000000000000000000000 --- a/Sources/Presentation/SideMenuPresentTransition.swift +++ /dev/null @@ -1,67 +0,0 @@ -import UIKit -import Shared - -final class SideMenuPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { - static let fromViewTag = UUID().hashValue - - init( - dismissInteractor: SideMenuDismissInteracting, - menuAnimator: SideMenuAnimating, - viewAnimator: UIViewAnimating.Type - ) { - self.dismissInteractor = dismissInteractor - self.menuAnimator = menuAnimator - self.viewAnimator = viewAnimator - super.init() - } - - let dismissInteractor: SideMenuDismissInteracting - let menuAnimator: SideMenuAnimating - let viewAnimator: UIViewAnimating.Type - - // MARK: UIViewControllerAnimatedTransitioning - - func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.25 } - - func animateTransition(using context: UIViewControllerContextTransitioning) { - guard let fromVC = context.viewController(forKey: .from), - let fromSnapshot = fromVC.view.snapshotView(afterScreenUpdates: true), - let toVC = context.viewController(forKey: .to) - else { - context.completeTransition(false) - return - } - - context.containerView.addSubview(toVC.view) - toVC.view.frame = context.containerView.bounds - - let fromView = UIView() - fromView.tag = Self.fromViewTag - context.containerView.addSubview(fromView) - fromView.frame = context.containerView.bounds - fromView.layer.shadowColor = UIColor.black.cgColor - fromView.layer.shadowOpacity = 1 - fromView.layer.shadowOffset = .zero - fromView.layer.shadowRadius = 32 - fromView.addSubview(fromSnapshot) - fromSnapshot.frame = fromView.bounds - fromSnapshot.layer.cornerRadius = 0 - fromSnapshot.layer.masksToBounds = true - - dismissInteractor.setup( - view: fromView, - action: { fromVC.dismiss(animated: true) } - ) - - viewAnimator.animate( - withDuration: transitionDuration(using: context), - animations: { - self.menuAnimator.animate(in: context.containerView, to: 1) - }, - completion: { _ in - let isCancelled = context.transitionWasCancelled - context.completeTransition(isCancelled == false) - } - ) - } -} diff --git a/Sources/Presentation/SideMenuPresenter.swift b/Sources/Presentation/SideMenuPresenter.swift deleted file mode 100644 index 5d9036dee7ef729973eeef8cf4f5d56e7b3d0bab..0000000000000000000000000000000000000000 --- a/Sources/Presentation/SideMenuPresenter.swift +++ /dev/null @@ -1,56 +0,0 @@ -import UIKit - -public final class SideMenuPresenter: NSObject, - Presenting, - UIViewControllerTransitioningDelegate { - - public init(dismissInteractor: SideMenuDismissInteracting = SideMenuDismissInteractor(), - menuAnimator: SideMenuAnimating = SideMenuAnimator(), - viewAnimator: UIViewAnimating.Type = UIView.self) { - self.dismissInteractor = dismissInteractor - self.menuAnimator = menuAnimator - self.viewAnimator = viewAnimator - super.init() - } - - let dismissInteractor: SideMenuDismissInteracting - let menuAnimator: SideMenuAnimating - let viewAnimator: UIViewAnimating.Type - - // MARK: Presenting - - public func present(_ viewControllers: UIViewController..., from parent: UIViewController) { - guard let screen = viewControllers.first else { - fatalError("Tried to present empty list of view controllers") - } - - screen.modalPresentationStyle = .overFullScreen - screen.transitioningDelegate = self - parent.present(screen, animated: true) - } - - // MARK: UIViewControllerTransitioningDelegate - - public func animationController( - forPresented presented: UIViewController, - presenting: UIViewController, - source: UIViewController - ) -> UIViewControllerAnimatedTransitioning? { - SideMenuPresentTransition(dismissInteractor: dismissInteractor, - menuAnimator: menuAnimator, - viewAnimator: viewAnimator) - } - - public func animationController( - forDismissed dismissed: UIViewController - ) -> UIViewControllerAnimatedTransitioning? { - SideMenuDismissTransition(menuAnimator: menuAnimator, - viewAnimator: viewAnimator) - } - - public func interactionControllerForDismissal( - using animator: UIViewControllerAnimatedTransitioning - ) -> UIViewControllerInteractiveTransitioning? { - dismissInteractor.interactionInProgress ? dismissInteractor : nil - } -} diff --git a/Sources/Presentation/UIViewAnimating.swift b/Sources/Presentation/UIViewAnimating.swift deleted file mode 100644 index 63210a2c3ef1b1cd921540d0f994418c18f089a5..0000000000000000000000000000000000000000 --- a/Sources/Presentation/UIViewAnimating.swift +++ /dev/null @@ -1,12 +0,0 @@ -import UIKit -import Shared - -public protocol UIViewAnimating { - static func animate( - withDuration duration: TimeInterval, - animations: @escaping EmptyClosure, - completion: ((Bool) -> Void)? - ) -} - -extension UIView: UIViewAnimating {} diff --git a/Sources/ProfileFeature/Controllers/ProfileController.swift b/Sources/ProfileFeature/Controllers/ProfileController.swift index d80d7a9c3f48079eaaacc2f7d5a859ff472745f2..3662169233784d0c2f6420e0e5d1c512a414334c 100644 --- a/Sources/ProfileFeature/Controllers/ProfileController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileController.swift @@ -65,7 +65,7 @@ public final class ProfileController: UIViewController { self.viewModel.didTapDelete(isEmail: true) } } else { - navigator.perform(PresentProfileEmail()) + navigator.perform(PresentProfileEmail(on: navigationController!)) } }.store(in: &cancellables) @@ -87,7 +87,7 @@ public final class ProfileController: UIViewController { self.viewModel.didTapDelete(isEmail: false) } } else { - navigator.perform(PresentProfilePhone()) + navigator.perform(PresentProfilePhone(on: navigationController!)) } }.store(in: &cancellables) @@ -112,10 +112,10 @@ public final class ProfileController: UIViewController { title: Localized.Profile.Photo.title, subtitle: Localized.Profile.Photo.subtitle, actionTitle: Localized.Profile.Photo.continue) { - self.navigator.perform(PresentPhotoLibrary()) + self.navigator.perform(PresentPhotoLibrary(from: self)) } case .libraryPermission: - self.navigator.perform(PresentPermissionRequest(type: .library)) + self.navigator.perform(PresentPermissionRequest(type: .library, from: self)) case .none: break } @@ -190,11 +190,11 @@ public final class ProfileController: UIViewController { spacingAfter: 37 ), actionButton - ])) + ], isDismissable: true, from: self)) } @objc private func didTapMenu() { - navigator.perform(PresentMenu(currentItem: .profile)) + navigator.perform(PresentMenu(currentItem: .profile, from: self)) } } diff --git a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift index 4fb0b42f4e56f373d28c45dc70fb2315fff4ba51..1bfc1c789b7f4de906f56aaad09c94f08a8fb7ba 100644 --- a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift @@ -64,7 +64,8 @@ public final class ProfileEmailController: UIViewController { PresentProfileCode( isEmail: true, content: $0.input, - confirmationId: id + confirmationId: id, + on: navigationController! ) ) }.store(in: &cancellables) diff --git a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift index a1e4216839cddacd3ce96622c6d8a872462b0cc5..bd9689a44bcc96f648bb1c6427de7b15148957a7 100644 --- a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift +++ b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift @@ -63,7 +63,7 @@ public final class ProfilePhoneController: UIViewController { navigator.perform(PresentCountryList(completion: { [weak self] in guard let self else { return } self.viewModel.didChooseCountry($0 as! Country) - })) + }, from: self)) }.store(in: &cancellables) viewModel @@ -76,7 +76,8 @@ public final class ProfilePhoneController: UIViewController { PresentProfileCode( isEmail: false, content: content, - confirmationId: id + confirmationId: id, + on: navigationController! ) ) }.store(in: &cancellables) diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift index a71cb16be3e8d4643d01112dfedb8200fb4924d5..2eadd36da44361a596cc1ddaa81dbbefb60c5d0b 100644 --- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift @@ -1,11 +1,11 @@ import Shared import Combine import XXClient -import Countries import InputField import Foundation import CombineSchedulers import XXMessengerClient +import CountryListFeature import DI final class ProfilePhoneViewModel { diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index 1b0803add61d03bba84bb92e4dc989e442c4908e..17daebf9d9bbbba24b545c59f7dca5c8a0e88660 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -3,12 +3,12 @@ import Shared import Combine import Defaults import XXClient -import Countries import Foundation import Permissions import BackupFeature import XXMessengerClient import CombineSchedulers +import CountryListFeature import DI enum ProfileNavigationRoutes { diff --git a/Sources/PushFeature/PushExtractor.swift b/Sources/PushFeature/PushExtractor.swift index c2848caeaf8f10aae030177b8367935d8ffffa3b..75c6a69070d0d301498122896ac90ab2ab48c987 100644 --- a/Sources/PushFeature/PushExtractor.swift +++ b/Sources/PushFeature/PushExtractor.swift @@ -1,6 +1,6 @@ import XXModels -import Foundation import XXClient +import Foundation import XXMessengerClient import DI diff --git a/Sources/ReportingFeature/MakeReportDrawer.swift b/Sources/ReportingFeature/MakeReportDrawer.swift index bd102a6ee2e046b03c787d250f8a0573d25af7c3..249683a0de30030e305fc399fcdc841dbee1bd75 100644 --- a/Sources/ReportingFeature/MakeReportDrawer.swift +++ b/Sources/ReportingFeature/MakeReportDrawer.swift @@ -1,86 +1,87 @@ -import DrawerFeature -import Shared import UIKit +import Shared +import AppResources +import DrawerFeature import XCTestDynamicOverlay public struct MakeReportDrawer { - public struct Config { - public init( - onReport: @escaping () -> Void = {}, - onCancel: @escaping () -> Void = {} - ) { - self.onReport = onReport - self.onCancel = onCancel - } - - public var onReport: () -> Void - public var onCancel: () -> Void + public struct Config { + public init( + onReport: @escaping () -> Void = {}, + onCancel: @escaping () -> Void = {} + ) { + self.onReport = onReport + self.onCancel = onCancel } - public var run: (Config) -> UIViewController + public var onReport: () -> Void + public var onCancel: () -> Void + } - public func callAsFunction(_ config: Config) -> UIViewController { - run(config) - } + public var run: (Config) -> UIViewController + + public func callAsFunction(_ config: Config) -> UIViewController { + run(config) + } } extension MakeReportDrawer { - public static let live = MakeReportDrawer { config in - let cancelButton = CapsuleButton() - cancelButton.setStyle(.seeThrough) - cancelButton.setTitle(Localized.Chat.Report.cancel, for: .normal) + public static let live = MakeReportDrawer { config in + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Chat.Report.cancel, for: .normal) - let reportButton = CapsuleButton() - reportButton.setStyle(.red) - reportButton.setTitle(Localized.Chat.Report.action, for: .normal) + let reportButton = CapsuleButton() + reportButton.setStyle(.red) + reportButton.setTitle(Localized.Chat.Report.action, for: .normal) - let drawer = DrawerController([ - DrawerImage( - image: Asset.drawerNegative.image - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 18.0), - text: Localized.Chat.Report.title, - color: Asset.neutralActive.color - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Chat.Report.subtitle, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ), - DrawerStack( - axis: .vertical, - spacing: 20.0, - views: [reportButton, cancelButton] - ) - ]) + let drawer = DrawerController([ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Chat.Report.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.Report.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + axis: .vertical, + spacing: 20.0, + views: [reportButton, cancelButton] + ) + ]) - reportButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned drawer] in - drawer.dismiss(animated: true) { - config.onReport() - } - } - .store(in: &drawer.cancellables) + reportButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned drawer] in + drawer.dismiss(animated: true) { + config.onReport() + } + } + .store(in: &drawer.cancellables) - cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned drawer] in - drawer.dismiss(animated: true) { - config.onCancel() - } - } - .store(in: &drawer.cancellables) + cancelButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned drawer] in + drawer.dismiss(animated: true) { + config.onCancel() + } + } + .store(in: &drawer.cancellables) - return drawer - } + return drawer + } } extension MakeReportDrawer { - public static let unimplemented = MakeReportDrawer( - run: XCTUnimplemented("\(Self.self)") - ) + public static let unimplemented = MakeReportDrawer( + run: XCTUnimplemented("\(Self.self)") + ) } diff --git a/Sources/Permissions/RequestPermissionController.swift b/Sources/RequestPermissionFeature/RequestPermissionController.swift similarity index 78% rename from Sources/Permissions/RequestPermissionController.swift rename to Sources/RequestPermissionFeature/RequestPermissionController.swift index 24b09405aa22942886d84ffa9b1012fcd2d34a80..2a8e364d2315742f0e0d674549830ac9a9778744 100644 --- a/Sources/Permissions/RequestPermissionController.swift +++ b/Sources/RequestPermissionFeature/RequestPermissionController.swift @@ -1,13 +1,14 @@ -import DI import UIKit import Shared import Combine -import Navigation +import AppResources +import StatusBarFeature +import PermissionsFeature +import ComposableArchitecture public final class RequestPermissionController: UIViewController { - @Dependency var navigator: Navigator - @Dependency var barStylist: StatusBarStylist - @Dependency var permissions: PermissionHandling + @Dependency(\.statusBar) var statusBar: StatusBarStyleManager + @Dependency(\.permissions) var permissions: PermissionsManager private lazy var screenView = RequestPermissionView() @@ -27,10 +28,7 @@ public final class RequestPermissionController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - barStylist.styleSubject.send(.darkContent) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) + statusBar.update(.darkContent) } public override func viewDidLoad() { @@ -62,7 +60,7 @@ public final class RequestPermissionController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(DismissModal(from: self)) + dismiss(animated: true) }.store(in: &cancellables) screenView @@ -72,17 +70,17 @@ public final class RequestPermissionController: UIViewController { .sink { [unowned self] in switch permissionType { case .camera: - permissions.requestCamera { [weak self] _ in + permissions.camera.request { [weak self] _ in guard let self else { return } self.shouldDismissModal() } case .library: - permissions.requestPhotos { [weak self] _ in + permissions.library.request { [weak self] _ in guard let self else { return } self.shouldDismissModal() } case .microphone: - permissions.requestMicrophone { [weak self] _ in + permissions.microphone.request { [weak self] _ in guard let self else { return } self.shouldDismissModal() } @@ -93,7 +91,7 @@ public final class RequestPermissionController: UIViewController { private func shouldDismissModal() { DispatchQueue.main.async { [weak self] in guard let self else { return } - self.navigator.perform(DismissModal(from: self)) + self.dismiss(animated: true) } } } diff --git a/Sources/RequestPermissionFeature/RequestPermissionView.swift b/Sources/RequestPermissionFeature/RequestPermissionView.swift new file mode 100644 index 0000000000000000000000000000000000000000..ab4405bd0844fce7bf2273f8e8a0944106e4c894 --- /dev/null +++ b/Sources/RequestPermissionFeature/RequestPermissionView.swift @@ -0,0 +1,103 @@ +import UIKit +import Shared +import AppResources + +final class RequestPermissionView: UIView { + let titleLabel = UILabel() + let iconImage = UIImageView() + let subtitleLabel = UILabel() + let littleLogo = UIImageView() + let notNowButton = UIButton() + let continueButton = CapsuleButton() + + init() { + super.init(frame: .zero) + littleLogo.image = Asset.permissionLogo.image + notNowButton.setTitle(Localized.Chat.Actions.Permission.notnow, for: .normal) + continueButton.set(style: .brandColored, title: Localized.Chat.Actions.Permission.continue) + + titleLabel.textAlignment = .center + + backgroundColor = Asset.neutralWhite.color + titleLabel.textColor = Asset.neutralActive.color + notNowButton.setTitleColor(Asset.neutralWeak.color, for: .normal) + + subtitleLabel.numberOfLines = 0 + + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + notNowButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 16) + + let actionsContainer = UIView() + actionsContainer.addSubview(continueButton) + actionsContainer.addSubview(notNowButton) + + addSubview(iconImage) + addSubview(titleLabel) + addSubview(littleLogo) + addSubview(subtitleLabel) + addSubview(actionsContainer) + + iconImage.snp.makeConstraints { + $0.centerX.equalToSuperview() + } + + titleLabel.snp.makeConstraints { + $0.top.equalTo(iconImage.snp.bottom).offset(34) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + } + + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(32) + $0.right.equalToSuperview().offset(-32) + $0.bottom.equalTo(snp.centerY) + } + + littleLogo.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-15) + } + + actionsContainer.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(subtitleLabel.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualTo(littleLogo.snp.top) + } + + continueButton.snp.makeConstraints { + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalTo(actionsContainer.snp.centerY).offset(-5) + } + + notNowButton.snp.makeConstraints { + $0.top.equalTo(actionsContainer.snp.centerY).offset(5) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func setup(title: String, subtitle: String, image: UIImage) { + iconImage.image = image + titleLabel.text = title + + let paragraph = NSMutableParagraphStyle() + paragraph.lineHeightMultiple = 1.5 + paragraph.alignment = .center + + subtitleLabel.attributedText = NSAttributedString( + string: subtitle, + attributes: [ + .paragraphStyle: paragraph, + .font: Fonts.Mulish.regular.font(size: 14.0), + .foregroundColor: Asset.neutralBody.color, + ] + ) + } +} diff --git a/Sources/RequestsFeature/Controllers/RequestsContainerController.swift b/Sources/RequestsFeature/Controllers/RequestsContainerController.swift index 3a1c2488cf57eeebbe90eb14706a9069d74aead1..97615508af52356b838b25614e59dc15d29da2b6 100644 --- a/Sources/RequestsFeature/Controllers/RequestsContainerController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsContainerController.swift @@ -77,7 +77,11 @@ public final class RequestsContainerController: UIViewController { .connectionsPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentSearch(replacing: false)) + navigator.perform(PresentSearch( + searching: nil, + replacing: false, + on: navigationController! + )) }.store(in: &cancellables) screenView @@ -108,7 +112,7 @@ public final class RequestsContainerController: UIViewController { } @objc private func didTapMenu() { - navigator.perform(PresentMenu(currentItem: .requests)) + navigator.perform(PresentMenu(currentItem: .requests, from: self)) } } diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift index fe07a5bd8a326a075143154fde4ae58e24d622f2..674e3bd10232c2d4e615200526e4c5ebe1c2264b 100644 --- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift @@ -2,9 +2,9 @@ import UIKit import Shared import Combine import XXModels -import Countries import Navigation import DrawerFeature +import CountryListFeature import DI final class RequestsReceivedController: UIViewController { @@ -183,7 +183,8 @@ extension RequestsReceivedController { guard let self else { return } self.drawerCancellables.removeAll() self.navigator.perform(PresentGroupChat( - model: self.viewModel.groupChatWith(group: group) + groupInfo: self.viewModel.groupChatWith(group: group), + on: self.navigationController! )) } }.store(in: &drawerCancellables) @@ -198,7 +199,7 @@ extension RequestsReceivedController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer(items: items)) + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) } } @@ -264,7 +265,7 @@ extension RequestsReceivedController { navigator.perform(DismissModal(from: self)) { [weak self] in guard let self else { return } self.drawerCancellables.removeAll() - self.navigator.perform(PresentChat(contact: contact)) + self.navigator.perform(PresentChat(contact: contact, on: navigationController!)) } }.store(in: &drawerCancellables) @@ -278,7 +279,7 @@ extension RequestsReceivedController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer(items: items)) + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) } } @@ -389,7 +390,7 @@ extension RequestsReceivedController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer(items: items)) + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) } } @@ -540,7 +541,7 @@ extension RequestsReceivedController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer(items: items)) + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) } } @@ -588,6 +589,6 @@ extension RequestsReceivedController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer(items: items)) + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) } } diff --git a/Sources/RequestsFeature/Views/RequestCell.swift b/Sources/RequestsFeature/Views/RequestCell.swift index e0af9ec6017ef1241ed0831ee0dbad71af7dc3d4..c1c81d28222654804ad1b0332608ce807c080c9d 100644 --- a/Sources/RequestsFeature/Views/RequestCell.swift +++ b/Sources/RequestsFeature/Views/RequestCell.swift @@ -1,259 +1,259 @@ import UIKit import Shared import Combine -import Countries +import CountryListFeature final class RequestCell: UICollectionViewCell { - let titleLabel = UILabel() - let leaderLabel = UILabel() - let emailLabel = UILabel() - let phoneLabel = UILabel() - let dateLabel = UILabel() - let stackView = UIStackView() - let avatarView = AvatarView() - let stateButton = RequestCellButton() - - var cancellables = Set<AnyCancellable>() - var didTapStateButton: (() -> Void)! - - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = Asset.neutralWhite.color - - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - emailLabel.font = Fonts.Mulish.regular.font(size: 14.0) - phoneLabel.font = Fonts.Mulish.regular.font(size: 14.0) - leaderLabel.font = Fonts.Mulish.regular.font(size: 14.0) - - emailLabel.textColor = Asset.neutralSecondaryAlternative.color - phoneLabel.textColor = Asset.neutralSecondaryAlternative.color - leaderLabel.textColor = Asset.neutralSecondaryAlternative.color - - dateLabel.font = Fonts.Mulish.regular.font(size: 10.0) - dateLabel.textColor = Asset.neutralWeak.color - - stackView.axis = .vertical - stackView.spacing = 4 - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(leaderLabel) - stackView.addArrangedSubview(emailLabel) - stackView.addArrangedSubview(phoneLabel) - stackView.addArrangedSubview(dateLabel) - - contentView.addSubview(avatarView) - contentView.addSubview(stateButton) - contentView.addSubview(stackView) - - avatarView.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.width.height.equalTo(36) - $0.left.equalToSuperview().offset(27) - $0.bottom.lessThanOrEqualToSuperview().offset(-15) - } - - stackView.snp.makeConstraints { - $0.top.equalTo(avatarView).offset(-5) - $0.left.equalTo(avatarView.snp.right).offset(20) - $0.right.lessThanOrEqualTo(stateButton.snp.left).offset(-20) - $0.bottom.lessThanOrEqualToSuperview().offset(-15) - } - - stateButton.snp.makeConstraints { - $0.centerY.equalTo(stackView) - $0.right.equalToSuperview().offset(-24) - } + let titleLabel = UILabel() + let leaderLabel = UILabel() + let emailLabel = UILabel() + let phoneLabel = UILabel() + let dateLabel = UILabel() + let stackView = UIStackView() + let avatarView = AvatarView() + let stateButton = RequestCellButton() + + var cancellables = Set<AnyCancellable>() + var didTapStateButton: (() -> Void)! + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + emailLabel.font = Fonts.Mulish.regular.font(size: 14.0) + phoneLabel.font = Fonts.Mulish.regular.font(size: 14.0) + leaderLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + emailLabel.textColor = Asset.neutralSecondaryAlternative.color + phoneLabel.textColor = Asset.neutralSecondaryAlternative.color + leaderLabel.textColor = Asset.neutralSecondaryAlternative.color + + dateLabel.font = Fonts.Mulish.regular.font(size: 10.0) + dateLabel.textColor = Asset.neutralWeak.color + + stackView.axis = .vertical + stackView.spacing = 4 + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(leaderLabel) + stackView.addArrangedSubview(emailLabel) + stackView.addArrangedSubview(phoneLabel) + stackView.addArrangedSubview(dateLabel) + + contentView.addSubview(avatarView) + contentView.addSubview(stateButton) + contentView.addSubview(stackView) + + avatarView.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.width.height.equalTo(36) + $0.left.equalToSuperview().offset(27) + $0.bottom.lessThanOrEqualToSuperview().offset(-15) } - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - titleLabel.text = nil - dateLabel.text = nil - phoneLabel.text = nil - emailLabel.text = nil - leaderLabel.text = nil - avatarView.prepareForReuse() - cancellables.removeAll() + stackView.snp.makeConstraints { + $0.top.equalTo(avatarView).offset(-5) + $0.left.equalTo(avatarView.snp.right).offset(20) + $0.right.lessThanOrEqualTo(stateButton.snp.left).offset(-20) + $0.bottom.lessThanOrEqualToSuperview().offset(-15) } - func setupFor(requestSent: RequestSent) { - cancellables.removeAll() - guard case .contact(let contact) = requestSent.request else { fatalError("A sent request -must- be of type contact") } - - var phone: String? - if let contactPhone = contact.phone { - phone = "\(Country.findFrom(contactPhone).prefix) \(contactPhone.dropLast(2))" - } - - setupContact( - title: (contact.nickname ?? contact.username) ?? "", - photo: contact.photo, - phone: phone, - email: contact.email, - createdAt: contact.createdAt, - backgroundColor: Asset.brandPrimary.color - ) - - var buttonTitle: String? = nil - var buttonImage: UIImage? = nil - var buttonTitleColor: UIColor? = nil - - if requestSent.isResent { - buttonTitle = Localized.Requests.Cell.resent - buttonImage = Asset.requestsResent.image - buttonTitleColor = Asset.neutralWeak.color - } else { - buttonTitle = Localized.Requests.Cell.requested - buttonImage = Asset.requestsResend.image - buttonTitleColor = Asset.brandPrimary.color - } - - setupStateButton( - image: buttonImage, - title: buttonTitle, - color: buttonTitleColor - ) + stateButton.snp.makeConstraints { + $0.centerY.equalTo(stackView) + $0.right.equalToSuperview().offset(-24) } - - func setupFor(requestFailed request: Request) { - cancellables.removeAll() - guard case .contact(let contact) = request else { fatalError("A failed request -must- be of type contact") } - - var phone: String? - if let contactPhone = contact.phone { - phone = "\(Country.findFrom(contactPhone).prefix) \(contactPhone.dropLast(2))" - } - - setupContact( - title: (contact.nickname ?? contact.username) ?? "", - photo: contact.photo, - phone: phone, - email: contact.email, - createdAt: contact.createdAt, - backgroundColor: Asset.brandPrimary.color - ) - - setupStateButton( - image: Asset.requestsResend.image, - title: Localized.Requests.Cell.failedRequest, - color: Asset.brandPrimary.color - ) + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + dateLabel.text = nil + phoneLabel.text = nil + emailLabel.text = nil + leaderLabel.text = nil + avatarView.prepareForReuse() + cancellables.removeAll() + } + + func setupFor(requestSent: RequestSent) { + cancellables.removeAll() + guard case .contact(let contact) = requestSent.request else { fatalError("A sent request -must- be of type contact") } + + var phone: String? + if let contactPhone = contact.phone { + phone = "\(Country.findFrom(contactPhone).prefix) \(contactPhone.dropLast(2))" } - func setupFor(requestReceived: RequestReceived, isHidden: Bool = false) { - cancellables.removeAll() - guard let request = requestReceived.request else { return } - let color = isHidden ? Asset.neutralDisabled.color : Asset.brandPrimary.color - - switch request { - case .group(let group): - setupGroup( - name: group.name, - createdAt: group.createdAt, - leader: requestReceived.leader, - backgroundColor: color - ) - - case .contact(let contact): - - var phone: String? - if let contactPhone = contact.phone { - phone = "\(Country.findFrom(contactPhone).prefix) \(contactPhone.dropLast(2))" - } - - setupContact( - title: (contact.nickname ?? contact.username) ?? "", - photo: contact.photo, - phone: phone, - email: contact.email, - createdAt: contact.createdAt, - backgroundColor: color - ) - - var buttonTitle: String? = nil - var buttonImage: UIImage? = nil - var buttonTitleColor: UIColor? = nil - - switch request.status { - case .verified, .confirming, .failedToConfirm: - break // TODO: These statuses don't need UI - - case .verifying: - buttonTitle = Localized.Requests.Cell.verifying - buttonTitleColor = Asset.neutralWeak.color - - case .failedToVerify: - buttonTitle = Localized.Requests.Cell.failedVerification - buttonImage = Asset.requestsVerificationFailed.image - buttonTitleColor = Asset.accentDanger.color - - case .requesting, .requested, .failedToRequest: - fatalError("A receivedRequest can never have the statuses: .requesting, .requested or .failedToRequest") - } - - setupStateButton( - image: buttonImage, - title: buttonTitle, - color: buttonTitleColor - ) - } + setupContact( + title: (contact.nickname ?? contact.username) ?? "", + photo: contact.photo, + phone: phone, + email: contact.email, + createdAt: contact.createdAt, + backgroundColor: Asset.brandPrimary.color + ) + + var buttonTitle: String? = nil + var buttonImage: UIImage? = nil + var buttonTitleColor: UIColor? = nil + + if requestSent.isResent { + buttonTitle = Localized.Requests.Cell.resent + buttonImage = Asset.requestsResent.image + buttonTitleColor = Asset.neutralWeak.color + } else { + buttonTitle = Localized.Requests.Cell.requested + buttonImage = Asset.requestsResend.image + buttonTitleColor = Asset.brandPrimary.color } - private func setupContact( - title: String, - photo: Data?, - phone: String?, - email: String?, - createdAt: Date, - backgroundColor: UIColor - ) { - titleLabel.text = title - phoneLabel.text = phone - emailLabel.text = email - dateLabel.text = createdAt.asRelativeFromNow() - avatarView.setupProfile(title: title, image: photo, size: .small) - - leaderLabel.isHidden = true - phoneLabel.isHidden = phone == nil - emailLabel.isHidden = email == nil - avatarView.backgroundColor = backgroundColor - } + setupStateButton( + image: buttonImage, + title: buttonTitle, + color: buttonTitleColor + ) + } + + func setupFor(requestFailed request: Request) { + cancellables.removeAll() + guard case .contact(let contact) = request else { fatalError("A failed request -must- be of type contact") } - private func setupGroup( - name: String, - createdAt: Date, - leader: String?, - backgroundColor: UIColor - ) { - titleLabel.text = name - stateButton.imageView.image = nil - stateButton.titleLabel.text = nil - avatarView.setupGroup(size: .small) - dateLabel.text = createdAt.asRelativeFromNow() - - leaderLabel.text = leader - leaderLabel.isHidden = false - phoneLabel.isHidden = true - emailLabel.isHidden = true - avatarView.backgroundColor = backgroundColor + var phone: String? + if let contactPhone = contact.phone { + phone = "\(Country.findFrom(contactPhone).prefix) \(contactPhone.dropLast(2))" } - private func setupStateButton( - image: UIImage?, - title: String?, - color: UIColor? - ) { - stateButton.imageView.image = image - stateButton.titleLabel.text = title - stateButton.titleLabel.textColor = color - - stateButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in didTapStateButton() } - .store(in: &cancellables) + setupContact( + title: (contact.nickname ?? contact.username) ?? "", + photo: contact.photo, + phone: phone, + email: contact.email, + createdAt: contact.createdAt, + backgroundColor: Asset.brandPrimary.color + ) + + setupStateButton( + image: Asset.requestsResend.image, + title: Localized.Requests.Cell.failedRequest, + color: Asset.brandPrimary.color + ) + } + + func setupFor(requestReceived: RequestReceived, isHidden: Bool = false) { + cancellables.removeAll() + guard let request = requestReceived.request else { return } + let color = isHidden ? Asset.neutralDisabled.color : Asset.brandPrimary.color + + switch request { + case .group(let group): + setupGroup( + name: group.name, + createdAt: group.createdAt, + leader: requestReceived.leader, + backgroundColor: color + ) + + case .contact(let contact): + + var phone: String? + if let contactPhone = contact.phone { + phone = "\(Country.findFrom(contactPhone).prefix) \(contactPhone.dropLast(2))" + } + + setupContact( + title: (contact.nickname ?? contact.username) ?? "", + photo: contact.photo, + phone: phone, + email: contact.email, + createdAt: contact.createdAt, + backgroundColor: color + ) + + var buttonTitle: String? = nil + var buttonImage: UIImage? = nil + var buttonTitleColor: UIColor? = nil + + switch request.status { + case .verified, .confirming, .failedToConfirm: + break // TODO: These statuses don't need UI + + case .verifying: + buttonTitle = Localized.Requests.Cell.verifying + buttonTitleColor = Asset.neutralWeak.color + + case .failedToVerify: + buttonTitle = Localized.Requests.Cell.failedVerification + buttonImage = Asset.requestsVerificationFailed.image + buttonTitleColor = Asset.accentDanger.color + + case .requesting, .requested, .failedToRequest: + fatalError("A receivedRequest can never have the statuses: .requesting, .requested or .failedToRequest") + } + + setupStateButton( + image: buttonImage, + title: buttonTitle, + color: buttonTitleColor + ) } + } + + private func setupContact( + title: String, + photo: Data?, + phone: String?, + email: String?, + createdAt: Date, + backgroundColor: UIColor + ) { + titleLabel.text = title + phoneLabel.text = phone + emailLabel.text = email + dateLabel.text = createdAt.asRelativeFromNow() + avatarView.setupProfile(title: title, image: photo, size: .small) + + leaderLabel.isHidden = true + phoneLabel.isHidden = phone == nil + emailLabel.isHidden = email == nil + avatarView.backgroundColor = backgroundColor + } + + private func setupGroup( + name: String, + createdAt: Date, + leader: String?, + backgroundColor: UIColor + ) { + titleLabel.text = name + stateButton.imageView.image = nil + stateButton.titleLabel.text = nil + avatarView.setupGroup(size: .small) + dateLabel.text = createdAt.asRelativeFromNow() + + leaderLabel.text = leader + leaderLabel.isHidden = false + phoneLabel.isHidden = true + emailLabel.isHidden = true + avatarView.backgroundColor = backgroundColor + } + + private func setupStateButton( + image: UIImage?, + title: String?, + color: UIColor? + ) { + stateButton.imageView.image = image + stateButton.titleLabel.text = title + stateButton.titleLabel.textColor = color + + stateButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in didTapStateButton() } + .store(in: &cancellables) + } } diff --git a/Sources/RequestsFeature/Views/RequestCellButton.swift b/Sources/RequestsFeature/Views/RequestCellButton.swift index 22332912310da3912439621b425f614b1123a32d..b876ba691252c0a938ea40e162856a4102fb0ae2 100644 --- a/Sources/RequestsFeature/Views/RequestCellButton.swift +++ b/Sources/RequestsFeature/Views/RequestCellButton.swift @@ -2,34 +2,34 @@ import UIKit import Shared final class RequestCellButton: UIControl { - let titleLabel = UILabel() - let imageView = UIImageView() - - init() { - super.init(frame: .zero) - titleLabel.numberOfLines = 0 - titleLabel.textAlignment = .right - titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - - addSubview(imageView) - addSubview(titleLabel) - - imageView.snp.makeConstraints { - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalToSuperview() - $0.centerY.equalToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalTo(imageView.snp.right).offset(5) - $0.centerY.equalToSuperview() - $0.right.equalToSuperview() - $0.width.equalTo(60) - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let imageView = UIImageView() + + init() { + super.init(frame: .zero) + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .right + titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + + addSubview(imageView) + addSubview(titleLabel) + + imageView.snp.makeConstraints { + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalToSuperview() + $0.centerY.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() } - - required init?(coder: NSCoder) { nil } + + titleLabel.snp.makeConstraints { + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalTo(imageView.snp.right).offset(5) + $0.centerY.equalToSuperview() + $0.right.equalToSuperview() + $0.width.equalTo(60) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RequestsFeature/Views/RequestReceivedEmptyCell.swift b/Sources/RequestsFeature/Views/RequestReceivedEmptyCell.swift index bf706b39c5eded757bcb02eeac4c0092c9f409e1..574d63b624404285a5c1310706453bbd2f0b8d63 100644 --- a/Sources/RequestsFeature/Views/RequestReceivedEmptyCell.swift +++ b/Sources/RequestsFeature/Views/RequestReceivedEmptyCell.swift @@ -2,33 +2,33 @@ import UIKit import Shared final class RequestReceivedEmptyCell: UICollectionViewCell { - private let titleLabel = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralWeak.color - titleLabel.font = Fonts.Mulish.regular.font(size: 14.0) - titleLabel.text = Localized.Requests.Received.placeholder - - contentView.addSubview(titleLabel) - - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(50) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview().offset(-50) - } - } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - titleLabel.text = nil - } - - func setup(title: String) { - titleLabel.text = title + private let titleLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralWeak.color + titleLabel.font = Fonts.Mulish.regular.font(size: 14.0) + titleLabel.text = Localized.Requests.Received.placeholder + + contentView.addSubview(titleLabel) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(50) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(-50) } + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + } + + func setup(title: String) { + titleLabel.text = title + } } diff --git a/Sources/RequestsFeature/Views/RequestSegmentedButton.swift b/Sources/RequestsFeature/Views/RequestSegmentedButton.swift index 22bd81e2da7387b54c70f93f8c8528cb4d0dc031..36077491610b6f25bddebd8bc5801058c4d634ca 100644 --- a/Sources/RequestsFeature/Views/RequestSegmentedButton.swift +++ b/Sources/RequestsFeature/Views/RequestSegmentedButton.swift @@ -2,31 +2,31 @@ import UIKit import Shared final class RequestSegmentedButton: UIControl { - let titleLabel = UILabel() - let imageView = UIImageView() - - init() { - super.init(frame: .zero) - - titleLabel.textAlignment = .center - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - imageView.setContentCompressionResistancePriority(.required, for: .vertical) - titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) - - addSubview(titleLabel) - addSubview(imageView) - - imageView.snp.makeConstraints { - $0.top.equalToSuperview().offset(7.5) - $0.centerX.equalTo(titleLabel) - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(imageView.snp.bottom).offset(2) - $0.centerX.equalToSuperview() - $0.bottom.equalToSuperview().offset(-7.5) - } + let titleLabel = UILabel() + let imageView = UIImageView() + + init() { + super.init(frame: .zero) + + titleLabel.textAlignment = .center + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + imageView.setContentCompressionResistancePriority(.required, for: .vertical) + titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + + addSubview(titleLabel) + addSubview(imageView) + + imageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(7.5) + $0.centerX.equalTo(titleLabel) } - - required init?(coder: NSCoder) { nil } + + titleLabel.snp.makeConstraints { + $0.top.equalTo(imageView.snp.bottom).offset(2) + $0.centerX.equalToSuperview() + $0.bottom.equalToSuperview().offset(-7.5) + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RequestsFeature/Views/RequestsContainerView.swift b/Sources/RequestsFeature/Views/RequestsContainerView.swift index 80cd844a803dd38d4cce568d5d077bb0b2e1a66d..a5188d080d1773a256ffb06cc6b8f7376c243f2b 100644 --- a/Sources/RequestsFeature/Views/RequestsContainerView.swift +++ b/Sources/RequestsFeature/Views/RequestsContainerView.swift @@ -2,57 +2,57 @@ import UIKit import Shared final class RequestsContainerView: UIView { - let scrollView = UIScrollView() - let sentController = RequestsSentController() - let failedController = RequestsFailedController() - let receivedController = RequestsReceivedController() - let segmentedControl = RequestsSegmentedControl() - - init() { - super.init(frame: .zero) - scrollView.bounces = false - scrollView.isScrollEnabled = false - backgroundColor = Asset.neutralWhite.color - - scrollView.addSubview(sentController.view) - scrollView.addSubview(failedController.view) - scrollView.addSubview(receivedController.view) - addSubview(segmentedControl) - addSubview(scrollView) - - scrollView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - segmentedControl.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).offset(10) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.height.equalTo(60) - } - - receivedController.view.snp.makeConstraints { - $0.top.equalTo(segmentedControl.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalTo(sentController.view.snp.left) - $0.bottom.equalTo(self) - $0.width.equalTo(self) - } - - sentController.view.snp.makeConstraints { - $0.top.equalTo(segmentedControl.snp.bottom) - $0.right.equalTo(failedController.view.snp.left) - $0.bottom.equalTo(self) - $0.width.equalTo(self) - } - - failedController.view.snp.makeConstraints { - $0.top.equalTo(segmentedControl.snp.bottom) - $0.right.equalToSuperview() - $0.bottom.equalTo(self) - $0.width.equalTo(self) - } + let scrollView = UIScrollView() + let sentController = RequestsSentController() + let failedController = RequestsFailedController() + let receivedController = RequestsReceivedController() + let segmentedControl = RequestsSegmentedControl() + + init() { + super.init(frame: .zero) + scrollView.bounces = false + scrollView.isScrollEnabled = false + backgroundColor = Asset.neutralWhite.color + + scrollView.addSubview(sentController.view) + scrollView.addSubview(failedController.view) + scrollView.addSubview(receivedController.view) + addSubview(segmentedControl) + addSubview(scrollView) + + scrollView.snp.makeConstraints { + $0.edges.equalToSuperview() } - - required init?(coder: NSCoder) { nil } + + segmentedControl.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(10) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.height.equalTo(60) + } + + receivedController.view.snp.makeConstraints { + $0.top.equalTo(segmentedControl.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalTo(sentController.view.snp.left) + $0.bottom.equalTo(self) + $0.width.equalTo(self) + } + + sentController.view.snp.makeConstraints { + $0.top.equalTo(segmentedControl.snp.bottom) + $0.right.equalTo(failedController.view.snp.left) + $0.bottom.equalTo(self) + $0.width.equalTo(self) + } + + failedController.view.snp.makeConstraints { + $0.top.equalTo(segmentedControl.snp.bottom) + $0.right.equalToSuperview() + $0.bottom.equalTo(self) + $0.width.equalTo(self) + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RequestsFeature/Views/RequestsFailedView.swift b/Sources/RequestsFeature/Views/RequestsFailedView.swift index 76f4adadd7e15d27781e944bd957427629c74379..86d76dc764f89478da0c6b4df19b02c95b02ebd4 100644 --- a/Sources/RequestsFeature/Views/RequestsFailedView.swift +++ b/Sources/RequestsFeature/Views/RequestsFailedView.swift @@ -2,39 +2,39 @@ import UIKit import Shared final class RequestsFailedView: UIView { - let titleLabel = UILabel() - - lazy var collectionView: UICollectionView = { - var config = UICollectionLayoutListConfiguration(appearance: .plain) - config.backgroundColor = Asset.neutralWhite.color - config.showsSeparators = false - let layout = UICollectionViewCompositionalLayout.list(using: config) - let collectionView = UICollectionView(frame: bounds, collectionViewLayout: layout) - collectionView.contentInset = .init(top: 15, left: 0, bottom: 0, right: 0) - return collectionView - }() - - init() { - super.init(frame: .zero) - - titleLabel.textAlignment = .center - titleLabel.text = Localized.Requests.Failed.empty - titleLabel.textColor = Asset.neutralWeak.color - titleLabel.font = Fonts.Mulish.regular.font(size: 14.0) - - addSubview(titleLabel) - addSubview(collectionView) - - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(48.5) - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - } - - collectionView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + let titleLabel = UILabel() + + lazy var collectionView: UICollectionView = { + var config = UICollectionLayoutListConfiguration(appearance: .plain) + config.backgroundColor = Asset.neutralWhite.color + config.showsSeparators = false + let layout = UICollectionViewCompositionalLayout.list(using: config) + let collectionView = UICollectionView(frame: bounds, collectionViewLayout: layout) + collectionView.contentInset = .init(top: 15, left: 0, bottom: 0, right: 0) + return collectionView + }() + + init() { + super.init(frame: .zero) + + titleLabel.textAlignment = .center + titleLabel.text = Localized.Requests.Failed.empty + titleLabel.textColor = Asset.neutralWeak.color + titleLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + addSubview(titleLabel) + addSubview(collectionView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(48.5) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) } - - required init?(coder: NSCoder) { nil } + + collectionView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RequestsFeature/Views/RequestsHiddenSectionHeader.swift b/Sources/RequestsFeature/Views/RequestsHiddenSectionHeader.swift index f6fab012ee3b934ae1adbd31dd54c10857fe9e7d..7a8aed798d0c459ce1dd48c028ff12a23dab195f 100644 --- a/Sources/RequestsFeature/Views/RequestsHiddenSectionHeader.swift +++ b/Sources/RequestsFeature/Views/RequestsHiddenSectionHeader.swift @@ -3,62 +3,62 @@ import Shared import Combine final class RequestsHiddenSectionHeader: UICollectionReusableView { - let titleLabel = UILabel() - let separatorView = UIView() - let switcherView = UISwitch() - var cancellables = Set<AnyCancellable>() - - override func prepareForReuse() { - super.prepareForReuse() - cancellables.removeAll() + let titleLabel = UILabel() + let separatorView = UIView() + let switcherView = UISwitch() + var cancellables = Set<AnyCancellable>() + + override func prepareForReuse() { + super.prepareForReuse() + cancellables.removeAll() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + titleLabel.text = Localized.Requests.Received.hidden + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + separatorView.backgroundColor = Asset.neutralLine.color + switcherView.onTintColor = Asset.brandPrimary.color + + addSubview(titleLabel) + addSubview(switcherView) + addSubview(separatorView) + + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.top.equalToSuperview().offset(10) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) } - - override init(frame: CGRect) { - super.init(frame: frame) - - titleLabel.text = Localized.Requests.Received.hidden - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) - separatorView.backgroundColor = Asset.neutralLine.color - switcherView.onTintColor = Asset.brandPrimary.color - - addSubview(titleLabel) - addSubview(switcherView) - addSubview(separatorView) - - separatorView.snp.makeConstraints { - $0.height.equalTo(1) - $0.top.equalToSuperview().offset(10) - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(separatorView.snp.bottom).offset(30) - $0.left.equalToSuperview().offset(24) - $0.bottom.equalToSuperview().offset(-20) - } - - switcherView.snp.makeConstraints { - $0.centerY.equalTo(titleLabel) - $0.right.equalToSuperview().offset(-24) - } + + titleLabel.snp.makeConstraints { + $0.top.equalTo(separatorView.snp.bottom).offset(30) + $0.left.equalToSuperview().offset(24) + $0.bottom.equalToSuperview().offset(-20) } - - required init?(coder: NSCoder) { nil } + + switcherView.snp.makeConstraints { + $0.centerY.equalTo(titleLabel) + $0.right.equalToSuperview().offset(-24) + } + } + + required init?(coder: NSCoder) { nil } } final class RequestsBlankSectionHeader: UICollectionReusableView { - private let view = UIView() - - override init(frame: CGRect) { - super.init(frame: frame) - addSubview(view) - view.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.height.equalTo(1) - } + private let view = UIView() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(view) + view.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.height.equalTo(1) } - - required init?(coder: NSCoder) { nil } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RequestsFeature/Views/RequestsReceivedView.swift b/Sources/RequestsFeature/Views/RequestsReceivedView.swift index d4df66fea6dae47ee46dac61451f45a05fa2b4e7..f8c55b664deabb2c5bc0cff54e2246d048907f6b 100644 --- a/Sources/RequestsFeature/Views/RequestsReceivedView.swift +++ b/Sources/RequestsFeature/Views/RequestsReceivedView.swift @@ -2,51 +2,51 @@ import UIKit import Shared final class RequestsReceivedView: UIView { - lazy var collectionView: UICollectionView = { - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .estimated(1) - ) - - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .estimated(1) - ) - - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - - let section = NSCollectionLayoutSection(group: group) - section.interGroupSpacing = 5 - section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) - - let headerFooterSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1.0), - heightDimension: .estimated(44) - ) - - let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( - layoutSize: headerFooterSize, - elementKind: UICollectionView.elementKindSectionHeader, - alignment: .top - ) - - section.boundarySupplementaryItems = [sectionHeader] - let layout = UICollectionViewCompositionalLayout(section: section) - - let collectionView = UICollectionView(frame: bounds, collectionViewLayout: layout) - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - collectionView.backgroundColor = Asset.neutralWhite.color - collectionView.contentInset = .init(top: 15, left: 0, bottom: 0, right: 0) - return collectionView - }() - - init() { - super.init(frame: .zero) - addSubview(collectionView) - } - - required init?(coder: NSCoder) { nil } + lazy var collectionView: UICollectionView = { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .estimated(1) + ) + + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .estimated(1) + ) + + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 5 + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) + + let headerFooterSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(44) + ) + + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerFooterSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + + section.boundarySupplementaryItems = [sectionHeader] + let layout = UICollectionViewCompositionalLayout(section: section) + + let collectionView = UICollectionView(frame: bounds, collectionViewLayout: layout) + collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + collectionView.backgroundColor = Asset.neutralWhite.color + collectionView.contentInset = .init(top: 15, left: 0, bottom: 0, right: 0) + return collectionView + }() + + init() { + super.init(frame: .zero) + addSubview(collectionView) + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RequestsFeature/Views/RequestsSegmentedControl.swift b/Sources/RequestsFeature/Views/RequestsSegmentedControl.swift index 0bdea739207c2b2a73cba676d40df85eac85d946..d58fbf874ba461f7ee112732fd88c8fed8075a30 100644 --- a/Sources/RequestsFeature/Views/RequestsSegmentedControl.swift +++ b/Sources/RequestsFeature/Views/RequestsSegmentedControl.swift @@ -3,92 +3,92 @@ import Shared import SnapKit final class RequestsSegmentedControl: UIView { - private let trackView = UIView() - private let stackView = UIStackView() - private var leftConstraint: Constraint? - private let trackIndicatorView = UIView() - private(set) var sentRequestsButton = RequestSegmentedButton() - private(set) var failedRequestsButton = RequestSegmentedButton() - private(set) var receivedRequestsButton = RequestSegmentedButton() - - init() { - super.init(frame: .zero) - trackView.backgroundColor = Asset.neutralLine.color - trackIndicatorView.backgroundColor = Asset.brandPrimary.color - - sentRequestsButton.titleLabel.text = Localized.Requests.Sent.title - failedRequestsButton.titleLabel.text = Localized.Requests.Failed.title - receivedRequestsButton.titleLabel.text = Localized.Requests.Received.title - - sentRequestsButton.titleLabel.textColor = Asset.neutralDisabled.color - failedRequestsButton.titleLabel.textColor = Asset.neutralDisabled.color - receivedRequestsButton.titleLabel.textColor = Asset.brandPrimary.color - - sentRequestsButton.imageView.tintColor = Asset.neutralDisabled.color - failedRequestsButton.imageView.tintColor = Asset.neutralDisabled.color - receivedRequestsButton.imageView.tintColor = Asset.brandPrimary.color - - sentRequestsButton.imageView.image = Asset.requestsTabSent.image - failedRequestsButton.imageView.image = Asset.requestsTabFailed.image - receivedRequestsButton.imageView.image = Asset.requestsTabReceived.image - - stackView.addArrangedSubview(receivedRequestsButton) - stackView.addArrangedSubview(sentRequestsButton) - stackView.addArrangedSubview(failedRequestsButton) - stackView.distribution = .fillEqually - stackView.backgroundColor = Asset.neutralWhite.color - - addSubview(stackView) - addSubview(trackView) - trackView.addSubview(trackIndicatorView) - - stackView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - trackView.snp.makeConstraints { - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - $0.height.equalTo(2) - } - - trackIndicatorView.snp.makeConstraints { - $0.top.equalToSuperview() - leftConstraint = $0.left.equalToSuperview().constraint - $0.width.equalToSuperview().dividedBy(3) - $0.bottom.equalToSuperview() - } - - sentRequestsButton.accessibilityIdentifier = Localized.Accessibility.Requests.Sent.tab - failedRequestsButton.accessibilityIdentifier = Localized.Accessibility.Requests.Failed.tab - receivedRequestsButton.accessibilityIdentifier = Localized.Accessibility.Requests.Received.tab + private let trackView = UIView() + private let stackView = UIStackView() + private var leftConstraint: Constraint? + private let trackIndicatorView = UIView() + private(set) var sentRequestsButton = RequestSegmentedButton() + private(set) var failedRequestsButton = RequestSegmentedButton() + private(set) var receivedRequestsButton = RequestSegmentedButton() + + init() { + super.init(frame: .zero) + trackView.backgroundColor = Asset.neutralLine.color + trackIndicatorView.backgroundColor = Asset.brandPrimary.color + + sentRequestsButton.titleLabel.text = Localized.Requests.Sent.title + failedRequestsButton.titleLabel.text = Localized.Requests.Failed.title + receivedRequestsButton.titleLabel.text = Localized.Requests.Received.title + + sentRequestsButton.titleLabel.textColor = Asset.neutralDisabled.color + failedRequestsButton.titleLabel.textColor = Asset.neutralDisabled.color + receivedRequestsButton.titleLabel.textColor = Asset.brandPrimary.color + + sentRequestsButton.imageView.tintColor = Asset.neutralDisabled.color + failedRequestsButton.imageView.tintColor = Asset.neutralDisabled.color + receivedRequestsButton.imageView.tintColor = Asset.brandPrimary.color + + sentRequestsButton.imageView.image = Asset.requestsTabSent.image + failedRequestsButton.imageView.image = Asset.requestsTabFailed.image + receivedRequestsButton.imageView.image = Asset.requestsTabReceived.image + + stackView.addArrangedSubview(receivedRequestsButton) + stackView.addArrangedSubview(sentRequestsButton) + stackView.addArrangedSubview(failedRequestsButton) + stackView.distribution = .fillEqually + stackView.backgroundColor = Asset.neutralWhite.color + + addSubview(stackView) + addSubview(trackView) + trackView.addSubview(trackIndicatorView) + + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() } - - required init?(coder: NSCoder) { nil } - - func updateSwipePercentage(_ percentageScrolled: CGFloat) { - let amountOfTabs = 3.0 - let tabWidth = bounds.width / amountOfTabs - let leftOffset = percentageScrolled * tabWidth - - leftConstraint?.update(offset: leftOffset) - - let receivedPercentage = percentageScrolled > 1 ? 1 : percentageScrolled - let failedPercentage = percentageScrolled <= 1 ? 0 : percentageScrolled - 1 - let sentPercentage = percentageScrolled > 1 ? 1 - (percentageScrolled-1) : percentageScrolled - - let sentColor = UIColor.fade(from: Asset.neutralDisabled.color, to: Asset.brandPrimary.color, pcent: sentPercentage) - let failedColor = UIColor.fade(from: Asset.neutralDisabled.color, to: Asset.brandPrimary.color, pcent: failedPercentage) - let receivedColor = UIColor.fade(from: Asset.brandPrimary.color, to: Asset.neutralDisabled.color, pcent: receivedPercentage) - - sentRequestsButton.imageView.tintColor = sentColor - sentRequestsButton.titleLabel.textColor = sentColor - - failedRequestsButton.imageView.tintColor = failedColor - failedRequestsButton.titleLabel.textColor = failedColor - - receivedRequestsButton.imageView.tintColor = receivedColor - receivedRequestsButton.titleLabel.textColor = receivedColor + + trackView.snp.makeConstraints { + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + $0.height.equalTo(2) + } + + trackIndicatorView.snp.makeConstraints { + $0.top.equalToSuperview() + leftConstraint = $0.left.equalToSuperview().constraint + $0.width.equalToSuperview().dividedBy(3) + $0.bottom.equalToSuperview() } + + sentRequestsButton.accessibilityIdentifier = Localized.Accessibility.Requests.Sent.tab + failedRequestsButton.accessibilityIdentifier = Localized.Accessibility.Requests.Failed.tab + receivedRequestsButton.accessibilityIdentifier = Localized.Accessibility.Requests.Received.tab + } + + required init?(coder: NSCoder) { nil } + + func updateSwipePercentage(_ percentageScrolled: CGFloat) { + let amountOfTabs = 3.0 + let tabWidth = bounds.width / amountOfTabs + let leftOffset = percentageScrolled * tabWidth + + leftConstraint?.update(offset: leftOffset) + + let receivedPercentage = percentageScrolled > 1 ? 1 : percentageScrolled + let failedPercentage = percentageScrolled <= 1 ? 0 : percentageScrolled - 1 + let sentPercentage = percentageScrolled > 1 ? 1 - (percentageScrolled-1) : percentageScrolled + + let sentColor = UIColor.fade(from: Asset.neutralDisabled.color, to: Asset.brandPrimary.color, pcent: sentPercentage) + let failedColor = UIColor.fade(from: Asset.neutralDisabled.color, to: Asset.brandPrimary.color, pcent: failedPercentage) + let receivedColor = UIColor.fade(from: Asset.brandPrimary.color, to: Asset.neutralDisabled.color, pcent: receivedPercentage) + + sentRequestsButton.imageView.tintColor = sentColor + sentRequestsButton.titleLabel.textColor = sentColor + + failedRequestsButton.imageView.tintColor = failedColor + failedRequestsButton.titleLabel.textColor = failedColor + + receivedRequestsButton.imageView.tintColor = receivedColor + receivedRequestsButton.titleLabel.textColor = receivedColor + } } diff --git a/Sources/RequestsFeature/Views/RequestsSentView.swift b/Sources/RequestsFeature/Views/RequestsSentView.swift index 9150c06bcb745e56036ff809568a4a4fbd2f351e..6c65a92b91b99464a4a1d7e3210ff655465987ba 100644 --- a/Sources/RequestsFeature/Views/RequestsSentView.swift +++ b/Sources/RequestsFeature/Views/RequestsSentView.swift @@ -2,52 +2,52 @@ import UIKit import Shared final class RequestsSentView: UIView { - let titleLabel = UILabel() - let connectionsButton = CapsuleButton() - - lazy var collectionView: UICollectionView = { - var config = UICollectionLayoutListConfiguration(appearance: .plain) - config.backgroundColor = Asset.neutralWhite.color - config.showsSeparators = false - let layout = UICollectionViewCompositionalLayout.list(using: config) - let collectionView = UICollectionView(frame: bounds, collectionViewLayout: layout) - collectionView.contentInset = .init(top: 15, left: 0, bottom: 0, right: 0) - return collectionView - }() - - init() { - super.init(frame: .zero) - - titleLabel.textAlignment = .center - titleLabel.text = Localized.Requests.Sent.empty - titleLabel.textColor = Asset.neutralWeak.color - titleLabel.font = Fonts.Mulish.regular.font(size: 14.0) - - connectionsButton.set( - style: .brandColored, - title: Localized.Requests.Sent.action - ) - - addSubview(titleLabel) - addSubview(connectionsButton) - addSubview(collectionView) - - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(48.5) - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - } - - connectionsButton.snp.makeConstraints { - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-16) - } - - collectionView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + let titleLabel = UILabel() + let connectionsButton = CapsuleButton() + + lazy var collectionView: UICollectionView = { + var config = UICollectionLayoutListConfiguration(appearance: .plain) + config.backgroundColor = Asset.neutralWhite.color + config.showsSeparators = false + let layout = UICollectionViewCompositionalLayout.list(using: config) + let collectionView = UICollectionView(frame: bounds, collectionViewLayout: layout) + collectionView.contentInset = .init(top: 15, left: 0, bottom: 0, right: 0) + return collectionView + }() + + init() { + super.init(frame: .zero) + + titleLabel.textAlignment = .center + titleLabel.text = Localized.Requests.Sent.empty + titleLabel.textColor = Asset.neutralWeak.color + titleLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + connectionsButton.set( + style: .brandColored, + title: Localized.Requests.Sent.action + ) + + addSubview(titleLabel) + addSubview(connectionsButton) + addSubview(collectionView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(48.5) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) } - required init?(coder: NSCoder) { nil } + connectionsButton.snp.makeConstraints { + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-16) + } + + collectionView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RestoreFeature/Controllers/RestoreController.swift b/Sources/RestoreFeature/Controllers/RestoreController.swift index 5e4c555b7cdd389f7d977c02ec64bdcd0c1e4d7f..a58bdd1f8200c759660ec8398337dcf50c6a42fb 100644 --- a/Sources/RestoreFeature/Controllers/RestoreController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreController.swift @@ -1,9 +1,10 @@ +import DI import UIKit import Shared import Combine import Navigation +import AppResources import DrawerFeature -import DI public final class RestoreController: UIViewController { @Dependency var navigator: Navigator @@ -130,6 +131,6 @@ extension RestoreController { spacingAfter: 37 ), actionButton - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift index 4d138ba3944c0c86ac5876cfabff5a65c9ce1a0a..14fdc78eb0ec8ba4dc1787ac002a3c799f38ba99 100644 --- a/Sources/RestoreFeature/Controllers/RestoreListController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -32,10 +32,10 @@ public final class RestoreListController: UIViewController { .sftpPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] _ in - navigator.perform(PresentSFTP { [weak self] host, username, password in + navigator.perform(PresentSFTP(completion: { [weak self] host, username, password in guard let self else { return } self.viewModel.setupSFTP(host: host, username: username, password: password) - }) + }, on: navigationController!)) }.store(in: &cancellables) viewModel.detailsPublisher @@ -116,6 +116,6 @@ extension RestoreListController { spacingAfter: 37 ), actionButton - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift index a1eec1bb1b4974f7a038ea833892304907540e3a..c413c18a7cca79e3a144310f8923b669137d18e9 100644 --- a/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift @@ -49,7 +49,7 @@ public final class RestoreSuccessController: UIViewController { .nextButton .publisher(for: .touchUpInside) .sink { [unowned self] in - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) }.store(in: &cancellables) } } diff --git a/Sources/ScanFeature/Controllers/ScanContainerController.swift b/Sources/ScanFeature/Controllers/ScanContainerController.swift index 806d852e4732e95457692d11c7c40f8da9533090..68f78d7a6b6e0b64340ded20b05ae4f2dd06f197 100644 --- a/Sources/ScanFeature/Controllers/ScanContainerController.swift +++ b/Sources/ScanFeature/Controllers/ScanContainerController.swift @@ -58,11 +58,11 @@ public final class ScanContainerController: UIViewController { } displayController.didTapAddEmail = { [weak self] in guard let self else { return } - self.navigator.perform(PresentProfileEmail()) + self.navigator.perform(PresentProfileEmail(on: self.navigationController!)) } displayController.didTapAddPhone = { [weak self] in guard let self else { return } - self.navigator.perform(PresentProfilePhone()) + self.navigator.perform(PresentProfilePhone(on: self.navigationController!)) } } @@ -106,7 +106,7 @@ public final class ScanContainerController: UIViewController { } @objc private func didTapMenu() { - navigator.perform(PresentMenu(currentItem: .scan)) + navigator.perform(PresentMenu(currentItem: .scan, from: self)) } private func presentInfo(title: String, subtitle: String) { @@ -146,7 +146,7 @@ public final class ScanContainerController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/ScanFeature/Controllers/ScanController.swift b/Sources/ScanFeature/Controllers/ScanController.swift index 52369e7514853a6e87348e9afb84ea085225888b..bf8875099a7ba127269d1b03e693015542c093bf 100644 --- a/Sources/ScanFeature/Controllers/ScanController.swift +++ b/Sources/ScanFeature/Controllers/ScanController.swift @@ -83,7 +83,7 @@ final class ScanController: UIViewController { .receive(on: DispatchQueue.main) .delay(for: 1, scheduler: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentContact(contact: $0)) + navigator.perform(PresentContact(contact: $0, on: navigationController!)) }.store(in: &cancellables) viewModel @@ -105,9 +105,9 @@ final class ScanController: UIViewController { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url, options: [:]) case .failed(.requestOpened): - navigator.perform(PresentRequests()) + navigator.perform(PresentRequests(on: navigationController!)) case .failed(.alreadyFriends): - navigator.perform(PresentContactList()) + navigator.perform(PresentContactList(on: navigationController!)) default: break } diff --git a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift index 17b985d80312b589d31f7640682fb00fa052aeef..5c20f1e3f9111ed699790503ffce76774f02cfe4 100644 --- a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift +++ b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift @@ -2,7 +2,6 @@ import UIKit import Shared import Combine import Defaults -import Countries import XXClient import DI import XXMessengerClient diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index 70f063ba28c834c86f2da92bad93cf978c65fa26..8090af4f689290756de3a67082641cb099c37794 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -180,6 +180,6 @@ extension SearchContainerController { distribution: .fillEqually, views: [enableButton, dismissButton] ) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index a1a2b3c8661cff08b69be4c35d45ec5ee6936396..5570d6c5d349b6d0140e8290189e19f6aae5a017 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -3,9 +3,9 @@ import Shared import Combine import XXModels import Defaults -import Countries import Navigation import DrawerFeature +import CountryListFeature import DI final class SearchLeftController: UIViewController { @@ -161,7 +161,7 @@ final class SearchLeftController: UIViewController { navigator.perform(PresentCountryList(completion: { [weak self] in guard let self else { return } self.viewModel.didPick(country: $0 as! Country) - })) + }, from: self)) }.store(in: &cancellables) screenView @@ -233,7 +233,7 @@ final class SearchLeftController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } private func presentSucessDrawerFor(contact: Contact) { @@ -423,7 +423,11 @@ final class SearchLeftController: UIViewController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer(items: items)) + navigator.perform(PresentDrawer( + items: items, + isDismissable: true, + from: self + )) } } @@ -445,7 +449,7 @@ extension SearchLeftController: UITableViewDelegate { private func didTap(contact: Contact) { guard contact.authStatus == .stranger else { - navigator.perform(PresentContact(contact: contact)) + navigator.perform(PresentContact(contact: contact, on: navigationController!)) return } diff --git a/Sources/SearchFeature/Controllers/SearchRightController.swift b/Sources/SearchFeature/Controllers/SearchRightController.swift index d8cc22ecaceac630d68468329463bd7db94d3995..9f4a7f796111c96667da875b9a63baaedfffa116 100644 --- a/Sources/SearchFeature/Controllers/SearchRightController.swift +++ b/Sources/SearchFeature/Controllers/SearchRightController.swift @@ -56,7 +56,7 @@ final class SearchRightController: UIViewController { .receive(on: DispatchQueue.main) .delay(for: 1, scheduler: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentContact(contact: $0)) + navigator.perform(PresentContact(contact: $0, on: navigationController!)) }.store(in: &cancellables) viewModel @@ -77,9 +77,9 @@ final class SearchRightController: UIViewController { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url, options: [:]) case .failed(.requestOpened): - navigator.perform(PresentRequests()) + navigator.perform(PresentRequests(on: navigationController!)) case .failed(.alreadyFriends): - navigator.perform(PresentContactList()) + navigator.perform(PresentContactList(on: navigationController!)) default: break } diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index 3ade6d3f5c1ece4aaa581ab61da99c9d8373f902..f2c1c947e68275031b2a0d3c561d6fa6f662bd59 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -5,12 +5,11 @@ import Combine import XXModels import XXClient import Defaults -import Countries import CustomDump -import NetworkMonitor import ReportingFeature import CombineSchedulers import XXMessengerClient +import CountryListFeature import DI typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift index f3bde0baafec9100e8adbf2a26b1bb208e4639f4..41fe4cacffab08c5fb743a4214493a5909b15e0f 100644 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift @@ -119,6 +119,6 @@ public final class AccountDeleteController: UIViewController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift index 0ca63236e03dc3281c4c46297c84c705d0823e3f..c4b434cda7e72f62b278ee82ac05da178f69ad92 100644 --- a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift @@ -87,7 +87,7 @@ public final class SettingsAdvancedController: UIViewController { .sharePublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentActivitySheet(items: [$0])) + navigator.perform(PresentActivitySheet(items: [$0], from: self)) }.store(in: &cancellables) viewModel diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift index c2d74e694d87e82af662ce8c919b204a0a633837..9186199140e0a55b37b2b48fd611cdd7b9afc795 100644 --- a/Sources/SettingsFeature/Controllers/SettingsController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsController.swift @@ -172,7 +172,7 @@ public final class SettingsController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentSettingsAccountDelete()) + navigator.perform(PresentSettingsAccountDelete(on: navigationController!)) }.store(in: &cancellables) screenView @@ -180,7 +180,7 @@ public final class SettingsController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentSettingsBackup()) + navigator.perform(PresentSettingsBackup(on: navigationController!)) }.store(in: &cancellables) screenView @@ -188,7 +188,7 @@ public final class SettingsController: UIViewController { .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in - navigator.perform(PresentSettingsAdvanced()) + navigator.perform(PresentSettingsAdvanced(on: navigationController!)) }.store(in: &cancellables) viewModel @@ -268,11 +268,11 @@ public final class SettingsController: UIViewController { spacing: 20.0, views: [actionButton, cancelButton] ) - ])) + ], isDismissable: true, from: self)) } @objc private func didTapMenu() { - navigator.perform(PresentMenu(currentItem: .settings)) + navigator.perform(PresentMenu(currentItem: .settings, from: self)) } } @@ -314,6 +314,6 @@ extension SettingsController { actionButton, FlexibleSpace() ]) - ])) + ], isDismissable: true, from: self)) } } diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index 48b97ddc0c5136d087fb67d2179e78a05105478e..f2fa2b54acb06a3836cdbc0021f6ce7423df5691 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -1,10 +1,10 @@ import UIKit import Shared import Combine +import XXClient import Defaults import Permissions import PushFeature -import XXClient import XXMessengerClient import UserNotifications import CombineSchedulers diff --git a/Sources/Shared/Controllers/DiffEditableDataSource.swift b/Sources/Shared/Controllers/DiffEditableDataSource.swift index 9103733b120a2f7ff7a3a25be34c9ce6c2f3a6eb..b964b3492790b3c1aee7608aaa3c4b08d039c0ef 100644 --- a/Sources/Shared/Controllers/DiffEditableDataSource.swift +++ b/Sources/Shared/Controllers/DiffEditableDataSource.swift @@ -1,13 +1,13 @@ import UIKit public struct SectionId: Hashable { - public init() {} + public init() {} } public final class DiffEditableDataSource<SectionIdentifierType, ItemIdentifierType> : UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { - - public override func tableView(_ tableView: UITableView, - canEditRowAt indexPath: IndexPath) -> Bool { true } + + public override func tableView(_ tableView: UITableView, + canEditRowAt indexPath: IndexPath) -> Bool { true } } diff --git a/Sources/Shared/Controllers/HUDController.swift b/Sources/Shared/Controllers/HUDController.swift deleted file mode 100644 index 7606cd7fd85f4255d6990c804baeed7b8b20d3b0..0000000000000000000000000000000000000000 --- a/Sources/Shared/Controllers/HUDController.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Combine -import Foundation - -public final class HUDController { - private var timer: Timer? - - var modelPublisher: AnyPublisher<HUDModel?, Never> { - modelSubject.eraseToAnyPublisher() - } - - private let modelSubject = PassthroughSubject<HUDModel?, Never>() - - public init() {} - - public func dismiss() { - modelSubject.send(nil) - } - - public func show(_ model: HUDModel? = nil) { - guard let model else { - modelSubject.send(.init(hasDotAnimation: true)) - return - } - - if model.isAutoDismissable { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in - guard let self else { return } - self.modelSubject.send(nil) - } - } - - modelSubject.send(model) - } -} diff --git a/Sources/Shared/Controllers/StatusBarStyling.swift b/Sources/Shared/Controllers/StatusBarStyling.swift deleted file mode 100644 index 07941553057aa151e0f9e7b5dd5eee84ed580eed..0000000000000000000000000000000000000000 --- a/Sources/Shared/Controllers/StatusBarStyling.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit -import Combine - -public struct StatusBarStylist { - public init() {} - public let styleSubject = CurrentValueSubject<UIStatusBarStyle, Never>(.lightContent) -} diff --git a/Sources/Shared/Controllers/ToastController.swift b/Sources/Shared/Controllers/ToastController.swift deleted file mode 100644 index 1b0fd60e66297ace82beb3a6c1e877185d5ad066..0000000000000000000000000000000000000000 --- a/Sources/Shared/Controllers/ToastController.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Combine - -public final class ToastController { - private let queue = CurrentValueSubject<[ToastModel], Never>([]) - - var currentToast: AnyPublisher<ToastModel, Never> { - queue.compactMap(\.first) - .removeDuplicates(by: { $0.id == $1.id }) - .eraseToAnyPublisher() - } - - public init() {} - - public func enqueueToast(model: ToastModel) { - queue.value.append(model) - } - - public func dismissCurrentToast() { - guard queue.value.isEmpty == false else { return } - _ = queue.value.removeFirst() - } -} diff --git a/Sources/Shared/EditStateHandler.swift b/Sources/Shared/EditStateHandler.swift index 1a8d4c7e89edb3449118679c03e87fc4c1afcc08..ca1fd06b2259b0cb3eafa4508cb0d8fab9ff9521 100644 --- a/Sources/Shared/EditStateHandler.swift +++ b/Sources/Shared/EditStateHandler.swift @@ -1,18 +1,12 @@ import Combine public final class EditStateHandler { - // MARK: Properties + public var isEditing: AnyPublisher<Bool, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<Bool, Never>(false) - public var isEditing: AnyPublisher<Bool, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<Bool, Never>(false) + public init() {} - // MARK: Lifecycle - - public init() {} - - // MARK: Public - - public func didSwitchEditing() { - stateRelay.value.toggle() - } + public func didSwitchEditing() { + stateRelay.value.toggle() + } } diff --git a/Sources/Shared/Extensions/BezierPath.swift b/Sources/Shared/Extensions/BezierPath.swift index 5238360a7ef06eb0ecf089d30f8a3ab869aa445c..7994776acf2e3894587f5e6cceffc7e286297a2f 100644 --- a/Sources/Shared/Extensions/BezierPath.swift +++ b/Sources/Shared/Extensions/BezierPath.swift @@ -1,7 +1,7 @@ import UIKit public extension UIBezierPath { - convenience init(_ size: CGSize, rad: CGFloat) { - self.init(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: rad) - } + convenience init(_ size: CGSize, rad: CGFloat) { + self.init(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: rad) + } } diff --git a/Sources/Shared/Extensions/Button.swift b/Sources/Shared/Extensions/Button.swift index 3148a0cdee30fa987dc5e1d3d0e0c08db0f90fa2..5c3c3e86833defd4d357eb2da52d6e0b983959ad 100644 --- a/Sources/Shared/Extensions/Button.swift +++ b/Sources/Shared/Extensions/Button.swift @@ -1,12 +1,13 @@ import UIKit +import AppResources public extension UIButton { - static func back(color: UIColor = Asset.neutralActive.color) -> UIButton { - let back = UIButton() - back.setImage(Asset.navigationBarBack.image, for: .normal) - back.tintColor = color - back.imageView?.contentMode = .center - back.snp.makeConstraints { $0.width.equalTo(50) } - return back - } + static func back(color: UIColor = Asset.neutralActive.color) -> UIButton { + let back = UIButton() + back.setImage(Asset.navigationBarBack.image, for: .normal) + back.tintColor = color + back.imageView?.contentMode = .center + back.snp.makeConstraints { $0.width.equalTo(50) } + return back + } } diff --git a/Sources/Shared/Extensions/CollectionView.swift b/Sources/Shared/Extensions/CollectionView.swift index ad5b3bf7ea0d77d43c664b22ea90271d9f96a63f..e2fcafa0a3e18368b6c8ce12cf92232ae51335d7 100644 --- a/Sources/Shared/Extensions/CollectionView.swift +++ b/Sources/Shared/Extensions/CollectionView.swift @@ -1,151 +1,152 @@ import UIKit import ChatLayout +import AppResources import DifferenceKit extension UICollectionReusableView: ReusableView {} public extension UICollectionView { - func register<T: UICollectionViewCell>(_: T.Type) { - register(T.self, forCellWithReuseIdentifier: T.reuseIdentifier) + func register<T: UICollectionViewCell>(_: T.Type) { + register(T.self, forCellWithReuseIdentifier: T.reuseIdentifier) + } + + func registerSectionHeader<T: UICollectionReusableView>(_: T.Type) { + register( + T.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: T.reuseIdentifier + ) + } + + func dequeueReusableCell<T: UICollectionViewCell>(forIndexPath indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") } - - func registerSectionHeader<T: UICollectionReusableView>(_: T.Type) { - register( - T.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: T.reuseIdentifier - ) - } - - func dequeueReusableCell<T: UICollectionViewCell>(forIndexPath indexPath: IndexPath) -> T { - guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { - fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") - } - - return cell + + return cell + } + + func dequeueSupplementaryView<T: UICollectionReusableView>(forIndexPath indexPath: IndexPath) -> T { + dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T + } + + convenience init(on view: UIView, with layout: CollectionViewChatLayout) { + self.init(frame: view.frame, collectionViewLayout: layout) + view.addSubview(self) + + frame = view.bounds + translatesAutoresizingMaskIntoConstraints = false + topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true + bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true + leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true + trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true + + alwaysBounceVertical = true + isPrefetchingEnabled = false + keyboardDismissMode = .interactive + showsHorizontalScrollIndicator = false + contentInsetAdjustmentBehavior = .always + backgroundColor = Asset.neutralSecondary.color + automaticallyAdjustsScrollIndicatorInsets = true + } + + func reload<C>( + using stagedChangeset: StagedChangeset<C>, + interrupt: ((Changeset<C>) -> Bool)? = nil, + onInterruptedReload: (() -> Void)? = nil, + completion: ((Bool) -> Void)? = nil, + setData: (C) -> Void + ) { + if case .none = window, let data = stagedChangeset.last?.data { + setData(data) + if let onInterruptedReload = onInterruptedReload { + onInterruptedReload() + } else { + reloadData() + } + completion?(false) + return } - - func dequeueSupplementaryView<T: UICollectionReusableView>(forIndexPath indexPath: IndexPath) -> T { - dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T + + let dispatchGroup: DispatchGroup? = completion != nil + ? DispatchGroup() + : nil + let completionHandler: ((Bool) -> Void)? = completion != nil + ? { _ in + dispatchGroup!.leave() } - - convenience init(on view: UIView, with layout: CollectionViewChatLayout) { - self.init(frame: view.frame, collectionViewLayout: layout) - view.addSubview(self) - - frame = view.bounds - translatesAutoresizingMaskIntoConstraints = false - topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true - bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true - leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true - trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true - - alwaysBounceVertical = true - isPrefetchingEnabled = false - keyboardDismissMode = .interactive - showsHorizontalScrollIndicator = false - contentInsetAdjustmentBehavior = .always - backgroundColor = Asset.neutralSecondary.color - automaticallyAdjustsScrollIndicatorInsets = true - } - - func reload<C>( - using stagedChangeset: StagedChangeset<C>, - interrupt: ((Changeset<C>) -> Bool)? = nil, - onInterruptedReload: (() -> Void)? = nil, - completion: ((Bool) -> Void)? = nil, - setData: (C) -> Void - ) { - if case .none = window, let data = stagedChangeset.last?.data { - setData(data) - if let onInterruptedReload = onInterruptedReload { - onInterruptedReload() - } else { - reloadData() - } - completion?(false) - return + : nil + + for changeset in stagedChangeset { + if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { + setData(data) + if let onInterruptedReload = onInterruptedReload { + onInterruptedReload() + } else { + reloadData() } - - let dispatchGroup: DispatchGroup? = completion != nil - ? DispatchGroup() - : nil - let completionHandler: ((Bool) -> Void)? = completion != nil - ? { _ in - dispatchGroup!.leave() - } - : nil - - for changeset in stagedChangeset { - if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { - setData(data) - if let onInterruptedReload = onInterruptedReload { - onInterruptedReload() - } else { - reloadData() - } - completion?(false) - return - } - - performBatchUpdates({ - setData(changeset.data) - dispatchGroup?.enter() - - if !changeset.sectionDeleted.isEmpty { - deleteSections(IndexSet(changeset.sectionDeleted)) - } - - if !changeset.sectionInserted.isEmpty { - insertSections(IndexSet(changeset.sectionInserted)) - } - - if !changeset.sectionUpdated.isEmpty { - reloadSections(IndexSet(changeset.sectionUpdated)) - } - - for (source, target) in changeset.sectionMoved { - moveSection(source, toSection: target) - } - - if !changeset.elementDeleted.isEmpty { - deleteItems(at: changeset.elementDeleted.map { - IndexPath(item: $0.element, section: $0.section) - }) - } - - if !changeset.elementInserted.isEmpty { - insertItems(at: changeset.elementInserted.map { - IndexPath(item: $0.element, section: $0.section) - }) - } - - if !changeset.elementUpdated.isEmpty { - reloadItems(at: changeset.elementUpdated.map { - IndexPath(item: $0.element, section: $0.section) - }) - } - - for (source, target) in changeset.elementMoved { - moveItem(at: IndexPath(item: source.element, section: source.section), to: IndexPath(item: target.element, section: target.section)) - } - }, completion: completionHandler) + completion?(false) + return + } + + performBatchUpdates({ + setData(changeset.data) + dispatchGroup?.enter() + + if !changeset.sectionDeleted.isEmpty { + deleteSections(IndexSet(changeset.sectionDeleted)) + } + + if !changeset.sectionInserted.isEmpty { + insertSections(IndexSet(changeset.sectionInserted)) } - dispatchGroup?.notify(queue: .main) { - completion!(true) + + if !changeset.sectionUpdated.isEmpty { + reloadSections(IndexSet(changeset.sectionUpdated)) } + + for (source, target) in changeset.sectionMoved { + moveSection(source, toSection: target) + } + + if !changeset.elementDeleted.isEmpty { + deleteItems(at: changeset.elementDeleted.map { + IndexPath(item: $0.element, section: $0.section) + }) + } + + if !changeset.elementInserted.isEmpty { + insertItems(at: changeset.elementInserted.map { + IndexPath(item: $0.element, section: $0.section) + }) + } + + if !changeset.elementUpdated.isEmpty { + reloadItems(at: changeset.elementUpdated.map { + IndexPath(item: $0.element, section: $0.section) + }) + } + + for (source, target) in changeset.elementMoved { + moveItem(at: IndexPath(item: source.element, section: source.section), to: IndexPath(item: target.element, section: target.section)) + } + }, completion: completionHandler) + } + dispatchGroup?.notify(queue: .main) { + completion!(true) } + } } public extension StagedChangeset { - func flattenIfPossible() -> StagedChangeset { - if count == 2, - self[0].sectionChangeCount == 0, - self[1].sectionChangeCount == 0, - self[0].elementDeleted.count == self[0].elementChangeCount, - self[1].elementInserted.count == self[1].elementChangeCount { - return StagedChangeset(arrayLiteral: Changeset(data: self[1].data, elementDeleted: self[0].elementDeleted, elementInserted: self[1].elementInserted)) - } - return self + func flattenIfPossible() -> StagedChangeset { + if count == 2, + self[0].sectionChangeCount == 0, + self[1].sectionChangeCount == 0, + self[0].elementDeleted.count == self[0].elementChangeCount, + self[1].elementInserted.count == self[1].elementChangeCount { + return StagedChangeset(arrayLiteral: Changeset(data: self[1].data, elementDeleted: self[0].elementDeleted, elementInserted: self[1].elementInserted)) } + return self + } } diff --git a/Sources/Shared/Extensions/Colors.swift b/Sources/Shared/Extensions/Colors.swift index 568f515724619f20a78636309adef24dcd506a51..28a21c17e94c28e043335ea6714c75af5289e0f5 100644 --- a/Sources/Shared/Extensions/Colors.swift +++ b/Sources/Shared/Extensions/Colors.swift @@ -1,18 +1,18 @@ import UIKit public extension UIColor { - static func fade(from color: UIColor, to: UIColor, pcent: CGFloat) -> UIColor { - var fromRed: CGFloat = 0, fromGreen: CGFloat = 0, fromBlue: CGFloat = 0, fromAlpha: CGFloat = 0 - color.getRed(&fromRed, green: &fromGreen, blue: &fromBlue, alpha: &fromAlpha) - - var toRed: CGFloat = 0, toGreen: CGFloat = 0, toBlue: CGFloat = 0, toAlpha: CGFloat = 0 - to.getRed(&toRed, green: &toGreen, blue: &toBlue, alpha: &toAlpha) - - let red = (toRed - fromRed) * pcent + fromRed - let green = (toGreen - fromGreen) * pcent + fromGreen - let blue = (toBlue - fromBlue) * pcent + fromBlue - let alpha = (toAlpha - fromAlpha) * pcent + fromAlpha - - return UIColor(red: red, green: green, blue: blue, alpha: alpha) - } + static func fade(from color: UIColor, to: UIColor, pcent: CGFloat) -> UIColor { + var fromRed: CGFloat = 0, fromGreen: CGFloat = 0, fromBlue: CGFloat = 0, fromAlpha: CGFloat = 0 + color.getRed(&fromRed, green: &fromGreen, blue: &fromBlue, alpha: &fromAlpha) + + var toRed: CGFloat = 0, toGreen: CGFloat = 0, toBlue: CGFloat = 0, toAlpha: CGFloat = 0 + to.getRed(&toRed, green: &toGreen, blue: &toBlue, alpha: &toAlpha) + + let red = (toRed - fromRed) * pcent + fromRed + let green = (toGreen - fromGreen) * pcent + fromGreen + let blue = (toBlue - fromBlue) * pcent + fromBlue + let alpha = (toAlpha - fromAlpha) * pcent + fromAlpha + + return UIColor(red: red, green: green, blue: blue, alpha: alpha) + } } diff --git a/Sources/Shared/Extensions/Date.swift b/Sources/Shared/Extensions/Date.swift index b88b81b5afcaa830c153f6be4d6af2bf67e74bec..93d9058f05357fc02caae4ee5a0889bc5bcba43b 100644 --- a/Sources/Shared/Extensions/Date.swift +++ b/Sources/Shared/Extensions/Date.swift @@ -1,60 +1,60 @@ import Foundation public extension Date { - func asDayOfMonth() -> String { - let formatter = DateFormatter() - formatter.dateFormat = DateFormatter.dateFormat( - fromTemplate: "d MMMM", - options: 0, - locale: Locale(identifier: "en_US") - ) - - return formatter.string(from: self) - } - - func asHoursAndMinutes() -> String { - let formatter = DateFormatter() - formatter.dateStyle = .none - formatter.timeStyle = .short - return formatter.string(from: self) - } - - func asRelativeFromNow() -> String { - let formatter = RelativeDateTimeFormatter() - formatter.dateTimeStyle = .named - return formatter.string(for: self) ?? "" - } - - func backupStyle() -> String { - let formatter = DateFormatter() - formatter.dateFormat = DateFormatter.dateFormat( - fromTemplate: "MMM d, YYYY - h:mm", - options: 0, - locale: Locale(identifier: "en_US") - ) - - return formatter.string(from: self) - } - - static var asTimestamp: Int { - Int(Date().timeIntervalSince1970).toNano() - } - - static func fromTimestamp(_ timestamp: Int) -> Date { - Date(timeIntervalSince1970: TimeInterval(timestamp.nanoToSeconds())) - } - - static func fromMSTimestamp(_ timestampMS: Int64) -> Date { - Date(timeIntervalSince1970: TimeInterval(timestampMS) / 1000) - } + func asDayOfMonth() -> String { + let formatter = DateFormatter() + formatter.dateFormat = DateFormatter.dateFormat( + fromTemplate: "d MMMM", + options: 0, + locale: Locale(identifier: "en_US") + ) + + return formatter.string(from: self) + } + + func asHoursAndMinutes() -> String { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter.string(from: self) + } + + func asRelativeFromNow() -> String { + let formatter = RelativeDateTimeFormatter() + formatter.dateTimeStyle = .named + return formatter.string(for: self) ?? "" + } + + func backupStyle() -> String { + let formatter = DateFormatter() + formatter.dateFormat = DateFormatter.dateFormat( + fromTemplate: "MMM d, YYYY - h:mm", + options: 0, + locale: Locale(identifier: "en_US") + ) + + return formatter.string(from: self) + } + + static var asTimestamp: Int { + Int(Date().timeIntervalSince1970).toNano() + } + + static func fromTimestamp(_ timestamp: Int) -> Date { + Date(timeIntervalSince1970: TimeInterval(timestamp.nanoToSeconds())) + } + + static func fromMSTimestamp(_ timestampMS: Int64) -> Date { + Date(timeIntervalSince1970: TimeInterval(timestampMS) / 1000) + } } private extension Int { - func nanoToSeconds() -> Int { - self / 1000000000 - } - - func toNano() -> Int { - self * 1000000000 - } + func nanoToSeconds() -> Int { + self / 1000000000 + } + + func toNano() -> Int { + self * 1000000000 + } } diff --git a/Sources/Shared/Extensions/Error.swift b/Sources/Shared/Extensions/Error.swift index 8ed7cb7677d80c088cdb3329c1d0d5f1274eb843..fc728460f381ad54294a2a211664b341c1b73c66 100644 --- a/Sources/Shared/Extensions/Error.swift +++ b/Sources/Shared/Extensions/Error.swift @@ -1,11 +1,11 @@ import Foundation public extension NSError { - static func create(_ string: String) -> NSError { - NSError( - domain: "Internal error", - code: 0, - userInfo: [NSLocalizedDescriptionKey: NSLocalizedString(string, comment: "")] - ) - } + static func create(_ string: String) -> NSError { + NSError( + domain: "Internal error", + code: 0, + userInfo: [NSLocalizedDescriptionKey: NSLocalizedString(string, comment: "")] + ) + } } diff --git a/Sources/Shared/Extensions/FileManager.swift b/Sources/Shared/Extensions/FileManager.swift index c8639b5f82820335b584fc77d9d4887d07b7cda4..3834f2451c77c94270bc20311dc303af2a836c7a 100644 --- a/Sources/Shared/Extensions/FileManager.swift +++ b/Sources/Shared/Extensions/FileManager.swift @@ -2,71 +2,71 @@ import UIKit import Foundation public extension FileManager { - static var root: URL { - self.default.urls(for: .documentDirectory, in: .userDomainMask) - .first!.appendingPathComponent("xxm/") + static var root: URL { + self.default.urls(for: .documentDirectory, in: .userDomainMask) + .first!.appendingPathComponent("xxm/") + } + + static var xxContents: [String]? { + try? self.default.contentsOfDirectory(atPath: root.path) + } + + static var xxPath: String { + if xxContents == nil { + do { + try self.default.createDirectory( + at: root, + withIntermediateDirectories: false, + attributes: nil + ) + } catch { + fatalError(error.localizedDescription) + } } - - static var xxContents: [String]? { - try? self.default.contentsOfDirectory(atPath: root.path) - } - - static var xxPath: String { - if xxContents == nil { - do { - try self.default.createDirectory( - at: root, - withIntermediateDirectories: false, - attributes: nil - ) - } catch { - fatalError(error.localizedDescription) - } - } - - return root.path - } - - static func xxCleanup() { - guard let files = xxContents else { return } - files.forEach { try? FileManager.default.removeItem(at: root.appendingPathComponent($0)) } - } - - static func url(for fileName: String) -> URL? { - root.appendingPathComponent("\(fileName)") - } - - static func store(data: Data, name: String, type: String) throws -> URL { - guard let url = Self.url(for: "\(name).\(type)") else { - throw NSError.create("The file path could not be retrieved") - } - - try data.write(to: url) - return url + + return root.path + } + + static func xxCleanup() { + guard let files = xxContents else { return } + files.forEach { try? FileManager.default.removeItem(at: root.appendingPathComponent($0)) } + } + + static func url(for fileName: String) -> URL? { + root.appendingPathComponent("\(fileName)") + } + + static func store(data: Data, name: String, type: String) throws -> URL { + guard let url = Self.url(for: "\(name).\(type)") else { + throw NSError.create("The file path could not be retrieved") } - - static func delete(name: String, type: String) { - if let url = Self.url(for: "\(name).\(type)") { - do { - try FileManager.default.removeItem(at: url) - } catch { - print(error.localizedDescription) - } - } - } - - static func dummyAudio() -> Data { - let url = Bundle.module.url(forResource: "dummy_audio", withExtension: "m4a") - return try! Data(contentsOf: url!) - } - - static func retrieve(name: String, type: String) -> Data? { - guard let url = Self.url(for: "\(name).\(type)") else { return nil } - return try? Data(contentsOf: url) - } - - static func retrieve(imageNamed name: String) -> UIImage? { - guard let url = Self.url(for: name) else { return nil } - return UIImage(contentsOfFile: url.path) + + try data.write(to: url) + return url + } + + static func delete(name: String, type: String) { + if let url = Self.url(for: "\(name).\(type)") { + do { + try FileManager.default.removeItem(at: url) + } catch { + print(error.localizedDescription) + } } + } + + static func dummyAudio() -> Data { + let url = Bundle.module.url(forResource: "dummy_audio", withExtension: "m4a") + return try! Data(contentsOf: url!) + } + + static func retrieve(name: String, type: String) -> Data? { + guard let url = Self.url(for: "\(name).\(type)") else { return nil } + return try? Data(contentsOf: url) + } + + static func retrieve(imageNamed name: String) -> UIImage? { + guard let url = Self.url(for: name) else { return nil } + return UIImage(contentsOfFile: url.path) + } } diff --git a/Sources/Shared/Extensions/Image.swift b/Sources/Shared/Extensions/Image.swift index 442f8ff735f6fe3ef9bcb5b640e97a82080d9d8d..d69873ca9375e4677e8aff70831fc58026fd6a08 100644 --- a/Sources/Shared/Extensions/Image.swift +++ b/Sources/Shared/Extensions/Image.swift @@ -1,56 +1,56 @@ import UIKit public extension UIImage { - static func fromBase64(_ base64String: String?) -> UIImage? { - guard let base64 = base64String, - let imageData = Data(base64Encoded: base64, options: .ignoreUnknownCharacters) else { return nil } - - return UIImage(data: imageData) - } + static func fromBase64(_ base64String: String?) -> UIImage? { + guard let base64 = base64String, + let imageData = Data(base64Encoded: base64, options: .ignoreUnknownCharacters) else { return nil } - static func color(_ color: UIColor, size: CGSize = .init(width: 1, height: 1)) -> UIImage { - UIGraphicsImageRenderer(size: size).image { context in - color.setFill() - context.fill(CGRect(origin: .zero, size: size)) - } + return UIImage(data: imageData) + } + + static func color(_ color: UIColor, size: CGSize = .init(width: 1, height: 1)) -> UIImage { + UIGraphicsImageRenderer(size: size).image { context in + color.setFill() + context.fill(CGRect(origin: .zero, size: size)) } - - func orientedUp() -> UIImage { - if imageOrientation == .up { return self } - let format = imageRendererFormat - return UIGraphicsImageRenderer(size: size, format: format).image { _ in draw(at: .zero) } + } + + func orientedUp() -> UIImage { + if imageOrientation == .up { return self } + let format = imageRendererFormat + return UIGraphicsImageRenderer(size: size, format: format).image { _ in draw(at: .zero) } + } + + func resized(withPercentage percentage: CGFloat, isOpaque: Bool = true) -> UIImage? { + let canvas = CGSize(width: size.width * percentage, height: size.height * percentage) + let format = imageRendererFormat + format.opaque = isOpaque + return UIGraphicsImageRenderer(size: canvas, format: format).image { + _ in draw(in: CGRect(origin: .zero, size: canvas)) } - - func resized(withPercentage percentage: CGFloat, isOpaque: Bool = true) -> UIImage? { - let canvas = CGSize(width: size.width * percentage, height: size.height * percentage) - let format = imageRendererFormat - format.opaque = isOpaque - return UIGraphicsImageRenderer(size: canvas, format: format).image { - _ in draw(in: CGRect(origin: .zero, size: canvas)) - } - } - - func compress(to kb: Int) -> Data { - let bytes = kb * 1024 - var compression: CGFloat = 1.0 - let step: CGFloat = 0.05 - var holderImage = self - var complete = false - - while(!complete) { - if let data = holderImage.jpegData(compressionQuality: 1.0) { - let ratio = data.count / bytes - if data.count < bytes { - complete = true - return data - } else { - let multiplier: CGFloat = CGFloat((ratio / 5) + 1) - compression -= (step * multiplier) - } - } - guard let newImage = holderImage.resized(withPercentage: compression) else { break } - holderImage = newImage + } + + func compress(to kb: Int) -> Data { + let bytes = kb * 1024 + var compression: CGFloat = 1.0 + let step: CGFloat = 0.05 + var holderImage = self + var complete = false + + while(!complete) { + if let data = holderImage.jpegData(compressionQuality: 1.0) { + let ratio = data.count / bytes + if data.count < bytes { + complete = true + return data + } else { + let multiplier: CGFloat = CGFloat((ratio / 5) + 1) + compression -= (step * multiplier) } - return Data() + } + guard let newImage = holderImage.resized(withPercentage: compression) else { break } + holderImage = newImage } + return Data() + } } diff --git a/Sources/Shared/Extensions/ItemProvider.swift b/Sources/Shared/Extensions/ItemProvider.swift index b0c6e48a2584ae174f86691bd28d9431577bc0db..1bad223d6127e41b0a96750309389d5e5fc8c828 100644 --- a/Sources/Shared/Extensions/ItemProvider.swift +++ b/Sources/Shared/Extensions/ItemProvider.swift @@ -2,27 +2,27 @@ import UIKit import Combine public extension NSItemProvider { - func loadImageObjectPublisher() -> AnyPublisher<UIImage, Error> { - Deferred { - Future { promise in - self.loadObject(ofClass: UIImage.self) { image, error in - if let error = error { - promise(.failure(error)) - return - } - - guard let safeImage = image as? UIImage else { - struct InvalidImageError: Error { - let image: NSItemProviderReading? - } - - promise(.failure(InvalidImageError(image: image))) - return - } - - promise(.success(safeImage)) - } + func loadImageObjectPublisher() -> AnyPublisher<UIImage, Error> { + Deferred { + Future { promise in + self.loadObject(ofClass: UIImage.self) { image, error in + if let error = error { + promise(.failure(error)) + return + } + + guard let safeImage = image as? UIImage else { + struct InvalidImageError: Error { + let image: NSItemProviderReading? } - }.eraseToAnyPublisher() - } + + promise(.failure(InvalidImageError(image: image))) + return + } + + promise(.success(safeImage)) + } + } + }.eraseToAnyPublisher() + } } diff --git a/Sources/Shared/Extensions/MutableAttributedString.swift b/Sources/Shared/Extensions/MutableAttributedString.swift index f963a1d5e53d3eb058dfdccfcd169c66d0bf90c6..badcd31c78aada923438ebc5189058c081bc72fc 100644 --- a/Sources/Shared/Extensions/MutableAttributedString.swift +++ b/Sources/Shared/Extensions/MutableAttributedString.swift @@ -1,75 +1,75 @@ import Foundation public extension NSMutableAttributedString { - func addAttribute(_ name: NSAttributedString.Key, value: Any) { - addAttribute(name, value: value, range: NSRange(string.startIndex..., in: string)) + func addAttribute(_ name: NSAttributedString.Key, value: Any) { + addAttribute(name, value: value, range: NSRange(string.startIndex..., in: string)) + } + + func addAttributes(_ attrs: [NSAttributedString.Key: Any]) { + addAttributes(attrs, range: NSRange(string.startIndex..., in: string)) + } + + func setAttributes(attributes: [NSAttributedString.Key: Any], betweenCharacters: String) { + let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) + for obj in ranges { + let thisValue: NSValue = obj + let range: NSRange = thisValue.rangeValue + setAttributes(attributes, range: range) } - - func addAttributes(_ attrs: [NSAttributedString.Key: Any]) { - addAttributes(attrs, range: NSRange(string.startIndex..., in: string)) - } - - func setAttributes(attributes: [NSAttributedString.Key: Any], betweenCharacters: String) { - let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) - for obj in ranges { - let thisValue: NSValue = obj - let range: NSRange = thisValue.rangeValue - setAttributes(attributes, range: range) - } + } + + func addAttribute(name: NSAttributedString.Key, value: Any, betweenCharacters: String) { + let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) + for obj in ranges { + let thisValue: NSValue = obj + let range: NSRange = thisValue.rangeValue + addAttribute(name, value: value, range: range) } - - func addAttribute(name: NSAttributedString.Key, value: Any, betweenCharacters: String) { - let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) - for obj in ranges { - let thisValue: NSValue = obj - let range: NSRange = thisValue.rangeValue - addAttribute(name, value: value, range: range) - } + } + + func addAttributes(attributes: [NSAttributedString.Key: Any], betweenCharacters: String) { + let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) + for obj in ranges { + let thisValue: NSValue = obj + let range: NSRange = thisValue.rangeValue + addAttributes(attributes, range: range) } - - func addAttributes(attributes: [NSAttributedString.Key: Any], betweenCharacters: String) { - let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) - for obj in ranges { - let thisValue: NSValue = obj - let range: NSRange = thisValue.rangeValue - addAttributes(attributes, range: range) - } - } - - func removeAttribute(name: NSAttributedString.Key, betweenCharacters: String) { - let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) - for obj in ranges { - let thisValue: NSValue = obj - let range: NSRange = thisValue.rangeValue - removeAttribute(name, range: range) - } + } + + func removeAttribute(name: NSAttributedString.Key, betweenCharacters: String) { + let ranges: Array = findRangesWithCharaters(charactersToFind: betweenCharacters) + for obj in ranges { + let thisValue: NSValue = obj + let range: NSRange = thisValue.rangeValue + removeAttribute(name, range: range) } - - func findRangesWithCharaters(charactersToFind: String) -> [NSValue] { - let resultArray = NSMutableArray() - var insideTheRange = false - var startingRangeLocation: Int = 0 - - while self.mutableString.range(of: charactersToFind).location != NSNotFound { - let charactersLocation: NSRange = self.mutableString.range(of: charactersToFind) - - if !insideTheRange { - startingRangeLocation = charactersLocation.location - insideTheRange = true - - self.mutableString.deleteCharacters(in: charactersLocation) - } else { - let range: NSRange = NSRange(location: startingRangeLocation, - length: charactersLocation.location - startingRangeLocation) - insideTheRange = false - - resultArray.add(NSValue(range: range)) - self.mutableString.deleteCharacters(in: charactersLocation) - } - } - - guard let result = resultArray.copy() as? [NSValue] else { return [] } - - return result + } + + func findRangesWithCharaters(charactersToFind: String) -> [NSValue] { + let resultArray = NSMutableArray() + var insideTheRange = false + var startingRangeLocation: Int = 0 + + while self.mutableString.range(of: charactersToFind).location != NSNotFound { + let charactersLocation: NSRange = self.mutableString.range(of: charactersToFind) + + if !insideTheRange { + startingRangeLocation = charactersLocation.location + insideTheRange = true + + self.mutableString.deleteCharacters(in: charactersLocation) + } else { + let range: NSRange = NSRange(location: startingRangeLocation, + length: charactersLocation.location - startingRangeLocation) + insideTheRange = false + + resultArray.add(NSValue(range: range)) + self.mutableString.deleteCharacters(in: charactersLocation) + } } + + guard let result = resultArray.copy() as? [NSValue] else { return [] } + + return result + } } diff --git a/Sources/Shared/Extensions/NavigationBar.swift b/Sources/Shared/Extensions/NavigationBar.swift index b7efcb698321a974752e5c07cab1542ca0d26d80..db3d7e5c32a0c9040a5ef05c7cf6569f7251e5b1 100644 --- a/Sources/Shared/Extensions/NavigationBar.swift +++ b/Sources/Shared/Extensions/NavigationBar.swift @@ -1,21 +1,22 @@ import UIKit +import AppResources public extension UINavigationBar { - func customize( - translucent: Bool = false, - backgroundColor: UIColor = .clear, - shadowColor: UIColor? = nil, - tint: UIColor = Asset.neutralActive.color - ) { - isTranslucent = translucent - let barAppearance = UINavigationBarAppearance() - barAppearance.backgroundColor = backgroundColor - barAppearance.backgroundEffect = .none - barAppearance.shadowColor = shadowColor - - tintColor = tint - compactAppearance = barAppearance - standardAppearance = barAppearance - scrollEdgeAppearance = barAppearance - } + func customize( + translucent: Bool = false, + backgroundColor: UIColor = .clear, + shadowColor: UIColor? = nil, + tint: UIColor = Asset.neutralActive.color + ) { + isTranslucent = translucent + let barAppearance = UINavigationBarAppearance() + barAppearance.backgroundColor = backgroundColor + barAppearance.backgroundEffect = .none + barAppearance.shadowColor = shadowColor + + tintColor = tint + compactAppearance = barAppearance + standardAppearance = barAppearance + scrollEdgeAppearance = barAppearance + } } diff --git a/Sources/Shared/Extensions/Publishers.swift b/Sources/Shared/Extensions/Publishers.swift deleted file mode 100644 index f7394821dd2b3d8d5ddefc707e204caa76d70199..0000000000000000000000000000000000000000 --- a/Sources/Shared/Extensions/Publishers.swift +++ /dev/null @@ -1,113 +0,0 @@ -import UIKit -import Combine - -public extension UIControl { - func publisher(for event: Event) -> EventPublisher { - EventPublisher( - control: self, - event: event - ) - } - - struct EventPublisher: Publisher { - public typealias Output = Void - public typealias Failure = Never - - fileprivate var control: UIControl - fileprivate var event: Event - - public func receive<S: Subscriber>( - subscriber: S - ) where S.Input == Output, S.Failure == Failure { - let subscription = EventSubscription<S>() - subscription.target = subscriber - subscriber.receive(subscription: subscription) - - control.addTarget(subscription, - action: #selector(subscription.trigger), - for: event - ) - } - } -} - -private extension UIControl { - class EventSubscription<Target: Subscriber>: Subscription - where Target.Input == Void { - - var target: Target? - - func request(_ demand: Subscribers.Demand) {} - - func cancel() { - target = nil - } - - @objc func trigger() { - _ = target?.receive(()) - } - } -} - -public extension UITextField { - var textPublisher: AnyPublisher<String, Never> { - publisher(for: .editingChanged) - .map { self.text ?? "" } - .eraseToAnyPublisher() - } - - var returnPublisher: AnyPublisher<Void, Never> { - publisher(for: .editingDidEndOnExit) - .eraseToAnyPublisher() - } -} - -public extension UITextView { - var textPublisher: Publishers.TextFieldPublisher { - Publishers.TextFieldPublisher(textField: self) - } -} - -public extension Publishers { - struct TextFieldPublisher: Publisher { - public typealias Output = String - public typealias Failure = Never - - private let textField: UITextView - - init(textField: UITextView) { self.textField = textField } - - public func receive<S>(subscriber: S) where S : Subscriber, Publishers.TextFieldPublisher.Failure == S.Failure, Publishers.TextFieldPublisher.Output == S.Input { - let subscription = TextFieldSubscription(subscriber: subscriber, textField: textField) - subscriber.receive(subscription: subscription) - } - } - - class TextFieldSubscription<S: Subscriber>: NSObject, Subscription, UITextViewDelegate where S.Input == String, S.Failure == Never { - - private var subscriber: S? - private weak var textField: UITextView? - - init(subscriber: S, textField: UITextView) { - super.init() - self.subscriber = subscriber - self.textField = textField - subscribe() - } - - public func request(_ demand: Subscribers.Demand) { } - - public func cancel() { - subscriber = nil - textField = nil - } - - private func subscribe() { - textField?.delegate = self - } - - public func textViewDidChange(_ textView: UITextView) { - _ = subscriber?.receive(textView.text) - } - } -} diff --git a/Sources/Shared/Extensions/StackView.swift b/Sources/Shared/Extensions/StackView.swift index 93cf61339a5d119c455fe82e43daff240d5f621d..4997ad49716673064d898cf144e3ecff0d6bf830 100644 --- a/Sources/Shared/Extensions/StackView.swift +++ b/Sources/Shared/Extensions/StackView.swift @@ -1,7 +1,7 @@ import UIKit public extension UIStackView { - func addArrangedSubviews(_ subviews: [UIView]) { - subviews.forEach(addArrangedSubview(_:)) - } + func addArrangedSubviews(_ subviews: [UIView]) { + subviews.forEach(addArrangedSubview(_:)) + } } diff --git a/Sources/Shared/Extensions/TableView.swift b/Sources/Shared/Extensions/TableView.swift index c05ccd74d2193cfdd01cc8c0fdd442e8d979a66a..973feeff2cf30d2f915d5f0fb76d9b654c1e26ba 100644 --- a/Sources/Shared/Extensions/TableView.swift +++ b/Sources/Shared/Extensions/TableView.swift @@ -4,34 +4,34 @@ extension UITableViewCell: ReusableView {} extension UITableViewHeaderFooterView: ReusableView {} public extension UITableView { - func register(cells: [AnyClass]) { - cells.forEach { cell in - register(cell, forCellReuseIdentifier: String(describing: cell)) - } + func register(cells: [AnyClass]) { + cells.forEach { cell in + register(cell, forCellReuseIdentifier: String(describing: cell)) } - - func registerHeaderFooter<T: UITableViewHeaderFooterView>(type: T.Type) { - register(T.self, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier) - } - - func register<T: UITableViewCell>(_: T.Type) { - register(T.self, forCellReuseIdentifier: T.reuseIdentifier) - } - - func dequeueReusableCell<T: UITableViewCell>(forIndexPath indexPath: IndexPath, - ofType type: T.Type? = nil) -> T { - guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { - fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") - } - - return cell + } + + func registerHeaderFooter<T: UITableViewHeaderFooterView>(type: T.Type) { + register(T.self, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier) + } + + func register<T: UITableViewCell>(_: T.Type) { + register(T.self, forCellReuseIdentifier: T.reuseIdentifier) + } + + func dequeueReusableCell<T: UITableViewCell>(forIndexPath indexPath: IndexPath, + ofType type: T.Type? = nil) -> T { + guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") } - - func dequeueReusableHeaderFooter<T: UITableViewHeaderFooterView>(ofType type: T.Type? = nil) -> T { - guard let view = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else { - fatalError("Could not dequeue header footer with identifier: \(T.reuseIdentifier)") - } - - return view + + return cell + } + + func dequeueReusableHeaderFooter<T: UITableViewHeaderFooterView>(ofType type: T.Type? = nil) -> T { + guard let view = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else { + fatalError("Could not dequeue header footer with identifier: \(T.reuseIdentifier)") } + + return view + } } diff --git a/Sources/Shared/Extensions/View.swift b/Sources/Shared/Extensions/View.swift index cdbc4f591dd6bbe26c08c47a2ec655f91528e954..0636e0d300e590d0082d847bfb161d83d5b908d2 100644 --- a/Sources/Shared/Extensions/View.swift +++ b/Sources/Shared/Extensions/View.swift @@ -4,78 +4,78 @@ import SnapKit protocol ReusableView {} extension ReusableView where Self: UIView { - static var reuseIdentifier: String { - return String(describing: self) - } + static var reuseIdentifier: String { + return String(describing: self) + } } public extension UIView { - enum PinningPosition { - case hCenter - case top(CGFloat) - case left(CGFloat) - case right(CGFloat) - case bottom(CGFloat) - case center(CGFloat) - } - - func pinning(at position: PinningPosition) -> UIView { - let container = UIView() - container.addSubview(self) - - self.snp.makeConstraints { make in - switch position { - case let .top(padding): - let flex = FlexibleSpace() - container.addSubview(flex) - flex.snp.makeConstraints { $0.bottom.equalToSuperview() } - - make.top.equalToSuperview().offset(padding) - make.left.right.equalToSuperview() - make.bottom.lessThanOrEqualTo(flex.snp.top) - - case let .left(padding): - let flex = FlexibleSpace() - container.addSubview(flex) - flex.snp.makeConstraints { $0.right.equalToSuperview() } - - make.top.bottom.equalToSuperview() - make.left.equalToSuperview().offset(padding) - make.right.lessThanOrEqualTo(flex.snp.left) - - case let .right(padding): - let flex = FlexibleSpace() - container.addSubview(flex) - flex.snp.makeConstraints { $0.bottom.equalToSuperview() } - - make.top.bottom.equalToSuperview() - make.right.equalToSuperview().offset(padding) - make.left.greaterThanOrEqualTo(flex.snp.right) - - case let .bottom(padding): - let flex = FlexibleSpace() - container.addSubview(flex) - flex.snp.makeConstraints { $0.top.equalToSuperview() } - - make.bottom.equalToSuperview().offset(padding) - make.left.right.equalToSuperview() - make.top.greaterThanOrEqualTo(flex.snp.bottom) - - case let .center(inset): - make.top.greaterThanOrEqualToSuperview().offset(inset) - make.left.greaterThanOrEqualToSuperview().offset(inset) - make.center.equalToSuperview() - make.right.lessThanOrEqualToSuperview().offset(-inset) - make.bottom.lessThanOrEqualToSuperview().offset(-inset) - case .hCenter: - make.top.equalToSuperview() - make.centerX.equalToSuperview() - make.left.greaterThanOrEqualToSuperview() - make.right.lessThanOrEqualToSuperview() - make.bottom.equalToSuperview() - } - } - - return container + enum PinningPosition { + case hCenter + case top(CGFloat) + case left(CGFloat) + case right(CGFloat) + case bottom(CGFloat) + case center(CGFloat) + } + + func pinning(at position: PinningPosition) -> UIView { + let container = UIView() + container.addSubview(self) + + self.snp.makeConstraints { make in + switch position { + case let .top(padding): + let flex = FlexibleSpace() + container.addSubview(flex) + flex.snp.makeConstraints { $0.bottom.equalToSuperview() } + + make.top.equalToSuperview().offset(padding) + make.left.right.equalToSuperview() + make.bottom.lessThanOrEqualTo(flex.snp.top) + + case let .left(padding): + let flex = FlexibleSpace() + container.addSubview(flex) + flex.snp.makeConstraints { $0.right.equalToSuperview() } + + make.top.bottom.equalToSuperview() + make.left.equalToSuperview().offset(padding) + make.right.lessThanOrEqualTo(flex.snp.left) + + case let .right(padding): + let flex = FlexibleSpace() + container.addSubview(flex) + flex.snp.makeConstraints { $0.bottom.equalToSuperview() } + + make.top.bottom.equalToSuperview() + make.right.equalToSuperview().offset(padding) + make.left.greaterThanOrEqualTo(flex.snp.right) + + case let .bottom(padding): + let flex = FlexibleSpace() + container.addSubview(flex) + flex.snp.makeConstraints { $0.top.equalToSuperview() } + + make.bottom.equalToSuperview().offset(padding) + make.left.right.equalToSuperview() + make.top.greaterThanOrEqualTo(flex.snp.bottom) + + case let .center(inset): + make.top.greaterThanOrEqualToSuperview().offset(inset) + make.left.greaterThanOrEqualToSuperview().offset(inset) + make.center.equalToSuperview() + make.right.lessThanOrEqualToSuperview().offset(-inset) + make.bottom.lessThanOrEqualToSuperview().offset(-inset) + case .hCenter: + make.top.equalToSuperview() + make.centerX.equalToSuperview() + make.left.greaterThanOrEqualToSuperview() + make.right.lessThanOrEqualToSuperview() + make.bottom.equalToSuperview() + } } + + return container + } } diff --git a/Sources/Shared/FeedbackPlayer.swift b/Sources/Shared/FeedbackPlayer.swift index 09d4773d68576b4fd625bdc741e9e086c9a5d7ff..6c2bf25ef593f3a54ced36b4b9aea53cdb602a0f 100644 --- a/Sources/Shared/FeedbackPlayer.swift +++ b/Sources/Shared/FeedbackPlayer.swift @@ -2,37 +2,33 @@ import AVFoundation import AudioToolbox struct DeviceFeedback { - enum Haptic: UInt32 { - case impact = 1520 - case notification = 1521 - case selection = 1519 - } - - enum Alert: UInt32 { - case smsSent = 1004 - case smsReceived = 1003 - case contactAdded = 1117 - } - - // MARK: Lifecycle - - private init() {} - - // MARK: Static - - static func sound(_ alert: Alert) { - try? AVAudioSession - .sharedInstance() - .setCategory(.ambient, mode: .default, options: .mixWithOthers) - - AudioServicesPlaySystemSound(alert.rawValue) - } - - static func shake(_ haptic: Haptic) { - try? AVAudioSession - .sharedInstance() - .setCategory(.ambient, mode: .default, options: .mixWithOthers) - - AudioServicesPlaySystemSound(haptic.rawValue) - } + enum Haptic: UInt32 { + case impact = 1520 + case notification = 1521 + case selection = 1519 + } + + enum Alert: UInt32 { + case smsSent = 1004 + case smsReceived = 1003 + case contactAdded = 1117 + } + + private init() {} + + static func sound(_ alert: Alert) { + try? AVAudioSession + .sharedInstance() + .setCategory(.ambient, mode: .default, options: .mixWithOthers) + + AudioServicesPlaySystemSound(alert.rawValue) + } + + static func shake(_ haptic: Haptic) { + try? AVAudioSession + .sharedInstance() + .setCategory(.ambient, mode: .default, options: .mixWithOthers) + + AudioServicesPlaySystemSound(haptic.rawValue) + } } diff --git a/Sources/Shared/Models/Country.swift b/Sources/Shared/Models/Country.swift index 8d25ca75aba8358d5fe18265a7febd7e4887e6f9..ba2ba1f76c889cd9d35a7a8b012b07ca6125b410 100644 --- a/Sources/Shared/Models/Country.swift +++ b/Sources/Shared/Models/Country.swift @@ -8,32 +8,32 @@ public struct Country { public var prefix: String public var example: String public var prefixWithFlag: String { "\(flag) \(prefix)" } - + public static func fromMyPhone() -> Self { let all = all() - + guard let country = all.filter({ $0.code == Locale.current.regionCode }).first else { return all.filter { $0.code == "US" }.first! } - + return country } - + public static func all() -> [Self] { guard let url = Bundle.module.url(forResource: "country_codes", withExtension: "json"), let data = try? Data(contentsOf: url), let countries = try? JSONDecoder().decode([Country].self, from: data) else { fatalError("Can't handle country codes json") } - + return countries } - + public static func findFrom(_ number: String) -> Self { all().first { country in let start = number.index(number.startIndex, offsetBy: number.count - 2) let end = number.index(start, offsetBy: number.count - (number.count - 2)) - + return country.code == String(number[start ..< end]) }! } diff --git a/Sources/Shared/Models/Payload.swift b/Sources/Shared/Models/Payload.swift index 8e6f519b5ca09531b97961d1616a63477325d002..eb7e67e3731adfcb6eb277fea1ede8554ed1cb60 100644 --- a/Sources/Shared/Models/Payload.swift +++ b/Sources/Shared/Models/Payload.swift @@ -1,37 +1,37 @@ import Foundation public struct Payload: Codable, Equatable, Hashable { - public var text: String - public var reply: Reply? - - public init(text: String, reply: Reply?) { - self.text = text - self.reply = reply + public var text: String + public var reply: Reply? + + public init(text: String, reply: Reply?) { + self.text = text + self.reply = reply + } + + public init(with marshaled: Data) throws { + let proto = try CMIXText(serializedData: marshaled) + + var reply: Reply? + + if proto.hasReply { + reply = Reply( + messageId: proto.reply.messageID, + senderId: proto.reply.senderID + ) } - - public init(with marshaled: Data) throws { - let proto = try CMIXText(serializedData: marshaled) - - var reply: Reply? - - if proto.hasReply { - reply = Reply( - messageId: proto.reply.messageID, - senderId: proto.reply.senderID - ) - } - - self.init(text: proto.text, reply: reply) - } - - public func asData() -> Data { - var protoModel = CMIXText() - protoModel.text = text - - if let reply = reply { - protoModel.reply = reply.asTextReply() - } - - return try! protoModel.serializedData() + + self.init(text: proto.text, reply: reply) + } + + public func asData() -> Data { + var protoModel = CMIXText() + protoModel.text = text + + if let reply = reply { + protoModel.reply = reply.asTextReply() } + + return try! protoModel.serializedData() + } } diff --git a/Sources/Shared/Models/Reply.swift b/Sources/Shared/Models/Reply.swift index 22fcf55aaa3c0b7f6c93618efe21531b1e4e36e6..edc61b01db652a5c57c93a4b893959655c7b37da 100644 --- a/Sources/Shared/Models/Reply.swift +++ b/Sources/Shared/Models/Reply.swift @@ -1,19 +1,19 @@ import Foundation public struct Reply: Codable, Equatable, Hashable { - public let messageId: Data - public let senderId: Data - - public init(messageId: Data, senderId: Data) { - self.messageId = messageId - self.senderId = senderId - } - - func asTextReply() -> TextReply { - var reply = TextReply() - reply.messageID = messageId - reply.senderID = senderId - - return reply - } + public let messageId: Data + public let senderId: Data + + public init(messageId: Data, senderId: Data) { + self.messageId = messageId + self.senderId = senderId + } + + func asTextReply() -> TextReply { + var reply = TextReply() + reply.messageID = messageId + reply.senderID = senderId + + return reply + } } diff --git a/Sources/Shared/Models/ToastModel.swift b/Sources/Shared/Models/ToastModel.swift deleted file mode 100644 index 001539d4dc907791e154a6c66ff8bf1b56e0c8c2..0000000000000000000000000000000000000000 --- a/Sources/Shared/Models/ToastModel.swift +++ /dev/null @@ -1,35 +0,0 @@ -import UIKit - -public struct ToastModel { - let id: UUID - let title: String - let color: UIColor - let subtitle: String? - let leftImage: UIImage - let timeToLive: Int - let buttonTitle: String? - let autodismissable: Bool - let onTapClosure: (() -> Void)? - - public init( - id: UUID = UUID(), - title: String, - color: UIColor = Asset.neutralOverlay.color, - subtitle: String? = nil, - leftImage: UIImage, - timeToLive: Int = 4, - buttonTitle: String? = nil, - onTapClosure: (() -> Void)? = nil, - autodismissable: Bool = true - ) { - self.id = id - self.title = title - self.color = color - self.subtitle = subtitle - self.leftImage = leftImage - self.timeToLive = timeToLive - self.buttonTitle = buttonTitle - self.onTapClosure = onTapClosure - self.autodismissable = autodismissable - } -} diff --git a/Sources/Shared/Publishers.swift b/Sources/Shared/Publishers.swift new file mode 100644 index 0000000000000000000000000000000000000000..cc837b449f284df069edc8e28c0c76860b6b7ee0 --- /dev/null +++ b/Sources/Shared/Publishers.swift @@ -0,0 +1,113 @@ +import UIKit +import Combine + +public extension UIControl { + func publisher(for event: Event) -> EventPublisher { + EventPublisher( + control: self, + event: event + ) + } + + struct EventPublisher: Publisher { + public typealias Output = Void + public typealias Failure = Never + + fileprivate var control: UIControl + fileprivate var event: Event + + public func receive<S: Subscriber>( + subscriber: S + ) where S.Input == Output, S.Failure == Failure { + let subscription = EventSubscription<S>() + subscription.target = subscriber + subscriber.receive(subscription: subscription) + + control.addTarget(subscription, + action: #selector(subscription.trigger), + for: event + ) + } + } +} + +private extension UIControl { + class EventSubscription<Target: Subscriber>: Subscription + where Target.Input == Void { + + var target: Target? + + func request(_ demand: Subscribers.Demand) {} + + func cancel() { + target = nil + } + + @objc func trigger() { + _ = target?.receive(()) + } + } +} + +public extension UITextField { + var textPublisher: AnyPublisher<String, Never> { + publisher(for: .editingChanged) + .map { self.text ?? "" } + .eraseToAnyPublisher() + } + + var returnPublisher: AnyPublisher<Void, Never> { + publisher(for: .editingDidEndOnExit) + .eraseToAnyPublisher() + } +} + +public extension UITextView { + var textPublisher: Publishers.TextFieldPublisher { + Publishers.TextFieldPublisher(textField: self) + } +} + +public extension Publishers { + struct TextFieldPublisher: Publisher { + public typealias Output = String + public typealias Failure = Never + + private let textField: UITextView + + init(textField: UITextView) { self.textField = textField } + + public func receive<S>(subscriber: S) where S : Subscriber, Publishers.TextFieldPublisher.Failure == S.Failure, Publishers.TextFieldPublisher.Output == S.Input { + let subscription = TextFieldSubscription(subscriber: subscriber, textField: textField) + subscriber.receive(subscription: subscription) + } + } + + class TextFieldSubscription<S: Subscriber>: NSObject, Subscription, UITextViewDelegate where S.Input == String, S.Failure == Never { + + private var subscriber: S? + private weak var textField: UITextView? + + init(subscriber: S, textField: UITextView) { + super.init() + self.subscriber = subscriber + self.textField = textField + subscribe() + } + + public func request(_ demand: Subscribers.Demand) { } + + public func cancel() { + subscriber = nil + textField = nil + } + + private func subscribe() { + textField?.delegate = self + } + + public func textViewDidChange(_ textView: UITextView) { + _ = subscriber?.receive(textView.text) + } + } +} diff --git a/Sources/Shared/Views/AttributeComponent.swift b/Sources/Shared/Views/AttributeComponent.swift index eefa73c5c6f6ab3f0b18a21fe099bf163cb534a7..b7ed4ede8177df01963b36e44f54baa51d1ce5eb 100644 --- a/Sources/Shared/Views/AttributeComponent.swift +++ b/Sources/Shared/Views/AttributeComponent.swift @@ -1,81 +1,82 @@ import UIKit +import AppResources public final class AttributeComponent: UIView { - public enum Style { - case steady - case interactive - case requiredEditable + public enum Style { + case steady + case interactive + case requiredEditable + } + + public let titleLabel = UILabel() + public let actionButton = UIButton() + public let contentLabel = UILabel() + + let placeholder = "None provided" + var buttonStyle: Style = .steady + + public private(set) var currentValue: String? { + didSet { contentLabel.text = currentValue ?? placeholder } + } + + public init() { + super.init(frame: .zero) + + titleLabel.textColor = Asset.neutralWeak.color + contentLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + contentLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + addSubview(titleLabel) + addSubview(actionButton) + addSubview(contentLabel) + + titleLabel.snp.makeConstraints { make in + make.top.left.equalToSuperview() + make.bottom.equalToSuperview().offset(-25) } - - public let titleLabel = UILabel() - public let actionButton = UIButton() - public let contentLabel = UILabel() - - let placeholder = "None provided" - var buttonStyle: Style = .steady - - public private(set) var currentValue: String? { - didSet { contentLabel.text = currentValue ?? placeholder } + + contentLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(6) + make.left.equalToSuperview() } - - public init() { - super.init(frame: .zero) - - titleLabel.textColor = Asset.neutralWeak.color - contentLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) - contentLabel.font = Fonts.Mulish.regular.font(size: 16.0) - - addSubview(titleLabel) - addSubview(actionButton) - addSubview(contentLabel) - - titleLabel.snp.makeConstraints { make in - make.top.left.equalToSuperview() - make.bottom.equalToSuperview().offset(-25) - } - - contentLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(6) - make.left.equalToSuperview() - } - - actionButton.snp.makeConstraints { $0.right.centerY.equalToSuperview() } + + actionButton.snp.makeConstraints { $0.right.centerY.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + public func set( + title: String, + value: String? = nil, + icon: UIImage? = nil, + style: Style = .steady + ) { + titleLabel.text = title.uppercased() + actionButton.setImage(icon, for: .normal) + buttonStyle = style + + set(value: value) + } + + public func set(value: String?) { + currentValue = value + + if buttonStyle == .requiredEditable { + actionButton.setImage(Asset.contactNicknameEdit.image, for: .normal) + return } - - required init?(coder: NSCoder) { nil } - - public func set( - title: String, - value: String? = nil, - icon: UIImage? = nil, - style: Style = .steady - ) { - titleLabel.text = title.uppercased() - actionButton.setImage(icon, for: .normal) - buttonStyle = style - - set(value: value) + + guard let _ = value else { + if buttonStyle == .interactive { + actionButton.setImage(Asset.profileAdd.image, for: .normal) + } + + return } - - public func set(value: String?) { - currentValue = value - - if buttonStyle == .requiredEditable { - actionButton.setImage(Asset.contactNicknameEdit.image, for: .normal) - return - } - - guard let _ = value else { - if buttonStyle == .interactive { - actionButton.setImage(Asset.profileAdd.image, for: .normal) - } - - return - } - - if buttonStyle == .interactive { - actionButton.setImage(Asset.profileDelete.image, for: .normal) - } + + if buttonStyle == .interactive { + actionButton.setImage(Asset.profileDelete.image, for: .normal) } + } } diff --git a/Sources/Shared/Views/AvatarCardComponent.swift b/Sources/Shared/Views/AvatarCardComponent.swift index c34a8650696b991a8677d10b1a99deaec7c44fa8..fcbe72787d87f9a13579a6b51b0f5d88c4b682b4 100644 --- a/Sources/Shared/Views/AvatarCardComponent.swift +++ b/Sources/Shared/Views/AvatarCardComponent.swift @@ -1,164 +1,165 @@ import UIKit +import AppResources public final class AvatarCardComponent: UIView { - public let nameLabel = UILabel() - public let stackView = UIStackView() - public let avatarView = EditableAvatarView() - public var nameContainer: UIView? - private let sendMessageView = AvatarSendMessageView() - - public var image: UIImage? { - didSet { - avatarView.imageView.image = nil - avatarView.imageView.image = image - avatarView.imageView.setNeedsDisplay() - avatarView.placeholderImageView.image = nil - } + public let nameLabel = UILabel() + public let stackView = UIStackView() + public let avatarView = EditableAvatarView() + public var nameContainer: UIView? + private let sendMessageView = AvatarSendMessageView() + + public var image: UIImage? { + didSet { + avatarView.imageView.image = nil + avatarView.imageView.image = image + avatarView.imageView.setNeedsDisplay() + avatarView.placeholderImageView.image = nil } - - public init() { - super.init(frame: .zero) - - backgroundColor = Asset.neutralBody.color - - nameLabel.textColor = Asset.neutralWhite.color - nameLabel.numberOfLines = 2 - nameLabel.textAlignment = .center - nameLabel.font = Fonts.Mulish.bold.font(size: 24.0) - - nameContainer = nameLabel.pinning(at: .center(0)) - let imageContainer = avatarView.pinning(at: .hCenter) - - stackView.axis = .vertical - stackView.addArrangedSubview(imageContainer) - stackView.addArrangedSubview(nameContainer ?? UIView()) - stackView.setCustomSpacing(24, after: imageContainer) - - addSubview(stackView) - - nameLabel.snp.makeConstraints { make in - make.top.bottom.centerX.equalToSuperview() - make.left.greaterThanOrEqualToSuperview().offset(10) - make.right.lessThanOrEqualToSuperview().offset(-10) - } - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(40) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.bottom.equalToSuperview().offset(-30) - } + } + + public init() { + super.init(frame: .zero) + + backgroundColor = Asset.neutralBody.color + + nameLabel.textColor = Asset.neutralWhite.color + nameLabel.numberOfLines = 2 + nameLabel.textAlignment = .center + nameLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + nameContainer = nameLabel.pinning(at: .center(0)) + let imageContainer = avatarView.pinning(at: .hCenter) + + stackView.axis = .vertical + stackView.addArrangedSubview(imageContainer) + stackView.addArrangedSubview(nameContainer ?? UIView()) + stackView.setCustomSpacing(24, after: imageContainer) + + addSubview(stackView) + + nameLabel.snp.makeConstraints { make in + make.top.bottom.centerX.equalToSuperview() + make.left.greaterThanOrEqualToSuperview().offset(10) + make.right.lessThanOrEqualToSuperview().offset(-10) } - - required init?(coder: NSCoder) { nil } - - public func setupButtons( - info: @escaping () -> Void, - send: @escaping () -> Void - ) { - let container = UIView() - container.addSubview(sendMessageView) - - sendMessageView.didTapInfo = info - sendMessageView.didTapSend = send - - sendMessageView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.greaterThanOrEqualToSuperview() - make.centerX.equalToSuperview() - make.right.lessThanOrEqualToSuperview() - make.bottom.equalToSuperview() - } - - if let nameContainer = nameContainer { - stackView.addArrangedSubview(container) - stackView.setCustomSpacing(48, after: nameContainer) - } + + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(40) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.bottom.equalToSuperview().offset(-30) } -} - -private final class AvatarSendMessageView: UIView { - let stackView = UIStackView() - let iconImageView = UIImageView() - let sendButton = UIButton() - let infoButton = UIButton() - - var didTapInfo: (() -> Void)? - var didTapSend: (() -> Void)? - - init() { - super.init(frame: .zero) - - iconImageView.contentMode = .center - iconImageView.image = Asset.contactSendMessage.image - - sendButton.setTitle("Send Message", for: .normal) - sendButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - sendButton.titleLabel?.font = Fonts.Mulish.regular.font(size: 13.0) - - infoButton.setImage(Asset.infoIconGrey.image, for: .normal) - - sendButton.addTarget(self, action: #selector(didTapSendButton), for: .touchUpInside) - infoButton.addTarget(self, action: #selector(didTapInfoButton), for: .touchUpInside) - - stackView.spacing = 8 - stackView.distribution = .equalSpacing - stackView.addArrangedSubview(iconImageView) - stackView.addArrangedSubview(sendButton) - stackView.addArrangedSubview(infoButton) - - addSubview(stackView) - stackView.snp.makeConstraints { $0.edges.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + public func setupButtons( + info: @escaping () -> Void, + send: @escaping () -> Void + ) { + let container = UIView() + container.addSubview(sendMessageView) + + sendMessageView.didTapInfo = info + sendMessageView.didTapSend = send + + sendMessageView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.greaterThanOrEqualToSuperview() + make.centerX.equalToSuperview() + make.right.lessThanOrEqualToSuperview() + make.bottom.equalToSuperview() } - - required init?(coder: NSCoder) { nil } - - @objc private func didTapSendButton() { - didTapSend?() + + if let nameContainer = nameContainer { + stackView.addArrangedSubview(container) + stackView.setCustomSpacing(48, after: nameContainer) } + } +} - @objc private func didTapInfoButton() { - didTapInfo?() - } +private final class AvatarSendMessageView: UIView { + let stackView = UIStackView() + let iconImageView = UIImageView() + let sendButton = UIButton() + let infoButton = UIButton() + + var didTapInfo: (() -> Void)? + var didTapSend: (() -> Void)? + + init() { + super.init(frame: .zero) + + iconImageView.contentMode = .center + iconImageView.image = Asset.contactSendMessage.image + + sendButton.setTitle("Send Message", for: .normal) + sendButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + sendButton.titleLabel?.font = Fonts.Mulish.regular.font(size: 13.0) + + infoButton.setImage(Asset.infoIconGrey.image, for: .normal) + + sendButton.addTarget(self, action: #selector(didTapSendButton), for: .touchUpInside) + infoButton.addTarget(self, action: #selector(didTapInfoButton), for: .touchUpInside) + + stackView.spacing = 8 + stackView.distribution = .equalSpacing + stackView.addArrangedSubview(iconImageView) + stackView.addArrangedSubview(sendButton) + stackView.addArrangedSubview(infoButton) + + addSubview(stackView) + stackView.snp.makeConstraints { $0.edges.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + @objc private func didTapSendButton() { + didTapSend?() + } + + @objc private func didTapInfoButton() { + didTapInfo?() + } } public final class EditableAvatarView: UIView { - public let editButton = UIButton() - public let imageView = UIImageView() - public let placeholderImageView = UIImageView() - - init() { - super.init(frame: .zero) - - imageView.layer.cornerRadius = 38 - imageView.layer.masksToBounds = true - imageView.contentMode = .scaleAspectFill - imageView.backgroundColor = Asset.brandPrimary.color - - placeholderImageView.contentMode = .center - placeholderImageView.image = Asset.profileImagePlaceholder.image - - editButton.setImage(Asset.profileImageButton.image, for: .normal) - - addSubview(imageView) - addSubview(editButton) - imageView.addSubview(placeholderImageView) - - editButton.snp.makeConstraints { make in - make.bottom.equalTo(imageView) - make.right.equalTo(imageView).offset(9) - } - - imageView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - make.width.height.equalTo(100) - } - - placeholderImageView.snp.makeConstraints { $0.center.equalToSuperview() } + public let editButton = UIButton() + public let imageView = UIImageView() + public let placeholderImageView = UIImageView() + + init() { + super.init(frame: .zero) + + imageView.layer.cornerRadius = 38 + imageView.layer.masksToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.backgroundColor = Asset.brandPrimary.color + + placeholderImageView.contentMode = .center + placeholderImageView.image = Asset.profileImagePlaceholder.image + + editButton.setImage(Asset.profileImageButton.image, for: .normal) + + addSubview(imageView) + addSubview(editButton) + imageView.addSubview(placeholderImageView) + + editButton.snp.makeConstraints { make in + make.bottom.equalTo(imageView) + make.right.equalTo(imageView).offset(9) } - - required init?(coder: NSCoder) { nil } + + imageView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.equalToSuperview() + make.width.height.equalTo(100) + } + + placeholderImageView.snp.makeConstraints { $0.center.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/Shared/Views/AvatarCell.swift b/Sources/Shared/Views/AvatarCell.swift index 43f0e3fd6686464b6d66c31770627134e9c075c0..d37bbb35cb2ef214140498d77b16800e512793b0 100644 --- a/Sources/Shared/Views/AvatarCell.swift +++ b/Sources/Shared/Views/AvatarCell.swift @@ -1,187 +1,188 @@ import UIKit import Combine +import AppResources final class AvatarCellButton: UIControl { - let titleLabel = UILabel() - let imageView = UIImageView() - - init() { - super.init(frame: .zero) - titleLabel.numberOfLines = 0 - titleLabel.textAlignment = .right - titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - - addSubview(imageView) - addSubview(titleLabel) - - imageView.snp.makeConstraints { - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalToSuperview() - $0.centerY.equalToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalTo(imageView.snp.right).offset(5) - $0.centerY.equalToSuperview() - $0.right.equalToSuperview() - $0.width.equalTo(60) - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let imageView = UIImageView() + + init() { + super.init(frame: .zero) + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .right + titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + + addSubview(imageView) + addSubview(titleLabel) + + imageView.snp.makeConstraints { + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalToSuperview() + $0.centerY.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() } - - required init?(coder: NSCoder) { nil } + + titleLabel.snp.makeConstraints { + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalTo(imageView.snp.right).offset(5) + $0.centerY.equalToSuperview() + $0.right.equalToSuperview() + $0.width.equalTo(60) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } } public final class AvatarCell: UITableViewCell { - let h1Label = UILabel() - let h2Label = UILabel() - let h3Label = UILabel() - let h4Label = UILabel() - let separatorView = UIView() - let avatarView = AvatarView() - let stackView = UIStackView() - let stateButton = AvatarCellButton() - - var cancellables = Set<AnyCancellable>() - public var didTapStateButton: (() -> Void)! - - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectedBackgroundView = UIView() - multipleSelectionBackgroundView = UIView() - backgroundColor = Asset.neutralWhite.color - - h1Label.textColor = Asset.neutralActive.color - h2Label.textColor = Asset.neutralSecondaryAlternative.color - h3Label.textColor = Asset.neutralSecondaryAlternative.color - h4Label.textColor = Asset.neutralSecondaryAlternative.color - - h1Label.font = Fonts.Mulish.semiBold.font(size: 14.0) - h2Label.font = Fonts.Mulish.regular.font(size: 14.0) - h3Label.font = Fonts.Mulish.regular.font(size: 14.0) - h4Label.font = Fonts.Mulish.regular.font(size: 14.0) - - stackView.spacing = 4 - stackView.axis = .vertical - - stackView.addArrangedSubview(h1Label) - stackView.addArrangedSubview(h2Label) - stackView.addArrangedSubview(h3Label) - stackView.addArrangedSubview(h4Label) - - separatorView.backgroundColor = Asset.neutralLine.color - - contentView.addSubview(stackView) - contentView.addSubview(avatarView) - contentView.addSubview(stateButton) - contentView.addSubview(separatorView) - - setupConstraints() + let h1Label = UILabel() + let h2Label = UILabel() + let h3Label = UILabel() + let h4Label = UILabel() + let separatorView = UIView() + let avatarView = AvatarView() + let stackView = UIStackView() + let stateButton = AvatarCellButton() + + var cancellables = Set<AnyCancellable>() + public var didTapStateButton: (() -> Void)! + + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + selectedBackgroundView = UIView() + multipleSelectionBackgroundView = UIView() + backgroundColor = Asset.neutralWhite.color + + h1Label.textColor = Asset.neutralActive.color + h2Label.textColor = Asset.neutralSecondaryAlternative.color + h3Label.textColor = Asset.neutralSecondaryAlternative.color + h4Label.textColor = Asset.neutralSecondaryAlternative.color + + h1Label.font = Fonts.Mulish.semiBold.font(size: 14.0) + h2Label.font = Fonts.Mulish.regular.font(size: 14.0) + h3Label.font = Fonts.Mulish.regular.font(size: 14.0) + h4Label.font = Fonts.Mulish.regular.font(size: 14.0) + + stackView.spacing = 4 + stackView.axis = .vertical + + stackView.addArrangedSubview(h1Label) + stackView.addArrangedSubview(h2Label) + stackView.addArrangedSubview(h3Label) + stackView.addArrangedSubview(h4Label) + + separatorView.backgroundColor = Asset.neutralLine.color + + contentView.addSubview(stackView) + contentView.addSubview(avatarView) + contentView.addSubview(stateButton) + contentView.addSubview(separatorView) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + public override func prepareForReuse() { + super.prepareForReuse() + h1Label.text = nil + h2Label.text = nil + h3Label.text = nil + h4Label.text = nil + + stateButton.imageView.image = nil + stateButton.titleLabel.text = nil + + avatarView.prepareForReuse() + cancellables.removeAll() + } + + public func setup( + title: String, + image: Data?, + firstSubtitle: String? = nil, + secondSubtitle: String? = nil, + thirdSubtitle: String? = nil, + showSeparator: Bool = true, + sent: Bool = false + ) { + h1Label.text = title + + if let firstSubtitle = firstSubtitle { + h2Label.isHidden = false + h2Label.text = firstSubtitle + } else { + h2Label.isHidden = true } - - required init?(coder: NSCoder) { nil } - - public override func prepareForReuse() { - super.prepareForReuse() - h1Label.text = nil - h2Label.text = nil - h3Label.text = nil - h4Label.text = nil - - stateButton.imageView.image = nil - stateButton.titleLabel.text = nil - - avatarView.prepareForReuse() - cancellables.removeAll() + + if let secondSubtitle = secondSubtitle { + h3Label.isHidden = false + h3Label.text = secondSubtitle + } else { + h3Label.isHidden = true } - - public func setup( - title: String, - image: Data?, - firstSubtitle: String? = nil, - secondSubtitle: String? = nil, - thirdSubtitle: String? = nil, - showSeparator: Bool = true, - sent: Bool = false - ) { - h1Label.text = title - - if let firstSubtitle = firstSubtitle { - h2Label.isHidden = false - h2Label.text = firstSubtitle - } else { - h2Label.isHidden = true - } - - if let secondSubtitle = secondSubtitle { - h3Label.isHidden = false - h3Label.text = secondSubtitle - } else { - h3Label.isHidden = true - } - - if let thirdSubtitle = thirdSubtitle { - h4Label.isHidden = false - h4Label.text = thirdSubtitle - } else { - h4Label.isHidden = true - } - - avatarView.setupProfile(title: title, image: image, size: .medium) - separatorView.alpha = showSeparator ? 1.0 : 0.0 - - cancellables.removeAll() - - if sent { - stateButton.imageView.image = Asset.requestsResend.image - stateButton.titleLabel.text = Localized.Requests.Cell.requested - stateButton.titleLabel.textColor = Asset.brandPrimary.color - - stateButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in didTapStateButton() } - .store(in: &cancellables) - } + + if let thirdSubtitle = thirdSubtitle { + h4Label.isHidden = false + h4Label.text = thirdSubtitle + } else { + h4Label.isHidden = true } - - public func updateToResent() { - stateButton.imageView.image = Asset.requestsResent.image - stateButton.titleLabel.text = Localized.Requests.Cell.resent - stateButton.titleLabel.textColor = Asset.neutralWeak.color - - cancellables.forEach { $0.cancel() } - cancellables.removeAll() + + avatarView.setupProfile(title: title, image: image, size: .medium) + separatorView.alpha = showSeparator ? 1.0 : 0.0 + + cancellables.removeAll() + + if sent { + stateButton.imageView.image = Asset.requestsResend.image + stateButton.titleLabel.text = Localized.Requests.Cell.requested + stateButton.titleLabel.textColor = Asset.brandPrimary.color + + stateButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in didTapStateButton() } + .store(in: &cancellables) } - - private func setupConstraints() { - avatarView.snp.makeConstraints { - $0.width.height.equalTo(36) - $0.left.equalToSuperview().offset(27) - $0.centerY.equalToSuperview() - } - - stackView.snp.makeConstraints { - $0.top.equalTo(avatarView) - $0.left.equalTo(avatarView.snp.right).offset(14) - $0.right.lessThanOrEqualToSuperview().offset(-10) - $0.bottom.greaterThanOrEqualTo(avatarView) - $0.bottom.lessThanOrEqualToSuperview() - } - - separatorView.snp.makeConstraints { - $0.height.equalTo(1) - $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(10) - $0.left.equalToSuperview().offset(25) - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - - stateButton.snp.makeConstraints { - $0.centerY.equalTo(stackView) - $0.right.equalToSuperview().offset(-24) - } + } + + public func updateToResent() { + stateButton.imageView.image = Asset.requestsResent.image + stateButton.titleLabel.text = Localized.Requests.Cell.resent + stateButton.titleLabel.textColor = Asset.neutralWeak.color + + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } + + private func setupConstraints() { + avatarView.snp.makeConstraints { + $0.width.height.equalTo(36) + $0.left.equalToSuperview().offset(27) + $0.centerY.equalToSuperview() + } + + stackView.snp.makeConstraints { + $0.top.equalTo(avatarView) + $0.left.equalTo(avatarView.snp.right).offset(14) + $0.right.lessThanOrEqualToSuperview().offset(-10) + $0.bottom.greaterThanOrEqualTo(avatarView) + $0.bottom.lessThanOrEqualToSuperview() + } + + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(10) + $0.left.equalToSuperview().offset(25) + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + + stateButton.snp.makeConstraints { + $0.centerY.equalTo(stackView) + $0.right.equalToSuperview().offset(-24) } + } } diff --git a/Sources/Shared/Views/AvatarView.swift b/Sources/Shared/Views/AvatarView.swift index a1104edb680c869092a0017ef5e8c87310ae7bd5..cacce36f254322f1bcf58141d62852ec3ca6e57b 100644 --- a/Sources/Shared/Views/AvatarView.swift +++ b/Sources/Shared/Views/AvatarView.swift @@ -1,95 +1,96 @@ import UIKit +import AppResources public final class AvatarView: UIView { - public enum Size { - case small - case medium - case large - } - - let imageView = UIImageView() - let monogramLabel = UILabel() - let iconImageView = UIImageView() - - public init() { - super.init(frame: .zero) + public enum Size { + case small + case medium + case large + } - layer.masksToBounds = true - backgroundColor = Asset.brandPrimary.color + let imageView = UIImageView() + let monogramLabel = UILabel() + let iconImageView = UIImageView() - iconImageView.contentMode = .center - imageView.contentMode = .scaleAspectFill - monogramLabel.textColor = Asset.neutralWhite.color + public init() { + super.init(frame: .zero) - addSubview(monogramLabel) - addSubview(iconImageView) - addSubview(imageView) + layer.masksToBounds = true + backgroundColor = Asset.brandPrimary.color - imageView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + iconImageView.contentMode = .center + imageView.contentMode = .scaleAspectFill + monogramLabel.textColor = Asset.neutralWhite.color - monogramLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } + addSubview(monogramLabel) + addSubview(iconImageView) + addSubview(imageView) - iconImageView.snp.makeConstraints { - $0.center.equalToSuperview() - } + imageView.snp.makeConstraints { + $0.edges.equalToSuperview() } - required init?(coder: NSCoder) { nil } + monogramLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } - public func prepareForReuse() { - imageView.image = nil - monogramLabel.text = nil - iconImageView.image = nil + iconImageView.snp.makeConstraints { + $0.center.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + public func prepareForReuse() { + imageView.image = nil + monogramLabel.text = nil + iconImageView.image = nil + } + + public func setupProfile(title: String, image: Data?, size: AvatarView.Size) { + iconImageView.image = nil + monogramLabel.text = title + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: " ", with: "") + .prefix(2) + .uppercased() + + monogramLabel.text = "\(title.prefix(2))".uppercased() + + // TODO: What are the font sizes and corner radius for small/medium avatars? + + switch size { + case .small: + layer.cornerRadius = 13.0 + monogramLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + case .medium: + layer.cornerRadius = 13.0 + monogramLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + case .large: + layer.cornerRadius = 18.0 + monogramLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) } - public func setupProfile(title: String, image: Data?, size: AvatarView.Size) { - iconImageView.image = nil - monogramLabel.text = title - .trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: " ", with: "") - .prefix(2) - .uppercased() - - monogramLabel.text = "\(title.prefix(2))".uppercased() - - // TODO: What are the font sizes and corner radius for small/medium avatars? - - switch size { - case .small: - layer.cornerRadius = 13.0 - monogramLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - case .medium: - layer.cornerRadius = 13.0 - monogramLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - case .large: - layer.cornerRadius = 18.0 - monogramLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) - } - - guard let image = image else { - imageView.image = nil - return - } - - imageView.image = UIImage(data: image) + guard let image = image else { + imageView.image = nil + return } - public func setupGroup(size: AvatarView.Size) { - switch size { - case .small: - layer.cornerRadius = 13.0 - case .medium: - layer.cornerRadius = 13.0 - case .large: - layer.cornerRadius = 18.0 - } - - imageView.image = nil - monogramLabel.text = nil - iconImageView.image = Asset.sharedGroup.image + imageView.image = UIImage(data: image) + } + + public func setupGroup(size: AvatarView.Size) { + switch size { + case .small: + layer.cornerRadius = 13.0 + case .medium: + layer.cornerRadius = 13.0 + case .large: + layer.cornerRadius = 18.0 } + + imageView.image = nil + monogramLabel.text = nil + iconImageView.image = Asset.sharedGroup.image + } } diff --git a/Sources/Shared/Views/BottomFeedbackComponent.swift b/Sources/Shared/Views/BottomFeedbackComponent.swift index aedff2e8003a07590bb41c9e7acbed0830986a41..dd5e583c7e0fe8e1563cae05c68379cb225ef981 100644 --- a/Sources/Shared/Views/BottomFeedbackComponent.swift +++ b/Sources/Shared/Views/BottomFeedbackComponent.swift @@ -1,83 +1,76 @@ import UIKit +import AppResources public struct BottomFeedbackStyle { - var color: UIColor - var iconColor: UIColor - var titleColor: UIColor - var actionColor: UIColor? + var color: UIColor + var iconColor: UIColor + var titleColor: UIColor + var actionColor: UIColor? } public extension BottomFeedbackStyle { - static let danger = BottomFeedbackStyle( - color: Asset.accentDanger.color, - iconColor: Asset.neutralWhite.color, - titleColor: Asset.neutralWhite.color - ) - - static let chill = BottomFeedbackStyle( - color: Asset.neutralSecondary.color, - iconColor: Asset.neutralDisabled.color, - titleColor: Asset.neutralBody.color - ) + static let danger = BottomFeedbackStyle( + color: Asset.accentDanger.color, + iconColor: Asset.neutralWhite.color, + titleColor: Asset.neutralWhite.color + ) + + static let chill = BottomFeedbackStyle( + color: Asset.neutralSecondary.color, + iconColor: Asset.neutralDisabled.color, + titleColor: Asset.neutralBody.color + ) } public final class BottomFeedbackComponent: UIView { - // MARK: UI - - public let title = UILabel() - public let icon = UIImageView() - public let stack = UIStackView() - public let button = CapsuleButton(height: 50.0, minimumWidth: 100.0) - - // MARK: Lifecycle - - public init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - // MARK: Public - - public func set( - icon: UIImage, - title: String, - style: BottomFeedbackStyle, - actionTitle: String? = nil, - actionStyle: CapsuleButtonStyle = .seeThroughWhite - ) { - backgroundColor = style.color - self.icon.tintColor = style.iconColor - self.title.textColor = style.titleColor - - self.title.text = title - self.icon.image = icon.withRenderingMode(.alwaysTemplate) - - guard let actionTitle = actionTitle else { return } - - button.setStyle(actionStyle) - button.setTitle(actionTitle, for: .normal) - stack.addArrangedSubview(button.pinning(at: .center(0))) - } - - // MARK: Private - - private func setup() { - layer.cornerRadius = 15 - icon.contentMode = .center - title.font = Fonts.Mulish.semiBold.font(size: 14.0) - - stack.spacing = 10 - stack.addArrangedSubview(icon) - stack.addArrangedSubview(title.pinning(at: .left(0))) - addSubview(stack) - - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview().offset(20) - make.right.equalToSuperview().offset(-20) - make.bottom.equalToSuperview().offset(-40) - } + public let title = UILabel() + public let icon = UIImageView() + public let stack = UIStackView() + public let button = CapsuleButton(height: 50.0, minimumWidth: 100.0) + + public init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + public func set( + icon: UIImage, + title: String, + style: BottomFeedbackStyle, + actionTitle: String? = nil, + actionStyle: CapsuleButtonStyle = .seeThroughWhite + ) { + backgroundColor = style.color + self.icon.tintColor = style.iconColor + self.title.textColor = style.titleColor + + self.title.text = title + self.icon.image = icon.withRenderingMode(.alwaysTemplate) + + guard let actionTitle = actionTitle else { return } + + button.setStyle(actionStyle) + button.setTitle(actionTitle, for: .normal) + stack.addArrangedSubview(button.pinning(at: .center(0))) + } + + private func setup() { + layer.cornerRadius = 15 + icon.contentMode = .center + title.font = Fonts.Mulish.semiBold.font(size: 14.0) + + stack.spacing = 10 + stack.addArrangedSubview(icon) + stack.addArrangedSubview(title.pinning(at: .left(0))) + addSubview(stack) + + stack.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + $0.bottom.equalToSuperview().offset(-40) } + } } diff --git a/Sources/Shared/Views/CapsuleButton.swift b/Sources/Shared/Views/CapsuleButton.swift index 4d681dc36b0f5c8ca9d214ac2ce026c940eadf01..e3dfc57373b2b7562c4a1297ab78d8d543327605 100644 --- a/Sources/Shared/Views/CapsuleButton.swift +++ b/Sources/Shared/Views/CapsuleButton.swift @@ -1,153 +1,142 @@ import UIKit +import AppResources public struct CapsuleButtonModel { - public var title: String - public var accessibility: String? - public var style: CapsuleButtonStyle - - public init( - title: String, - style: CapsuleButtonStyle, - accessibility: String? = nil - ) { - self.title = title - self.style = style - self.accessibility = accessibility - } + public var title: String + public var accessibility: String? + public var style: CapsuleButtonStyle + + public init( + title: String, + style: CapsuleButtonStyle, + accessibility: String? = nil + ) { + self.title = title + self.style = style + self.accessibility = accessibility + } } public struct CapsuleButtonStyle { - var fill: UIImage - var borderWidth: CGFloat - var borderColor: UIColor? - var titleColor: UIColor - var disabledTitleColor: UIColor + var fill: UIImage + var borderWidth: CGFloat + var borderColor: UIColor? + var titleColor: UIColor + var disabledTitleColor: UIColor } public extension CapsuleButtonStyle { - static let white = CapsuleButtonStyle( - fill: .color(Asset.neutralWhite.color), - borderWidth: 0, - borderColor: nil, - titleColor: Asset.brandPrimary.color, - disabledTitleColor: Asset.neutralWhite.color.withAlphaComponent(0.5) - ) - - static let brandColored = CapsuleButtonStyle( - fill: .color(Asset.brandPrimary.color), - borderWidth: 0, - borderColor: nil, - titleColor: Asset.neutralWhite.color, - disabledTitleColor: Asset.neutralWhite.color - ) - - static let red = CapsuleButtonStyle( - fill: .color(Asset.accentDanger.color), - borderWidth: 0, - borderColor: nil, - titleColor: Asset.neutralWhite.color, - disabledTitleColor: Asset.neutralWhite.color - ) - - static let seeThroughWhite = CapsuleButtonStyle( - fill: .color(UIColor.clear), - borderWidth: 2, - borderColor: Asset.neutralWhite.color, - titleColor: Asset.neutralWhite.color, - disabledTitleColor: Asset.neutralWhite.color.withAlphaComponent(0.5) - ) - - static let seeThrough = CapsuleButtonStyle( - fill: .color(UIColor.clear), - borderWidth: 2, - borderColor: Asset.brandPrimary.color, - titleColor: Asset.brandPrimary.color, - disabledTitleColor: Asset.brandPrimary.color.withAlphaComponent(0.5) - ) - - static let simplestColoredRed = CapsuleButtonStyle( - fill: .color(UIColor.clear), - borderWidth: 0, - borderColor: nil, - titleColor: Asset.accentDanger.color, - disabledTitleColor: Asset.brandDefault.color.withAlphaComponent(0.5) - ) - - static let simplestColoredBrand = CapsuleButtonStyle( - fill: .color(UIColor.clear), - borderWidth: 0, - borderColor: nil, - titleColor: Asset.brandPrimary.color, - disabledTitleColor: Asset.brandDefault.color.withAlphaComponent(0.5) - ) + static let white = CapsuleButtonStyle( + fill: .color(Asset.neutralWhite.color), + borderWidth: 0, + borderColor: nil, + titleColor: Asset.brandPrimary.color, + disabledTitleColor: Asset.neutralWhite.color.withAlphaComponent(0.5) + ) + + static let brandColored = CapsuleButtonStyle( + fill: .color(Asset.brandPrimary.color), + borderWidth: 0, + borderColor: nil, + titleColor: Asset.neutralWhite.color, + disabledTitleColor: Asset.neutralWhite.color + ) + + static let red = CapsuleButtonStyle( + fill: .color(Asset.accentDanger.color), + borderWidth: 0, + borderColor: nil, + titleColor: Asset.neutralWhite.color, + disabledTitleColor: Asset.neutralWhite.color + ) + + static let seeThroughWhite = CapsuleButtonStyle( + fill: .color(UIColor.clear), + borderWidth: 2, + borderColor: Asset.neutralWhite.color, + titleColor: Asset.neutralWhite.color, + disabledTitleColor: Asset.neutralWhite.color.withAlphaComponent(0.5) + ) + + static let seeThrough = CapsuleButtonStyle( + fill: .color(UIColor.clear), + borderWidth: 2, + borderColor: Asset.brandPrimary.color, + titleColor: Asset.brandPrimary.color, + disabledTitleColor: Asset.brandPrimary.color.withAlphaComponent(0.5) + ) + + static let simplestColoredRed = CapsuleButtonStyle( + fill: .color(UIColor.clear), + borderWidth: 0, + borderColor: nil, + titleColor: Asset.accentDanger.color, + disabledTitleColor: Asset.brandDefault.color.withAlphaComponent(0.5) + ) + + static let simplestColoredBrand = CapsuleButtonStyle( + fill: .color(UIColor.clear), + borderWidth: 0, + borderColor: nil, + titleColor: Asset.brandPrimary.color, + disabledTitleColor: Asset.brandDefault.color.withAlphaComponent(0.5) + ) } public final class CapsuleButton: UIButton { - // MARK: Properties - - private let height: CGFloat - private let minimumWidth: CGFloat - - // MARK: Lifecycle - - public init( - height: CGFloat = 55.0, - minimumWidth: CGFloat = 200 - ) { - self.height = height - self.minimumWidth = minimumWidth - - super.init(frame: .zero) - setup() + private let height: CGFloat + private let minimumWidth: CGFloat + + public init( + height: CGFloat = 55.0, + minimumWidth: CGFloat = 200 + ) { + self.height = height + self.minimumWidth = minimumWidth + super.init(frame: .zero) + + layer.cornerRadius = 55/2 + layer.masksToBounds = true + titleLabel!.font = Fonts.Mulish.semiBold.font(size: 16.0) + adjustsImageWhenHighlighted = false + + setBackgroundImage(.color(Asset.neutralDisabled.color), for: .disabled) + + snp.makeConstraints { + $0.height.equalTo(height) + $0.width.greaterThanOrEqualTo(minimumWidth) } - - required init?(coder: NSCoder) { nil } - - // MARK: Public - - public func set( - style: CapsuleButtonStyle, - title: String, - accessibility: String? = nil - ) { - setTitle(title, for: .normal) - accessibilityIdentifier = accessibility - layer.borderWidth = style.borderWidth - - if let color = style.borderColor { - layer.borderColor = color.cgColor - } - - setBackgroundImage(style.fill, for: .normal) - setTitleColor(style.titleColor, for: .normal) - setTitleColor(style.disabledTitleColor, for: .disabled) - } - - public func setStyle(_ style: CapsuleButtonStyle) { - layer.borderWidth = style.borderWidth - - if let color = style.borderColor { - layer.borderColor = color.cgColor - } - - setBackgroundImage(style.fill, for: .normal) - setTitleColor(style.titleColor, for: .normal) - setTitleColor(style.disabledTitleColor, for: .disabled) + } + + required init?(coder: NSCoder) { nil } + + public func set( + style: CapsuleButtonStyle, + title: String, + accessibility: String? = nil + ) { + setTitle(title, for: .normal) + accessibilityIdentifier = accessibility + layer.borderWidth = style.borderWidth + + if let color = style.borderColor { + layer.borderColor = color.cgColor } - - // MARK: Private - - private func setup() { - layer.cornerRadius = 55/2 - layer.masksToBounds = true - titleLabel!.font = Fonts.Mulish.semiBold.font(size: 16.0) - adjustsImageWhenHighlighted = false - - setBackgroundImage(.color(Asset.neutralDisabled.color), for: .disabled) - - snp.makeConstraints { make in - make.height.equalTo(height) - make.width.greaterThanOrEqualTo(minimumWidth) - } + + setBackgroundImage(style.fill, for: .normal) + setTitleColor(style.titleColor, for: .normal) + setTitleColor(style.disabledTitleColor, for: .disabled) + } + + public func setStyle(_ style: CapsuleButtonStyle) { + layer.borderWidth = style.borderWidth + + if let color = style.borderColor { + layer.borderColor = color.cgColor } + + setBackgroundImage(style.fill, for: .normal) + setTitleColor(style.titleColor, for: .normal) + setTitleColor(style.disabledTitleColor, for: .disabled) + } } diff --git a/Sources/Shared/Views/DetailRowButton.swift b/Sources/Shared/Views/DetailRowButton.swift index 132d499ac0e0223be1f9333cbd6e1643cfff7157..fe0eea6ce263a6cffcc11e0872d3f2ed538c04ee 100644 --- a/Sources/Shared/Views/DetailRowButton.swift +++ b/Sources/Shared/Views/DetailRowButton.swift @@ -1,48 +1,47 @@ import UIKit +import AppResources public final class DetailRowButton: UIControl { - let titleLabel = UILabel() - let valueLabel = UILabel() - let rowIndicator = UIImageView() - - public init() { - super.init(frame: .zero) - - rowIndicator.contentMode = .center - rowIndicator.image = Asset.settingsDisclosure.image - - titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) - valueLabel.font = Fonts.Mulish.regular.font(size: 16.0) - - titleLabel.textColor = Asset.neutralWeak.color - valueLabel.textColor = Asset.neutralActive.color - - addSubview(titleLabel) - addSubview(valueLabel) - addSubview(rowIndicator) - - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - } - - valueLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(4) - make.left.equalToSuperview() - make.bottom.equalToSuperview() - } - - rowIndicator.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.right.equalToSuperview() - } + let titleLabel = UILabel() + let valueLabel = UILabel() + let rowIndicator = UIImageView() + + public init() { + super.init(frame: .zero) + + rowIndicator.contentMode = .center + rowIndicator.image = Asset.settingsDisclosure.image + + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + valueLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + titleLabel.textColor = Asset.neutralWeak.color + valueLabel.textColor = Asset.neutralActive.color + + addSubview(titleLabel) + addSubview(valueLabel) + addSubview(rowIndicator) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() } - - required init?(coder: NSCoder) { nil } - - public func setup(title: String, value: String, hasArrow: Bool = true) { - titleLabel.text = title - valueLabel.text = value - rowIndicator.isHidden = !hasArrow + valueLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(4) + $0.left.equalToSuperview() + $0.bottom.equalToSuperview() + } + rowIndicator.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.right.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + public func setup(title: String, value: String, hasArrow: Bool = true) { + titleLabel.text = title + valueLabel.text = value + rowIndicator.isHidden = !hasArrow + } } diff --git a/Sources/Shared/Views/DotAnimation.swift b/Sources/Shared/Views/DotAnimation.swift index bcf182b9180b7d4cfc5ef4e511083ca97727a7e6..9cda32e30765b2220e40aa1aa41d7c68a7e9a044 100644 --- a/Sources/Shared/Views/DotAnimation.swift +++ b/Sources/Shared/Views/DotAnimation.swift @@ -1,4 +1,5 @@ import UIKit +import AppResources public final class DotAnimation: UIView { let leftDot = UIView() @@ -48,7 +49,7 @@ public final class DotAnimation: UIView { required init?(coder: NSCoder) { nil } - func setColor(_ color: UIColor = Asset.brandPrimary.color) { + public func setColor(_ color: UIColor = Asset.brandPrimary.color) { leftDot.backgroundColor = color middleDot.backgroundColor = color rightDot.backgroundColor = color diff --git a/Sources/Shared/Views/FlexibleSpace.swift b/Sources/Shared/Views/FlexibleSpace.swift index c3b323ea8d352421b5e7247402047d982cf7d4c5..082a2cf95c9e5ef3f6f8766cfd5b8471f13a6309 100644 --- a/Sources/Shared/Views/FlexibleSpace.swift +++ b/Sources/Shared/Views/FlexibleSpace.swift @@ -1,17 +1,17 @@ import UIKit public final class FlexibleSpace: UIView { - public override init(frame: CGRect) { - super.init(frame: frame) - setContentHuggingPriority(.defaultLow, for: .horizontal) - setContentHuggingPriority(.defaultLow, for: .vertical) - setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - setContentCompressionResistancePriority(.defaultLow, for: .vertical) - } - - public convenience init() { - self.init(frame: .zero) - } - - required init?(coder: NSCoder) { nil } + public override init(frame: CGRect) { + super.init(frame: frame) + setContentHuggingPriority(.defaultLow, for: .horizontal) + setContentHuggingPriority(.defaultLow, for: .vertical) + setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + setContentCompressionResistancePriority(.defaultLow, for: .vertical) + } + + public convenience init() { + self.init(frame: .zero) + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/Shared/Views/RowButton.swift b/Sources/Shared/Views/RowButton.swift index 689a705af98ce2364251f7d1d654de6eea3bd799..574b4b0908b56ba13d6c4c55f7f20c26195d97e6 100644 --- a/Sources/Shared/Views/RowButton.swift +++ b/Sources/Shared/Views/RowButton.swift @@ -1,78 +1,79 @@ import UIKit +import AppResources public struct RowButtonStyle { - var color: UIColor - var accessory: UIImage? + var color: UIColor + var accessory: UIImage? } public extension RowButtonStyle { - static let clean = RowButtonStyle( - color: Asset.neutralActive.color, - accessory: Asset.settingsDisclosure.image - ) - - static let delete = RowButtonStyle( - color: Asset.accentDanger.color, - accessory: nil - ) + static let clean = RowButtonStyle( + color: Asset.neutralActive.color, + accessory: Asset.settingsDisclosure.image + ) + + static let delete = RowButtonStyle( + color: Asset.accentDanger.color, + accessory: nil + ) } public final class RowButton: UIControl { - public let title = UILabel() - public let icon = UIImageView() - public let separator = UIView() - public let stack = UIStackView() - public let accessory = UIImageView() - - public init() { - super.init(frame: .zero) - - icon.contentMode = .center - title.font = Fonts.Mulish.semiBold.font(size: 14.0) - separator.backgroundColor = Asset.neutralLine.color - icon.setContentHuggingPriority(.required, for: .horizontal) - - stack.spacing = 10 - stack.addArrangedSubview(icon) - stack.addArrangedSubview(title.pinning(at: .left(0))) - stack.addArrangedSubview(accessory.pinning(at: .top(10))) - - addSubview(stack) - addSubview(separator) - - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(-20) - } - - separator.snp.makeConstraints { make in - make.height.equalTo(1) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } - - subviews.forEach { $0.isUserInteractionEnabled = false } + public let title = UILabel() + public let icon = UIImageView() + public let separator = UIView() + public let stack = UIStackView() + public let accessory = UIImageView() + + public init() { + super.init(frame: .zero) + + icon.contentMode = .center + title.font = Fonts.Mulish.semiBold.font(size: 14.0) + separator.backgroundColor = Asset.neutralLine.color + icon.setContentHuggingPriority(.required, for: .horizontal) + + stack.spacing = 10 + stack.addArrangedSubview(icon) + stack.addArrangedSubview(title.pinning(at: .left(0))) + stack.addArrangedSubview(accessory.pinning(at: .top(10))) + + addSubview(stack) + addSubview(separator) + + stack.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(-20) } - - required init?(coder: NSCoder) { nil } - - public func setup( - title: String, - icon: UIImage, - style: RowButtonStyle = .clean, - separator: Bool = true - ) { - self.icon.image = icon - self.title.text = title - self.title.textColor = style.color - self.accessory.image = style.accessory - - guard separator == true else { - self.separator.removeFromSuperview() - return - } + + separator.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + + subviews.forEach { $0.isUserInteractionEnabled = false } + } + + required init?(coder: NSCoder) { nil } + + public func setup( + title: String, + icon: UIImage, + style: RowButtonStyle = .clean, + separator: Bool = true + ) { + self.icon.image = icon + self.title.text = title + self.title.textColor = style.color + self.accessory.image = style.accessory + + guard separator == true else { + self.separator.removeFromSuperview() + return } + } } diff --git a/Sources/Shared/Views/RowSwitchableButton.swift b/Sources/Shared/Views/RowSwitchableButton.swift index 38136c3ef7f656e8df8907ef6b5a23d461b1adee..175d93878e7b372c907267764180879177768c39 100644 --- a/Sources/Shared/Views/RowSwitchableButton.swift +++ b/Sources/Shared/Views/RowSwitchableButton.swift @@ -1,88 +1,89 @@ import UIKit +import AppResources public enum RowSwitchableButtonState { - case disclosure - case switcher(Bool) + case disclosure + case switcher(Bool) } public final class RowSwitchableButton: UIControl { - public let title = UILabel() - public let icon = UIImageView() - public let separator = UIView() - - public let switcher = UISwitch() - public let disclosureIcon = UIImageView() - - public init() { - super.init(frame: .zero) - - icon.contentMode = .center - title.font = Fonts.Mulish.semiBold.font(size: 14.0) - separator.backgroundColor = Asset.neutralLine.color - title.textColor = Asset.neutralActive.color - disclosureIcon.image = Asset.settingsDisclosure.image - switcher.onTintColor = Asset.brandLight.color - - addSubview(icon) - addSubview(title) - addSubview(disclosureIcon) - addSubview(switcher) - addSubview(separator) - - icon.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview().offset(36) - make.bottom.equalToSuperview().offset(-20) - } - - title.snp.makeConstraints { make in - make.left.equalTo(icon.snp.right).offset(25) - make.centerY.equalTo(icon) - } - - disclosureIcon.snp.makeConstraints { make in - make.centerY.equalTo(icon) - make.right.equalToSuperview().offset(-48) - } - - switcher.snp.makeConstraints { make in - make.right.equalToSuperview().offset(-25) - make.centerY.equalTo(icon) - } - - separator.snp.makeConstraints { make in - make.height.equalTo(1) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview() - } + public let title = UILabel() + public let icon = UIImageView() + public let separator = UIView() + + public let switcher = UISwitch() + public let disclosureIcon = UIImageView() + + public init() { + super.init(frame: .zero) + + icon.contentMode = .center + title.font = Fonts.Mulish.semiBold.font(size: 14.0) + separator.backgroundColor = Asset.neutralLine.color + title.textColor = Asset.neutralActive.color + disclosureIcon.image = Asset.settingsDisclosure.image + switcher.onTintColor = Asset.brandLight.color + + addSubview(icon) + addSubview(title) + addSubview(disclosureIcon) + addSubview(switcher) + addSubview(separator) + + icon.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.left.equalToSuperview().offset(36) + $0.bottom.equalToSuperview().offset(-20) } - - public required init?(coder: NSCoder) { nil } - - public func setup( - title: String, - icon: UIImage, - state: RowSwitchableButtonState = .disclosure, - separator: Bool = true - ) { - self.icon.image = icon - self.title.text = title - - switch state { - case .disclosure: - switcher.isHidden = true - disclosureIcon.isHidden = false - - case .switcher(let bool): - switcher.isOn = bool - switcher.isHidden = false - disclosureIcon.isHidden = true - } - - guard separator == true else { - self.separator.removeFromSuperview() - return - } + + title.snp.makeConstraints { + $0.left.equalTo(icon.snp.right).offset(25) + $0.centerY.equalTo(icon) + } + + disclosureIcon.snp.makeConstraints { + $0.centerY.equalTo(icon) + $0.right.equalToSuperview().offset(-48) + } + + switcher.snp.makeConstraints { + $0.right.equalToSuperview().offset(-25) + $0.centerY.equalTo(icon) + } + + separator.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalToSuperview() + } + } + + public required init?(coder: NSCoder) { nil } + + public func setup( + title: String, + icon: UIImage, + state: RowSwitchableButtonState = .disclosure, + separator: Bool = true + ) { + self.icon.image = icon + self.title.text = title + + switch state { + case .disclosure: + switcher.isHidden = true + disclosureIcon.isHidden = false + + case .switcher(let bool): + switcher.isOn = bool + switcher.isHidden = false + disclosureIcon.isHidden = true + } + + guard separator == true else { + self.separator.removeFromSuperview() + return } + } } diff --git a/Sources/Shared/Views/SearchComponent.swift b/Sources/Shared/Views/SearchComponent.swift index 2edd280f23bb8ee82fb79b0003eb9eebcbee7359..5fb6fe2ad99153b117a9115fc5ad38fc9858eeb1 100644 --- a/Sources/Shared/Views/SearchComponent.swift +++ b/Sources/Shared/Views/SearchComponent.swift @@ -1,185 +1,186 @@ import UIKit import Combine +import AppResources public final class SearchComponent: UIView { - let rightButton = UIButton() - let leftImageView = UIImageView() - let containerView = UIView() - let inputField = UITextField() - - public var rightPublisher: AnyPublisher<Void, Never> { - rightSubject.eraseToAnyPublisher() - } - - public var textPublisher: AnyPublisher<String, Never> { - textSubject.eraseToAnyPublisher() - } - - public var returnPublisher: AnyPublisher<Void, Never> { - returnSubject.eraseToAnyPublisher() - } - - private var rightImage = Asset.sharedScan.image { - didSet { - rightButton.setImage(rightImage, for: .normal) - } - } - - public var isEditingPublisher: AnyPublisher<Bool, Never> { - isEditingSubject.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let rightSubject = PassthroughSubject<Void, Never>() - private let textSubject = PassthroughSubject<String, Never>() - private let returnSubject = PassthroughSubject<Void, Never>() - private let isEditingSubject = CurrentValueSubject<Bool, Never>(false) - - public init() { - super.init(frame: .zero) - - containerView.layer.cornerRadius = 25 - containerView.backgroundColor = Asset.neutralSecondary.color - - leftImageView.image = Asset.lens.image - leftImageView.contentMode = .center - leftImageView.tintColor = Asset.neutralDisabled.color - - rightButton.tintColor = Asset.neutralBody.color - rightButton.setImage(rightImage, for: .normal) - rightButton.setContentHuggingPriority(.required, for: .horizontal) - rightButton.setContentCompressionResistancePriority(.required, for: .horizontal) - - inputField.delegate = self - inputField.autocapitalizationType = .none - inputField.textColor = Asset.neutralActive.color - inputField.font = Fonts.Mulish.regular.font(size: 16.0) - - let attrPlaceholder - = NSAttributedString( - string: Localized.Shared.Search.placeholder, - attributes: [ - .font: Fonts.Mulish.regular.font(size: 14.0) as Any, - .foregroundColor: Asset.neutralWeak.color - ]) - - inputField.attributedPlaceholder = attrPlaceholder - - inputField.textPublisher - .sink { [weak textSubject] in textSubject?.send($0) } - .store(in: &cancellables) - - rightButton.publisher(for: .touchUpInside) - .sink { [weak rightSubject, self] in - if isEditingSubject.value == true { - abortEditing() - } else { - rightSubject?.send() - } - }.store(in: &cancellables) - - addSubview(containerView) - containerView.addSubview(inputField) - containerView.addSubview(leftImageView) - containerView.addSubview(rightButton) - - setupConstraints() + let rightButton = UIButton() + let leftImageView = UIImageView() + let containerView = UIView() + let inputField = UITextField() + + public var rightPublisher: AnyPublisher<Void, Never> { + rightSubject.eraseToAnyPublisher() + } + + public var textPublisher: AnyPublisher<String, Never> { + textSubject.eraseToAnyPublisher() + } + + public var returnPublisher: AnyPublisher<Void, Never> { + returnSubject.eraseToAnyPublisher() + } + + private var rightImage = Asset.sharedScan.image { + didSet { + rightButton.setImage(rightImage, for: .normal) } - - required init?(coder: NSCoder) { nil } - - public func set( - placeholder: String? = nil, - imageAtRight: UIImage? = nil, - inputAccessibility: String? = nil, - rightAccessibility: String? = nil - ) { - inputField.accessibilityIdentifier = inputAccessibility - rightButton.accessibilityIdentifier = rightAccessibility - - if let placeholder = placeholder { - let attrPlaceholder - = NSAttributedString( - string: placeholder, - attributes: [ - .font: Fonts.Mulish.regular.font(size: 14.0) as Any, - .foregroundColor: Asset.neutralWeak.color - ]) - - inputField.attributedPlaceholder = attrPlaceholder - } - - if let image = imageAtRight { - self.rightImage = image + } + + public var isEditingPublisher: AnyPublisher<Bool, Never> { + isEditingSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let rightSubject = PassthroughSubject<Void, Never>() + private let textSubject = PassthroughSubject<String, Never>() + private let returnSubject = PassthroughSubject<Void, Never>() + private let isEditingSubject = CurrentValueSubject<Bool, Never>(false) + + public init() { + super.init(frame: .zero) + + containerView.layer.cornerRadius = 25 + containerView.backgroundColor = Asset.neutralSecondary.color + + leftImageView.image = Asset.lens.image + leftImageView.contentMode = .center + leftImageView.tintColor = Asset.neutralDisabled.color + + rightButton.tintColor = Asset.neutralBody.color + rightButton.setImage(rightImage, for: .normal) + rightButton.setContentHuggingPriority(.required, for: .horizontal) + rightButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + inputField.delegate = self + inputField.autocapitalizationType = .none + inputField.textColor = Asset.neutralActive.color + inputField.font = Fonts.Mulish.regular.font(size: 16.0) + + let attrPlaceholder + = NSAttributedString( + string: Localized.Shared.Search.placeholder, + attributes: [ + .font: Fonts.Mulish.regular.font(size: 14.0) as Any, + .foregroundColor: Asset.neutralWeak.color + ]) + + inputField.attributedPlaceholder = attrPlaceholder + + inputField.textPublisher + .sink { [weak textSubject] in textSubject?.send($0) } + .store(in: &cancellables) + + rightButton.publisher(for: .touchUpInside) + .sink { [weak rightSubject, self] in + if isEditingSubject.value == true { + abortEditing() } else { - rightButton.isHidden = true + rightSubject?.send() } - } - - public func update(content: String) { - inputField.text = content - } - - public func update(placeholder: String) { - inputField.attributedPlaceholder = NSAttributedString( - string: placeholder, - attributes: [ - .font: Fonts.Mulish.regular.font(size: 14.0) as Any, - .foregroundColor: Asset.neutralWeak.color + }.store(in: &cancellables) + + addSubview(containerView) + containerView.addSubview(inputField) + containerView.addSubview(leftImageView) + containerView.addSubview(rightButton) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + public func set( + placeholder: String? = nil, + imageAtRight: UIImage? = nil, + inputAccessibility: String? = nil, + rightAccessibility: String? = nil + ) { + inputField.accessibilityIdentifier = inputAccessibility + rightButton.accessibilityIdentifier = rightAccessibility + + if let placeholder = placeholder { + let attrPlaceholder + = NSAttributedString( + string: placeholder, + attributes: [ + .font: Fonts.Mulish.regular.font(size: 14.0) as Any, + .foregroundColor: Asset.neutralWeak.color ]) + + inputField.attributedPlaceholder = attrPlaceholder } - - public func abortEditing() { - inputField.text = nil - textSubject.send("") - inputField.endEditing(true) - isEditingSubject.send(false) + + if let image = imageAtRight { + self.rightImage = image + } else { + rightButton.isHidden = true } - - private func setupConstraints() { - containerView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - $0.height.equalTo(50) - } - - leftImageView.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.left.equalToSuperview().offset(13) - $0.bottom.equalToSuperview().offset(-10) - $0.height.equalTo(leftImageView.snp.width) - } - - inputField.snp.makeConstraints { - $0.top.bottom.equalToSuperview() - $0.left.equalTo(leftImageView.snp.right).offset(20) - $0.right.equalTo(rightButton.snp.left).offset(-32) - } - - rightButton.snp.makeConstraints { - $0.top.equalToSuperview() - $0.right.equalToSuperview().offset(-13) - $0.bottom.equalToSuperview() - } + } + + public func update(content: String) { + inputField.text = content + } + + public func update(placeholder: String) { + inputField.attributedPlaceholder = NSAttributedString( + string: placeholder, + attributes: [ + .font: Fonts.Mulish.regular.font(size: 14.0) as Any, + .foregroundColor: Asset.neutralWeak.color + ]) + } + + public func abortEditing() { + inputField.text = nil + textSubject.send("") + inputField.endEditing(true) + isEditingSubject.send(false) + } + + private func setupConstraints() { + containerView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + $0.height.equalTo(50) } - - public func textFieldDidBeginEditing(_ textField: UITextField) { - rightButton.setImage(Asset.sharedCross.image, for: .normal) - isEditingSubject.send(true) + + leftImageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.left.equalToSuperview().offset(13) + $0.bottom.equalToSuperview().offset(-10) + $0.height.equalTo(leftImageView.snp.width) } - - public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - inputField.resignFirstResponder() - returnSubject.send(()) - return true + + inputField.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.left.equalTo(leftImageView.snp.right).offset(20) + $0.right.equalTo(rightButton.snp.left).offset(-32) } - - public func textFieldDidEndEditing(_ textField: UITextField) { - rightButton.setImage(rightImage, for: .normal) - isEditingSubject.send(false) + + rightButton.snp.makeConstraints { + $0.top.equalToSuperview() + $0.right.equalToSuperview().offset(-13) + $0.bottom.equalToSuperview() } + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + rightButton.setImage(Asset.sharedCross.image, for: .normal) + isEditingSubject.send(true) + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + inputField.resignFirstResponder() + returnSubject.send(()) + return true + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + rightButton.setImage(rightImage, for: .normal) + isEditingSubject.send(false) + } } extension SearchComponent: UITextFieldDelegate {} diff --git a/Sources/Shared/Views/SearchCountryComponent.swift b/Sources/Shared/Views/SearchCountryComponent.swift index 186c58b95981ce45a799c93e27c8a53530b16cf4..5975925f63c1f8e25661b1bab138895d4eb139af 100644 --- a/Sources/Shared/Views/SearchCountryComponent.swift +++ b/Sources/Shared/Views/SearchCountryComponent.swift @@ -1,57 +1,58 @@ import UIKit +import AppResources public final class SearchCountryComponent: UIControl { - let flagLabel = UILabel() - let prefixLabel = UILabel() - let containerView = UIView() - - public init() { - super.init(frame: .zero) - - containerView.layer.cornerRadius = 25 - containerView.backgroundColor = Asset.neutralSecondary.color - - flagLabel.text = "🇺🇸" - prefixLabel.text = "+1" - prefixLabel.textColor = Asset.neutralDisabled.color - prefixLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - addSubview(containerView) - containerView.addSubview(flagLabel) - containerView.addSubview(prefixLabel) - - containerView.isUserInteractionEnabled = false - - setupConstraints() - flagLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - prefixLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + let flagLabel = UILabel() + let prefixLabel = UILabel() + let containerView = UIView() + + public init() { + super.init(frame: .zero) + + containerView.layer.cornerRadius = 25 + containerView.backgroundColor = Asset.neutralSecondary.color + + flagLabel.text = "🇺🇸" + prefixLabel.text = "+1" + prefixLabel.textColor = Asset.neutralDisabled.color + prefixLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + addSubview(containerView) + containerView.addSubview(flagLabel) + containerView.addSubview(prefixLabel) + + containerView.isUserInteractionEnabled = false + + setupConstraints() + flagLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + prefixLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + } + + required init?(coder: NSCoder) { nil } + + public func setFlag(_ flag: String, prefix: String) { + flagLabel.text = flag + prefixLabel.text = prefix + } + + private func setupConstraints() { + containerView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + $0.height.equalTo(50) } - - required init?(coder: NSCoder) { nil } - - public func setFlag(_ flag: String, prefix: String) { - flagLabel.text = flag - prefixLabel.text = prefix + + flagLabel.snp.makeConstraints { + $0.left.equalToSuperview().offset(13) + $0.centerY.equalToSuperview() } - - private func setupConstraints() { - containerView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - $0.height.equalTo(50) - } - - flagLabel.snp.makeConstraints { - $0.left.equalToSuperview().offset(13) - $0.centerY.equalToSuperview() - } - - prefixLabel.snp.makeConstraints { - $0.left.equalTo(flagLabel.snp.right).offset(10) - $0.right.equalToSuperview().offset(-13) - $0.centerY.equalToSuperview() - } + + prefixLabel.snp.makeConstraints { + $0.left.equalTo(flagLabel.snp.right).offset(10) + $0.right.equalToSuperview().offset(-13) + $0.centerY.equalToSuperview() } + } } diff --git a/Sources/Shared/Views/SheetCardComponent.swift b/Sources/Shared/Views/SheetCardComponent.swift index c5aa0b219e2fef53fc062cc777e7f58f3e45ccfc..6f60bb1688304323b59d439915c7ac87739b71a9 100644 --- a/Sources/Shared/Views/SheetCardComponent.swift +++ b/Sources/Shared/Views/SheetCardComponent.swift @@ -1,40 +1,30 @@ import UIKit +import AppResources public final class SheetCardComponent: UIView { - // MARK: UI - - public let stack = UIStackView() - - // MARK: Lifecycle - - public init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - // MARK: Public - - public func set(buttons: [CapsuleButton]) { - buttons.forEach { stack.addArrangedSubview($0) } - } - - // MARK: Private - - private func setup() { - layer.cornerRadius = 24 - backgroundColor = Asset.neutralSecondary.color - - stack.spacing = 20 - stack.axis = .vertical - addSubview(stack) - - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-24) - } + public let stackView = UIStackView() + + public init() { + super.init(frame: .zero) + + layer.cornerRadius = 24 + backgroundColor = Asset.neutralSecondary.color + + stackView.spacing = 20 + stackView.axis = .vertical + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalToSuperview().offset(-24) } + } + + required init?(coder: NSCoder) { nil } + + public func set(buttons: [CapsuleButton]) { + buttons.forEach { stackView.addArrangedSubview($0) } + } } diff --git a/Sources/Shared/Views/SnackBar.swift b/Sources/Shared/Views/SnackBar.swift index 9240ccf9537d12cf8377bcfb4e0d222442325f4c..4ca6961d64080ebc48b300cd73e4cc104d8e1f72 100644 --- a/Sources/Shared/Views/SnackBar.swift +++ b/Sources/Shared/Views/SnackBar.swift @@ -1,35 +1,36 @@ import UIKit +import AppResources public final class SnackBar: UIView { - private let titleLabel = UILabel() - private let imageView = UIImageView() - private let stackView = UIStackView() - - public init() { - super.init(frame: .zero) - - //alpha = 0.0 - backgroundColor = Asset.brandPrimary.color - - imageView.contentMode = .center - titleLabel.text = Localized.Shared.SnackBar.title - titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - titleLabel.textColor = Asset.neutralWhite.color - imageView.image = Asset.sharedWhiteExclamation.image - - stackView.spacing = 14 - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(titleLabel) - - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.top.equalToSuperview().offset(16) - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - $0.bottom.equalToSuperview().offset(-16) - } + private let titleLabel = UILabel() + private let imageView = UIImageView() + private let stackView = UIStackView() + + public init() { + super.init(frame: .zero) + + //alpha = 0.0 + backgroundColor = Asset.brandPrimary.color + + imageView.contentMode = .center + titleLabel.text = Localized.Shared.SnackBar.title + titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + titleLabel.textColor = Asset.neutralWhite.color + imageView.image = Asset.sharedWhiteExclamation.image + + stackView.spacing = 14 + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(titleLabel) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + $0.bottom.equalToSuperview().offset(-16) } - - required init?(coder: NSCoder) { nil } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/Shared/Views/TextWithInfoView.swift b/Sources/Shared/Views/TextWithInfoView.swift index 62133c1d6136e300762dcb718a16ca57a02009a0..0fbe08bed56850680bc3c1dcc72c7cd50ffbe0b1 100644 --- a/Sources/Shared/Views/TextWithInfoView.swift +++ b/Sources/Shared/Views/TextWithInfoView.swift @@ -1,63 +1,64 @@ import UIKit +import AppResources public final class TextWithInfoView: UIView { - private let textView = UITextView() - public private(set) var didTapInfo: (() -> Void)? - - public init() { - super.init(frame: .zero) - textView.backgroundColor = .clear - - textView.isEditable = false - textView.isScrollEnabled = false - textView.isSelectable = false - - addSubview(textView) - textView.snp.makeConstraints { $0.edges.equalToSuperview() } - } - - required init?(coder: NSCoder) { nil } - - public func setup( - text: String, - attributes: [NSAttributedString.Key: Any], - didTapInfo: @escaping () -> Void - ) { - let mutable = NSMutableAttributedString(string: "\(text) ", attributes: attributes) - - let imageAttachment = NSTextAttachment() - imageAttachment.image = Asset.infoIcon.image - - let imageString = NSAttributedString(attachment: imageAttachment) - mutable.append(imageString) - textView.attributedText = mutable - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tappedTextView(_:))) - textView.addGestureRecognizer(tapGesture) - - self.didTapInfo = didTapInfo - } - - @objc private func tappedTextView(_ sender: UITapGestureRecognizer) { - let textView = sender.view as! UITextView - let layoutManager = textView.layoutManager - - var location = sender.location(in: textView) - location.x -= textView.textContainerInset.left; - location.y -= textView.textContainerInset.top; - - let characterIndex = layoutManager.characterIndex( - for: location, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil - ) - - if characterIndex < textView.textStorage.length { - let attributeValue = textView.attributedText.attribute( - NSAttributedString.Key.attachment, at: characterIndex, effectiveRange: nil - ) as? NSTextAttachment - - if let _ = attributeValue { - didTapInfo?() - } - } + private let textView = UITextView() + public private(set) var didTapInfo: (() -> Void)? + + public init() { + super.init(frame: .zero) + textView.backgroundColor = .clear + + textView.isEditable = false + textView.isScrollEnabled = false + textView.isSelectable = false + + addSubview(textView) + textView.snp.makeConstraints { $0.edges.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + public func setup( + text: String, + attributes: [NSAttributedString.Key: Any], + didTapInfo: @escaping () -> Void + ) { + let mutable = NSMutableAttributedString(string: "\(text) ", attributes: attributes) + + let imageAttachment = NSTextAttachment() + imageAttachment.image = Asset.infoIcon.image + + let imageString = NSAttributedString(attachment: imageAttachment) + mutable.append(imageString) + textView.attributedText = mutable + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tappedTextView(_:))) + textView.addGestureRecognizer(tapGesture) + + self.didTapInfo = didTapInfo + } + + @objc private func tappedTextView(_ sender: UITapGestureRecognizer) { + let textView = sender.view as! UITextView + let layoutManager = textView.layoutManager + + var location = sender.location(in: textView) + location.x -= textView.textContainerInset.left; + location.y -= textView.textContainerInset.top; + + let characterIndex = layoutManager.characterIndex( + for: location, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil + ) + + if characterIndex < textView.textStorage.length { + let attributeValue = textView.attributedText.attribute( + NSAttributedString.Key.attachment, at: characterIndex, effectiveRange: nil + ) as? NSTextAttachment + + if let _ = attributeValue { + didTapInfo?() + } } + } } diff --git a/Sources/Shared/Views/UnselectableTextView.swift b/Sources/Shared/Views/UnselectableTextView.swift index c664c022bb340d50ccf007940a9d97ca3ba740cc..5da1855737f7a36acde4c09ad8c51018b962a862 100644 --- a/Sources/Shared/Views/UnselectableTextView.swift +++ b/Sources/Shared/Views/UnselectableTextView.swift @@ -1,19 +1,19 @@ import UIKit public final class UnselectableTextView: UITextView { - public override var selectedTextRange: UITextRange? { - get { return nil } - set {} - } - - public override func point( - inside point: CGPoint, - with event: UIEvent? - ) -> Bool { - guard let pos = closestPosition(to: point) else { return false } - guard let range = tokenizer.rangeEnclosingPosition(pos, with: .character, inDirection: .layout(.left)) else { return false } - - let startIndex = offset(from: beginningOfDocument, to: range.start) - return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil - } + public override var selectedTextRange: UITextRange? { + get { return nil } + set {} + } + + public override func point( + inside point: CGPoint, + with event: UIEvent? + ) -> Bool { + guard let pos = closestPosition(to: point) else { return false } + guard let range = tokenizer.rangeEnclosingPosition(pos, with: .character, inDirection: .layout(.left)) else { return false } + + let startIndex = offset(from: beginningOfDocument, to: range.start) + return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil + } } diff --git a/Sources/StatusBarFeature/StatusBarDependency.swift b/Sources/StatusBarFeature/StatusBarDependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..2d04e3932df4074598f04beeb297baed54cbaf50 --- /dev/null +++ b/Sources/StatusBarFeature/StatusBarDependency.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum StatusBarDependencyKey: DependencyKey { + static let liveValue: StatusBarStyleManager = .live() + static let testValue: StatusBarStyleManager = .unimplemented +} + +extension DependencyValues { + public var statusBar: StatusBarStyleManager { + get { self[StatusBarDependencyKey.self] } + set { self[StatusBarDependencyKey.self] = newValue } + } +} diff --git a/Sources/StatusBarFeature/StatusBarStyleFetch.swift b/Sources/StatusBarFeature/StatusBarStyleFetch.swift new file mode 100644 index 0000000000000000000000000000000000000000..e57027ce0f9e9df7d87bf7e850ed06c2698d8a1a --- /dev/null +++ b/Sources/StatusBarFeature/StatusBarStyleFetch.swift @@ -0,0 +1,16 @@ +import UIKit +import XCTestDynamicOverlay + +public struct StatusBarStyleFetch { + public var run: () -> UIStatusBarStyle + + public func callAsFunction() -> UIStatusBarStyle { + run() + } +} + +extension StatusBarStyleFetch { + public static let unimplemented = StatusBarStyleFetch( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/StatusBarFeature/StatusBarStyleManager.swift b/Sources/StatusBarFeature/StatusBarStyleManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..3dbfe089beefd930fb13eafd35bea7fcd2734a3f --- /dev/null +++ b/Sources/StatusBarFeature/StatusBarStyleManager.swift @@ -0,0 +1,39 @@ +import UIKit +import Combine +import XCTestDynamicOverlay + +public struct StatusBarStyleManager { + public var update: StatusBarStyleUpdate + public var current: StatusBarStyleFetch + public var observe: StatusBarStyleObserve +} + +extension StatusBarStyleManager { + public static func live() -> StatusBarStyleManager { + class Context { + let styleSubject = CurrentValueSubject<UIStatusBarStyle, Never>(.lightContent) + } + + let context = Context() + + return .init( + update: .init { + context.styleSubject.send($0) + }, + current: .init { + context.styleSubject.value + }, + observe: .init { + context.styleSubject.eraseToAnyPublisher() + } + ) + } +} + +extension StatusBarStyleManager { + public static let unimplemented = StatusBarStyleManager( + update: .unimplemented, + current: .unimplemented, + observe: .unimplemented + ) +} diff --git a/Sources/StatusBarFeature/StatusBarStyleObserve.swift b/Sources/StatusBarFeature/StatusBarStyleObserve.swift new file mode 100644 index 0000000000000000000000000000000000000000..77c70223e0893b91a9df3b13365f76910adce573 --- /dev/null +++ b/Sources/StatusBarFeature/StatusBarStyleObserve.swift @@ -0,0 +1,17 @@ +import UIKit +import Combine +import XCTestDynamicOverlay + +public struct StatusBarStyleObserve { + public var run: () -> AnyPublisher<UIStatusBarStyle, Never> + + public func callAsFunction() -> AnyPublisher<UIStatusBarStyle, Never> { + run() + } +} + +extension StatusBarStyleObserve { + public static let unimplemented = StatusBarStyleObserve( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/StatusBarFeature/StatusBarStyleUpdate.swift b/Sources/StatusBarFeature/StatusBarStyleUpdate.swift new file mode 100644 index 0000000000000000000000000000000000000000..95831faa90eb248a64de1e5e9b31b2d3d2820b8e --- /dev/null +++ b/Sources/StatusBarFeature/StatusBarStyleUpdate.swift @@ -0,0 +1,16 @@ +import UIKit +import XCTestDynamicOverlay + +public struct StatusBarStyleUpdate { + public var run: (UIStatusBarStyle) -> Void + + public func callAsFunction(_ style: UIStatusBarStyle) -> Void { + run(style) + } +} + +extension StatusBarStyleUpdate { + public static let unimplemented = StatusBarStyleUpdate( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/TermsFeature/RadioButton.swift b/Sources/TermsFeature/RadioButton.swift index 201aa3b9c19b2325a06168c4e9abfba4cecf0cd5..77b2a084b2a6726527416844cb2dd55e0486240b 100644 --- a/Sources/TermsFeature/RadioButton.swift +++ b/Sources/TermsFeature/RadioButton.swift @@ -1,53 +1,54 @@ import UIKit import Shared +import AppResources final class RadioButton: UIControl { - private let filledView = UIView() - private let containerView = UIView() + private let filledView = UIView() + private let containerView = UIView() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - containerView.layer.borderWidth = 1 - containerView.layer.cornerRadius = 15 - containerView.layer.masksToBounds = true - containerView.layer.borderColor = Asset.neutralWhite.color.cgColor + containerView.layer.borderWidth = 1 + containerView.layer.cornerRadius = 15 + containerView.layer.masksToBounds = true + containerView.layer.borderColor = Asset.neutralWhite.color.cgColor - filledView.isHidden = true - filledView.layer.cornerRadius = 10 - filledView.layer.masksToBounds = true - filledView.backgroundColor = Asset.neutralWhite.color + filledView.isHidden = true + filledView.layer.cornerRadius = 10 + filledView.layer.masksToBounds = true + filledView.backgroundColor = Asset.neutralWhite.color - containerView.isUserInteractionEnabled = false - filledView.isUserInteractionEnabled = false + containerView.isUserInteractionEnabled = false + filledView.isUserInteractionEnabled = false - addSubview(containerView) - containerView.addSubview(filledView) + addSubview(containerView) + containerView.addSubview(filledView) - setupConstraints() - } + setupConstraints() + } + + required init?(coder: NSCoder) { nil } - required init?(coder: NSCoder) { nil } + func set(enabled: Bool) { + filledView.isHidden = !enabled + } - func set(enabled: Bool) { - filledView.isHidden = !enabled + private func setupConstraints() { + containerView.snp.makeConstraints { + $0.width.equalTo(30) + $0.height.equalTo(30) + $0.top.equalToSuperview().offset(5) + $0.left.equalToSuperview().offset(5) + $0.right.equalToSuperview().offset(-5) + $0.bottom.equalToSuperview().offset(-5) } - private func setupConstraints() { - containerView.snp.makeConstraints { - $0.width.equalTo(30) - $0.height.equalTo(30) - $0.top.equalToSuperview().offset(5) - $0.left.equalToSuperview().offset(5) - $0.right.equalToSuperview().offset(-5) - $0.bottom.equalToSuperview().offset(-5) - } - - filledView.snp.makeConstraints { - $0.top.equalToSuperview().offset(5) - $0.left.equalToSuperview().offset(5) - $0.right.equalToSuperview().offset(-5) - $0.bottom.equalToSuperview().offset(-5) - } + filledView.snp.makeConstraints { + $0.top.equalToSuperview().offset(5) + $0.left.equalToSuperview().offset(5) + $0.right.equalToSuperview().offset(-5) + $0.bottom.equalToSuperview().offset(-5) } + } } diff --git a/Sources/TermsFeature/RadioTextComponent.swift b/Sources/TermsFeature/RadioTextComponent.swift index 8f6509f21562ebc8d3a0941cff1a5db53b997908..0d7906aea227f922387d4642c893e93d0ee9abf1 100644 --- a/Sources/TermsFeature/RadioTextComponent.swift +++ b/Sources/TermsFeature/RadioTextComponent.swift @@ -1,40 +1,37 @@ import UIKit import Shared +import AppResources final class RadioTextComponent: UIView { - let titleLabel = UILabel() - let radioButton = RadioButton() + let titleLabel = UILabel() + let radioButton = RadioButton() - var isEnabled: Bool = false { - didSet { radioButton.set(enabled: isEnabled) } - } + var isEnabled: Bool = false { + didSet { radioButton.set(enabled: isEnabled) } + } - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - titleLabel.numberOfLines = 0 - titleLabel.textColor = Asset.neutralWhite.color - titleLabel.font = Fonts.Mulish.regular.font(size: 13.0) + titleLabel.numberOfLines = 0 + titleLabel.textColor = Asset.neutralWhite.color + titleLabel.font = Fonts.Mulish.regular.font(size: 13.0) - addSubview(titleLabel) - addSubview(radioButton) + addSubview(titleLabel) + addSubview(radioButton) - setupConstraints() + titleLabel.snp.makeConstraints { + $0.left.equalTo(radioButton.snp.right).offset(7) + $0.centerY.equalTo(radioButton) + $0.right.equalToSuperview() } - required init?(coder: NSCoder) { nil } - - private func setupConstraints() { - titleLabel.snp.makeConstraints { - $0.left.equalTo(radioButton.snp.right).offset(7) - $0.centerY.equalTo(radioButton) - $0.right.equalToSuperview() - } - - radioButton.snp.makeConstraints { - $0.left.equalToSuperview() - $0.top.greaterThanOrEqualToSuperview() - $0.bottom.equalToSuperview() - } + radioButton.snp.makeConstraints { + $0.left.equalToSuperview() + $0.top.greaterThanOrEqualToSuperview() + $0.bottom.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/TermsFeature/TermsConditionsController.swift b/Sources/TermsFeature/TermsConditionsController.swift index 9485704fe85d1fe461dc22ccf6c513049368089d..23fd543b54b13ff0599436d5e4301677795cab37 100644 --- a/Sources/TermsFeature/TermsConditionsController.swift +++ b/Sources/TermsFeature/TermsConditionsController.swift @@ -5,6 +5,7 @@ import Shared import Combine import Defaults import Navigation +import AppResources public final class TermsConditionsController: UIViewController { @Dependency var navigator: Navigator @@ -61,9 +62,9 @@ public final class TermsConditionsController: UIViewController { .sink { [unowned self] in didAcceptTerms = true if username != nil { - navigator.perform(PresentChatList()) + navigator.perform(PresentChatList(on: navigationController!)) } else { - navigator.perform(PresentOnboardingUsername()) + navigator.perform(PresentOnboardingUsername(on: navigationController!)) } }.store(in: &cancellables) diff --git a/Sources/TermsFeature/TermsConditionsView.swift b/Sources/TermsFeature/TermsConditionsView.swift index 2f3ff8fa327956951be5299f32b6537d7ad4824d..e3dd19a9427f823ebd4d54700176a4deeebcf1f3 100644 --- a/Sources/TermsFeature/TermsConditionsView.swift +++ b/Sources/TermsFeature/TermsConditionsView.swift @@ -1,56 +1,57 @@ import UIKit import Shared +import AppResources final class TermsConditionsView: UIView { - let nextButton = CapsuleButton() - let logoImageView = UIImageView() - let showTermsButton = CapsuleButton() - let radioComponent = RadioTextComponent() + let nextButton = CapsuleButton() + let logoImageView = UIImageView() + let showTermsButton = CapsuleButton() + let radioComponent = RadioTextComponent() - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color - logoImageView.contentMode = .center - logoImageView.image = Asset.onboardingLogoStart.image - radioComponent.titleLabel.text = Localized.Terms.radio + logoImageView.contentMode = .center + logoImageView.image = Asset.onboardingLogoStart.image + radioComponent.titleLabel.text = Localized.Terms.radio - nextButton.isEnabled = false - nextButton.set(style: .white, title: Localized.Terms.accept) - showTermsButton.set(style: .seeThroughWhite, title: Localized.Terms.show) + nextButton.isEnabled = false + nextButton.set(style: .white, title: Localized.Terms.accept) + showTermsButton.set(style: .seeThroughWhite, title: Localized.Terms.show) - addSubview(logoImageView) - addSubview(nextButton) - addSubview(radioComponent) - addSubview(showTermsButton) + addSubview(logoImageView) + addSubview(nextButton) + addSubview(radioComponent) + addSubview(showTermsButton) - setupConstraints() + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + private func setupConstraints() { + logoImageView.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(30) + $0.centerX.equalToSuperview() + } + + radioComponent.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(nextButton.snp.top).offset(-20) + } + + nextButton.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(showTermsButton.snp.top).offset(-10) } - required init?(coder: NSCoder) { nil } - - private func setupConstraints() { - logoImageView.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).offset(30) - $0.centerX.equalToSuperview() - } - - radioComponent.snp.makeConstraints { - $0.left.equalToSuperview().offset(40) - $0.right.equalToSuperview().offset(-40) - $0.bottom.equalTo(nextButton.snp.top).offset(-20) - } - - nextButton.snp.makeConstraints { - $0.left.equalToSuperview().offset(40) - $0.right.equalToSuperview().offset(-40) - $0.bottom.equalTo(showTermsButton.snp.top).offset(-10) - } - - showTermsButton.snp.makeConstraints { - $0.left.equalToSuperview().offset(40) - $0.right.equalToSuperview().offset(-40) - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-40) - } + showTermsButton.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-40) } + } } diff --git a/Sources/Voxophone/Voxophone.swift b/Sources/Voxophone/Voxophone.swift deleted file mode 100644 index 578a46a85df147350013fc526a5d8cd63c0718dd..0000000000000000000000000000000000000000 --- a/Sources/Voxophone/Voxophone.swift +++ /dev/null @@ -1,229 +0,0 @@ -import AVFoundation -import Combine -import Shared - -public final class Voxophone: NSObject, AVAudioRecorderDelegate, AVAudioPlayerDelegate { - public enum State: Equatable { - case empty(isLoudspeaker: Bool) - case idle(URL, duration: TimeInterval, isLoudspeaker: Bool) - case recording(URL, time: TimeInterval, isLoudspeaker: Bool) - case playing(URL, duration: TimeInterval, time: TimeInterval, isLoudspeaker: Bool) - } - - public override init() { - super.init() - } - - deinit { - destroyPlayer() - destroyRecorder() - stopTimer() - } - - @Published public private(set) var state: State = .empty(isLoudspeaker: false) - - private let session: AVAudioSession = .sharedInstance() - private var recorder: AVAudioRecorder? - private var player: AVAudioPlayer? - private var timer: Timer? - - public func reset() { - destroyPlayer() - destroyRecorder() - state = .empty(isLoudspeaker: false) - } - - public func toggleLoudspeaker() { - state.isLoudspeaker.toggle() - setupSessionCategory() - } - - public func load(_ url: URL) { - destroyPlayer() - destroyRecorder() - let player = setupPlayer(url: url) - state = .idle(url, duration: player.duration, isLoudspeaker: state.isLoudspeaker) - } - - public func play() { - guard let player = player, let url = player.url else { return } - destroyRecorder() - state = .playing(url, duration: player.duration, time: player.currentTime, isLoudspeaker: state.isLoudspeaker) - startPlayback() - } - - public func record() { - let url = URL(fileURLWithPath: FileManager.xxPath + "/recording_\(Date.asTimestamp).m4a") - - destroyPlayer() - destroyRecorder() - let recorder = setupRecorder(url: url) - state = .recording(url, time: recorder.currentTime, isLoudspeaker: state.isLoudspeaker) - startRecording() - } - - public func stop() { - switch state { - case .empty, .idle: - return - - case .recording: - finishRecording() - - case .playing(let url, let duration, _, let isLoudspeaker): - stopPlayback() - state = .idle(url, duration: duration, isLoudspeaker: isLoudspeaker) - } - } - - // MARK: - Player - - private func setupPlayer(url: URL) -> AVAudioPlayer { - let player = try! AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.m4a.rawValue) - self.player = player - return player - } - - private func setupSessionCategory() { - switch state { - case .playing(_, _, _, let isLoud): - if isLoud, session.category != .playback { - try! session.setCategory(.playback, options: .duckOthers) - } - - if !isLoud, session.category != .playAndRecord { - try! session.setCategory(.playAndRecord, options: .duckOthers) - } - case .recording(_, _, _): - if session.category != .playAndRecord { - try! session.setCategory(.playAndRecord, options: .duckOthers) - } - default: - break - } - } - - private func startPlayback() { - guard let player = player else { return } - try! session.setActive(true) - setupSessionCategory() - player.delegate = self - player.prepareToPlay() - player.play() - startTimer() - } - - private func stopPlayback() { - guard let player = player else { return } - player.stop() - } - - private func destroyPlayer() { - player?.delegate = nil - player?.stop() - player = nil - } - - // MARK: - Recorder - - private func setupRecorder(url: URL) -> AVAudioRecorder { - let recorder = try! AVAudioRecorder(url: url, settings: [ - AVFormatIDKey: kAudioFormatMPEG4AAC, - AVSampleRateKey: 12000, - AVNumberOfChannelsKey: 1 - ]) - self.recorder = recorder - return recorder - } - - private func startRecording() { - guard let recorder = recorder else { return } - try! session.setActive(true) - setupSessionCategory() - recorder.delegate = self - recorder.record() - startTimer() - } - - private func finishRecording() { - guard let recorder = recorder else { return } - recorder.stop() - } - - private func destroyRecorder() { - recorder?.delegate = nil - recorder?.stop() - recorder = nil - } - - // MARK: - Timer - - private func startTimer() { - stopTimer() - timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in - self.timerTick() - } - } - - private func timerTick() { - switch state { - case .empty, .idle: - stopTimer() - - case .recording(_, _, let isLoud): - guard let recorder = recorder else { return } - state = .recording(recorder.url, time: recorder.currentTime, isLoudspeaker: isLoud) - - case .playing(_, _, _, let isLoud): - guard let player = player, let url = player.url else { return } - state = .playing(url, duration: player.duration, time: player.currentTime, isLoudspeaker: isLoud) - } - } - - private func stopTimer() { - timer?.invalidate() - timer = nil - } - - // MARK: - AVAudioRecorderDelegate - - public func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { - guard flag else { - state = .empty(isLoudspeaker: state.isLoudspeaker) - return - } - load(recorder.url) - } - - // MARK: - AVAudioPlayerDelegate - - public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - guard flag, let url = player.url else { - state = .empty(isLoudspeaker: state.isLoudspeaker) - return - } - load(url) - } -} - -public extension Voxophone.State { - var isLoudspeaker: Bool { - get { - switch self { - case .playing(_, _, _, let isLoud), .idle(_, _, let isLoud), .empty(let isLoud), .recording(_, _, let isLoud): - return isLoud - } - } set { - switch self { - case .empty(_): - self = .empty(isLoudspeaker: newValue) - case let .idle(url, duration, _): - self = .idle(url, duration: duration, isLoudspeaker: newValue) - case let .playing(url, duration, time, _): - self = .playing(url, duration: duration, time: time, isLoudspeaker: newValue) - case let .recording(url, time, _): - self = .recording(url, time: time, isLoudspeaker: newValue) - } - } - } -} diff --git a/Sources/XXLogger/Logger.swift b/Sources/XXLogger/Logger.swift deleted file mode 100644 index 805887aabfde4b6a27c3fdb58fc14114a4a5bbb3..0000000000000000000000000000000000000000 --- a/Sources/XXLogger/Logger.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation -import SwiftyBeaver - -public typealias LogClosure = (Any, String, String, Int) -> Void - -public struct XXLogger { - var logInfo: LogClosure - var logDebug: LogClosure - var logError: LogClosure - var logWarning: LogClosure - var logVerbose: LogClosure - - public init( - info: @escaping LogClosure, - debug: @escaping LogClosure, - error: @escaping LogClosure, - warning: @escaping LogClosure, - verbose: @escaping LogClosure - ) { - self.logInfo = info - self.logDebug = debug - self.logError = error - self.logWarning = warning - self.logVerbose = verbose - } - - public func info(_ contents: Any, file: String = #file, function: String = #function, line: Int = #line) { - logInfo(contents, file, function, line) - } - - public func debug(_ contents: Any, file: String = #file, function: String = #function, line: Int = #line) { - logDebug(contents, file, function, line) - } - - public func error(_ contents: Any, file: String = #file, function: String = #function, line: Int = #line) { - logError(contents, file, function, line) - } - - public func warning(_ contents: Any, file: String = #file, function: String = #function, line: Int = #line) { - logWarning(contents, file, function, line) - } - - public func verbose(_ contents: Any, file: String = #file, function: String = #function, line: Int = #line) { - logVerbose(contents, file, function, line) - } -} - -public extension XXLogger { - static func stop() { - let log = SwiftyBeaver.self - log.removeAllDestinations() - - let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - .appendingPathComponent("swiftybeaver.log") - - try? "".write(to: url, atomically: false, encoding: .utf8) - } - - static func start() { - let log = SwiftyBeaver.self - - let console = ConsoleDestination() - console.levelString.error = "🟥" - console.levelString.info = "✅" - console.levelString.warning = "[BACKEND]" - console.levelString.verbose = "[VERBOSE]" - console.format = "$DHH:mm:ss$d $L $N.$F:$l $M" - - let file = FileDestination() - file.levelString.error = "🟥" - file.levelString.info = "✅" - file.levelString.warning = "[BACKEND]" - file.minLevel = .debug - file.format = "$DHH:mm:ss$d $L $N.$F:$l $M" - - log.addDestination(console) - log.addDestination(file) - } - - static func live() -> Self { - let log = SwiftyBeaver.self - - return .init { - log.info($0, $1, $2, line: $3) - } debug: { - log.debug($0, $1, $2, line: $3) - } error: { - log.error($0, $1, $2, line: $3) - } warning: { - log.warning($0, $1, $2, line: $3) - } verbose: { - log.verbose($0, $1, $2, line: $3) - } - } - - static let noop: Self = .init( - info: { _,_,_,_ in }, - debug: { _,_,_,_ in }, - error: { _,_,_,_ in }, - warning: { _,_,_,_ in }, - verbose: { _,_,_,_ in } - ) -} diff --git a/Tests/AppTests/General/DateTests.swift b/Tests/AppFeatureTests/General/DateTests.swift similarity index 100% rename from Tests/AppTests/General/DateTests.swift rename to Tests/AppFeatureTests/General/DateTests.swift diff --git a/Tests/AppTests/General/InvitationTests.swift b/Tests/AppFeatureTests/General/InvitationTests.swift similarity index 100% rename from Tests/AppTests/General/InvitationTests.swift rename to Tests/AppFeatureTests/General/InvitationTests.swift diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6006a39d34ebab1c7bbe76132c3f428df725ee71..fa732be43c432b37cc0f8bd9f88a1e9c9d2f4592 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -293,8 +293,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "7346701ea29da0a85d4403cf3d7a589a58ae3dee", - "version" : "0.9.2" + "revision" : "bb436421f57269fbcfe7360735985321585a86e5", + "version" : "0.10.1" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172", + "version" : "0.2.0" } }, { @@ -311,8 +320,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "9ea8c763061287052a68d5e6723fed45e898b7d9", - "version" : "0.40.2" + "revision" : "1fcd53fc875bade47d850749ea53c324f74fd64d", + "version" : "0.45.0" } }, { @@ -360,15 +369,6 @@ "version" : "0.8.1" } }, - { - "identity" : "swiftybeaver", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver.git", - "state" : { - "revision" : "12b5acf96d98f91d50de447369bd18df74600f1a", - "version" : "1.9.6" - } - }, { "identity" : "swiftydropbox", "kind" : "remoteSourceControl", @@ -386,6 +386,15 @@ "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784", "version" : "0.5.0" } + }, + { + "identity" : "xxm-di", + "kind" : "remoteSourceControl", + "location" : "https://git.xx.network/elixxir/xxm-di", + "state" : { + "revision" : "43b1e12c32109f1753fcc62e5b0b21e479ee27e3", + "version" : "1.0.0" + } } ], "version" : 2