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/AppFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AppFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..f456abbc6723aaeea5b0855be66dfd2b2bcc16b8 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AppFeature.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 = "AppFeature" + BuildableName = "AppFeature" + BlueprintName = "AppFeature" + 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 = "AppFeature" + BuildableName = "AppFeature" + BlueprintName = "AppFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AppNavigation.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AppNavigation.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..15cbf312c024090afcf8a19dc2a95dc46942b6ae --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AppNavigation.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 = "AppNavigation" + BuildableName = "AppNavigation" + BlueprintName = "AppNavigation" + 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 = "AppNavigation" + BuildableName = "AppNavigation" + BlueprintName = "AppNavigation" + 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/Defaults.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Defaults.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..8d32953525d76ceaaeafe999fcdec2c2b6a64c44 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Defaults.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 = "Defaults" + BuildableName = "Defaults" + BlueprintName = "Defaults" + 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 = "Defaults" + BuildableName = "Defaults" + BlueprintName = "Defaults" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Keychain.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Keychain.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..e1efe3f4ab10b6ff34298955c87ae081d23eafea --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Keychain.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 = "Keychain" + BuildableName = "Keychain" + BlueprintName = "Keychain" + 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 = "Keychain" + BuildableName = "Keychain" + BlueprintName = "Keychain" + 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/SettingsFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SettingsFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..244b1a0ce2d97ea860b30062ec59ec322e831e1a --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/SettingsFeature.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 = "SettingsFeature" + BuildableName = "SettingsFeature" + BlueprintName = "SettingsFeature" + 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 = "SettingsFeature" + BuildableName = "SettingsFeature" + BlueprintName = "SettingsFeature" + 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/.swiftpm/xcode/xcshareddata/xcschemes/UpdateErrors.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/UpdateErrors.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..1998517fa48f8d42029ba164bc4429012ad24652 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/UpdateErrors.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 = "UpdateErrors" + BuildableName = "UpdateErrors" + BlueprintName = "UpdateErrors" + 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 = "UpdateErrors" + BuildableName = "UpdateErrors" + BlueprintName = "UpdateErrors" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/App/NotificationExtension/NotificationService.swift b/App/NotificationExtension/NotificationService.swift index 495958b9ab1e9b26981c9996f088b17b5fa1053d..d9657fb8cd68d0cd4dcdd2d41d0cfd4800d5d6ed 100644 --- a/App/NotificationExtension/NotificationService.swift +++ b/App/NotificationExtension/NotificationService.swift @@ -1,13 +1,97 @@ -import PushFeature +import XXModels +import XXClient +import XXDatabase +import ReportingFeature +import XXMessengerClient import UserNotifications final class NotificationService: UNNotificationServiceExtension { - private let pushHandler = PushHandler() + override func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { + guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { return } - override func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) { - pushHandler.handlePush(request, contentHandler) + var environment = MessengerEnvironment.live() + environment.serviceList = .userDefaults(key: "preImage", userDefaults: defaults) + let messenger = Messenger.live(environment) + let userInfo = request.content.userInfo + let dbPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("xxm_databasse") + .appendingPathExtension("sqlite").path + + guard let csv = userInfo["notificationData"] as? String, + let reports = try? messenger.getNotificationReports(notificationCSV: csv) else { return } + reports + .filter { $0.forMe } + .filter { $0.type != .silent } + .filter { $0.type != .default } + .compactMap { + let content = UNMutableNotificationContent() + content.badge = 1 + content.sound = .default + content.threadIdentifier = "new_message_identifier" + content.userInfo["type"] = $0.type.rawValue + content.userInfo["source"] = $0.source + content.body = getBodyForUnknownWith(type: $0.type) + + guard let db = try? Database.onDisk(path: dbPath), + let contact = try? db.fetchContacts(.init(id: [$0.source])).first else { + return content + } + if ReportingStatus.live().isEnabled(), (contact.isBlocked || contact.isBanned) { + return nil + } + if let showSender = defaults.value(forKey: "isShowingUsernames") as? Bool, showSender == true { + let name = (contact.nickname ?? contact.username) ?? "" + content.body = getBodyFor(name: name, with: $0.type) + } + return content + }.forEach { + contentHandler($0) + } + } + + private func getBodyForUnknownWith(type: NotificationReport.ReportType) -> String { + switch type { + case .`default`, .silent: + fatalError() + case .request: + return "Request received" + case .reset: + return "One of your contacts has restored their account" + case .confirm: + return "Request accepted" + case .e2e: + return "New private message" + case .group: + return "New group message" + case .endFT: + return "New media received" + case .groupRQ: + return "Group request received" + } + } + + private func getBodyFor(name: String, with type: NotificationReport.ReportType) -> String { + switch type { + case .silent, .`default`: + fatalError() + case .e2e: + return String(format: "%@ sent you a private message", name) + case .reset: + return String(format: "%@ restored their account", name) + case .endFT: + return String(format: "%@ sent you a file", name) + case .group: + return String(format: "%@ sent you a group message", name) + case .groupRQ: + return String(format: "%@ sent you a group request", name) + case .confirm: + return String(format: "%@ confirmed your contact request", name) + case .request: + return String(format: "%@ sent you a contact request", name) } + } } diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index 3e09d58a41130302b5a9477bbef829531567752b..0f9849c8e5b29c44bd270caee0cc7ba2c25f0a8d 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 */; @@ -448,7 +448,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 294; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -463,7 +463,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.8; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -487,7 +487,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 294; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -503,7 +503,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.8; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -522,7 +522,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 294; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -536,7 +536,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.1.3; + MARKETING_VERSION = 1.1.8; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock.notifications; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -553,7 +553,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 294; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -567,7 +567,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.1.3; + MARKETING_VERSION = 1.1.8; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.notifications; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -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.xcodeproj/xcshareddata/xcschemes/Release.xcscheme b/App/client-ios.xcodeproj/xcshareddata/xcschemes/Release.xcscheme index 800c200df2b4ad94d75e5144e7951bdcd91611f2..b47f627ae04ed4310d32bd7d0d0f1e5176aa6b0b 100644 --- a/App/client-ios.xcodeproj/xcshareddata/xcschemes/Release.xcscheme +++ b/App/client-ios.xcodeproj/xcshareddata/xcschemes/Release.xcscheme @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1200" - version = "1.7"> + version = "1.8"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> diff --git a/App/client-ios/Resources/GoogleService-Info.plist b/App/client-ios/Resources/GoogleService-Info.plist index 03e09469daae0502a5202f6e63aca9db140d1d77..10566b5ece72bfa2c34ad23c1d289d327d757327 100644 --- a/App/client-ios/Resources/GoogleService-Info.plist +++ b/App/client-ios/Resources/GoogleService-Info.plist @@ -3,34 +3,34 @@ <plist version="1.0"> <dict> <key>CLIENT_ID</key> - <string></string> + <string>662236151640-r1lrlppqdcmhb4p8urq32fo7784cdoal.apps.googleusercontent.com</string> <key>REVERSED_CLIENT_ID</key> - <string></string> + <string>com.googleusercontent.apps.662236151640-r1lrlppqdcmhb4p8urq32fo7784cdoal</string> <key>ANDROID_CLIENT_ID</key> - <string></string> + <string>662236151640-2ughgo2dvc59dm4o39b45lbdungp2mct.apps.googleusercontent.com</string> <key>API_KEY</key> - <string></string> + <string>AIzaSyCbI2yQ7pbuVSRvraqanjGcS9CDrjD7lNU</string> <key>GCM_SENDER_ID</key> - <string></string> + <string>662236151640</string> <key>PLIST_VERSION</key> - <string></string> + <string>1</string> <key>BUNDLE_ID</key> - <string></string> + <string>io.xxlabs.messenger</string> <key>PROJECT_ID</key> - <string></string> + <string>xx-messenger-6e03e</string> <key>STORAGE_BUCKET</key> - <string></string> + <string>xx-messenger-6e03e.appspot.com</string> <key>IS_ADS_ENABLED</key> <false/> <key>IS_ANALYTICS_ENABLED</key> <false/> <key>IS_APPINVITE_ENABLED</key> - <false/> + <true/> <key>IS_GCM_ENABLED</key> - <false/> + <true/> <key>IS_SIGNIN_ENABLED</key> - <false/> + <true/> <key>GOOGLE_APP_ID</key> - <string></string> + <string>1:662236151640:ios:24badb58ab07515d8cef2d</string> </dict> </plist> diff --git a/App/client-ios/Resources/Info.plist b/App/client-ios/Resources/Info.plist index d8cb845b40392068f453251c9c0aa72cd00979db..76cc3739f247885e2336b18a22b9965a16fe9965 100644 --- a/App/client-ios/Resources/Info.plist +++ b/App/client-ios/Resources/Info.plist @@ -67,6 +67,10 @@ <key>NSAllowsArbitraryLoadsInWebContent</key> <true/> </dict> + <key>NSBonjourServices</key> + <array> + <string>_pulse._tcp</string> + </array> <key>NSCameraUsageDescription</key> <string>This permission is required for scanning qr codes</string> <key>NSFaceIDUsageDescription</key> @@ -105,6 +109,6 @@ <key>UIViewControllerBasedStatusBarAppearance</key> <true/> <key>isReportingOptional</key> - <false/> + <true/> </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 f5678619dec83e0af6ca965e92d6303b0bc6f373..ef4dc8f0544a8d56f504f00192f82458c4032dc0 100644 --- a/Package.swift +++ b/Package.swift @@ -2,758 +2,615 @@ import PackageDescription let package = Package( - name: "client-ios", - defaultLocalization: "en", - platforms: [ - .iOS(.v14), - ], - products: [ - .library(name: "App", targets: ["App"]), - .library(name: "HUD", targets: ["HUD"]), - .library(name: "Theme", targets: ["Theme"]), - .library(name: "Shared", targets: ["Shared"]), - .library(name: "Models", targets: ["Models"]), - .library(name: "XXLogger", targets: ["XXLogger"]), - .library(name: "Defaults", targets: ["Defaults"]), - .library(name: "Keychain", targets: ["Keychain"]), - .library(name: "Voxophone", targets: ["Voxophone"]), - .library(name: "Countries", targets: ["Countries"]), - .library(name: "InputField", targets: ["InputField"]), - .library(name: "TestHelpers", targets: ["TestHelpers"]), - .library(name: "ScanFeature", targets: ["ScanFeature"]), - .library(name: "Permissions", targets: ["Permissions"]), - .library(name: "MenuFeature", targets: ["MenuFeature"]), - .library(name: "Integration", targets: ["Integration"]), - .library(name: "ChatFeature", targets: ["ChatFeature"]), - .library(name: "PushFeature", targets: ["PushFeature"]), - .library(name: "SFTPFeature", targets: ["SFTPFeature"]), - .library(name: "CrashService", targets: ["CrashService"]), - .library(name: "TermsFeature", targets: ["TermsFeature"]), - .library(name: "Presentation", targets: ["Presentation"]), - .library(name: "ToastFeature", targets: ["ToastFeature"]), - .library(name: "BackupFeature", targets: ["BackupFeature"]), - .library(name: "LaunchFeature", targets: ["LaunchFeature"]), - .library(name: "iCloudFeature", targets: ["iCloudFeature"]), - .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: "DropboxFeature", targets: ["DropboxFeature"]), - .library(name: "VersionChecking", targets: ["VersionChecking"]), - .library(name: "SettingsFeature", targets: ["SettingsFeature"]), - .library(name: "ChatListFeature", targets: ["ChatListFeature"]), - .library(name: "RequestsFeature", targets: ["RequestsFeature"]), - .library(name: "ChatInputFeature", targets: ["ChatInputFeature"]), - .library(name: "OnboardingFeature", targets: ["OnboardingFeature"]), - .library(name: "GoogleDriveFeature", targets: ["GoogleDriveFeature"]), - .library(name: "ContactListFeature", targets: ["ContactListFeature"]), - .library(name: "DependencyInjection", targets: ["DependencyInjection"]), - .library(name: "ReportingFeature", targets: ["ReportingFeature"]), - ], - dependencies: [ - .package( - url: "https://github.com/Quick/Quick", - .upToNextMajor(from: "3.0.0") - ), - .package( - url: "https://github.com/Quick/Nimble", - .upToNextMajor(from: "9.0.0") - ), - .package( - url: "https://github.com/SnapKit/SnapKit", - .upToNextMajor(from: "5.0.1") - ), - .package( - url: "https://github.com/icanzilb/Retry.git", - .upToNextMajor(from: "0.6.3") - ), - .package( - url: "https://github.com/ekazaev/ChatLayout", - .upToNextMajor(from: "1.1.14") - ), - .package( - url: "https://github.com/ra1028/DifferenceKit", - .upToNextMajor(from: "1.2.0") - ), - .package( - url: "https://github.com/apple/swift-protobuf", - .upToNextMajor(from: "1.14.0") - ), - .package( - url: "https://github.com/google/GoogleSignIn-iOS", - .upToNextMajor(from: "6.1.0") - ), - .package( - url: "https://github.com/dropbox/SwiftyDropbox.git", - .upToNextMajor(from: "8.2.1") - ), - .package( - url: "https://github.com/amosavian/FileProvider.git", - .upToNextMajor(from: "0.26.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") - ), - .package( - url: "https://github.com/pointfreeco/combine-schedulers", - .upToNextMajor(from: "0.5.0") - ), - .package( - url: "https://github.com/kishikawakatsumi/KeychainAccess", - .upToNextMajor(from: "4.2.1") - ), - .package( - url: "https://github.com/google/google-api-objectivec-client-for-rest", - .upToNextMajor(from: "1.6.0") - ), - .package( - url: "https://git.xx.network/elixxir/client-ios-db.git", - .upToNextMajor(from: "1.1.0") - ), - .package( - url: "https://github.com/firebase/firebase-ios-sdk.git", - .upToNextMajor(from: "8.10.0") - ), - .package( - url: "https://github.com/darrarski/Shout.git", - revision: "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0" - ), - .package( - url: "https://github.com/pointfreeco/swift-composable-architecture.git", - .upToNextMajor(from: "0.32.0") - ), - .package( - url: "https://github.com/pointfreeco/swift-custom-dump.git", - .upToNextMajor(from: "0.5.0") - ), - .package( - url: "https://github.com/swiftcsv/SwiftCSV.git", - from: "0.8.0" - ), - .package( - url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", - .upToNextMajor(from: "0.3.3") - ), - ], - targets: [ - .target( - name: "App", - dependencies: [ - .target(name: "Keychain"), - .target(name: "Voxophone"), - .target(name: "Permissions"), - .target(name: "ScanFeature"), - .target(name: "ChatFeature"), - .target(name: "MenuFeature"), - .target(name: "PushFeature"), - .target(name: "SFTPFeature"), - .target(name: "TermsFeature"), - .target(name: "ToastFeature"), - .target(name: "CrashService"), - .target(name: "BackupFeature"), - .target(name: "SearchFeature"), - .target(name: "LaunchFeature"), - .target(name: "iCloudFeature"), - .target(name: "DropboxFeature"), - .target(name: "ContactFeature"), - .target(name: "RestoreFeature"), - .target(name: "ProfileFeature"), - .target(name: "CrashReporting"), - .target(name: "ChatListFeature"), - .target(name: "SettingsFeature"), - .target(name: "RequestsFeature"), - .target(name: "ReportingFeature"), - .target(name: "OnboardingFeature"), - .target(name: "GoogleDriveFeature"), - .target(name: "ContactListFeature"), - ] - ), - .testTarget( - name: "AppTests", - dependencies: [ - .target(name: "App"), - ] - ), - .target( - name: "CrashReporting" - ), - .target( - name: "NetworkMonitor" - ), - .target( - name: "VersionChecking" - ), - .target( - name: "DependencyInjection" - ), - .testTarget( - name: "DependencyInjectionTests", - dependencies: [ - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "InputField", - dependencies: [ - .target(name: "Shared"), - ] - ), - .binaryTarget( - name: "Bindings", - path: "XCFrameworks/Bindings.xcframework" - ), - .target( - name: "Permissions", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "PushFeature", - dependencies: [ - .target(name: "Models"), - .target(name: "Defaults"), - .target(name: "Integration"), - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "TestHelpers", - dependencies: [ - .target(name: "Models"), - .target(name: "Presentation"), - ] - ), - .target( - name: "Keychain", - dependencies: [ - .product(name: "KeychainAccess", package: "KeychainAccess"), - ] - ), - .target( - name: "Voxophone", - dependencies: [ - .target(name: "Shared"), - ] - ), - .target( - name: "Models", - dependencies: [ - .product(name: "DifferenceKit", package: "DifferenceKit"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), - ] - ), - .target( - name: "Defaults", - dependencies: [ - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "ToastFeature", - dependencies: [ - .target(name: "Shared"), - ] - ), - .target( - name: "CrashService", - dependencies: [ - .target(name: "CrashReporting"), - .product(name: "FirebaseCrashlytics", package: "firebase-ios-sdk"), - ] - ), - .target( - name: "SFTPFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Models"), - .target(name: "Shared"), - .target(name: "Keychain"), - .target(name: "InputField"), - .target(name: "Presentation"), - .target(name: "DependencyInjection"), - .product(name: "Shout", package: "Shout"), - ] - ), - .target( - name: "GoogleDriveFeature", - dependencies: [ - .product(name: "GoogleSignIn", package: "GoogleSignIn-iOS"), - .product(name: "GoogleAPIClientForREST_Drive", package: "google-api-objectivec-client-for-rest"), - ], - resources: [ - .process("Resources"), - ] - ), - .target( - name: "iCloudFeature", - dependencies: [ - .product(name: "FilesProvider", package: "FileProvider"), - ] - ), - .target( - name: "DropboxFeature", - dependencies: [ - .product(name: "SwiftyDropbox", package: "SwiftyDropbox"), - ], - resources: [ - .process("Resources"), - ] - ), - .target( - name: "Countries", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "DependencyInjection"), - ], - resources: [ - .process("Resources"), - ] - ), - .target( - name: "Theme", - dependencies: [ - .target(name: "Defaults"), - .target(name: "DependencyInjection"), - ] - ), - .testTarget( - name: "ThemeTests", - dependencies: [ - .target(name: "Theme"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "DrawerFeature", - dependencies: [ - .target(name: "Shared"), - .target(name: "InputField"), - .product(name: "ScrollViewController", package: "ScrollViewController"), - ] - ), - .target( - name: "HUD", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .product(name: "SnapKit", package: "SnapKit"), - ] - ), - .target( - name: "XXLogger", - dependencies: [ - .product(name: "SwiftyBeaver", package: "SwiftyBeaver"), - ] - ), - .target( - name: "Shared", - dependencies: [ - .product(name: "SnapKit", package: "SnapKit"), - .product(name: "ChatLayout", package: "ChatLayout"), - .product(name: "DifferenceKit", package: "DifferenceKit"), - ], - exclude: [ - "swiftgen.yml", - ], - resources: [ - .process("Resources"), - ] - ), - .target( - name: "Integration", - dependencies: [ - .target(name: "Shared"), - .target(name: "Bindings"), - .target(name: "XXLogger"), - .target(name: "Keychain"), - .target(name: "ToastFeature"), - .target(name: "BackupFeature"), - .target(name: "CrashReporting"), - .target(name: "NetworkMonitor"), - .target(name: "DependencyInjection"), - .product(name: "Retry", package: "Retry"), - .product(name: "XXDatabase", package: "client-ios-db"), - .product(name: "XXLegacyDatabaseMigrator", package: "client-ios-db"), - ], - resources: [ - .process("Resources"), - ] - ), - .target( - name: "Presentation", - dependencies: [ - .target(name: "Theme"), - .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"), - ] - ), - .target( - name: "RestoreFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Shared"), - .target(name: "SFTPFeature"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "iCloudFeature"), - .target(name: "DropboxFeature"), - .target(name: "GoogleDriveFeature"), - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "ContactFeature", - dependencies: [ - .target(name: "Shared"), - .target(name: "InputField"), - .target(name: "ChatFeature"), - .target(name: "Presentation"), - .product(name: "CombineSchedulers", package: "combine-schedulers"), - .product(name: "ScrollViewController", package: "ScrollViewController"), - ] - ), - .testTarget( - name: "ContactFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "ContactFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "ChatFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Defaults"), - .target(name: "Keychain"), - .target(name: "Voxophone"), - .target(name: "Integration"), - .target(name: "Permissions"), - .target(name: "Presentation"), - .target(name: "DrawerFeature"), - .target(name: "ChatInputFeature"), - .target(name: "ReportingFeature"), - .target(name: "DependencyInjection"), - .product(name: "ChatLayout", package: "ChatLayout"), - .product(name: "DifferenceKit", package: "DifferenceKit"), - .product(name: "ScrollViewController", package: "ScrollViewController"), - ] - ), - .testTarget( - name: "ChatFeatureTests", - dependencies: [ - .target(name: "ChatFeature"), - .target(name: "TestHelpers"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "SearchFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Shared"), - .target(name: "Countries"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "ContactFeature"), - .target(name: "DependencyInjection"), - ] - ), - .testTarget( - name: "SearchFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "SearchFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "LaunchFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Defaults"), - .target(name: "PushFeature"), - .target(name: "Integration"), - .target(name: "Permissions"), - .target(name: "DropboxFeature"), - .target(name: "VersionChecking"), - .target(name: "ReportingFeature"), - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "TermsFeature", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Defaults"), - .target(name: "Presentation"), - ] - ), - .target( - name: "RequestsFeature", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Integration"), - .target(name: "ToastFeature"), - .target(name: "ContactFeature"), - .target(name: "DependencyInjection"), - .product(name: "DifferenceKit", package: "DifferenceKit"), - ] - ), - .testTarget( - name: "RequestsFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "RequestsFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "ProfileFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Keychain"), - .target(name: "Defaults"), - .target(name: "Countries"), - .target(name: "InputField"), - .target(name: "MenuFeature"), - .target(name: "Permissions"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "DrawerFeature"), - .target(name: "DependencyInjection"), - .product(name: "CombineSchedulers", package: "combine-schedulers"), - .product(name: "ScrollViewController", package: "ScrollViewController"), - ] - ), - .testTarget( - name: "ProfileFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "ProfileFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "ChatListFeature", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Defaults"), - .target(name: "MenuFeature"), - .target(name: "ChatFeature"), - .target(name: "ProfileFeature"), - .target(name: "SettingsFeature"), - .target(name: "ContactListFeature"), - .target(name: "DependencyInjection"), - .product(name: "DifferenceKit", package: "DifferenceKit"), - ] - ), - .testTarget( - name: "ChatListFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "ChatListFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "OnboardingFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Shared"), - .target(name: "Defaults"), - .target(name: "Keychain"), - .target(name: "Countries"), - .target(name: "InputField"), - .target(name: "Permissions"), - .target(name: "PushFeature"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "DrawerFeature"), - .target(name: "VersionChecking"), - .target(name: "DependencyInjection"), - .product(name: "CombineSchedulers", package: "combine-schedulers"), - .product(name: "ScrollViewController", package: "ScrollViewController"), - ] - ), - .testTarget( - name: "OnboardingFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "OnboardingFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "MenuFeature", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Defaults"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "BackupFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Shared"), - .target(name: "Models"), - .target(name: "InputField"), - .target(name: "SFTPFeature"), - .target(name: "Presentation"), - .target(name: "iCloudFeature"), - .target(name: "DrawerFeature"), - .target(name: "DropboxFeature"), - .target(name: "GoogleDriveFeature"), - .target(name: "DependencyInjection"), - ] - ), - .target( - name: "ScanFeature", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Countries"), - .target(name: "Permissions"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "ContactFeature"), - .target(name: "DependencyInjection"), - .product(name: "SnapKit", package: "SnapKit"), - ] - ), - .testTarget( - name: "ScanFeatureTests", - dependencies: [ - .target(name: "ScanFeature"), - .target(name: "TestHelpers"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "ContactListFeature", - dependencies: [ - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "ContactFeature"), - .target(name: "DependencyInjection"), - .product(name: "DifferenceKit", package: "DifferenceKit"), - ] - ), - .testTarget( - name: "ContactListFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "ContactListFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .target( - name: "SettingsFeature", - dependencies: [ - .target(name: "HUD"), - .target(name: "Theme"), - .target(name: "Shared"), - .target(name: "Defaults"), - .target(name: "Keychain"), - .target(name: "InputField"), - .target(name: "PushFeature"), - .target(name: "Permissions"), - .target(name: "MenuFeature"), - .target(name: "Integration"), - .target(name: "Presentation"), - .target(name: "DrawerFeature"), - .target(name: "DependencyInjection"), - .product(name: "CombineSchedulers", package: "combine-schedulers"), - .product(name: "ScrollViewController", package: "ScrollViewController"), - ] - ), - .testTarget( - name: "SettingsFeatureTests", - dependencies: [ - .target(name: "TestHelpers"), - .target(name: "SettingsFeature"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble"), - ] - ), - .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: [ - .target(name: "DrawerFeature"), - .target(name: "Shared"), - .product(name: "SwiftCSV", package: "SwiftCSV"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), - ], - resources: [ - .process("Resources"), - ] - ), - ] + name: "client-ios", + defaultLocalization: "en", + platforms: [ + .iOS(.v14), + ], + products: [ + .library(name: "Shared", targets: ["Shared"]), + .library(name: "AppCore", targets: ["AppCore"]), + .library(name: "Defaults", targets: ["Defaults"]), + .library(name: "Keychain", targets: ["Keychain"]), + .library(name: "Voxophone", targets: ["Voxophone"]), + .library(name: "AppFeature", targets: ["AppFeature"]), + .library(name: "InputField", targets: ["InputField"]), + .library(name: "ScanFeature", targets: ["ScanFeature"]), + .library(name: "MenuFeature", targets: ["MenuFeature"]), + .library(name: "ChatFeature", targets: ["ChatFeature"]), + .library(name: "CrashReport", targets: ["CrashReport"]), + .library(name: "UpdateErrors", targets: ["UpdateErrors"]), + .library(name: "CheckVersion", targets: ["CheckVersion"]), + .library(name: "AppResources", targets: ["AppResources"]), + .library(name: "TermsFeature", targets: ["TermsFeature"]), + .library(name: "AppNavigation", targets: ["AppNavigation"]), + .library(name: "BackupFeature", targets: ["BackupFeature"]), + .library(name: "LaunchFeature", targets: ["LaunchFeature"]), + .library(name: "SearchFeature", targets: ["SearchFeature"]), + .library(name: "DrawerFeature", targets: ["DrawerFeature"]), + .library(name: "WebsiteFeature", targets: ["WebsiteFeature"]), + .library(name: "RestoreFeature", targets: ["RestoreFeature"]), + .library(name: "ProfileFeature", targets: ["ProfileFeature"]), + .library(name: "ContactFeature", targets: ["ContactFeature"]), + .library(name: "FetchBannedList", targets: ["FetchBannedList"]), + .library(name: "SettingsFeature", targets: ["SettingsFeature"]), + .library(name: "ChatListFeature", targets: ["ChatListFeature"]), + .library(name: "RequestsFeature", targets: ["RequestsFeature"]), + .library(name: "ReportingFeature", targets: ["ReportingFeature"]), + .library(name: "ChatInputFeature", targets: ["ChatInputFeature"]), + .library(name: "GroupDraftFeature", targets: ["GroupDraftFeature"]), + .library(name: "ProcessBannedList", targets: ["ProcessBannedList"]), + .library(name: "OnboardingFeature", targets: ["OnboardingFeature"]), + .library(name: "CreateGroupFeature", targets: ["CreateGroupFeature"]), + .library(name: "CountryListFeature", targets: ["CountryListFeature"]), + .library(name: "PermissionsFeature", targets: ["PermissionsFeature"]), + .library(name: "ContactListFeature", targets: ["ContactListFeature"]), + .library(name: "RequestPermissionFeature", targets: ["RequestPermissionFeature"]), + ], + dependencies: [ + .package( + url: "https://github.com/SnapKit/SnapKit", + .upToNextMajor(from: "5.0.1") + ), + .package( + url: "https://github.com/icanzilb/Retry.git", + .upToNextMajor(from: "0.6.3") + ), + .package( + url: "https://github.com/ekazaev/ChatLayout", + .upToNextMajor(from: "1.1.14") + ), + .package( + url: "https://github.com/ra1028/DifferenceKit", + .upToNextMajor(from: "1.2.0") + ), + .package( + url: "https://github.com/apple/swift-protobuf", + .upToNextMajor(from: "1.14.0") + ), + .package( + url: "https://github.com/darrarski/ScrollViewController", + .upToNextMajor(from: "1.2.0") + ), + .package( + url: "https://github.com/pointfreeco/combine-schedulers", + .upToNextMajor(from: "0.5.0") + ), + .package( + url: "https://github.com/kishikawakatsumi/KeychainAccess", + .upToNextMajor(from: "4.2.1") + ), + .package( + url: "https://git.xx.network/elixxir/elixxir-dapps-sdk-swift", + .upToNextMajor(from: "1.0.0") + ), + .package( + url: "https://git.xx.network/elixxir/client-ios-db.git", + .upToNextMajor(from: "1.1.0") + ), + .package( + url: "https://git.xx.network/elixxir/xxm-cloud-providers.git", + .upToNextMajor(from: "1.0.2") + ), + .package( + url: "https://github.com/firebase/firebase-ios-sdk.git", + .upToNextMajor(from: "8.10.0") + ), + .package( + url: "https://github.com/pointfreeco/swift-composable-architecture.git", + .upToNextMajor(from: "0.43.0") + ), + .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/kean/Pulse.git", + .upToNextMajor(from: "2.1.3") + ), + .package( + url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", + .upToNextMajor(from: "0.3.3") + ), + ], + targets: [ + .target( + name: "AppFeature", + dependencies: [ + .target(name: "AppCore"), + .target(name: "Keychain"), + .target(name: "ScanFeature"), + .target(name: "ChatFeature"), + .target(name: "MenuFeature"), + .target(name: "CrashReport"), + .target(name: "TermsFeature"), + .target(name: "BackupFeature"), + .target(name: "SearchFeature"), + .target(name: "LaunchFeature"), + .target(name: "ContactFeature"), + .target(name: "WebsiteFeature"), + .target(name: "RestoreFeature"), + .target(name: "ProfileFeature"), + .target(name: "ChatListFeature"), + .target(name: "SettingsFeature"), + .target(name: "RequestsFeature"), + .target(name: "ReportingFeature"), + .target(name: "GroupDraftFeature"), + .target(name: "OnboardingFeature"), + .target(name: "CreateGroupFeature"), + .target(name: "ContactListFeature"), + .target(name: "RequestPermissionFeature"), + .product(name: "PulseUI", package: "Pulse"), // TO REMOVE + .product(name: "PulseLogHandler", package: "Pulse"), // TO REMOVE + ] + ), + .testTarget( + name: "AppFeatureTests", + dependencies: [ + .target(name: "AppFeature"), + ] + ), + .target( + name: "AppCore", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppResources"), + .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: "CheckVersion", + dependencies: [ + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "Voxophone", + dependencies: [ + .target(name: "Shared"), + ] + ), + .target(name: "WebsiteFeature"), + .target( + name: "CrashReport", + dependencies: [ + .product( + name: "FirebaseCrashlytics", + package: "firebase-ios-sdk" + ), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "AppNavigation", + dependencies: [ + .product( + name: "XXModels", + package: "client-ios-db" + ), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "CreateGroupFeature", + dependencies: [ + .target(name: "AppCore") + ] + ), + .target( + name: "GroupDraftFeature", + dependencies: [ + .target(name: "AppCore") + ] + ), + .target( + name: "PermissionsFeature", + dependencies: [ + .product( + name: "XCTestDynamicOverlay", + package: "xctest-dynamic-overlay" + ), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "AppResources", + exclude: [ + "swiftgen.yml", + ], + resources: [ + .process("Resources") + ] + ), + .target( + name: "InputField", + dependencies: [ + .target(name: "Shared"), + ] + ), + .target( + name: "RequestPermissionFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppCore"), + .target(name: "AppResources"), + .target(name: "AppNavigation"), + .target(name: "PermissionsFeature"), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "Keychain", + dependencies: [ + .product( + name: "KeychainAccess", + package: "KeychainAccess" + ), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "Defaults", + dependencies: [ + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "CountryListFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppCore") + ] + ), + .target( + name: "DrawerFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "InputField"), + .product(name: "ScrollViewController", package: "ScrollViewController"), + ] + ), + .target( + name: "Shared", + dependencies: [ + .product(name: "SnapKit", package: "SnapKit"), + .product(name: "ChatLayout", package: "ChatLayout"), + .product(name: "DifferenceKit", package: "DifferenceKit"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + ], + resources: [ + .process("Resources"), + ] + ), + .target( + name: "ChatInputFeature", + dependencies: [ + .target( + name: "Voxophone" + ), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "RestoreFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppCore"), + .product(name: "XXDatabase", package: "client-ios-db"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "CloudFilesDrive", package: "xxm-cloud-providers"), + .product(name: "CloudFilesDropbox", package: "xxm-cloud-providers"), + .product(name: "CloudFilesSFTP", package: "xxm-cloud-providers"), + .product(name: "CloudFilesICloud", package: "xxm-cloud-providers"), + ] + ), + .target( + name: "ContactFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "InputField"), + .target(name: "ChatFeature"), + .product(name: "CombineSchedulers", package: "combine-schedulers"), + .product(name: "ScrollViewController", package: "ScrollViewController"), + ] + ), + .target( + name: "ChatFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppCore"), + .target(name: "Defaults"), + .target(name: "Keychain"), + .target(name: "Voxophone"), + .target(name: "DrawerFeature"), + .target(name: "ChatInputFeature"), + .target(name: "ReportingFeature"), + .target(name: "RequestPermissionFeature"), + .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: "ContactFeature"), + .target(name: "CountryListFeature"), + .product(name: "Retry", package: "Retry"), + .product(name: "XXDatabase", package: "client-ios-db"), + ] + ), + .target( + name: "LaunchFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "Defaults"), + .target(name: "UpdateErrors"), + .target(name: "CheckVersion"), + .target(name: "BackupFeature"), + .target(name: "FetchBannedList"), + .target(name: "ReportingFeature"), + .target(name: "ProcessBannedList"), + .target(name: "RequestPermissionFeature"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "CloudFilesSFTP", package: "xxm-cloud-providers"), + .product(name: "CombineSchedulers", package: "combine-schedulers"), + .product(name: "CloudFilesDropbox", package: "xxm-cloud-providers"), + .product(name: "XXLegacyDatabaseMigrator", package: "client-ios-db"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + ] + ), + .target( + name: "TermsFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "Defaults"), + .target(name: "AppNavigation"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "UpdateErrors", + dependencies: [ + .product( + name: "XXClient", + package: "elixxir-dapps-sdk-swift" + ), + .product( + name: "XCTestDynamicOverlay", + package: "xctest-dynamic-overlay" + ), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "ProcessBannedList", + dependencies: [ + .product( + name: "SwiftCSV", + package: "SwiftCSV" + ), + .product( + name: "XCTestDynamicOverlay", + package: "xctest-dynamic-overlay" + ), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "FetchBannedList", + dependencies: [ + .product( + name: "XCTestDynamicOverlay", + package: "xctest-dynamic-overlay" + ), + .product( + name: "Dependencies", + package: "swift-composable-architecture" + ), + ] + ), + .target( + name: "RequestsFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "ContactFeature"), + .product( + name: "DifferenceKit", + package: "DifferenceKit" + ), + ] + ), + .target( + name: "ProfileFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "Keychain"), + .target(name: "Defaults"), + .target(name: "InputField"), + .target(name: "MenuFeature"), + .target(name: "DrawerFeature"), + .target(name: "BackupFeature"), + .target(name: "CountryListFeature"), + .target(name: "RequestPermissionFeature"), + .product(name: "CombineSchedulers", package: "combine-schedulers"), + .product(name: "ScrollViewController", package: "ScrollViewController"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + ] + ), + .target( + name: "ChatListFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "Defaults"), + .target(name: "MenuFeature"), + .target(name: "ChatFeature"), + .target(name: "ProfileFeature"), + .target(name: "SettingsFeature"), + .target(name: "ContactListFeature"), + .product(name: "DifferenceKit", package: "DifferenceKit"), + ] + ), + .target( + name: "OnboardingFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppCore"), + .target(name: "Defaults"), + .target(name: "Keychain"), + .target(name: "InputField"), + .target(name: "DrawerFeature"), + .target(name: "AppNavigation"), + .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( + name: "MenuFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppCore"), + .target(name: "Defaults"), + .target(name: "DrawerFeature"), + .target(name: "ReportingFeature"), + .product( + name: "XXClient", + package: "elixxir-dapps-sdk-swift" + ), + ] + ), + .target( + name: "BackupFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "AppCore"), + .target(name: "InputField"), + .target(name: "DrawerFeature"), + .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: "ContactFeature"), + .target(name: "CountryListFeature"), + .target(name: "RequestPermissionFeature"), + .product(name: "SnapKit", package: "SnapKit"), + ] + ), + .target( + name: "ContactListFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "ContactFeature"), + .product(name: "DifferenceKit", package: "DifferenceKit"), + ] + ), + .target( + name: "SettingsFeature", + dependencies: [ + .target(name: "Shared"), + .target(name: "Defaults"), + .target(name: "Keychain"), + .target(name: "InputField"), + .target(name: "MenuFeature"), + .target(name: "CrashReport"), + .target(name: "DrawerFeature"), + .target(name: "RequestPermissionFeature"), + .product(name: "CombineSchedulers", package: "combine-schedulers"), + .product(name: "ScrollViewController", package: "ScrollViewController"), + ] + ), + .target( + name: "ReportingFeature", + dependencies: [ + .target(name: "DrawerFeature"), + .target(name: "Shared"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + ], + resources: [ + .process("Resources"), + ] + ), + ] ) diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift deleted file mode 100644 index 4143a5f270c7179e9f11892702bf7f7db79a6800..0000000000000000000000000000000000000000 --- a/Sources/App/AppDelegate.swift +++ /dev/null @@ -1,204 +0,0 @@ -import UIKit -import BackgroundTasks - -import Theme -import XXModels -import XXLogger -import Defaults -import Integration -import PushFeature -import ToastFeature -import SwiftyDropbox -import LaunchFeature -import DropboxFeature -import CrashReporting -import DependencyInjection - -public class AppDelegate: UIResponder, UIApplicationDelegate { - @Dependency private var pushRouter: PushRouter - @Dependency private var pushHandler: PushHandling - @Dependency private var crashReporter: CrashReporter - @Dependency private var dropboxService: DropboxInterface - - @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool - @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool - @KeyObject(.crashReporting, defaultValue: true) var isCrashReportingEnabled: Bool - - var calledStopNetwork = false - var forceFailedPendingMessages = false - - var coverView: UIView? - var backgroundTimer: Timer? - public var window: UIWindow? - - public func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - #if DEBUG - DependencyRegistrator.registerForMock() - #else - DependencyRegistrator.registerForLive() - #endif - - if recordingLogs { - XXLogger.start() - } - - crashReporter.configure() - crashReporter.setEnabled(isCrashReportingEnabled) - - UNUserNotificationCenter.current().delegate = self - - let window = Window() - let navController = UINavigationController(rootViewController: LaunchController()) - window.rootViewController = StatusBarViewController(ToastViewController(navController)) - window.backgroundColor = UIColor.white - window.makeKeyAndVisible() - self.window = window - - DependencyInjection.Container.shared.register( - PushRouter.live(navigationController: navController) - ) - - return true - } - - public func application(application: UIApplication, shouldAllowExtensionPointIdentifier: String) -> Bool { - false - } - - public func applicationDidEnterBackground(_ application: UIApplication) { - if let session = try? DependencyInjection.Container.shared.resolve() as SessionType { - let backgroundTask = application.beginBackgroundTask(withName: "xx.stop.network") {} - - // An option here would be: create async completion closure - - backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in - guard UIApplication.shared.backgroundTimeRemaining > 8 else { - if !self.calledStopNetwork { - self.calledStopNetwork = true - session.stop() - } else { - if session.hasRunningTasks == false { - application.endBackgroundTask(backgroundTask) - timer.invalidate() - } - } - - return - } - - guard UIApplication.shared.backgroundTimeRemaining > 9 else { - if !self.forceFailedPendingMessages { - self.forceFailedPendingMessages = true - - let query = Message.Query(status: [.sending]) - let assignment = Message.Assignments(status: .sendingFailed) - _ = try? session.dbManager.bulkUpdateMessages(query, assignment) - } - - return - } - } - } - } - - public func applicationWillResignActive(_ application: UIApplication) { - if hideAppList { - coverView?.removeFromSuperview() - coverView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) - coverView?.frame = window?.bounds ?? .zero - window?.addSubview(coverView!) - } - } - - public func applicationWillTerminate(_ application: UIApplication) { - if let session = try? DependencyInjection.Container.shared.resolve() as SessionType { - session.stop() - } - } - - public func applicationWillEnterForeground(_ application: UIApplication) { - if backgroundTimer != nil { - backgroundTimer?.invalidate() - backgroundTimer = nil - } - - if let session = try? DependencyInjection.Container.shared.resolve() as SessionType { - guard self.calledStopNetwork == true else { return } - session.start() - self.calledStopNetwork = false - } - } - - public func applicationDidBecomeActive(_ application: UIApplication) { - application.applicationIconBadgeNumber = 0 - coverView?.removeFromSuperview() - } - - public func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey : Any] = [:] - ) -> Bool { - dropboxService.handleOpenUrl(url) - } - - public func application( - _ application: UIApplication, - continue userActivity: NSUserActivity, - restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void - ) -> Bool { - guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let incomingURL = userActivity.webpageURL, - let username = getUsernameFromInvitationDeepLink(incomingURL) else { - return false - } - - let router = try! DependencyInjection.Container.shared.resolve() as PushRouter - router.navigateTo(.search(username: username), {}) - return true - } -} - -func getUsernameFromInvitationDeepLink(_ url: URL) -> String? { - if let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - components.scheme == "https", - components.host == "elixxir.io", - components.path == "/connect", - let queryItem = components.queryItems?.first(where: { $0.name == "username" }), - let username = queryItem.value { - return username - } - - return nil -} - -// MARK: Notifications - -extension AppDelegate: UNUserNotificationCenterDelegate { - public func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) { - let userInfo = response.notification.request.content.userInfo - pushHandler.handleAction(pushRouter, userInfo, completionHandler) - } - - public func application( - _ application: UIApplication, - didReceiveRemoteNotification notification: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - pushHandler.handlePush(notification, completionHandler) - } - - public func application( - _: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - pushHandler.registerToken(deviceToken) - } -} diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift deleted file mode 100644 index 182ac2f5dbe6af2ee532e45b08e9b73d49a1e0a8..0000000000000000000000000000000000000000 --- a/Sources/App/DependencyRegistrator.swift +++ /dev/null @@ -1,272 +0,0 @@ -// MARK: SDK - -import UIKit -import Network -import QuickLook -import MobileCoreServices - -// MARK: Isolated features - -import HUD -import Theme -import Bindings -import XXLogger -import Keychain -import Defaults -import Countries -import Voxophone -import Integration -import Permissions -import PushFeature -import SFTPFeature -import CrashService -import ToastFeature -import iCloudFeature -import CrashReporting -import NetworkMonitor -import DropboxFeature -import VersionChecking -import ReportingFeature -import GoogleDriveFeature -import DependencyInjection - -// MARK: UI Features - -import ScanFeature -import ChatFeature -import MenuFeature -import TermsFeature -import BackupFeature -import SearchFeature -import LaunchFeature -import RestoreFeature -import ContactFeature -import ProfileFeature -import ChatListFeature -import SettingsFeature -import RequestsFeature -import OnboardingFeature -import ContactListFeature - -struct DependencyRegistrator { - static private let container = DependencyInjection.Container.shared - - // MARK: MOCK - - static func registerForMock() { - container.register(XXLogger.noop) - container.register(CrashReporter.noop) - container.register(VersionChecker.mock) - container.register(ReportingStatus.mock()) - container.register(SendReport.mock()) - container.register(XXNetwork<BindingsMock>() as XXNetworking) - container.register(MockNetworkMonitor() as NetworkMonitoring) - container.register(KeyObjectStore.userDefaults) - container.register(MockPushHandler() as PushHandling) - container.register(MockKeychainHandler() as KeychainHandling) - container.register(MockPermissionHandler() as PermissionHandling) - - /// Restore / Backup - - container.register(SFTPService.mock) - container.register(iCloudServiceMock() as iCloudInterface) - container.register(DropboxServiceMock() as DropboxInterface) - container.register(GoogleDriveServiceMock() as GoogleDriveInterface) - - registerCommonDependencies() - } - - // MARK: LIVE - - static func registerForLive() { - container.register(KeyObjectStore.userDefaults) - container.register(XXLogger.live()) - container.register(CrashReporter.live) - container.register(VersionChecker.live()) - container.register(ReportingStatus.live()) - container.register(SendReport.live) - - container.register(XXNetwork<BindingsClient>() as XXNetworking) - container.register(NetworkMonitor() as NetworkMonitoring) - container.register(PushHandler() as PushHandling) - container.register(KeychainHandler() as KeychainHandling) - container.register(PermissionHandler() as PermissionHandling) - - /// Restore / Backup - - container.register(SFTPService.live) - container.register(iCloudService() as iCloudInterface) - container.register(DropboxService() as DropboxInterface) - container.register(GoogleDriveService() as GoogleDriveInterface) - - registerCommonDependencies() - } - - // MARK: COMMON - - static private func registerCommonDependencies() { - 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(HUD()) - container.register(ThemeController() as ThemeControlling) - container.register(ToastController()) - container.register(StatusBarController() as StatusBarStyleControlling) - - // MARK: Coordinators - - container.register( - TermsCoordinator.live( - usernameFactory: OnboardingUsernameController.init(_:), - chatListFactory: ChatListController.init - ) - ) - - container.register( - LaunchCoordinator( - termsFactory: TermsConditionsController.init(_:), - searchFactory: SearchContainerController.init, - requestsFactory: RequestsContainerController.init, - chatListFactory: ChatListController.init, - onboardingFactory: OnboardingStartController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:) - ) as LaunchCoordinating) - - container.register( - BackupCoordinator( - passphraseFactory: BackupPassphraseController.init(_:_:) - ) as BackupCoordinating) - - container.register( - MenuCoordinator( - scanFactory: ScanContainerController.init, - chatsFactory: ChatListController.init, - profileFactory: ProfileController.init, - settingsFactory: SettingsController.init, - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init - ) as MenuCoordinating) - - container.register( - SearchCoordinator( - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - countriesFactory: CountryListController.init(_:) - ) as SearchCoordinating) - - container.register( - ProfileCoordinator( - emailFactory: ProfileEmailController.init, - phoneFactory: ProfilePhoneController.init, - imagePickerFactory: UIImagePickerController.init, - permissionFactory: RequestPermissionController.init, - sideMenuFactory: MenuController.init(_:_:), - countriesFactory: CountryListController.init(_:), - codeFactory: ProfileCodeController.init(_:_:) - ) as ProfileCoordinating) - - container.register( - SettingsCoordinator( - backupFactory: BackupController.init, - advancedFactory: SettingsAdvancedController.init, - accountDeleteFactory: AccountDeleteController.init, - sideMenuFactory: MenuController.init(_:_:) - ) as SettingsCoordinating) - - container.register( - RestoreCoordinator( - successFactory: RestoreSuccessController.init, - chatListFactory: ChatListController.init, - restoreFactory: RestoreController.init(_:_:), - passphraseFactory: RestorePassphraseController.init(_:) - ) as RestoreCoordinating) - - container.register( - ChatCoordinator( - retryFactory: RetrySheetController.init, - webFactory: WebScreen.init(url:), - previewFactory: QLPreviewController.init, - contactFactory: ContactController.init(_:), - imagePickerFactory: UIImagePickerController.init, - permissionFactory: RequestPermissionController.init - ) as ChatCoordinating) - - container.register( - ContactCoordinator( - requestsFactory: RequestsContainerController.init, - singleChatFactory: SingleChatController.init(_:), - imagePickerFactory: UIImagePickerController.init, - nicknameFactory: NicknameController.init(_:_:) - ) as ContactCoordinating) - - container.register( - RequestsCoordinator( - searchFactory: SearchContainerController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:), - nicknameFactory: NicknameController.init(_:_:) - ) as RequestsCoordinating) - - container.register( - OnboardingCoordinator( - emailFactory: OnboardingEmailController.init, - phoneFactory: OnboardingPhoneController.init, - searchFactory: SearchContainerController.init, - welcomeFactory: OnboardingWelcomeController.init, - chatListFactory: ChatListController.init, - termsFactory: TermsConditionsController.init(_:), - usernameFactory: OnboardingUsernameController.init(_:), - restoreListFactory: RestoreListController.init(_:), - successFactory: OnboardingSuccessController.init(_:), - countriesFactory: CountryListController.init(_:), - phoneConfirmationFactory: OnboardingPhoneConfirmationController.init(_:_:), - emailConfirmationFactory: OnboardingEmailConfirmationController.init(_:_:) - ) as OnboardingCoordinating) - - container.register( - ContactListCoordinator( - scanFactory: ScanContainerController.init, - searchFactory: SearchContainerController.init, - newGroupFactory: CreateGroupController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:), - groupDrawerFactory: CreateDrawerController.init(_:_:) - ) as ContactListCoordinating) - - container.register( - ScanCoordinator( - emailFactory: ProfileEmailController.init, - phoneFactory: ProfilePhoneController.init, - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - sideMenuFactory: MenuController.init(_:_:) - ) as ScanCoordinating) - - - container.register( - ChatListCoordinator( - scanFactory: ScanContainerController.init, - searchFactory: SearchContainerController.init, - newGroupFactory: CreateGroupController.init, - contactsFactory: ContactListController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:) - ) as ChatListCoordinating) - } -} diff --git a/Sources/App/PushRouter.swift b/Sources/App/PushRouter.swift deleted file mode 100644 index a2d2d809d54818566c1aec693ed4ad07937c6999..0000000000000000000000000000000000000000 --- a/Sources/App/PushRouter.swift +++ /dev/null @@ -1,52 +0,0 @@ -import UIKit -import PushFeature -import Integration -import ChatFeature -import SearchFeature -import LaunchFeature -import ChatListFeature -import RequestsFeature -import DependencyInjection - -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 _ = try? DependencyInjection.Container.shared.resolve() as SessionType, - !(navigationController.viewControllers.last is SearchContainerController) { - navigationController.setViewControllers([ - ChatListController(), - SearchContainerController(username) - ], animated: true) - } - case .contactChat(id: let id): - if let session = try? DependencyInjection.Container.shared.resolve() as SessionType, - let contact = try? session.dbManager.fetchContacts(.init(id: [id])).first { - navigationController.setViewControllers([ - ChatListController(), - SingleChatController(contact) - ], animated: true) - } - case .groupChat(id: let id): - if let session = try? DependencyInjection.Container.shared.resolve() as SessionType, - let info = try? session.dbManager.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..9c4c392e6b47067a5ca46d226e6ce3ecf623e59e --- /dev/null +++ b/Sources/AppCore/AppDependencies.swift @@ -0,0 +1,185 @@ +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay +import ComposableArchitecture + +public struct AppDependencies { + public var networkMonitor: NetworkMonitor + public var toastManager: ToastManager + public var backupHandler: BackupCallbackHandler + public var hudManager: HUDManager + public var dbManager: DBManager + public var groupRequest: GroupRequestHandler + public var groupMessageHandler: GroupMessageHandler + public var statusBar: StatusBarStylist + 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( + url: FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.elixxir.messenger" + )! + ) + 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(), + backupHandler: .live( + messenger: messenger + ), + hudManager: .live(), + dbManager: dbManager, + groupRequest: .live( + messenger: messenger, + db: dbManager.getDB + ), + groupMessageHandler: .live( + messenger: messenger, + db: dbManager.getDB + ), + statusBar: .live(), + messenger: messenger, + authHandler: .live( + messenger: messenger, + handleRequest: .live( + db: dbManager.getDB, + messenger: messenger, + now: now + ), + handleConfirm: .live(db: dbManager.getDB), + handleReset: .live(db: dbManager.getDB) + ), + backupStorage: .onDisk(), + mainQueue: DispatchQueue.main.eraseToAnyScheduler(), + bgQueue: DispatchQueue(label: "xx-messenger", qos: .userInitiated).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, + backupHandler: .unimplemented, + hudManager: .unimplemented, + dbManager: .unimplemented, + groupRequest: .unimplemented, + groupMessageHandler: .unimplemented, + statusBar: .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> +""" +} + +private enum StoredDummyTrafficKey: DependencyKey { + static var liveValue = Stored<DummyTraffic?>.inMemory() + static var testValue = Stored<DummyTraffic?>.unimplemented() +} + +extension DependencyValues { + public var dummyTraffic: Stored<DummyTraffic?> { + get { self[StoredDummyTrafficKey.self] } + set { self[StoredDummyTrafficKey.self] = newValue } + } +} diff --git a/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..be0ca1047096e8390ce9b98b03a16b8906b878f2 --- /dev/null +++ b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift @@ -0,0 +1,49 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +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..60d86e9a66eb83a792aaa7d01b664fd62aaa47da --- /dev/null +++ b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift @@ -0,0 +1,32 @@ +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.isRecent = true + 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..4ea820682a7c8c5f772e77fba26b05f3aba94496 --- /dev/null +++ b/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift @@ -0,0 +1,54 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +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, + messenger: Messenger, + 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 = .verificationInProgress + dbContact.createdAt = now() + dbContact = try db().saveContact(dbContact) + do { + try messenger.waitForNetwork() + if try messenger.verifyContact(xxContact) { + dbContact.authStatus = .verified + dbContact = try db().saveContact(dbContact) + } else { + try db().deleteContact(dbContact) + } + } catch { + dbContact.authStatus = .verificationFailed + 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/BackupCallbackHandler/BackupCallbackHandler.swift b/Sources/AppCore/BackupCallbackHandler/BackupCallbackHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..d3ddb18feb39077fd2979b0fecc9e65dcd8b1837 --- /dev/null +++ b/Sources/AppCore/BackupCallbackHandler/BackupCallbackHandler.swift @@ -0,0 +1,45 @@ +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +public struct BackupCallbackHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension BackupCallbackHandler { + public static func live( + messenger: Messenger + ) -> BackupCallbackHandler { + BackupCallbackHandler { onError in + let callback = UpdateBackupFunc { data in + do { + let url = try FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + .appendingPathComponent("backup") + .appendingPathExtension("xxm") + try data.write(to: url) + } catch { + onError(error) + } + } + return messenger.registerBackupCallback(callback) + } + } +} + +extension BackupCallbackHandler { + public static let unimplemented = BackupCallbackHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} 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..a3e514ae75ae674e9bf6a10f951fbd4957b6c72e --- /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("xxm_database") + .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/GroupHandlers/GroupMessageHandler.swift b/Sources/AppCore/GroupHandlers/GroupMessageHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..262a53eb914987433dca7cb6a14a3d6f9f1cdb39 --- /dev/null +++ b/Sources/AppCore/GroupHandlers/GroupMessageHandler.swift @@ -0,0 +1,55 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +public struct GroupMessageHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension GroupMessageHandler { + public static func live( + messenger: Messenger, + db: DBManagerGetDB + ) -> GroupMessageHandler { + GroupMessageHandler { onError in + messenger.registerGroupChatProcessor(.init { result in + switch result { + case .success(let callback): + do { + let payload = try MessagePayload.decode(callback.decryptedMessage.payload) + try db().saveMessage(.init( + networkId: callback.decryptedMessage.messageId, + senderId: callback.decryptedMessage.senderId, + recipientId: nil, + groupId: callback.decryptedMessage.groupId, + date: Date.fromTimestamp(Int(callback.decryptedMessage.timestamp)), + status: .received, + isUnread: true, + text: payload.text, + replyMessageId: payload.replyingTo, + roundURL: callback.roundUrl + )) + } catch { + onError(error) + } + case .failure(let error): + onError(error) + } + }) + } + } +} + +extension GroupMessageHandler { + public static let unimplemented = GroupMessageHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Sources/AppCore/GroupHandlers/GroupRequestHandler.swift b/Sources/AppCore/GroupHandlers/GroupRequestHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..14afb9026aaf52338c7b6af7b7c5f8d5b094a268 --- /dev/null +++ b/Sources/AppCore/GroupHandlers/GroupRequestHandler.swift @@ -0,0 +1,99 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +public struct GroupRequestHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension GroupRequestHandler { + public static func live( + messenger: Messenger, + db: DBManagerGetDB + ) -> GroupRequestHandler { + GroupRequestHandler { onError in + messenger.registerGroupRequestHandler(.init { group in + do { + if let _ = try db().fetchGroups(.init(id: [group.getId()])).first { + return + } + guard let leader = try group.getMembership().first else { + return // Failed to get group membership/leader + } + try db().saveGroup(.init( + id: group.getId(), + name: String(data: group.getName(), encoding: .utf8)!, + leaderId: leader.id, + createdAt: Date.fromMSTimestamp(group.getCreatedMS()), + authStatus: .pending, + serialized: group.serialize() + )) + if let initialMessageData = group.getInitMessage(), + let initialMessage = String(data: initialMessageData, encoding: .utf8) { + try db().saveMessage(.init( + senderId: leader.id, + recipientId: nil, + groupId: group.getId(), + date: Date.fromMSTimestamp(group.getCreatedMS()), + status: .received, + isUnread: true, + text: initialMessage + )) + } + let members = try group.getMembership() + let friends = try db().fetchContacts(.init(id: Set(members.map(\.id)), authStatus: [ + .friend, .hidden, .confirming, + .verified, .requested, .requesting, + .verificationInProgress, .requestFailed, + .verificationFailed, .confirmationFailed + ])) + let strangers = Set(members.map(\.id)).subtracting(Set(friends.map(\.id))) + try strangers.forEach { + if let stranger = try? db().fetchContacts(.init(id: [$0])).first { + print(stranger) + } else { + try db().saveContact(.init( + id: $0, + username: "Fetching...", + authStatus: .stranger, + isRecent: false, + isBlocked: false, + isBanned: false, + createdAt: Date.fromMSTimestamp(group.getCreatedMS()) + )) + } + } + try members.map { + XXModels.GroupMember(groupId: group.getId(), contactId: $0.id) + }.forEach { + try db().saveGroupMember($0) + } + let multilookup = try messenger.lookupContacts(ids: strangers.map { $0 }) + for user in multilookup.contacts { + if var foo = try? db().fetchContacts(.init(id: [user.getId()])).first, + let username = try? user.getFact(.username)?.value { + foo.username = username + _ = try? db().saveContact(foo) + } + } + } catch { + onError(error) + } + }) + } + } +} + +extension GroupRequestHandler { + public static let unimplemented = GroupRequestHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} 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..60fc7bf3a56e55125324c3142dea8310e5aa027e --- /dev/null +++ b/Sources/AppCore/Logger/Logger.swift @@ -0,0 +1,51 @@ +import Logging +import Foundation +import XCTestDynamicOverlay + +public struct Logger { + public enum Message: Equatable { + case info(String) + 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 .info(let text): + logger.info( + .init(stringLiteral: text), + file: file, + function: function, + line: line + ) + 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..8103b2674704e57f682006e6e23cd4f263fa8bca --- /dev/null +++ b/Sources/AppCore/MessageListenerHandler/MessageListenerHandler.swift @@ -0,0 +1,51 @@ +import XXModels +import XXClient +import Foundation +import XXMessengerClient +import XCTestDynamicOverlay + +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, + replyMessageId: payload.replyingTo, + 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/AppCore/Models/HUDModel.swift b/Sources/AppCore/Models/HUDModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..8d2679e6e870fa7507d8593ae683ee3b76ab0fcb --- /dev/null +++ b/Sources/AppCore/Models/HUDModel.swift @@ -0,0 +1,39 @@ +import UIKit +import AppResources + +public struct HUDModel { + var title: String? + var content: String? + var actionTitle: String? + var hasDotAnimation: Bool + var isAutoDismissable: Bool + var onTapClosure: (() -> Void)? + + public init( + title: String? = nil, + content: String? = nil, + actionTitle: String? = nil, + hasDotAnimation: Bool = false, + onTapClosure: (() -> Void)? = nil + ) { + self.title = title + self.content = content + self.actionTitle = actionTitle + self.onTapClosure = onTapClosure + self.hasDotAnimation = hasDotAnimation + self.isAutoDismissable = onTapClosure == nil && !hasDotAnimation + } + + public init( + error: Error, + actionTitle: String? = Localized.Hud.Error.action, + onTapClosure: (() -> Void)? = nil + ) { + self.hasDotAnimation = false + self.actionTitle = actionTitle + self.onTapClosure = onTapClosure + self.title = Localized.Hud.Error.title + self.isAutoDismissable = onTapClosure == nil + self.content = error.localizedDescription + } +} diff --git a/Sources/AppCore/Models/MessagePayload.swift b/Sources/AppCore/Models/MessagePayload.swift new file mode 100644 index 0000000000000000000000000000000000000000..7ae774337944fdb62b7c5b862cfdd28d7a3a3295 --- /dev/null +++ b/Sources/AppCore/Models/MessagePayload.swift @@ -0,0 +1,29 @@ +import Foundation + +public struct MessagePayload: Equatable { + public init( + text: String, + replyingTo: Data? = nil + ) { + self.text = text + self.replyingTo = replyingTo + } + + public var text: String + public var replyingTo: Data? +} + +extension MessagePayload: Codable { + enum CodingKeys: String, CodingKey { + case text + case replyingTo + } + + 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/NetworkMonitor/GetNetworkConnType.swift b/Sources/AppCore/NetworkMonitor/GetNetworkConnType.swift new file mode 100644 index 0000000000000000000000000000000000000000..99b460f76069535017a7cb730c3def9e3888bcf6 --- /dev/null +++ b/Sources/AppCore/NetworkMonitor/GetNetworkConnType.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct GetNetworkConnType { + public init(run: @escaping () -> NetworkMonitor.ConnType) { + self.run = run + } + + public var run: () -> NetworkMonitor.ConnType + + public func callAsFunction() -> NetworkMonitor.ConnType { + run() + } +} + +extension GetNetworkConnType { + public static let unimplemented = GetNetworkConnType( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/NetworkMonitor/GetNetworkStatus.swift b/Sources/AppCore/NetworkMonitor/GetNetworkStatus.swift new file mode 100644 index 0000000000000000000000000000000000000000..e8d6cfb053ccfb6801e206d2f6beb6976d157e29 --- /dev/null +++ b/Sources/AppCore/NetworkMonitor/GetNetworkStatus.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct GetNetworkStatus { + public init(run: @escaping () -> NetworkMonitor.Status) { + self.run = run + } + + public var run: () -> NetworkMonitor.Status + + public func callAsFunction() -> NetworkMonitor.Status { + run() + } +} + +extension GetNetworkStatus { + public static let unimplemented = GetNetworkStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/NetworkMonitor/NetworkMonitor.swift b/Sources/AppCore/NetworkMonitor/NetworkMonitor.swift new file mode 100644 index 0000000000000000000000000000000000000000..db79b79d26f877fbd3b04470378a4d434f2e6cfd --- /dev/null +++ b/Sources/AppCore/NetworkMonitor/NetworkMonitor.swift @@ -0,0 +1,96 @@ +import Combine +import Network + +public struct NetworkMonitor { + 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: StartNetworkMonitor + public var update: UpdateNetworkStatus + public var getStatus: GetNetworkStatus + public var connType: GetNetworkConnType + public var observeStatus: ObserveNetworkStatus +} + +extension NetworkMonitor { + public static func live() -> NetworkMonitor { + 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) + }, + getStatus: .init { + guard let xxAvailability = context.xxAvailability.value else { + return .xxNotAvailable + } + return xxAvailability ? .available : .xxNotAvailable + }, + connType: .init { + context.currentConnType.value + }, + observeStatus: .init { + context + .internetAvailability + .combineLatest(context.xxAvailability) + .map { (internet, xx) -> Status in + guard let internet, let xx else { return .unknown} + switch (internet, xx) { + case (true, true): + return .available + case (true, false): + return .xxNotAvailable + case (false, _): + return .internetNotAvailable + } + }.removeDuplicates() + .eraseToAnyPublisher() + } + ) + } +} + +extension NetworkMonitor { + public static let unimplemented = NetworkMonitor( + start: .unimplemented, + update: .unimplemented, + getStatus: .unimplemented, + connType: .unimplemented, + observeStatus: .unimplemented + ) +} diff --git a/Sources/AppCore/NetworkMonitor/ObserveNetworkStatus.swift b/Sources/AppCore/NetworkMonitor/ObserveNetworkStatus.swift new file mode 100644 index 0000000000000000000000000000000000000000..f6df5d96ebc6f4b243fbd8afa84aa5f45c39bec6 --- /dev/null +++ b/Sources/AppCore/NetworkMonitor/ObserveNetworkStatus.swift @@ -0,0 +1,20 @@ +import Combine +import XCTestDynamicOverlay + +public struct ObserveNetworkStatus { + public init(run: @escaping () -> AnyPublisher<NetworkMonitor.Status, Never>) { + self.run = run + } + + public var run: () -> AnyPublisher<NetworkMonitor.Status, Never> + + public func callAsFunction() -> AnyPublisher<NetworkMonitor.Status, Never> { + run() + } +} + +extension ObserveNetworkStatus { + public static let unimplemented = ObserveNetworkStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/AppCore/NetworkMonitor/StartNetworkMonitor.swift b/Sources/AppCore/NetworkMonitor/StartNetworkMonitor.swift new file mode 100644 index 0000000000000000000000000000000000000000..6fa6e45a7f79d810d9ae341293cb44761e03cbb5 --- /dev/null +++ b/Sources/AppCore/NetworkMonitor/StartNetworkMonitor.swift @@ -0,0 +1,20 @@ +import XCTestDynamicOverlay + +public struct StartNetworkMonitor { + public init(run: @escaping () -> Void) { + self.run = run + } + + public var run: () -> Void + + public func callAsFunction() -> Void { + run() + } +} + +extension StartNetworkMonitor { + public static let unimplemented = StartNetworkMonitor( + run: XCTUnimplemented("\(Self.self)") + ) +} + diff --git a/Sources/AppCore/NetworkMonitor/UpdateNetworkStatus.swift b/Sources/AppCore/NetworkMonitor/UpdateNetworkStatus.swift new file mode 100644 index 0000000000000000000000000000000000000000..825945096def2084b6bd9c135bba04291ec60a53 --- /dev/null +++ b/Sources/AppCore/NetworkMonitor/UpdateNetworkStatus.swift @@ -0,0 +1,19 @@ +import XCTestDynamicOverlay + +public struct UpdateNetworkStatus { + public init(run: @escaping (Bool) -> Void) { + self.run = run + } + + public var run: (Bool) -> Void + + public func callAsFunction(_ status: Bool) -> Void { + run(status) + } +} + +extension UpdateNetworkStatus { + public static let unimplemented = UpdateNetworkStatus( + 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/AppCore/RootViewController.swift b/Sources/AppCore/RootViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..2c794a01704c45e4f0e234c736b8b1e4fed8321e --- /dev/null +++ b/Sources/AppCore/RootViewController.swift @@ -0,0 +1,194 @@ +import UIKit +import Combine +import Dependencies + +public final class RootViewController: UIViewController { + @Dependency(\.app.statusBar) var statusBar + @Dependency(\.app.hudManager) var hudManager + @Dependency(\.app.toastManager) var 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 { + statusBar.get() + } + + 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) + + statusBar + .observe() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + UIView.animate(withDuration: 0.2) { + self?.setNeedsStatusBarAppearanceUpdate() + } + }.store(in: &cancellables) + + 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) + + hudManager + .observe() + .receive(on: DispatchQueue.main) + .sink { [unowned self] model in + guard let model else { + guard let hud else { return } + UIView.animate(withDuration: 0.2) { + hud.alpha = 0.0 + } completion: { _ in + hud.removeFromSuperview() + self.hud = nil + } + return + } + add(hudView: HUDView().setup(model: model)) + }.store(in: &cancellables) + } +} + +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() + self.view.layoutIfNeeded() + } completion: { _ in + toastView.isUserInteractionEnabled = true + toastView.removeFromSuperview() + 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, + usingSpringWithDamping: 1, + initialSpringVelocity: 0.5, + options: .curveEaseInOut + ) { + self.view.setNeedsLayout() + 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 } + self.dismiss(toastView: toastView) + } + } + } +} + +extension RootViewController { + private func add(hudView: HUDView) { + if let hud { + 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 + } +} 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..d8ad8813c09b85fc9a6c33ef674680cb8943a2a3 --- /dev/null +++ b/Sources/AppCore/SendMessage/SendMessage.swift @@ -0,0 +1,86 @@ +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?, Data, @escaping OnError, @escaping Completion) -> Void + + public func callAsFunction( + text: String, + replyingTo: Data?, + to recipientId: Data, + onError: @escaping OnError, + completion: @escaping Completion + ) { + run(text, replyingTo, recipientId, onError, completion) + } +} + +extension SendMessage { + public static func live( + messenger: Messenger, + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> SendMessage { + SendMessage { text, replyingTo, 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, + replyMessageId: replyingTo + )) + let payload = MessagePayload(text: message.text, replyingTo: replyingTo) + 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/StatusBar/StatusBarStylist.swift b/Sources/AppCore/StatusBar/StatusBarStylist.swift new file mode 100644 index 0000000000000000000000000000000000000000..7eaa87fdcf0db2ac136179bcc86b11cd718fd7c9 --- /dev/null +++ b/Sources/AppCore/StatusBar/StatusBarStylist.swift @@ -0,0 +1,59 @@ +import UIKit +import Combine +import XCTestDynamicOverlay + +public struct StatusBarStylist { + public var set: SetStyle + public var get: GetStyle + public var observe: ObserveStyle + + public static func live() -> StatusBarStylist { + let styleSubject = CurrentValueSubject<UIStatusBarStyle, Never>(.lightContent) + return .init( + set: .init { styleSubject.send($0) }, + get: .init { styleSubject.value }, + observe: .init { styleSubject.eraseToAnyPublisher() } + ) + } + public static let unimplemented = StatusBarStylist( + set: .unimplemented, + get: .unimplemented, + observe: .unimplemented + ) +} + +public struct GetStyle { + public var run: () -> UIStatusBarStyle + + public func callAsFunction() -> UIStatusBarStyle { + run() + } + + public static let unimplemented = GetStyle( + run: XCTUnimplemented("\(Self.self)") + ) +} + +public struct SetStyle { + public var run: (UIStatusBarStyle) -> Void + + public func callAsFunction(_ style: UIStatusBarStyle) -> Void { + run(style) + } + + public static let unimplemented = SetStyle( + run: XCTUnimplemented("\(Self.self)") + ) +} + +public struct ObserveStyle { + public var run: () -> AnyPublisher<UIStatusBarStyle, Never> + + public func callAsFunction() -> AnyPublisher<UIStatusBarStyle, Never> { + run() + } + + public static let unimplemented = ObserveStyle( + 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/AppCore/UI/HUDView.swift b/Sources/AppCore/UI/HUDView.swift new file mode 100644 index 0000000000000000000000000000000000000000..926e36420b97b084cfa35255c981885cbf240eb4 --- /dev/null +++ b/Sources/AppCore/UI/HUDView.swift @@ -0,0 +1,81 @@ +import UIKit +import Shared +import Combine +import SnapKit +import AppResources + +final class HUDView: UIView { + let titleLabel = UILabel() + let contentLabel = UILabel() + let stackView = UIStackView() + let backgroundView = UIView() + let actionButton = CapsuleButton() + let animationView = DotAnimation() + var cancellables = Set<AnyCancellable>() + + init() { + super.init(frame: .zero) + stackView.spacing = 20 + stackView.axis = .vertical + + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralWhite.color + titleLabel.font = Fonts.Mulish.bold.font(size: 30.0) + + contentLabel.numberOfLines = 0 + contentLabel.textAlignment = .center + contentLabel.textColor = Asset.neutralWhite.color + contentLabel.font = Fonts.Mulish.regular.font(size: 15.0) + + animationView.setColor(Asset.neutralWhite.color) + backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.9) + + addSubview(backgroundView) + backgroundView.addSubview(stackView) + + backgroundView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.left.equalToSuperview().offset(30) + $0.right.equalToSuperview().offset(-30) + } + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + $0.bottom.equalToSuperview().offset(-20) + } + } + + required init?(coder: NSCoder) { nil } + + func setup(model: HUDModel) -> Self { + if let title = model.title { + titleLabel.text = title + stackView.addArrangedSubview(titleLabel) + } + if let content = model.content { + contentLabel.text = content + stackView.addArrangedSubview(contentLabel) + } + if model.hasDotAnimation { + animationView.snp.makeConstraints { + $0.height.equalTo(20) + } + stackView.addArrangedSubview(animationView) + } + if let actionTitle = model.actionTitle { + actionButton.set( + style: .seeThroughWhite, + title: actionTitle + ) + actionButton + .publisher(for: .touchUpInside) + .sink { model.onTapClosure?() } + .store(in: &cancellables) + stackView.addArrangedSubview(actionButton) + } + return self + } +} diff --git a/Sources/AppCore/UI/ToastView.swift b/Sources/AppCore/UI/ToastView.swift new file mode 100644 index 0000000000000000000000000000000000000000..a5202803704d20d44720f87a78c5149fd49c4291 --- /dev/null +++ b/Sources/AppCore/UI/ToastView.swift @@ -0,0 +1,78 @@ +import UIKit +import Combine +import AppResources + +final class ToastView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let leftImageView = UIImageView() + let rightButton = UIButton() + 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/AppFeature/AppDelegate.swift b/Sources/AppFeature/AppDelegate.swift new file mode 100644 index 0000000000000000000000000000000000000000..4d8769464d0db69ad9c684c510c2f3753ea05c8c --- /dev/null +++ b/Sources/AppFeature/AppDelegate.swift @@ -0,0 +1,254 @@ +import UIKit +import AppCore +import Defaults +import XXClient +import Dependencies +import LaunchFeature +import XXMessengerClient + +// MARK: - TO REMOVE FROM PRODUCTION: +import Logging +import PulseUI +import AppNavigation +import PulseLogHandler +// MARK: - + +public class AppDelegate: UIResponder, UIApplicationDelegate { + public var window: UIWindow? + private var coverView: UIView? + private var backgroundTimer: Timer? + private var backgroundTask: UIBackgroundTaskIdentifier? + + @Dependency(\.app.log) var log + @Dependency(\.navigator) var navigator + @Dependency(\.app.messenger) var messenger + @Dependency(\.pushNotificationRouter) var pushNotificationRouter + + @KeyObject(.hideAppList, defaultValue: false) var shouldHideAppInAppList + @KeyObject(.pushNotifications, defaultValue: false) var isPushNotificationsEnabled + + public func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + LoggingSystem.bootstrap(PersistentLogHandler.init) + + UNUserNotificationCenter.current().delegate = self + + let navController = UINavigationController(rootViewController: LaunchController()) + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = RootViewController(navController) + window?.makeKeyAndVisible() + + pushNotificationRouter.set(.live(navigationController: navController)) + +// #if DEBUG + NotificationCenter.default.addObserver( + forName: UIApplication.userDidTakeScreenshotNotification, + object: nil, + queue: OperationQueue.main + ) { [weak self] _ in + guard let self else { return } + let pulseViewController = PulseUI.MainViewController(store: .shared) + self.navigator.perform( + PresentModal( + pulseViewController, + from: navController.topViewController! + ) + ) + } +// #endif + + return true + } + + public func applicationWillResignActive(_ application: UIApplication) { + if shouldHideAppInAppList { + coverView?.removeFromSuperview() + coverView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + coverView?.frame = window?.bounds ?? .zero + window?.addSubview(coverView!) + } + } + + public func applicationDidBecomeActive(_ application: UIApplication) { + application.applicationIconBadgeNumber = 0 + coverView?.removeFromSuperview() + } + + public func applicationWillEnterForeground(_ application: UIApplication) { + resumeMessenger(application) + } + + public func applicationDidEnterBackground(_ application: UIApplication) { + stopMessenger(application) + } + + public func application( + application: UIApplication, + shouldAllowExtensionPointIdentifier identifier: String + ) -> Bool { + if identifier == UIApplication.ExtensionPointIdentifier.keyboard.rawValue { + return false /// Disable custom keyboards + } + return true + } + + public func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let incomingURL = userActivity.webpageURL, + let username = getUsernameFromInvitationDeepLink(incomingURL), + let router = pushNotificationRouter.get() else { + return false + } + + router.navigateTo(.search(username: username), {}) + return true + } +} + +extension AppDelegate: UNUserNotificationCenterDelegate { + public func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + if messenger.isConnected() { + do { + try messenger.registerForNotifications(token: deviceToken) + isPushNotificationsEnabled = true + } catch { + isPushNotificationsEnabled = false + log(.error(error as NSError)) + print(error.localizedDescription) + } + } + } + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + let userInfo = response.notification.request.content.userInfo + guard let string = userInfo["type"] as? String, + let type = NotificationReport.ReportType(rawValue: string) else { + completionHandler() + return + } + var route: PushNotificationRouter.Route? + switch type { + case .e2e, .group: + guard let source = userInfo["source"] as? Data else { + completionHandler() + return + } + if type == .e2e { + route = .contactChat(id: source) + } else { + route = .groupChat(id: source) + } + default: + break + } + + if let route, let router = pushNotificationRouter.get() { + router.navigateTo(route, completionHandler) + } + } + + public func application( + _ application: UIApplication, + didReceiveRemoteNotification notification: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + if application.applicationState == .background, + let csv = notification["notificationData"] as? String, + let reports = try? messenger.getNotificationReports(notificationCSV: csv) { + reports + .filter { $0.forMe } + .filter { $0.type != .silent } + .filter { $0.type != .default } + .map { + let content = UNMutableNotificationContent() + content.badge = 1 + content.body = "" + content.sound = .default + content.userInfo["source"] = $0.source + content.userInfo["type"] = $0.type.rawValue + content.threadIdentifier = "new_message_identifier" + return content + }.map { + UNNotificationRequest( + identifier: Bundle.main.bundleIdentifier!, + content: $0, + trigger: UNTimeIntervalNotificationTrigger( + timeInterval: 1, + repeats: false + ) + ) + }.forEach { + UNUserNotificationCenter.current().add($0) { error in + error == nil ? completionHandler(.newData) : completionHandler(.failed) + } + } + } else { + completionHandler(.noData) + } + } +} + +extension AppDelegate { + private func resumeMessenger(_ application: UIApplication) { + backgroundTimer?.invalidate() + backgroundTimer = nil + if let backgroundTask { + application.endBackgroundTask(backgroundTask) + } + do { + if messenger.isLoaded() { + try messenger.start() + } + } catch { + log(.error(error as NSError)) + print(error.localizedDescription) + } + } + + private func stopMessenger(_ application: UIApplication) { + guard messenger.isLoaded() else { return } + + backgroundTask = application.beginBackgroundTask(withName: "STOPPING_NETWORK") + backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in + guard let self else { return } + + if application.backgroundTimeRemaining <= 5 { + do { + self.backgroundTimer?.invalidate() + try self.messenger.stop() + } catch { + self.log(.error(error as NSError)) + print(error.localizedDescription) + } + if let backgroundTask = self.backgroundTask { + application.endBackgroundTask(backgroundTask) + } + } + } + } +} + +func getUsernameFromInvitationDeepLink(_ url: URL) -> String? { + if let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + components.scheme == "https", + components.host == "elixxir.io", + components.path == "/connect", + let queryItem = components.queryItems?.first(where: { $0.name == "username" }), + let username = queryItem.value { + return username + } + return nil +} diff --git a/Sources/AppFeature/Dependencies.swift b/Sources/AppFeature/Dependencies.swift new file mode 100644 index 0000000000000000000000000000000000000000..16f2bf788c5f2c81625012d6805eeec163f77f24 --- /dev/null +++ b/Sources/AppFeature/Dependencies.swift @@ -0,0 +1,148 @@ +import ScanFeature +import ChatFeature +import MenuFeature +import TermsFeature +import Dependencies +import AppNavigation +import BackupFeature +import DrawerFeature +import SearchFeature +import RestoreFeature +import ContactFeature +import WebsiteFeature +import ProfileFeature +import ChatListFeature +import SettingsFeature +import RequestsFeature +import GroupDraftFeature +import OnboardingFeature +import CountryListFeature +import CreateGroupFeature +import ContactListFeature +import RequestPermissionFeature + +extension NavigatorKey: DependencyKey { + public static let liveValue: Navigator = CombinedNavigator( + PresentModalNavigator(), + DismissModalNavigator(), + PushNavigator(), + PopToRootNavigator(), + PopToNavigator(), + SetStackNavigator(), + OpenUpNavigator(), + OpenLeftNavigator(), + + PresentPhotoLibraryNavigator(), + PresentActivitySheetNavigator(), + + PresentWebsiteNavigator( + WebsiteController.init(_:) + ), + PresentCreateGroupNavigator( + CreateGroupController.init(_:) + ), + PresentGroupDraftNavigator( + GroupDraftController.init + ), + PresentMenuNavigator( + MenuController.init(_:_:) + ), + PresentProfileNavigator( + ProfileController.init + ), + PresentChatListNavigator( + ChatListController.init + ), + PresentDrawerNavigator( + DrawerController.init(_:) + ), + PresentScanNavigator( + ScanContainerController.init + ), + PresentChatNavigator( + SingleChatController.init(_:) + ), + PresentContactNavigator( + ContactController.init(_:) + ), + PresentSettingsNavigator( + SettingsMainController.init + ), + PresentSettingsBackupNavigator( + BackupController.init + ), + PresentRestoreListNavigator( + RestoreListController.init + ), + PresentContactListNavigator( + ContactListController.init + ), + PresentGroupChatNavigator( + GroupChatController.init(_:) + ), + PresentProfileEmailNavigator( + ProfileEmailController.init + ), + PresentProfilePhoneNavigator( + ProfilePhoneController.init + ), + PresentSearchNavigator( + ChatListController.init, + SearchContainerController.init(_:) + ), + PresentRequestsNavigator( + RequestsContainerController.init + ), + PresentCountryListNavigator( + CountryListController.init(_:) + ), + PresentOnboardingEmailNavigator( + OnboardingEmailController.init + ), + PresentOnboardingPhoneNavigator( + OnboardingPhoneController.init + ), + PresentProfileCodeNavigator( + ProfileCodeController.init(_:_:_:) + ), + PresentOnboardingStartNavigator( + OnboardingStartController.init + ), + PresentSettingsAdvancedNavigator( + SettingsAdvancedController.init + ), + PresentTermsAndConditionsNavigator( + TermsConditionsController.init + ), + PresentPermissionRequestNavigator( + RequestPermissionController.init + ), + PresentOnboardingWelcomeNavigator( + OnboardingWelcomeController.init + ), + PresentSettingsAccountDeleteNavigator( + SettingsDeleteController.init + ), + PresentOnboardingUsernameNavigator( + OnboardingUsernameController.init + ), + PresentOnboardingCodeNavigator( + OnboardingCodeController.init(_:_:_:) + ) + ) +} + +import LaunchFeature +import XXMessengerClient + +private enum PushNotificationRouterKey: DependencyKey { + static var liveValue = Stored<PushNotificationRouter?>.inMemory() + static var testValue = Stored<PushNotificationRouter?>.unimplemented() +} + +extension DependencyValues { + public var pushNotificationRouter: Stored<PushNotificationRouter?> { + get { self[PushNotificationRouterKey.self] } + set { self[PushNotificationRouterKey.self] = newValue } + } +} diff --git a/Sources/AppFeature/PushNotificationRouter.swift b/Sources/AppFeature/PushNotificationRouter.swift new file mode 100644 index 0000000000000000000000000000000000000000..0ff84204ecacef4c9e12684c3678d33b555f5a48 --- /dev/null +++ b/Sources/AppFeature/PushNotificationRouter.swift @@ -0,0 +1,57 @@ +import UIKit +import Dependencies +import AppNavigation + +import ChatFeature +import LaunchFeature +import SearchFeature +import ChatListFeature +import RequestsFeature + +extension PushNotificationRouter { + public static func live(navigationController: UINavigationController) -> PushNotificationRouter { + PushNotificationRouter { route, completion in + @Dependency(\.navigator) var navigator + @Dependency(\.app.dbManager) var dbManager + + if let launchController = navigationController.viewControllers.last as? LaunchController { + launchController.pendingPushNotificationRoute = route + } else { + switch route { + case .requests: + if !(navigationController.viewControllers.last is RequestsContainerController) { + navigator.perform(PresentRequests(on: navigationController)) + } + + case .search(username: let username): + if !(navigationController.viewControllers.last is SearchContainerController) { + navigator.perform(PresentSearch( + searching: username, + fromOnboarding: true, + on: navigationController, + animated: true + )) + } else { + (navigationController.viewControllers.last as? SearchContainerController)? + .startSearchingFor(username) + } + + case .contactChat(id: let id): + if let contact = try? dbManager.getDB().fetchContacts(.init(id: [id])).first { + navigator.perform(SetStack([ + ChatListController(), SingleChatController(contact) + ], on: navigationController)) + } + + case .groupChat(id: let id): + if let groupInfo = try? dbManager.getDB().fetchGroupInfos(.init(groupId: id)).first { + navigator.perform(SetStack([ + ChatListController(), GroupChatController(groupInfo) + ], on: navigationController)) + } + } + } + completion() + } + } +} diff --git a/Sources/AppNavigation/Action.swift b/Sources/AppNavigation/Action.swift new file mode 100644 index 0000000000000000000000000000000000000000..ff697eeb21a0937ded7ee2c33961f8cde8592c69 --- /dev/null +++ b/Sources/AppNavigation/Action.swift @@ -0,0 +1,2 @@ +/// Navigation action +public protocol Action {} diff --git a/Sources/AppNavigation/CombinedNavigator.swift b/Sources/AppNavigation/CombinedNavigator.swift new file mode 100644 index 0000000000000000000000000000000000000000..fdbb1741cfe97da7c9295797f57711bfadf17553 --- /dev/null +++ b/Sources/AppNavigation/CombinedNavigator.swift @@ -0,0 +1,30 @@ +/// Combines multiple navigators into a single one +/// +/// - Action is performed using the first navigator that can handle it +/// - When there is no navigator that can handle given action, assertion is thrown +/// - When there are multiple navigators that can handle given action, assertion is thrown +public struct CombinedNavigator: Navigator { + public init(_ navigators: Navigator...) { + self.navigators = navigators + } + + public init(_ navigators: [Navigator]) { + self.navigators = navigators + } + + public func perform(_ action: Action, completion: @escaping () -> Void) { + let navigators = self.navigators.filter { $0.canPerform(action) } + guard let firstNavigator = navigators.first else { + assertionFailure("No navigator to perform action: \(action)", #file, #line) + return + } + guard navigators.count == 1 else { + assertionFailure("Multiple navigators can perform action: \(action), \(navigators)", #file, #line) + return + } + firstNavigator.perform(action, completion: completion) + } + + let navigators: [Navigator] + var assertionFailure: (@autoclosure () -> String, StaticString, UInt) -> Void = Swift.assertionFailure +} diff --git a/Sources/AppNavigation/CustomTransitions/BottomTransition.swift b/Sources/AppNavigation/CustomTransitions/BottomTransition.swift new file mode 100644 index 0000000000000000000000000000000000000000..66b422c1f833a2b6690cea8947ee07f9481ba070 --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/BottomTransition.swift @@ -0,0 +1,136 @@ +import UIKit +import Combine + +final class BottomTransition: NSObject, UIViewControllerAnimatedTransitioning { + enum Direction { + case present + case dismiss + } + + let isDismissableOnBackground: Bool + var direction: Direction = .present + private let onDismissal: (() -> Void)? + private weak var darkOverlayView: UIControl? + private weak var topConstraint: NSLayoutConstraint? + private weak var bottomConstraint: NSLayoutConstraint? + private var cancellables = Set<AnyCancellable>() + + private var presentedConstraints: [NSLayoutConstraint] = [] + private var dismissedConstraints: [NSLayoutConstraint] = [] + private var presentingController: UIViewController? + + init( + _ isDismissableOnBackground: Bool = true, + onDismissal: (() -> Void)? + ) { + self.onDismissal = onDismissal + self.isDismissableOnBackground = isDismissableOnBackground + 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 + + if isDismissableOnBackground { + darkOverlayView.addTarget(self, action: #selector(didTapOverlay), for: .touchUpInside) + self.presentingController = presentingController + } + + 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?() + } + ) + } + + @objc private func didTapOverlay() { + if let presentingController, isDismissableOnBackground { + presentingController.dismiss(animated: true) + } + } +} diff --git a/Sources/AppNavigation/CustomTransitions/BottomTransitioningDelegate.swift b/Sources/AppNavigation/CustomTransitions/BottomTransitioningDelegate.swift new file mode 100644 index 0000000000000000000000000000000000000000..b30e549738b953c01702f1ca6897b6f50899fa9c --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/BottomTransitioningDelegate.swift @@ -0,0 +1,25 @@ +import UIKit + +final class BottomTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { + var isDismissableOnBackgroundTouch: Bool = true + private var transition: BottomTransition? + + func animationController( + forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + transition = BottomTransition(isDismissableOnBackgroundTouch) { [weak self] in + guard let self else { return } + self.transition = nil + } + return transition + } + + func animationController( + forDismissed dismissed: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + transition?.direction = .dismiss + return transition + } +} diff --git a/Sources/AppNavigation/CustomTransitions/LeftAnimator.swift b/Sources/AppNavigation/CustomTransitions/LeftAnimator.swift new file mode 100644 index 0000000000000000000000000000000000000000..6944551740e8cd93364a9ddf06e2cc8adac38b0d --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/LeftAnimator.swift @@ -0,0 +1,23 @@ +import UIKit + +protocol LeftAnimating { + func animate(in containerView: UIView, to progress: CGFloat) +} + +struct LeftAnimator: LeftAnimating { + func animate(in containerView: UIView, to progress: CGFloat) { + guard let fromView = containerView.viewWithTag(LeftPresentTransition.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/AppNavigation/CustomTransitions/LeftDismissInteractor.swift b/Sources/AppNavigation/CustomTransitions/LeftDismissInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..c7d7f7868da64721df0ebb52249c7807813f64bb --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/LeftDismissInteractor.swift @@ -0,0 +1,69 @@ +import UIKit + +protocol LeftDismissInteracting: UIViewControllerInteractiveTransitioning { + var interactionInProgress: Bool { get } + func setup(view: UIView, action: @escaping (() -> Void)) +} + +final class LeftDismissInteractor: + UIPercentDrivenInteractiveTransition, LeftDismissInteracting { + private var action: (() -> Void)? + public var interactionInProgress = false + private var shouldFinishTransition = false + + func setup(view: UIView, action: @escaping (() -> Void)) { + view.addGestureRecognizer(UIPanGestureRecognizer( + target: self, + action: #selector(handlePanGesture(_:)) + )) + view.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(handleTapGesture(_:)) + )) + self.action = action + } + + @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/AppNavigation/CustomTransitions/LeftDismissTransition.swift b/Sources/AppNavigation/CustomTransitions/LeftDismissTransition.swift new file mode 100644 index 0000000000000000000000000000000000000000..001ccf8ace53ab9fa411eb2a6014cfe6acaf36b9 --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/LeftDismissTransition.swift @@ -0,0 +1,34 @@ +import UIKit + +final class LeftDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { + let menuAnimator: LeftAnimating + let viewAnimator: UIViewAnimating.Type + + init( + menuAnimator: LeftAnimating, + viewAnimator: UIViewAnimating.Type + ) { + self.menuAnimator = menuAnimator + self.viewAnimator = viewAnimator + super.init() + } + + 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/AppNavigation/CustomTransitions/LeftPresentTransition.swift b/Sources/AppNavigation/CustomTransitions/LeftPresentTransition.swift new file mode 100644 index 0000000000000000000000000000000000000000..3af434fff9f4d7248e28c44ba56d00c93b2e6ad1 --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/LeftPresentTransition.swift @@ -0,0 +1,68 @@ +import UIKit + +final class LeftPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { + let dismissInteractor: LeftDismissInteracting + let menuAnimator: LeftAnimating + let viewAnimator: UIViewAnimating.Type + + static let fromViewTag = UUID().hashValue + + init( + dismissInteractor: LeftDismissInteracting, + menuAnimator: LeftAnimating, + viewAnimator: UIViewAnimating.Type + ) { + self.dismissInteractor = dismissInteractor + self.menuAnimator = menuAnimator + self.viewAnimator = viewAnimator + super.init() + } + + 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/AppNavigation/CustomTransitions/LeftTransitioningDelegate.swift b/Sources/AppNavigation/CustomTransitions/LeftTransitioningDelegate.swift new file mode 100644 index 0000000000000000000000000000000000000000..7b918587e8b49a67227fd4d04a9a08aec095a979 --- /dev/null +++ b/Sources/AppNavigation/CustomTransitions/LeftTransitioningDelegate.swift @@ -0,0 +1,55 @@ +import UIKit + +protocol UIViewAnimating { + static func animate( + withDuration duration: TimeInterval, + animations: @escaping (() -> Void), + completion: ((Bool) -> Void)? + ) +} + +extension UIView: UIViewAnimating {} + +final class LeftTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { + let menuAnimator: LeftAnimating + let viewAnimator: UIViewAnimating.Type + let dismissInteractor: LeftDismissInteracting + + init( + dismissInteractor: LeftDismissInteracting = LeftDismissInteractor(), + menuAnimator: LeftAnimating = LeftAnimator(), + viewAnimator: UIViewAnimating.Type = UIView.self + ) { + self.dismissInteractor = dismissInteractor + self.menuAnimator = menuAnimator + self.viewAnimator = viewAnimator + super.init() + } + + func animationController( + forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + LeftPresentTransition( + dismissInteractor: dismissInteractor, + menuAnimator: menuAnimator, + viewAnimator: viewAnimator + ) + } + + func animationController( + forDismissed dismissed: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + LeftDismissTransition( + menuAnimator: menuAnimator, + viewAnimator: viewAnimator + ) + } + + func interactionControllerForDismissal( + using animator: UIViewControllerAnimatedTransitioning + ) -> UIViewControllerInteractiveTransitioning? { + dismissInteractor.interactionInProgress ? dismissInteractor : nil + } +} diff --git a/Sources/AppNavigation/Dependency.swift b/Sources/AppNavigation/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..c11e4ce52060f110c1a42a5436eed5a3bde96740 --- /dev/null +++ b/Sources/AppNavigation/Dependency.swift @@ -0,0 +1,26 @@ +import Dependencies +import XCTestDynamicOverlay + +public enum NavigatorKey: TestDependencyKey { + public static let testValue: Navigator = UnimplementedNavigator() +} + +public extension DependencyValues { + var navigator: Navigator { + get { self[NavigatorKey.self] } + set { self[NavigatorKey.self] = newValue } + } +} + +public struct UnimplementedNavigator: Navigator { + public init() {} + + public func perform(_ action: 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/AppNavigation/Generic/DismissModal.swift b/Sources/AppNavigation/Generic/DismissModal.swift new file mode 100644 index 0000000000000000000000000000000000000000..5511e991db9441133e56b4336bfe71eb5a6e4591 --- /dev/null +++ b/Sources/AppNavigation/Generic/DismissModal.swift @@ -0,0 +1,33 @@ +import UIKit + +/// Dismiss view controller presented from provided view controller +public struct DismissModal: Action { + /// - Parameters: + /// - parent: Parent view controller from which dismiss happens + /// - animated: Animate the transition + public init( + from parent: UIViewController, + animated: Bool = true + ) { + self.parent = parent + self.animated = animated + } + + /// Parent view controller from which dismiss happens + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `DismissModal` action +public struct DismissModalNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: DismissModal, completion: @escaping () -> Void) { + action.parent.dismiss( + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/Generic/OpenLeft.swift b/Sources/AppNavigation/Generic/OpenLeft.swift new file mode 100644 index 0000000000000000000000000000000000000000..90302e792da6cc98fa2456e800820b4d4e00088b --- /dev/null +++ b/Sources/AppNavigation/Generic/OpenLeft.swift @@ -0,0 +1,45 @@ +import UIKit + +/// Open left view controller on provided parent view controller +public struct OpenLeft: Action { + /// - Parameters: + /// - viewController: View controller to present + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + _ viewController: UIViewController, + from parent: UIViewController, + animated: Bool = true + ) { + self.viewController = viewController + self.parent = parent + self.animated = animated + } + + /// View controller to present + public var viewController: UIViewController + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `OpenLeft` action +public struct OpenLeftNavigator: TypedNavigator { + let transitioningDelegate = LeftTransitioningDelegate() + + public init() {} + + public func perform(_ action: OpenLeft, completion: @escaping () -> Void) { + action.viewController.transitioningDelegate = transitioningDelegate + action.viewController.modalPresentationStyle = .overFullScreen + + action.parent.present( + action.viewController, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/Generic/OpenUp.swift b/Sources/AppNavigation/Generic/OpenUp.swift new file mode 100644 index 0000000000000000000000000000000000000000..dfcedc3fafa521134e3ca871e803538503b43f02 --- /dev/null +++ b/Sources/AppNavigation/Generic/OpenUp.swift @@ -0,0 +1,52 @@ +import UIKit + +/// Open up view controller on provided parent view controller +public struct OpenUp: Action { + /// - Parameters: + /// - viewController: View controller to present + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + /// - dismissable: Dismissable upon background touch flag + public init( + _ viewController: UIViewController, + from parent: UIViewController, + animated: Bool = true, + dismissable: Bool = true + ) { + self.viewController = viewController + self.parent = parent + self.animated = animated + self.dismissable = dismissable + } + + /// View controller to present + public var viewController: UIViewController + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool + + /// Dismissable upon background touch flag + public var dismissable: Bool +} + +/// Performs `OpenUp` action +public struct OpenUpNavigator: TypedNavigator { + let transitioningDelegate = BottomTransitioningDelegate() + + public init() {} + + public func perform(_ action: OpenUp, completion: @escaping () -> Void) { + transitioningDelegate.isDismissableOnBackgroundTouch = action.dismissable + action.viewController.transitioningDelegate = transitioningDelegate + action.viewController.modalPresentationStyle = .overFullScreen + + action.parent.present( + action.viewController, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/Generic/PopTo.swift b/Sources/AppNavigation/Generic/PopTo.swift new file mode 100644 index 0000000000000000000000000000000000000000..15af276aa8f70d29b7a3266be72555c0bd69bbb9 --- /dev/null +++ b/Sources/AppNavigation/Generic/PopTo.swift @@ -0,0 +1,44 @@ +import UIKit + +/// Pop to the view controller on a given navigation controller +public struct PopTo: Action { + /// - Parameters: + /// - viewController: View controller to which should pop + /// - navigationController: Navigation controller on which pop should happen + /// - animated: Animate the transition + public init( + _ viewController: UIViewController, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.viewController = viewController + self.navigationController = navigationController + self.animated = animated + } + + /// View controller to which should pop + public var viewController: UIViewController + + /// Navigation controller on which pop should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PopTo` action +public struct PopToNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: PopTo, completion: @escaping () -> Void) { + action.navigationController.popToViewController( + action.viewController, + animated: action.animated + ) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/Generic/PopToRoot.swift b/Sources/AppNavigation/Generic/PopToRoot.swift new file mode 100644 index 0000000000000000000000000000000000000000..5918a3476f9ba0a23d6413173dc764e7df7a7052 --- /dev/null +++ b/Sources/AppNavigation/Generic/PopToRoot.swift @@ -0,0 +1,35 @@ +import UIKit + +/// Pops to root view controller on a given navigation controller +public struct PopToRoot: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which pop should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which pop should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PopToRoot` action +public struct PopToRootNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: PopToRoot, completion: @escaping () -> Void) { + action.navigationController.popToRootViewController(animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/Generic/PresentModal.swift b/Sources/AppNavigation/Generic/PresentModal.swift new file mode 100644 index 0000000000000000000000000000000000000000..23db172f067f1131a7e557f70c356c00c27d70cd --- /dev/null +++ b/Sources/AppNavigation/Generic/PresentModal.swift @@ -0,0 +1,40 @@ +import UIKit + +/// Present view controller on provided parent view controller +public struct PresentModal: Action { + /// - Parameters: + /// - viewController: View controller to present + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + _ viewController: UIViewController, + from parent: UIViewController, + animated: Bool = true + ) { + self.viewController = viewController + self.parent = parent + self.animated = animated + } + + /// View controller to present + public var viewController: UIViewController + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentModal` action +public struct PresentModalNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: PresentModal, completion: @escaping () -> Void) { + action.parent.present( + action.viewController, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/Generic/Push.swift b/Sources/AppNavigation/Generic/Push.swift new file mode 100644 index 0000000000000000000000000000000000000000..a0a01ea670a9bfe255e128b30547eeb79dfbcca6 --- /dev/null +++ b/Sources/AppNavigation/Generic/Push.swift @@ -0,0 +1,41 @@ +import UIKit + +/// Push view controller on a given navigation controller +public struct Push: Action { + /// - Parameters: + /// - viewController: View controller to which should be pushed + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + _ viewController: UIViewController, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.viewController = viewController + self.navigationController = navigationController + self.animated = animated + } + + /// View controller to which should be pushed + public var viewController: UIViewController + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `Push` action +public struct PushNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: Push, completion: @escaping () -> Void) { + action.navigationController.pushViewController(action.viewController, animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/Generic/SetStack.swift b/Sources/AppNavigation/Generic/SetStack.swift new file mode 100644 index 0000000000000000000000000000000000000000..23ca3a679bf6ddbdb6869aff208a776172161594 --- /dev/null +++ b/Sources/AppNavigation/Generic/SetStack.swift @@ -0,0 +1,44 @@ +import UIKit + +/// Sets view controllers on a given navigation controller +public struct SetStack: Action { + /// - Parameters: + /// - viewControllers: View controllers that should be set + /// - navigationController: Navigation controller on which view controllers should be set + /// - animated: Animate the transition + public init( + _ viewControllers: [UIViewController], + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.viewControllers = viewControllers + self.navigationController = navigationController + self.animated = animated + } + + /// View controllers that should be set + public var viewControllers: [UIViewController] + + /// Navigation controller on which view controllers should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `SetStack` action +public struct SetStackNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: SetStack, completion: @escaping () -> Void) { + action.navigationController.setViewControllers( + action.viewControllers, + animated: action.animated + ) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/Navigator.swift b/Sources/AppNavigation/Navigator.swift new file mode 100644 index 0000000000000000000000000000000000000000..c37ca8d190ffd9f19fb667dcf37fd1718a9f9e64 --- /dev/null +++ b/Sources/AppNavigation/Navigator.swift @@ -0,0 +1,23 @@ +/// Navigator that performs a navigation action +public protocol Navigator { + /// Returns true if the navigator can perform the action + /// - Default implementation returns true for any action + /// - Parameter action: navigation action + func canPerform(_ action: Action) -> Bool + + /// Performs the navigation action + /// - Parameters: + /// - action: navigation action + /// - completion: closure that will be executed after performing the action + func perform(_ action: Action, completion: @escaping () -> Void) +} + +public extension Navigator { + func canPerform(_ action: Action) -> Bool { true } + + /// Performs the navigation action with empty completion closure + /// - Parameter action: navigation action + func perform(_ action: Action) { + perform(action, completion: {}) + } +} diff --git a/Sources/AppNavigation/PresentActivitySheet.swift b/Sources/AppNavigation/PresentActivitySheet.swift new file mode 100644 index 0000000000000000000000000000000000000000..1ba69910f41607fc78fd86d99f75ee46f02a6de7 --- /dev/null +++ b/Sources/AppNavigation/PresentActivitySheet.swift @@ -0,0 +1,43 @@ +import UIKit + +/// Presents `UIActivityViewController` on a given parent view controller +public struct PresentActivitySheet: Action { + /// - Parameters: + /// - items: Items to be displayed at the activity sheet controller + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + items: [Any], + from parent: UIViewController, + animated: Bool = true + ) { + self.items = items + self.parent = parent + self.animated = animated + } + + /// Items to be displayed at the activity sheet controller + public var items: [Any] + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentActivitySheet` action +public struct PresentActivitySheetNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: PresentActivitySheet, completion: @escaping () -> Void) { + action.parent.present( + UIActivityViewController( + activityItems: action.items, + applicationActivities: nil + ), + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentCamera.swift b/Sources/AppNavigation/PresentCamera.swift new file mode 100644 index 0000000000000000000000000000000000000000..f5fa18ef1f7f2446a637e0a35fadc906590b3534 --- /dev/null +++ b/Sources/AppNavigation/PresentCamera.swift @@ -0,0 +1,37 @@ +import UIKit + +/// Presents `Camera` on provided parent view controller +public struct PresentCamera: Action { + /// - Parameters: + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + from parent: UIViewController, + animated: Bool = true + ) { + self.parent = parent + self.animated = animated + } + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentCamera` action +public struct PresentCameraNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: PresentCamera, completion: @escaping () -> Void) { + let imagePickerController = UIImagePickerController() + imagePickerController.sourceType = .camera + imagePickerController.delegate = action.parent as? UIImagePickerControllerDelegate & UINavigationControllerDelegate + action.parent.present( + imagePickerController, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentChat.swift b/Sources/AppNavigation/PresentChat.swift new file mode 100644 index 0000000000000000000000000000000000000000..ef6ab52f5e7d00be89a1d0b5b329aa7496727e7a --- /dev/null +++ b/Sources/AppNavigation/PresentChat.swift @@ -0,0 +1,49 @@ +import UIKit +import XXModels + +/// Pushes `Chat` on a given navigation controller +public struct PresentChat: Action { + /// - Parameters: + /// - contact: Model to build the view controller which will be pushed + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + contact: Contact, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.contact = contact + self.navigationController = navigationController + self.animated = animated + } + + /// Model to build the view controller which will be pushed + public var contact: Contact + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentChat` action +public struct PresentChatNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: (Contact) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping (Contact) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentChat, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(action.contact), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentChatList.swift b/Sources/AppNavigation/PresentChatList.swift new file mode 100644 index 0000000000000000000000000000000000000000..1cdca2246f9a9de7e3796caa57cef56f67c65270 --- /dev/null +++ b/Sources/AppNavigation/PresentChatList.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `ChatList` on a given navigation controller stack +public struct PresentChatList: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentChatList` action +public struct PresentChatListNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentChatList, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentContact.swift b/Sources/AppNavigation/PresentContact.swift new file mode 100644 index 0000000000000000000000000000000000000000..dd1fca4beb590248d1baff41b5c70e1c735182bc --- /dev/null +++ b/Sources/AppNavigation/PresentContact.swift @@ -0,0 +1,49 @@ +import UIKit +import XXModels + +/// Pushes `Contact` on a given navigation controller +public struct PresentContact: Action { + /// - Parameters: + /// - contact: Model to build the view controller which will be pushed + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + contact: Contact, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.contact = contact + self.navigationController = navigationController + self.animated = animated + } + + /// Model to build the view controller which will be pushed + public var contact: Contact + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentContact` action +public struct PresentContactNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: (Contact) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping (Contact) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentContact, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(action.contact), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentContactList.swift b/Sources/AppNavigation/PresentContactList.swift new file mode 100644 index 0000000000000000000000000000000000000000..bdf9d30fc3a9118ced62cb0ae8e6c44322f48f0e --- /dev/null +++ b/Sources/AppNavigation/PresentContactList.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `ContactList` on a given navigation controller stack +public struct PresentContactList: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentContactList` action +public struct PresentContactListNavigator: TypedNavigator { + /// View controller to which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentContactList, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentCountryList.swift b/Sources/AppNavigation/PresentCountryList.swift new file mode 100644 index 0000000000000000000000000000000000000000..5365cf6b1e2c925fdf8fee7dd7a34b17efe0c499 --- /dev/null +++ b/Sources/AppNavigation/PresentCountryList.swift @@ -0,0 +1,47 @@ +import UIKit + +/// Presents `CountryList` on a given parent view controller +public struct PresentCountryList: Action { + /// - Parameters: + /// - completion: Completion closure with the selected country model + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + completion: @escaping (Any) -> Void, + from parent: UIViewController, + animated: Bool = true + ) { + self.completion = completion + self.parent = parent + self.animated = animated + } + + /// Completion closure with the selected country model + public var completion: (Any) -> Void + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentCountryList` action +public struct PresentCountryListNavigator: TypedNavigator { + /// View controller which should be presented + var viewController: (@escaping (Any) -> Void) -> UIViewController + + /// - Parameters: + /// - viewController: view controller which should be presented + public init(_ viewController: @escaping (@escaping (Any) -> Void) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentCountryList, completion: @escaping () -> Void) { + action.parent.present( + viewController(action.completion), + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentCreateGroupDrawer.swift b/Sources/AppNavigation/PresentCreateGroupDrawer.swift new file mode 100644 index 0000000000000000000000000000000000000000..8abf07fdaa88842ef7d9ef9bdc8b657d375dc872 --- /dev/null +++ b/Sources/AppNavigation/PresentCreateGroupDrawer.swift @@ -0,0 +1,56 @@ +import UIKit +import XXModels + +/// Opens up `CreateGroup` on a given parent view controller +public struct PresentCreateGroup: Action { + /// - Parameters: + /// - members: Collection of contacts that will be in the group + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + members: [Contact], + from parent: UIViewController, + animated: Bool = true + ) { + self.members = members + self.parent = parent + self.animated = animated + } + + /// Collection of contacts that will be in the group + public var members: [Contact] + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentCreateGroup` action +public struct PresentCreateGroupNavigator: TypedNavigator { + /// Custom transitioning delegate + let transitioningDelegate = BottomTransitioningDelegate() + + /// View controller which should be opened up + var viewController: ([Contact]) -> UIViewController + + /// - Parameters: + /// - viewController: view controller which should be presented + public init(_ viewController: @escaping ([Contact]) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentCreateGroup, completion: @escaping () -> Void) { + transitioningDelegate.isDismissableOnBackgroundTouch = true + let controller = viewController(action.members) + controller.transitioningDelegate = transitioningDelegate + controller.modalPresentationStyle = .overFullScreen + + action.parent.present( + controller, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentDrawer.swift b/Sources/AppNavigation/PresentDrawer.swift new file mode 100644 index 0000000000000000000000000000000000000000..8e49f814efafa6552f2c3f2b8c37ef79a83bef8f --- /dev/null +++ b/Sources/AppNavigation/PresentDrawer.swift @@ -0,0 +1,61 @@ +import UIKit + +/// Opens up `Drawer` on a given parent view controller +public struct PresentDrawer: Action { + /// - Parameters: + /// - items: Collection of drawer items that will be present on the view controller + /// - isDismissable: Flag that differentiates whether this presentation is dismissable on background touch + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + items: [Any], + isDismissable: Bool, + from parent: UIViewController, + animated: Bool = true + ) { + self.items = items + self.isDismissable = isDismissable + self.parent = parent + self.animated = animated + } + + /// Collection of drawer items that will be present on the view controller + public var items: [Any] + + /// Flag that differentiates whether this presentation is dismissable on background touch + public var isDismissable: Bool + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentDrawer` action +public struct PresentDrawerNavigator: TypedNavigator { + /// Custom transitioning delegate + let transitioningDelegate = BottomTransitioningDelegate() + + /// View controller which should be opened up + var viewController: ([Any]) -> UIViewController + + /// - Parameters: + /// - viewController: view controller which should be presented + public init(_ viewController: @escaping ([Any]) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentDrawer, completion: @escaping () -> Void) { + transitioningDelegate.isDismissableOnBackgroundTouch = action.isDismissable + let controller = viewController(action.items) + controller.transitioningDelegate = transitioningDelegate + controller.modalPresentationStyle = .overFullScreen + + action.parent.present( + controller, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentGroupChat.swift b/Sources/AppNavigation/PresentGroupChat.swift new file mode 100644 index 0000000000000000000000000000000000000000..e5695a778eca7820292b449d0209d697fa006162 --- /dev/null +++ b/Sources/AppNavigation/PresentGroupChat.swift @@ -0,0 +1,49 @@ +import UIKit +import XXModels + +/// Pushes `GroupChat` on a given navigation controller +public struct PresentGroupChat: Action { + /// - Parameters: + /// - groupInfo: Model to build the view controller which will be pushed + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + groupInfo: GroupInfo, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.groupInfo = groupInfo + self.navigationController = navigationController + self.animated = animated + } + + /// Model to build the view controller which will be pushed + public var groupInfo: GroupInfo + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentGroupChat` action +public struct PresentGroupChatNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: (GroupInfo) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping (GroupInfo) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentGroupChat, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(action.groupInfo), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentGroupDraft.swift b/Sources/AppNavigation/PresentGroupDraft.swift new file mode 100644 index 0000000000000000000000000000000000000000..9190507f7e89baf10c89357c3245162d6f1126fe --- /dev/null +++ b/Sources/AppNavigation/PresentGroupDraft.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `GroupDraft` on a given navigation controller +public struct PresentGroupDraft: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentGroupDraft` action +public struct PresentGroupDraftNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentGroupDraft, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentMemberList.swift b/Sources/AppNavigation/PresentMemberList.swift new file mode 100644 index 0000000000000000000000000000000000000000..3fa6582f85d7e92b9d476248454c3dda3629efc8 --- /dev/null +++ b/Sources/AppNavigation/PresentMemberList.swift @@ -0,0 +1,14 @@ +import XXModels + +public struct PresentMemberList: Action { + public var members: [Contact] + public var animated: Bool + + public init( + members: [Contact], + animated: Bool = true + ) { + self.members = members + self.animated = animated + } +} diff --git a/Sources/AppNavigation/PresentMenu.swift b/Sources/AppNavigation/PresentMenu.swift new file mode 100644 index 0000000000000000000000000000000000000000..59561cc1acf0f0f5121f26d89f1b5b14a22a4cd1 --- /dev/null +++ b/Sources/AppNavigation/PresentMenu.swift @@ -0,0 +1,66 @@ +import UIKit + +/// Options that can be lead to a flow on the menu UI +public enum MenuItem { + case join + case scan + case chats + case share + case profile + case contacts + case requests + case settings + case dashboard +} + +/// Opens left `Menu` on a given parent view controller +public struct PresentMenu: Action { + /// - Parameters: + /// - currentItem: A correlation with the flow that this controller is being presented + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + currentItem: MenuItem, + from parent: UIViewController, + animated: Bool = true + ) { + self.currentItem = currentItem + self.parent = parent + self.animated = animated + } + + /// A correlation with the flow that this controller is being presented + public var currentItem: MenuItem + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentMenu` action +public struct PresentMenuNavigator: TypedNavigator { + /// Custom transitioning delegate + let transitioningDelegate = LeftTransitioningDelegate() + + /// View controller which should be opened left + var viewController: (MenuItem, UINavigationController?) -> UIViewController + + /// - Parameters: + /// - viewController: view controller which should be presented + public init(_ viewController: @escaping (MenuItem, UINavigationController?) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentMenu, completion: @escaping () -> Void) { + let controller = viewController(action.currentItem, action.parent.navigationController) + controller.transitioningDelegate = transitioningDelegate + controller.modalPresentationStyle = .overFullScreen + action.parent.present( + controller, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentNickname.swift b/Sources/AppNavigation/PresentNickname.swift new file mode 100644 index 0000000000000000000000000000000000000000..8c6bd57c346a9beed71e8b40595e2b7367590600 --- /dev/null +++ b/Sources/AppNavigation/PresentNickname.swift @@ -0,0 +1,60 @@ +import UIKit + +/// Opens up `Nickname` on a given parent view controller +public struct PresentNickname: Action { + /// - Parameters: + /// - prefilled: Optional value to be set as placeholder/pre-existent text + /// - completion: Closure that passes the value of the text set + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + prefilled: String?, + completion: @escaping (String) -> Void, + from parent: UIViewController, + animated: Bool = true + ) { + self.prefilled = prefilled + self.completion = completion + self.parent = parent + self.animated = animated + } + + /// Optional value to be set as placeholder/pre-existent text + public var prefilled: String? + + /// Closure that passes the value of the text set + public var completion: (String) -> Void + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentNickname` action +public struct PresentNicknameNavigator: TypedNavigator { + /// Custom transitioning delegate + let transitioningDelegate = BottomTransitioningDelegate() + + /// View controller which should be opened up + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: view controller which should be presented + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentNickname, completion: @escaping () -> Void) { + let controller = viewController() + controller.transitioningDelegate = transitioningDelegate + controller.modalPresentationStyle = .overFullScreen + + action.parent.present( + controller, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentOnboardingCode.swift b/Sources/AppNavigation/PresentOnboardingCode.swift new file mode 100644 index 0000000000000000000000000000000000000000..7ed2f50c8dd725db9673ff22e71762cbef3d8ecf --- /dev/null +++ b/Sources/AppNavigation/PresentOnboardingCode.swift @@ -0,0 +1,61 @@ +import UIKit + +/// Pushes `OnboardingCode` on a given navigation controller +public struct PresentOnboardingCode: Action { + /// - Parameters: + /// - isEmail: Flag to differentiate email or phone code + /// - content: Content that is being set if confirmation code gets validated + /// - confirmationId: Confirmation id to validate with third-party + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + isEmail: Bool, + content: String, + confirmationId: String, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.isEmail = isEmail + self.content = content + self.confirmationId = confirmationId + self.navigationController = navigationController + self.animated = animated + } + + /// Flag to differentiate email or phone code + public var isEmail: Bool + + /// Content that is being set if confirmation code gets validated + public var content: String + + /// Confirmation id to validate with third-party + public var confirmationId: String + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentOnboardingCode` action +public struct PresentOnboardingCodeNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: (Bool, String, String) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping (Bool, String, String) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentOnboardingCode, completion: @escaping () -> Void) { + let controller = viewController(action.isEmail, action.content, action.confirmationId) + action.navigationController.pushViewController(controller, animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentOnboardingEmail.swift b/Sources/AppNavigation/PresentOnboardingEmail.swift new file mode 100644 index 0000000000000000000000000000000000000000..4f57119e0157d8b480cc55c41fea90bde38eac2e --- /dev/null +++ b/Sources/AppNavigation/PresentOnboardingEmail.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `OnboardingEmail` on a given navigation controller stack +public struct PresentOnboardingEmail: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentOnboardingEmail` action +public struct PresentOnboardingEmailNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentOnboardingEmail, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentOnboardingPhone.swift b/Sources/AppNavigation/PresentOnboardingPhone.swift new file mode 100644 index 0000000000000000000000000000000000000000..eca7c89605f99e1f6ec8e982150907f7f7ef80d0 --- /dev/null +++ b/Sources/AppNavigation/PresentOnboardingPhone.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `OnboardingPhone` on a given navigation controller stack +public struct PresentOnboardingPhone: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentOnboardingPhone` action +public struct PresentOnboardingPhoneNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentOnboardingPhone, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentOnboardingStart.swift b/Sources/AppNavigation/PresentOnboardingStart.swift new file mode 100644 index 0000000000000000000000000000000000000000..e531c6909ea2e1087dfea80e608dcd384c7dafef --- /dev/null +++ b/Sources/AppNavigation/PresentOnboardingStart.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `OnboardingStart` on a given navigation controller stack +public struct PresentOnboardingStart: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentOnboardingStart` action +public struct PresentOnboardingStartNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentOnboardingStart, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentOnboardingUsername.swift b/Sources/AppNavigation/PresentOnboardingUsername.swift new file mode 100644 index 0000000000000000000000000000000000000000..428dc24a0b84c014e3473e575d7f2058ff92180e --- /dev/null +++ b/Sources/AppNavigation/PresentOnboardingUsername.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `OnboardingUsername` on a given navigation controller +public struct PresentOnboardingUsername: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentOnboardingUsername` action +public struct PresentOnboardingUsernameNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentOnboardingUsername, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentOnboardingWelcome.swift b/Sources/AppNavigation/PresentOnboardingWelcome.swift new file mode 100644 index 0000000000000000000000000000000000000000..0df955e7e77208e5dcc675220621b7f800403f82 --- /dev/null +++ b/Sources/AppNavigation/PresentOnboardingWelcome.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `OnboardingWelcome` on a given navigation controller stack +public struct PresentOnboardingWelcome: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentOnboardingWelcome` action +public struct PresentOnboardingWelcomeNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentOnboardingWelcome, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentPassphrase.swift b/Sources/AppNavigation/PresentPassphrase.swift new file mode 100644 index 0000000000000000000000000000000000000000..6e208f26b048d5649c4badf2cafd5a3d43fd9d0e --- /dev/null +++ b/Sources/AppNavigation/PresentPassphrase.swift @@ -0,0 +1,15 @@ +public struct PresentPassphrase: Action { + public var onCancel: () -> Void + public var onPasspharse: (String) -> Void + public var animated: Bool + + public init( + onCancel: @escaping () -> Void, + onPassphrase: @escaping (String) -> Void, + animated: Bool = true + ) { + self.onCancel = onCancel + self.onPasspharse = onPassphrase + self.animated = animated + } +} diff --git a/Sources/AppNavigation/PresentPermissionRequest.swift b/Sources/AppNavigation/PresentPermissionRequest.swift new file mode 100644 index 0000000000000000000000000000000000000000..76c89f1c94a814061473dfcb003466dc9795cddb --- /dev/null +++ b/Sources/AppNavigation/PresentPermissionRequest.swift @@ -0,0 +1,59 @@ +import UIKit + +/// Types of permissions that can be requested to the user +public enum PermissionType { + /// Device camera permission type + case camera + + /// Camera roll and library permission type + case library + + /// Device microphone permission type + case microphone +} + +/// Presents `PermissionRequest` on provided parent view controller +public struct PresentPermissionRequest: Action { + /// - Parameters: + /// - type: Type of permission that is being requested + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + type: PermissionType, + from parent: UIViewController, + animated: Bool = true + ) { + self.type = type + self.parent = parent + self.animated = animated + } + + /// Type of permission that is being requested + public var type: PermissionType + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentPermissionRequest` action +public struct PresentPermissionRequestNavigator: TypedNavigator { + /// View controller which should be presented + var viewController: (PermissionType) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be presented + public init(_ viewController: @escaping (PermissionType) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentPermissionRequest, completion: @escaping () -> Void) { + action.parent.present( + viewController(action.type), + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentPhotoLibrary.swift b/Sources/AppNavigation/PresentPhotoLibrary.swift new file mode 100644 index 0000000000000000000000000000000000000000..886b3c8b3c5bcf4468f02113d20865e3f2387d4d --- /dev/null +++ b/Sources/AppNavigation/PresentPhotoLibrary.swift @@ -0,0 +1,36 @@ +import UIKit + +/// Presents `PhotoLibrary` on provided parent view controller +public struct PresentPhotoLibrary: Action { + /// - Parameters: + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + from parent: UIViewController, + animated: Bool = true + ) { + self.parent = parent + self.animated = animated + } + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentPhotoLibrary` action +public struct PresentPhotoLibraryNavigator: TypedNavigator { + public init() {} + + public func perform(_ action: PresentPhotoLibrary, completion: @escaping () -> Void) { + let imagePickerController = UIImagePickerController() + imagePickerController.delegate = action.parent as? UIImagePickerControllerDelegate & UINavigationControllerDelegate + action.parent.present( + imagePickerController, + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/PresentProfile.swift b/Sources/AppNavigation/PresentProfile.swift new file mode 100644 index 0000000000000000000000000000000000000000..42363e49108b8bb363987fd17c998240e8fc9554 --- /dev/null +++ b/Sources/AppNavigation/PresentProfile.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `Profile` on a given navigation controller stack +public struct PresentProfile: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentProfile` action +public struct PresentProfileNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentProfile, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentProfileCode.swift b/Sources/AppNavigation/PresentProfileCode.swift new file mode 100644 index 0000000000000000000000000000000000000000..db50a72d8f3ef9a5e40e6188025a68d44b924d5b --- /dev/null +++ b/Sources/AppNavigation/PresentProfileCode.swift @@ -0,0 +1,61 @@ +import UIKit + +/// Pushes `ProfileCode` on a given navigation controller +public struct PresentProfileCode: Action { + /// - Parameters: + /// - isEmail: Flag to differentiate email or phone code + /// - content: Content that is being set if confirmation code gets validated + /// - confirmationId: Confirmation id to validate with third-party + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + isEmail: Bool, + content: String, + confirmationId: String, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.isEmail = isEmail + self.content = content + self.confirmationId = confirmationId + self.navigationController = navigationController + self.animated = animated + } + + /// Flag to differentiate email or phone code + public var isEmail: Bool + + /// Content that is being set if confirmation code gets validated + public var content: String + + /// Confirmation id to validate with third-party + public var confirmationId: String + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentProfileCode` action +public struct PresentProfileCodeNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: (Bool, String, String) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping (Bool, String, String) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentProfileCode, completion: @escaping () -> Void) { + let controller = viewController(action.isEmail, action.content, action.confirmationId) + action.navigationController.pushViewController(controller, animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentProfileEmail.swift b/Sources/AppNavigation/PresentProfileEmail.swift new file mode 100644 index 0000000000000000000000000000000000000000..0ea2c524781d1046b806c65edd9be0931d0f7bc7 --- /dev/null +++ b/Sources/AppNavigation/PresentProfileEmail.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `ProfileEmail` on a given navigation controller +public struct PresentProfileEmail: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentProfileEmail` action +public struct PresentProfileEmailNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentProfileEmail, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentProfilePhone.swift b/Sources/AppNavigation/PresentProfilePhone.swift new file mode 100644 index 0000000000000000000000000000000000000000..c669afe41c355ee6ef39fa1c38dbea2dae2a7583 --- /dev/null +++ b/Sources/AppNavigation/PresentProfilePhone.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `ProfilePhone` on a given navigation controller +public struct PresentProfilePhone: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentProfilePhone` action +public struct PresentProfilePhoneNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentProfilePhone, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentRequests.swift b/Sources/AppNavigation/PresentRequests.swift new file mode 100644 index 0000000000000000000000000000000000000000..aa346ff45292234cc1576135d517b49625f04916 --- /dev/null +++ b/Sources/AppNavigation/PresentRequests.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `Requests` on a given navigation controller stack +public struct PresentRequests: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentRequests` action +public struct PresentRequestsNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentRequests, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentRestoreList.swift b/Sources/AppNavigation/PresentRestoreList.swift new file mode 100644 index 0000000000000000000000000000000000000000..977c224537b219dff54e4ed87f87115c01af945d --- /dev/null +++ b/Sources/AppNavigation/PresentRestoreList.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `RestoreList` on a given navigation controller +public struct PresentRestoreList: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentRestoreList` action +public struct PresentRestoreListNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentRestoreList, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentSFTP.swift b/Sources/AppNavigation/PresentSFTP.swift new file mode 100644 index 0000000000000000000000000000000000000000..dee1bde64055af2fb2fa7fa4e8006bed8270c20b --- /dev/null +++ b/Sources/AppNavigation/PresentSFTP.swift @@ -0,0 +1,49 @@ +import UIKit + +/// Pushes `SFTP` on a given navigation controller +public struct PresentSFTP: Action { + /// - Parameters: + /// - completion: Completion closure with host, username and password + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + completion: @escaping (String, String, String) -> Void, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.completion = completion + self.navigationController = navigationController + self.animated = animated + } + + /// Completion closure with host, username and password + public var completion: (String, String, String) -> Void + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentSFTP` action +public struct PresentSFTPNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: (@escaping (String, String, String) -> Void) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping (@escaping (String, String, String) -> Void) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentSFTP, completion: @escaping () -> Void) { + let controller = viewController(action.completion) + action.navigationController.pushViewController(controller, animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentScan.swift b/Sources/AppNavigation/PresentScan.swift new file mode 100644 index 0000000000000000000000000000000000000000..d4fb08cb2be013598154342439480972f26f4e23 --- /dev/null +++ b/Sources/AppNavigation/PresentScan.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `Scan` on a given navigation controller stack +public struct PresentScan: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentScan` action +public struct PresentScanNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentScan, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentSearch.swift b/Sources/AppNavigation/PresentSearch.swift new file mode 100644 index 0000000000000000000000000000000000000000..4b553b174121c096bd8673ce0636478b181ad459 --- /dev/null +++ b/Sources/AppNavigation/PresentSearch.swift @@ -0,0 +1,67 @@ +import UIKit + +/// Sets or Pushes `Search` on a given navigation controller +public struct PresentSearch: Action { + /// - Parameters: + /// - searching: Optional string to be searched upon further viewModel intialization + /// - fromOnboarding: Flag that differentiates if should be a push or a set stack + /// - navigationController: Navigation controller on which will be pushed or stack should be set + /// - animated: Animate the transition + public init( + searching: String? = nil, + fromOnboarding: Bool = false, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.searching = searching + self.fromOnboarding = fromOnboarding + self.navigationController = navigationController + self.animated = animated + } + + /// Optional string to be searched upon further viewModel intialization + public var searching: String? + + /// Flag that differentiates if should be a push or a set stack + public var fromOnboarding: Bool + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentSearch` action +public struct PresentSearchNavigator: TypedNavigator { + /// View controller which should be pushed or set in navigation stack + var viewController: (String?) -> UIViewController + + /// View controller which might have to be pushed below in navigation stack + var otherViewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed or set in navigation stack + /// - otherViewController: View controller which might have to be pushed below in navigation stack + public init( + _ otherViewController: @escaping () -> UIViewController, + _ viewController: @escaping (String?) -> UIViewController + ) { + self.viewController = viewController + self.otherViewController = otherViewController + } + + public func perform(_ action: PresentSearch, completion: @escaping () -> Void) { + if action.fromOnboarding { + action.navigationController.setViewControllers([otherViewController(), viewController(action.searching)], animated: action.animated) + } else { + action.navigationController.pushViewController(viewController(action.searching), animated: action.animated) + } + + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentSettings.swift b/Sources/AppNavigation/PresentSettings.swift new file mode 100644 index 0000000000000000000000000000000000000000..e6a4871612234c122da83bdd6be5869b895062e8 --- /dev/null +++ b/Sources/AppNavigation/PresentSettings.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Sets `Settings` on a given navigation controller stack +public struct PresentSettings: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which stack should be set + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentSettings` action +public struct PresentSettingsNavigator: TypedNavigator { + /// View controller which should be set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentSettings, completion: @escaping () -> Void) { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentSettingsAccountDelete.swift b/Sources/AppNavigation/PresentSettingsAccountDelete.swift new file mode 100644 index 0000000000000000000000000000000000000000..e5691b4cda89a52c223c2c463fd96743465b1cd4 --- /dev/null +++ b/Sources/AppNavigation/PresentSettingsAccountDelete.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `SettingsAccountDelete` on a given navigation controller +public struct PresentSettingsAccountDelete: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentSettingsAccountDelete` action +public struct PresentSettingsAccountDeleteNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentSettingsAccountDelete, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentSettingsAdvanced.swift b/Sources/AppNavigation/PresentSettingsAdvanced.swift new file mode 100644 index 0000000000000000000000000000000000000000..737f02fa2f6d97a8da754f821d1860502dcfa957 --- /dev/null +++ b/Sources/AppNavigation/PresentSettingsAdvanced.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `SettingsAdvanced` on a given navigation controller +public struct PresentSettingsAdvanced: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentSettingsAdvanced` action +public struct PresentSettingsAdvancedNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentSettingsAdvanced, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentSettingsBackup.swift b/Sources/AppNavigation/PresentSettingsBackup.swift new file mode 100644 index 0000000000000000000000000000000000000000..f1d2ec5e3808f5bc45e8a968a03f8a4c0272f89a --- /dev/null +++ b/Sources/AppNavigation/PresentSettingsBackup.swift @@ -0,0 +1,42 @@ +import UIKit + +/// Pushes `SettingsBackup` on a given navigation controller +public struct PresentSettingsBackup: Action { + /// - Parameters: + /// - navigationController: Navigation controller on which push should happen + /// - animated: Animate the transition + public init( + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.navigationController = navigationController + self.animated = animated + } + + /// Navigation controller on which push should happen + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentSettingsBackup` action +public struct PresentSettingsBackupNavigator: TypedNavigator { + /// View controller which should be pushed + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentSettingsBackup, completion: @escaping () -> Void) { + action.navigationController.pushViewController(viewController(), animated: action.animated) + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentTermsAndConditions.swift b/Sources/AppNavigation/PresentTermsAndConditions.swift new file mode 100644 index 0000000000000000000000000000000000000000..8baa2e06fb19f87e770eb5792b898f97e94bee65 --- /dev/null +++ b/Sources/AppNavigation/PresentTermsAndConditions.swift @@ -0,0 +1,53 @@ +import UIKit + +/// Sets or Pushes `TermsAndConditions` on a given navigation controller +public struct PresentTermsAndConditions: Action { + /// - Parameters: + /// - replacing: Flag to differentiate if should be a push or a set stack + /// - navigationController: Navigation controller on which will be pushed or stack should be set + /// - animated: Animate the transition + public init( + replacing: Bool, + on navigationController: UINavigationController, + animated: Bool = true + ) { + self.replacing = replacing + self.navigationController = navigationController + self.animated = animated + } + + /// Flag to differentiate if should be a push or a set stack + public var replacing: Bool + + /// Navigation controller on which stack should be set + public var navigationController: UINavigationController + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentTermsAndConditions` action +public struct PresentTermsAndConditionsNavigator: TypedNavigator { + /// View controller which should be pushed or set in navigation stack + var viewController: () -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be pushed or set in navigation stack + public init(_ viewController: @escaping () -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentTermsAndConditions, completion: @escaping () -> Void) { + if action.replacing { + action.navigationController.setViewControllers([viewController()], animated: action.animated) + } else { + action.navigationController.pushViewController(viewController(), animated: action.animated) + } + + if action.animated, let coordinator = action.navigationController.transitionCoordinator { + coordinator.animate(alongsideTransition: nil, completion: { _ in completion() }) + } else { + completion() + } + } +} diff --git a/Sources/AppNavigation/PresentWebsite.swift b/Sources/AppNavigation/PresentWebsite.swift new file mode 100644 index 0000000000000000000000000000000000000000..02abd2eef9dfb3709e1704d66c662b93524d6471 --- /dev/null +++ b/Sources/AppNavigation/PresentWebsite.swift @@ -0,0 +1,48 @@ +import UIKit +import WebKit + +/// Presents `Website` on a given parent view controller +public struct PresentWebsite: Action { + /// - Parameters: + /// - urlString: Url that will be loaded on the web view + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + public init( + urlString: String, + from parent: UIViewController, + animated: Bool = true + ) { + self.urlString = urlString + self.parent = parent + self.animated = animated + } + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Url that will be loaded on the web view + public var urlString: String + + /// Animate the transition + public var animated: Bool +} + +/// Performs `PresentWebsite` action +public struct PresentWebsiteNavigator: TypedNavigator { + /// View controller which should be presented + var viewController: (String) -> UIViewController + + /// - Parameters: + /// - viewController: View controller which should be presented + public init(_ viewController: @escaping (String) -> UIViewController) { + self.viewController = viewController + } + + public func perform(_ action: PresentWebsite, completion: @escaping () -> Void) { + action.parent.present( + viewController(action.urlString), + animated: action.animated, + completion: completion + ) + } +} diff --git a/Sources/AppNavigation/TypedNavigator.swift b/Sources/AppNavigation/TypedNavigator.swift new file mode 100644 index 0000000000000000000000000000000000000000..c351c98a2257a610fd7bbd291dcdab614597ee0d --- /dev/null +++ b/Sources/AppNavigation/TypedNavigator.swift @@ -0,0 +1,46 @@ +/// Navigation that can perform action of a concrete type +public protocol TypedNavigator: Navigator { + /// Type of the action that the navigator can perform + associatedtype ActionType: Action + + /// Returns true if the action can be performed by the navigator + /// - Default implementation returns true for any action + /// - Parameter action: navigation action + func canPerform(_ action: ActionType) -> Bool + + /// Performs the navigation action + /// - Parameters: + /// - action: navigation action + /// - completion: closure that will be executed after performing the action + func perform(_ action: ActionType, completion: @escaping () -> Void) +} + +public extension TypedNavigator { + /// Returns true if the navigation action is of the type handled by the navigator + /// - Parameter action: navigation action + /// - Returns: true if action can be performed + func canPerform(_ action: Action) -> Bool { + if let action = action as? ActionType { + return canPerform(action) + } + return false + } + + func canPerform(_ action: ActionType) -> Bool { true } + + /// Performs the navigation action with empty completion closure + /// - Parameter action: navigation action + func perform(_ action: ActionType) { + perform(action, completion: {}) + } + + /// Performs the navigation action if its type matches `ActionType` handled by the navigator + /// - Parameters: + /// - action: navigation action + /// - completion: closure that will be executed after performing the action + func perform(_ action: Action, completion: @escaping () -> Void) { + if let action = action as? ActionType { + perform(action, completion: 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 99% rename from Sources/Shared/Resources/en.lproj/Localizable.strings rename to Sources/AppResources/Resources/en.lproj/Localizable.strings index a82b151e1ff1fb0a65ddffb5060c922057679532..3dfbb4a622ecde1973cc79fb0e692d78add2cefa 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/AppResources/Resources/en.lproj/Localizable.strings @@ -668,6 +668,10 @@ = "Set password and continue"; "backup.passphrase.cancel" = "Cancel"; +"backup.restore.passphrase.title" += "Backup password"; +"backup.restore.passphrase.subtitle" += "Please enter your backup password that you used when you did the backup setup"; "backup.iCloud" = "iCloud"; @@ -983,7 +987,7 @@ "createGroup.drawer.title" = "Create Group"; "createGroup.drawer.subtitle" -= "You are about to create a group message with %@ users. The information below will be visible to all members of the group."; += "You are about to create a group message with other %@ users. The information below will be visible to all members of the group."; "createGroup.drawer.input" = "Group Name"; "createGroup.drawer.otherInput" diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/AppResources/Strings.swift similarity index 99% rename from Sources/Shared/AutoGenerated/Strings.swift rename to Sources/AppResources/Strings.swift index 3177da720c32c5e4209fa4dc0c9c1211d6e65b5e..4ced950dd511ef22ad7f25f4ddb1c1192587ed7e 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/AppResources/Strings.swift @@ -290,6 +290,14 @@ public enum Localized { public static let title = Localized.tr("Localizable", "backup.passphrase.input.title") } } + public enum Restore { + public enum Passphrase { + /// Please enter your backup password that you used when you did the backup setup + public static let subtitle = Localized.tr("Localizable", "backup.restore.passphrase.subtitle") + /// Backup password + public static let title = Localized.tr("Localizable", "backup.restore.passphrase.title") + } + } public enum Setup { /// Setup your #backup service#. public static let title = Localized.tr("Localizable", "backup.setup.title") @@ -617,7 +625,7 @@ public enum Localized { public static let otherPlaceholder = Localized.tr("Localizable", "createGroup.drawer.otherPlaceholder") /// Secret Family public static let placeholder = Localized.tr("Localizable", "createGroup.drawer.placeholder") - /// You are about to create a group message with %@ users. The information below will be visible to all members of the group. + /// You are about to create a group message with other %@ users. The information below will be visible to all members of the group. public static func subtitle(_ p1: Any) -> String { return Localized.tr("Localizable", "createGroup.drawer.subtitle", String(describing: p1)) } diff --git a/Sources/Shared/swiftgen.yml b/Sources/AppResources/swiftgen.yml similarity index 77% rename from Sources/Shared/swiftgen.yml rename to Sources/AppResources/swiftgen.yml index 1811db25daa24e73f82a0022cc191d14e15f664b..3e4548e0eb8a79eefb0bb9f37d8d3ef7cc9338da 100644 --- a/Sources/Shared/swiftgen.yml +++ b/Sources/AppResources/swiftgen.yml @@ -2,7 +2,7 @@ strings: inputs: Resources/en.lproj outputs: templateName: structured-swift5 - output: AutoGenerated/Strings.swift + output: Strings.swift params: enumName: Localized publicAccess: true @@ -11,7 +11,7 @@ xcassets: inputs: Resources/Assets.xcassets outputs: templateName: swift5 - output: AutoGenerated/Assets.swift + output: Assets.swift params: publicAccess: true @@ -19,7 +19,7 @@ fonts: inputs: Resources/Fonts outputs: templateName: swift5 - output: AutoGenerated/Fonts.swift + output: Fonts.swift params: enumName: Fonts publicAccess: true diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift index a901e6b9668fc89df2cd1ae1184ca655fa8bdb1b..f2e0220380a545b5d8a79cc5c7517d7676930ba1 100644 --- a/Sources/BackupFeature/Controllers/BackupConfigController.swift +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -1,334 +1,335 @@ import UIKit -import Models import Shared import Combine +import CloudFiles +import AppResources +import AppNavigation import DrawerFeature -import DependencyInjection +import ComposableArchitecture final class BackupConfigController: UIViewController { - @Dependency private var coordinator: BackupCoordinating - - lazy private var screenView = BackupConfigView() - - private let viewModel: BackupConfigViewModel - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() + @Dependency(\.navigator) var navigator: Navigator + + private lazy var screenView = BackupConfigView() + + private let viewModel: BackupConfigViewModel + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + private var wifiOnly = false + private var manualBackups = false + private var serviceName: String = "" + + override func loadView() { + view = screenView + } + + init(_ viewModel: BackupConfigViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + setupBindings() + } + + private func setupBindings() { + viewModel.actionState() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in screenView.actionView.setState($0) } + .store(in: &cancellables) + + viewModel.connectedServices() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in decorate(connectedServices: $0) } + .store(in: &cancellables) + + viewModel.enabledService() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in decorate(enabledService: $0) } + .store(in: &cancellables) + + viewModel.automatic() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.frequencyDetailView.subtitleLabel.text = $0 ? "Automatic" : "Manual" + manualBackups = !$0 + }.store(in: &cancellables) + + viewModel.wifiOnly() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.infrastructureDetailView.subtitleLabel.text = $0 ? "Wi-Fi Only" : "Wi-Fi and Cellular" + wifiOnly = $0 + }.store(in: &cancellables) + + viewModel.lastBackup() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let backup = $0 else { + screenView.latestBackupDetailView.subtitleLabel.text = "Never" + return + } - private var wifiOnly = false - private var manualBackups = false - private var serviceName: String = "" + screenView.latestBackupDetailView.subtitleLabel.text = backup.lastModified.backupStyle() + }.store(in: &cancellables) + + screenView.actionView.backupNowButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapBackupNow() } + .store(in: &cancellables) + + screenView.frequencyDetailView + .publisher(for: .touchUpInside) + .sink { [unowned self] in presentFrequencyDrawer(manual: manualBackups) } + .store(in: &cancellables) + + screenView.infrastructureDetailView + .publisher(for: .touchUpInside) + .sink { [unowned self] in presentInfrastructureDrawer(wifiOnly: wifiOnly) } + .store(in: &cancellables) + + screenView.googleDriveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.drive, self) } + .store(in: &cancellables) + + screenView.googleDriveButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .drive, screenView.googleDriveButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.dropboxButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .dropbox, screenView.dropboxButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.sftpButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .sftp, screenView.sftpButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.iCloudButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .icloud, screenView.iCloudButton.switcherView.isOn) } + .store(in: &cancellables) + + screenView.dropboxButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.dropbox, self) } + .store(in: &cancellables) + + screenView.sftpButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.sftp, self) } + .store(in: &cancellables) + + screenView.iCloudButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.icloud, self) } + .store(in: &cancellables) + } + + private func decorate(enabledService: CloudService?) { + var button: BackupSwitcherButton? + + switch enabledService { + case .none: + break + case .icloud: + serviceName = Localized.Backup.iCloud + button = screenView.iCloudButton + case .dropbox: + serviceName = Localized.Backup.dropbox + button = screenView.dropboxButton + case .drive: + serviceName = Localized.Backup.googleDrive + button = screenView.googleDriveButton + case .sftp: + serviceName = Localized.Backup.sftp + button = screenView.sftpButton + } - override func loadView() { - view = screenView + screenView.enabledSubtitleLabel.text + = Localized.Backup.Config.disclaimer(serviceName) + screenView.frequencyDetailView.titleLabel.text + = Localized.Backup.Config.frequency(serviceName).uppercased() + + guard let button = button else { + screenView.sftpButton.isHidden = false + screenView.iCloudButton.isHidden = false + screenView.dropboxButton.isHidden = false + screenView.googleDriveButton.isHidden = false + + screenView.sftpButton.switcherView.isOn = false + screenView.iCloudButton.switcherView.isOn = false + screenView.dropboxButton.switcherView.isOn = false + screenView.googleDriveButton.switcherView.isOn = false + + screenView.frequencyDetailView.isHidden = true + screenView.enabledSubtitleView.isHidden = true + screenView.latestBackupDetailView.isHidden = true + screenView.infrastructureDetailView.isHidden = true + return } - init(_ viewModel: BackupConfigViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + screenView.frequencyDetailView.isHidden = false + screenView.enabledSubtitleView.isHidden = false + screenView.latestBackupDetailView.isHidden = false + screenView.infrastructureDetailView.isHidden = false + + [screenView.iCloudButton, + screenView.dropboxButton, + screenView.googleDriveButton, + screenView.sftpButton].forEach { + $0.isHidden = $0 != button + $0.switcherView.isOn = $0 == button } + } - required init?(coder: NSCoder) { nil } + private func decorate(connectedServices: Set<CloudService>) { + if connectedServices.contains(.icloud) { + screenView.iCloudButton.showSwitcher(enabled: false) + } else { + screenView.iCloudButton.showChevron() + } - override func viewDidLoad() { - super.viewDidLoad() - setupBindings() + if connectedServices.contains(.dropbox) { + screenView.dropboxButton.showSwitcher(enabled: false) + } else { + screenView.dropboxButton.showChevron() } - private func setupBindings() { - viewModel.actionState() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.actionView.setState($0) } - .store(in: &cancellables) - - viewModel.connectedServices() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in decorate(connectedServices: $0) } - .store(in: &cancellables) - - viewModel.enabledService() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in decorate(enabledService: $0) } - .store(in: &cancellables) - - viewModel.automatic() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.frequencyDetailView.subtitleLabel.text = $0 ? "Automatic" : "Manual" - manualBackups = !$0 - }.store(in: &cancellables) - - viewModel.wifiOnly() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.infrastructureDetailView.subtitleLabel.text = $0 ? "Wi-Fi Only" : "Wi-Fi and Cellular" - wifiOnly = $0 - }.store(in: &cancellables) - - viewModel.lastBackup() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard let backup = $0 else { - screenView.latestBackupDetailView.subtitleLabel.text = "Never" - return - } - - screenView.latestBackupDetailView.subtitleLabel.text = backup.date.backupStyle() - }.store(in: &cancellables) - - screenView.actionView.backupNowButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapBackupNow() } - .store(in: &cancellables) - - screenView.frequencyDetailView - .publisher(for: .touchUpInside) - .sink { [unowned self] in presentFrequencyDrawer(manual: manualBackups) } - .store(in: &cancellables) - - screenView.infrastructureDetailView - .publisher(for: .touchUpInside) - .sink { [unowned self] in presentInfrastructureDrawer(wifiOnly: wifiOnly) } - .store(in: &cancellables) - - screenView.googleDriveButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.drive, self) } - .store(in: &cancellables) - - screenView.googleDriveButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .drive, screenView.googleDriveButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.dropboxButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .dropbox, screenView.dropboxButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.sftpButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .sftp, screenView.sftpButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.iCloudButton.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(self, .icloud, screenView.iCloudButton.switcherView.isOn) } - .store(in: &cancellables) - - screenView.dropboxButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.dropbox, self) } - .store(in: &cancellables) - - screenView.sftpButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.sftp, self) } - .store(in: &cancellables) - - screenView.iCloudButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapService(.icloud, self) } - .store(in: &cancellables) + if connectedServices.contains(.drive) { + screenView.googleDriveButton.showSwitcher(enabled: false) + } else { + screenView.googleDriveButton.showChevron() } - private func decorate(enabledService: CloudService?) { - var button: BackupSwitcherButton? - - switch enabledService { - case .none: - break - case .icloud: - serviceName = Localized.Backup.iCloud - button = screenView.iCloudButton - case .dropbox: - serviceName = Localized.Backup.dropbox - button = screenView.dropboxButton - case .drive: - serviceName = Localized.Backup.googleDrive - button = screenView.googleDriveButton - case .sftp: - serviceName = Localized.Backup.sftp - button = screenView.sftpButton + if connectedServices.contains(.sftp) { + screenView.sftpButton.showSwitcher(enabled: false) + } else { + screenView.sftpButton.showChevron() + } + } + + private func presentInfrastructureDrawer(wifiOnly: Bool) { + let cancelButton = DrawerCapsuleButton(model: .init( + title: Localized.ChatList.Dashboard.cancel, + style: .seeThrough + )) + let wifiOnlyButton = DrawerRadio( + title: "Wi-Fi Only", + isSelected: wifiOnly + ) + let wifiAndCellularButton = DrawerRadio( + title: "Wi-Fi and Cellular", + isSelected: !wifiOnly, + spacingAfter: 40 + ) + + wifiOnlyButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseWifiOnly(true) } - - screenView.enabledSubtitleLabel.text - = Localized.Backup.Config.disclaimer(serviceName) - screenView.frequencyDetailView.titleLabel.text - = Localized.Backup.Config.frequency(serviceName).uppercased() - - guard let button = button else { - screenView.sftpButton.isHidden = false - screenView.iCloudButton.isHidden = false - screenView.dropboxButton.isHidden = false - screenView.googleDriveButton.isHidden = false - - screenView.sftpButton.switcherView.isOn = false - screenView.iCloudButton.switcherView.isOn = false - screenView.dropboxButton.switcherView.isOn = false - screenView.googleDriveButton.switcherView.isOn = false - - screenView.frequencyDetailView.isHidden = true - screenView.enabledSubtitleView.isHidden = true - screenView.latestBackupDetailView.isHidden = true - screenView.infrastructureDetailView.isHidden = true - return + }.store(in: &drawerCancellables) + + wifiAndCellularButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseWifiOnly(false) } - - screenView.frequencyDetailView.isHidden = false - screenView.enabledSubtitleView.isHidden = false - screenView.latestBackupDetailView.isHidden = false - screenView.infrastructureDetailView.isHidden = false - - [screenView.iCloudButton, - screenView.dropboxButton, - screenView.googleDriveButton, - screenView.sftpButton].forEach { - $0.isHidden = $0 != button - $0.switcherView.isOn = $0 == button + }.store(in: &drawerCancellables) + + cancelButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() } - } - - private func decorate(connectedServices: Set<CloudService>) { - if connectedServices.contains(.icloud) { - screenView.iCloudButton.showSwitcher(enabled: false) - } else { - screenView.iCloudButton.showChevron() + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.extraBold.font(size: 28.0), + text: Localized.Backup.Config.infrastructure, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 30 + ), + wifiOnlyButton, + wifiAndCellularButton, + cancelButton + ], isDismissable: true, from: self)) + } + + private func presentFrequencyDrawer(manual: Bool) { + let cancelButton = DrawerCapsuleButton(model: .init( + title: Localized.ChatList.Dashboard.cancel, + style: .seeThrough + )) + let manualButton = DrawerRadio( + title: "Manual", + isSelected: manual + ) + let automaticButton = DrawerRadio( + title: "Automatic", + isSelected: !manual, + spacingAfter: 40 + ) + manualButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseAutomatic(false) } - - if connectedServices.contains(.dropbox) { - screenView.dropboxButton.showSwitcher(enabled: false) - } else { - screenView.dropboxButton.showChevron() + }.store(in: &drawerCancellables) + + automaticButton + .action + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didChooseAutomatic(true) } - - if connectedServices.contains(.drive) { - screenView.googleDriveButton.showSwitcher(enabled: false) - } else { - screenView.googleDriveButton.showChevron() + }.store(in: &drawerCancellables) + + cancelButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() } - - if connectedServices.contains(.sftp) { - screenView.sftpButton.showSwitcher(enabled: false) - } else { - screenView.sftpButton.showChevron() - } - } - - private func presentInfrastructureDrawer(wifiOnly: Bool) { - let cancelButton = DrawerCapsuleButton(model: .init( - title: Localized.ChatList.Dashboard.cancel, - style: .seeThrough - )) - - let wifiOnlyButton = DrawerRadio( - title: "Wi-Fi Only", - isSelected: wifiOnly - ) - - let wifiAndCellularButton = DrawerRadio( - title: "Wi-Fi and Cellular", - isSelected: !wifiOnly, - spacingAfter: 40 - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.extraBold.font(size: 28.0), - text: Localized.Backup.Config.infrastructure, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 30 - ), - wifiOnlyButton, - wifiAndCellularButton, - cancelButton - ]) - - wifiOnlyButton.action - .sink { [unowned self] in - viewModel.didChooseWifiOnly(true) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - wifiAndCellularButton.action - .sink { [unowned self] in - viewModel.didChooseWifiOnly(false) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - cancelButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } - - private func presentFrequencyDrawer(manual: Bool) { - let cancelButton = DrawerCapsuleButton(model: .init( - title: Localized.ChatList.Dashboard.cancel, - style: .seeThrough - )) - - let manualButton = DrawerRadio( - title: "Manual", - isSelected: manual - ) - - let automaticButton = DrawerRadio( - title: "Automatic", - isSelected: !manual, - spacingAfter: 40 - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.extraBold.font(size: 28.0), - text: Localized.Backup.Config.frequency(serviceName), - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 30 - ), - manualButton, - automaticButton, - cancelButton - ]) - - manualButton.action - .sink { [unowned self] in - viewModel.didChooseAutomatic(false) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - automaticButton.action - .sink { [unowned self] in - viewModel.didChooseAutomatic(true) - - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - cancelButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.extraBold.font(size: 28.0), + text: Localized.Backup.Config.frequency(serviceName), + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 30 + ), + manualButton, + automaticButton, + cancelButton + ], isDismissable: true, from: self)) + } } diff --git a/Sources/BackupFeature/Controllers/BackupController.swift b/Sources/BackupFeature/Controllers/BackupController.swift index 822ebbc77c3fa46acd0d842579382101aac5b3f9..ca82c50e8c1d470f1c0433f16e3a6d182ddd0604 100644 --- a/Sources/BackupFeature/Controllers/BackupController.swift +++ b/Sources/BackupFeature/Controllers/BackupController.swift @@ -1,73 +1,65 @@ -import HUD import UIKit import Shared -import Models import Combine -import DependencyInjection +import AppResources public final class BackupController: UIViewController { - @Dependency var hud: HUD + 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 - hud.update(with: .on) + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = Asset.neutralWhite.color + setupNavigationBar() + setupBindings() + } - 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 - hud.update(with: .none) - - 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/Controllers/BackupPassphraseController.swift b/Sources/BackupFeature/Controllers/BackupPassphraseController.swift index 97c23d21b497a643a7850f3b9147723053dd1e9e..35b97b35cab02ea52f5bc5b9f800ccca3a4cba17 100644 --- a/Sources/BackupFeature/Controllers/BackupPassphraseController.swift +++ b/Sources/BackupFeature/Controllers/BackupPassphraseController.swift @@ -4,70 +4,70 @@ import Combine import InputField public final class BackupPassphraseController: UIViewController { - lazy private var screenView = BackupPassphraseView() + private lazy var screenView = BackupPassphraseView() - private var passphrase = "" { - didSet { - switch Validator.backupPassphrase.validate(passphrase) { - case .success: - screenView.continueButton.isEnabled = true - case .failure: - screenView.continueButton.isEnabled = false - } - } + private var passphrase = "" { + didSet { + switch Validator.backupPassphrase.validate(passphrase) { + case .success: + screenView.continueButton.isEnabled = true + case .failure: + screenView.continueButton.isEnabled = false + } } + } - private let cancelClosure: EmptyClosure - private let stringClosure: StringClosure - private var cancellables = Set<AnyCancellable>() + private let cancelClosure: () -> Void + private let stringClosure: (String) -> Void + private var cancellables = Set<AnyCancellable>() - public init( - _ cancelClosure: @escaping EmptyClosure, - _ stringClosure: @escaping StringClosure - ) { - self.stringClosure = stringClosure - self.cancelClosure = cancelClosure - super.init(nibName: nil, bundle: nil) - } + public init( + _ cancelClosure: @escaping () -> Void, + _ stringClosure: @escaping (String) -> Void + ) { + self.stringClosure = stringClosure + self.cancelClosure = cancelClosure + super.init(nibName: nil, bundle: nil) + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - public override func loadView() { - view = screenView - } + public override func loadView() { + view = screenView + } - public override func viewDidLoad() { - super.viewDidLoad() - setupBindings() - } + public override func viewDidLoad() { + super.viewDidLoad() + setupBindings() + } - private func setupBindings() { - screenView - .inputField - .returnPublisher - .sink { [unowned self] in - screenView.inputField.endEditing(true) - }.store(in: &cancellables) + private func setupBindings() { + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) - screenView - .inputField - .textPublisher - .sink { [unowned self] in - passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) - }.store(in: &cancellables) + screenView + .inputField + .textPublisher + .sink { [unowned self] in + passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) + }.store(in: &cancellables) - screenView - .continueButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { self.stringClosure(self.passphrase) } - }.store(in: &cancellables) + screenView + .continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { self.stringClosure(self.passphrase) } + }.store(in: &cancellables) - screenView - .cancelButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { self.cancelClosure() } - }.store(in: &cancellables) - } + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { self.cancelClosure() } + }.store(in: &cancellables) + } } diff --git a/Sources/BackupFeature/Controllers/BackupSFTPController.swift b/Sources/BackupFeature/Controllers/BackupSFTPController.swift new file mode 100644 index 0000000000000000000000000000000000000000..7b6af4f5897855d740b6e7918071090f04765729 --- /dev/null +++ b/Sources/BackupFeature/Controllers/BackupSFTPController.swift @@ -0,0 +1,79 @@ +import UIKit +import Combine +import ScrollViewController + +public final class BackupSFTPController: UIViewController { + private lazy var screenView = BackupSFTPView() + private lazy var scrollViewController = ScrollViewController() + + private let completion: (String, String, String) -> Void + private let viewModel = BackupSFTPViewModel() + private var cancellables = Set<AnyCancellable>() + + public init(_ completion: @escaping (String, String, String) -> Void) { + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + } + + private func setupScrollView() { + scrollViewController.scrollView.backgroundColor = .white + + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + } + + private func setupBindings() { + viewModel.authPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] params in + completion(params.0, params.1, params.2) + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + + screenView.hostField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterHost($0) } + .store(in: &cancellables) + + screenView.usernameField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterUsername($0) } + .store(in: &cancellables) + + screenView.passwordField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterPassword($0) } + .store(in: &cancellables) + + viewModel.statePublisher + .receive(on: DispatchQueue.main) + .map(\.isButtonEnabled) + .sink { [unowned self] in screenView.loginButton.isEnabled = $0 } + .store(in: &cancellables) + + screenView.loginButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapLogin() } + .store(in: &cancellables) + } +} diff --git a/Sources/BackupFeature/Controllers/BackupSetupController.swift b/Sources/BackupFeature/Controllers/BackupSetupController.swift index e22de517680d7ab5fc348b56cbb396ca80c4e091..7e697535ebfba84a6ef932e582882f89cc8718eb 100644 --- a/Sources/BackupFeature/Controllers/BackupSetupController.swift +++ b/Sources/BackupFeature/Controllers/BackupSetupController.swift @@ -1,10 +1,8 @@ import UIKit -import Models import Combine -import DependencyInjection final class BackupSetupController: UIViewController { - lazy private var screenView = BackupSetupView() + private lazy var screenView = BackupSetupView() private let viewModel: BackupSetupViewModel private var cancellables = Set<AnyCancellable>() diff --git a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift deleted file mode 100644 index f16acd561f4ca41c9f2abe0db1101a58df4cfcad..0000000000000000000000000000000000000000 --- a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift +++ /dev/null @@ -1,69 +0,0 @@ -import UIKit -import Shared -import Presentation -import ScrollViewController - -public protocol BackupCoordinating { - func toDrawer( - _: UIViewController, - from: UIViewController - ) - - func toPassphrase( - from: UIViewController, - cancelClosure: @escaping EmptyClosure, - passphraseClosure: @escaping StringClosure - ) -} - -public struct BackupCoordinator: BackupCoordinating { - var fullscreenPresenter: Presenting = FullscreenPresenter() - - var passphraseFactory: ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController - - public init( - passphraseFactory: @escaping ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController - ) { - self.passphraseFactory = passphraseFactory - } -} - -public extension BackupCoordinator { - func toDrawer( - _ screen: UIViewController, - from parent: UIViewController - ) { - let target = ScrollViewController.embedding(screen) - fullscreenPresenter.present(target, from: parent) - } - - func toPassphrase( - from parent: UIViewController, - cancelClosure: @escaping EmptyClosure, - passphraseClosure: @escaping StringClosure - ) { - let screen = passphraseFactory(cancelClosure, passphraseClosure) - let target = ScrollViewController.embedding(screen) - fullscreenPresenter.present(target, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift index 626adba515d28c1a9969b067da742d07e94c8da5..c024227d5e77cc240f456052e6b4cc96beff4543 100644 --- a/Sources/BackupFeature/Service/BackupService.swift +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -1,337 +1,258 @@ +import AppCore import UIKit -import Models import Combine +import XXClient import Defaults -import Keychain -import SFTPFeature -import iCloudFeature -import DropboxFeature -import NetworkMonitor -import GoogleDriveFeature -import DependencyInjection +import CloudFiles +import CloudFilesSFTP +import KeychainAccess +import XXMessengerClient +import ComposableArchitecture public final class BackupService { - @Dependency private var sftpService: SFTPService - @Dependency private var icloudService: iCloudInterface - @Dependency private var dropboxService: DropboxInterface - @Dependency private var networkManager: NetworkMonitoring - @Dependency private var keychainHandler: KeychainHandling - @Dependency private var driveService: GoogleDriveInterface - - @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data - - public var passphrase: String? - - public var settingsPublisher: AnyPublisher<BackupSettings, Never> { - settings.handleEvents(receiveSubscription: { [weak self] _ in - guard let self = self else { return } - - let lastRefreshDate = self.settingsLastRefreshedDate ?? Date.distantPast - - if Date().timeIntervalSince(lastRefreshDate) < 10 { return } - - self.settingsLastRefreshedDate = Date() - self.refreshConnections() - self.refreshBackups() - }).eraseToAnyPublisher() + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.networkMonitor) var networkMonitor + + @KeyObject(.email, defaultValue: nil) var email: String? + @KeyObject(.phone, defaultValue: nil) var phone: String? + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.backupSettings, defaultValue: nil) var storedSettings: Data? + + public var backupsPublisher: AnyPublisher<[CloudService: Fetch.Metadata], Never> { + backupSubject.eraseToAnyPublisher() + } + + public var connectedServicesPublisher: AnyPublisher<Set<CloudService>, Never> { + connectedServicesSubject.eraseToAnyPublisher() + } + + public var settingsPublisher: AnyPublisher<CloudSettings, Never> { + settings.handleEvents(receiveSubscription: { [weak self] _ in + guard let self else { return } + self.connectedServicesSubject.send(CloudFilesManager.all.linkedServices()) + self.fetchBackupOnAllProviders() + }).eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let connectedServicesSubject = CurrentValueSubject<Set<CloudService>, Never>([]) + private let backupSubject = CurrentValueSubject<[CloudService: Fetch.Metadata], Never>([:]) + private lazy var settings = CurrentValueSubject<CloudSettings, Never>(.init(fromData: storedSettings)) + + public init() { + settings + .dropFirst() + .removeDuplicates() + .sink { [unowned self] in + storedSettings = $0.toData() + }.store(in: &cancellables) + } + + func didSetWiFiOnly(enabled: Bool) { + settings.value.wifiOnlyBackup = enabled + } + + func didSetAutomaticBackup(enabled: Bool) { + settings.value.automaticBackups = enabled + shouldBackupIfSetAutomatic() + } + + func toggle(service: CloudService, enabling: Bool) { + settings.value.enabledService = enabling ? service : nil + } + + func didForceBackup() { + if let lastBackup = try? Data(contentsOf: getBackupURL()) { + performUpload(of: lastBackup) } - - private var connType: ConnectionType = .wifi - private var settingsLastRefreshedDate: Date? - private var cancellables = Set<AnyCancellable>() - private lazy var settings = CurrentValueSubject<BackupSettings, Never>(.init(fromData: storedSettings)) - - public init() { - 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) + } + + public func didUpdateFacts() { + storeFacts() + } + + public func updateLocalBackup(_ data: Data) { + do { + try data.write(to: getBackupURL()) + shouldBackupIfSetAutomatic() + } catch { + fatalError("Couldn't write backup to fileurl") } -} + } -extension BackupService { - public func performBackupIfAutomaticIsEnabled() { - guard settings.value.automaticBackups == true else { return } - performBackup() + private func shouldBackupIfSetAutomatic() { + guard let lastBackup = try? Data(contentsOf: getBackupURL()) else { + return // No stored backup so won't upload anything } - - public func performBackup() { - guard let directoryUrl = try? FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) else { fatalError("Couldn't generate the URL to persist the backup") } - - let fileUrl = directoryUrl - .appendingPathComponent("backup") - .appendingPathExtension("xxm") - - guard let data = try? Data(contentsOf: fileUrl) else { - print(">>> Tried to backup arbitrarily but there was nothing to be backed up. Aborting...") - return - } - - performBackup(data: data) + guard settings.value.automaticBackups else { + return // Backups are not set to automatic } - - public func updateBackup(data: Data) { - guard let directoryUrl = try? FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) else { fatalError("Couldn't generate the URL to persist the backup") } - - let fileUrl = directoryUrl - .appendingPathComponent("backup") - .appendingPathExtension("xxm") - - do { - try data.write(to: fileUrl) - } catch { - fatalError("Couldn't write backup to fileurl") - } - - let isWifiOnly = settings.value.wifiOnlyBackup - let isAutomaticEnabled = settings.value.automaticBackups - let hasEnabledService = settings.value.enabledService != nil - - if isWifiOnly { - guard connType == .wifi else { return } - } else { - guard connType != .unknown else { return } - } - - if isAutomaticEnabled && hasEnabledService { - performBackup() - } + guard settings.value.enabledService != nil else { + return // No service enabled to upload } - - public func setBackupOnlyOnWifi(_ enabled: Bool) { - settings.value.wifiOnlyBackup = enabled + if settings.value.wifiOnlyBackup { + guard networkMonitor.connType() == .wifi else { + return // WiFi only backups, and connType != Wifi + } + } else { + guard networkMonitor.connType() != .unknown else { + return // Connectivity is unknown + } } - - public func setBackupAutomatically(_ enabled: Bool) { - settings.value.automaticBackups = enabled - - guard enabled else { return } - performBackup() + performUpload(of: lastBackup) + } + + // MARK: - Messenger + + func initializeBackup(passphrase: String) { + do { + try messenger.startBackup( + password: passphrase + ) + } catch { + print(">>> Exception when calling `messenger.startBackup`: \(error.localizedDescription)") } - - public func toggle(service: CloudService, enabling: Bool) { - settings.value.enabledService = enabling ? service : nil + } + + func stopBackups() { + if messenger.isBackupRunning() == true { + do { + try messenger.stopBackup() + } catch { + print(">>> Exception when calling `messenger.stopBackup`: \(error.localizedDescription)") + } } - - public func authorize(service: CloudService, presenting screen: UIViewController) { - switch service { - case .drive: - driveService.authorize(presenting: screen) { [weak self] _ in - guard let self = self else { return } - self.refreshConnections() - self.refreshBackups() - } - case .icloud: - if !icloudService.isAuthorized() { - icloudService.openSettings() - } else { - refreshConnections() - refreshBackups() - } - case .dropbox: - if !dropboxService.isAuthorized() { - dropboxService.authorize(presenting: screen) - .sink { [weak self] _ in - guard let self = self else { return } - self.refreshConnections() - self.refreshBackups() - }.store(in: &cancellables) - } - case .sftp: - if !sftpService.isAuthorized() { - sftpService.authorizeFlow((screen, { [weak self] in - guard let self = self else { return } - screen.navigationController?.popViewController(animated: true) - self.refreshConnections() - self.refreshBackups() - })) - } - } + } + + func storeFacts() { + var facts: [String: String] = [:] + facts["username"] = username! + facts["email"] = email + facts["phone"] = phone + facts["timestamp"] = "\(Date.asTimestamp)" + guard let backupManager = messenger.backup.get() else { + print(">>> Tried to store facts in JSON but there's no backup manager instance") + return } -} - -extension BackupService { - private func refreshConnections() { - if icloudService.isAuthorized() && !settings.value.connectedServices.contains(.icloud) { - settings.value.connectedServices.insert(.icloud) - } else if !icloudService.isAuthorized() && settings.value.connectedServices.contains(.icloud) { - settings.value.connectedServices.remove(.icloud) - } - - if dropboxService.isAuthorized() && !settings.value.connectedServices.contains(.dropbox) { - settings.value.connectedServices.insert(.dropbox) - } else if !dropboxService.isAuthorized() && settings.value.connectedServices.contains(.dropbox) { - settings.value.connectedServices.remove(.dropbox) - } - - if sftpService.isAuthorized() && !settings.value.connectedServices.contains(.sftp) { - settings.value.connectedServices.insert(.sftp) - } else if !sftpService.isAuthorized() && settings.value.connectedServices.contains(.sftp) { - settings.value.connectedServices.remove(.sftp) - } - - driveService.isAuthorized { [weak settings] isAuthorized in - guard let settings = settings else { return } - - if isAuthorized && !settings.value.connectedServices.contains(.drive) { - settings.value.connectedServices.insert(.drive) - } else if !isAuthorized && settings.value.connectedServices.contains(.drive) { - settings.value.connectedServices.remove(.drive) - } - } + guard let data = try? JSONSerialization.data(withJSONObject: facts) else { + print(">>> Tried to generate data with json dictionary but failed") + return } - - private func refreshBackups() { - if icloudService.isAuthorized() { - icloudService.downloadMetadata { [weak settings] in - guard let settings = settings else { return } - - guard let metadata = try? $0.get() else { - settings.value.backups[.icloud] = nil - return - } - - settings.value.backups[.icloud] = Backup( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - } - } - - if sftpService.isAuthorized() { - sftpService.fetchMetadata { [weak settings] in - guard let settings = settings else { return } - - guard let metadata = try? $0.get()?.backup else { - settings.value.backups[.sftp] = nil - return - } - - settings.value.backups[.sftp] = Backup( - id: metadata.id, - date: metadata.date, - size: metadata.size - ) - } - } - - if dropboxService.isAuthorized() { - dropboxService.downloadMetadata { [weak settings] in - guard let settings = settings else { return } - - guard let metadata = try? $0.get() else { - settings.value.backups[.dropbox] = nil - return - } - - settings.value.backups[.dropbox] = Backup( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - } - } - - driveService.isAuthorized { [weak settings] isAuthorized in - guard let settings = settings else { return } - - if isAuthorized { - self.driveService.downloadMetadata { - guard let metadata = try? $0.get() else { return } - - settings.value.backups[.drive] = Backup( - id: metadata.identifier, - date: metadata.modifiedDate, - size: metadata.size - ) - } - } else { - settings.value.backups[.drive] = nil - } + guard let string = String(data: data, encoding: .utf8) else { + print(">>> Tried to extract string from json dict object but failed") + return + } + backupManager.addJSON(string) + } + + func setupSFTP(host: String, username: String, password: String) { + let sftpManager = CloudFilesManager.sftp( + host: host, + username: username, + password: password, + fileName: "backup.xxm" + ) + + CloudFilesManager.all[.sftp] = sftpManager + + do { + try sftpManager.fetch { [weak self] in + guard let self else { return } + switch $0 { + case .success(let metadata): + self.backupSubject.value[.sftp] = metadata + case .failure(let error): + print(">>> Error fetching sftp: \(error.localizedDescription)") } + } + } catch { + print(">>> Exception fetching sftp: \(error.localizedDescription)") } - - private func performBackup(data: Data) { - guard let enabledService = settings.value.enabledService else { - fatalError("Trying to backup but nothing is enabled") + } + + func authorize( + service: CloudService, + presenting screen: UIViewController + ) { + guard let manager = CloudFilesManager.all[service] else { + print(">>> Tried to link/auth but the enabled service is not set") + return + } + do { + try manager.link(screen) { [weak self] in + guard let self else { return } + switch $0 { + case .success: + self.connectedServicesSubject.value.insert(service) + self.fetchBackupOnAllProviders() + case .failure(let error): + self.connectedServicesSubject.value.remove(service) + print(">>> Failed to link/auth \(service): \(error.localizedDescription)") } + } + } catch { + print(">>> Exception trying to link/auth \(service): \(error.localizedDescription)") + } + } - let url = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent(UUID().uuidString) - - do { - try data.write(to: url, options: .atomic) - } catch { - print("Couldn't write to temp: \(error.localizedDescription)") - return - } + func fetchBackupOnAllProviders() { + CloudFilesManager.all.lastBackups { [weak self] in + guard let self else { return } + self.backupSubject.send($0) + } + } - switch enabledService { - case .drive: - driveService.uploadBackup(url) { - switch $0 { - case .success(let metadata): - self.settings.value.backups[.drive] = .init( - id: metadata.identifier, - date: metadata.modifiedDate, - size: metadata.size - ) - case .failure(let error): - print(error.localizedDescription) - } - } - case .icloud: - icloudService.uploadBackup(url) { - switch $0 { - case .success(let metadata): - self.settings.value.backups[.icloud] = .init( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - case .failure(let error): - print(error.localizedDescription) - } - } - case .dropbox: - dropboxService.uploadBackup(url) { - switch $0 { - case .success(let metadata): - self.settings.value.backups[.dropbox] = .init( - id: metadata.path, - date: metadata.modifiedDate, - size: metadata.size - ) - case .failure(let error): - print(error.localizedDescription) - } - } - case .sftp: - sftpService.uploadBackup(url: url) { - switch $0 { - case .success(let backup): - self.settings.value.backups[.sftp] = backup - case .failure(let error): - print(error.localizedDescription) - } - } + func performUpload(of data: Data) { + guard let enabledService = settings.value.enabledService else { + fatalError(">>> Trying to backup but nothing is enabled") + } + if enabledService == .sftp { + let keychain = Keychain(service: "SFTP-XXM") + guard let host = try? keychain.get("host"), + let password = try? keychain.get("pwd"), + let username = try? keychain.get("username") else { + fatalError(">>> Tried to perform an sftp backup but its not configured") + } + CloudFilesManager.all[.sftp] = .sftp( + host: host, + username: username, + password: password, + fileName: "backup.xxm" + ) + } + guard let manager = CloudFilesManager.all[enabledService] else { + return // Tried to upload but the enabled service is not set + } + do { + try manager.upload(data) { [weak self] in + guard let self else { return } + + switch $0 { + case .success(let metadata): + self.backupSubject.value[enabledService] = .init( + size: metadata.size, + lastModified: metadata.lastModified + ) + case .failure(let error): + print(">>> Failed to perform a backup upload: \(error.localizedDescription)") } + } + } catch { + print(">>> Exception performing a backup upload: \(error.localizedDescription)") } + } + + private func getBackupURL() -> URL { + guard let folderURL = try? FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) else { fatalError(">>> Couldn't generate the URL for backup") } + + return folderURL + .appendingPathComponent("backup") + .appendingPathExtension("xxm") + } } + diff --git a/Sources/BackupFeature/Service/Dependency.swift b/Sources/BackupFeature/Service/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..9d2b0df290ff5e9e90937473f0e6e4ccd0d26955 --- /dev/null +++ b/Sources/BackupFeature/Service/Dependency.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum BackupServiceDependencyKey: DependencyKey { + static let liveValue: BackupService = .init() + static let testValue: BackupService = .init() +} + +extension DependencyValues { + public var backupService: BackupService { + get { self[BackupServiceDependencyKey.self] } + set { self[BackupServiceDependencyKey.self] = newValue } + } +} diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index 0731bc4a94f5be17d6e224d5f67573ea68ea608f..410535e63a8f348ef7882aa9dfad4bd90674ae4a 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -1,107 +1,115 @@ import UIKit -import Models import Shared +import AppCore import Combine -import Foundation -import DependencyInjection -import HUD +import XXClient +import Defaults +import CloudFiles +import AppNavigation +import ComposableArchitecture enum BackupActionState { - case backupFinished - case backupAllowed(Bool) - case backupInProgress(Float, Float) + case backupFinished + case backupAllowed(Bool) + case backupInProgress(Float, Float) } struct BackupConfigViewModel { - var didTapBackupNow: () -> Void - var didChooseWifiOnly: (Bool) -> Void - var didChooseAutomatic: (Bool) -> Void - var didToggleService: (UIViewController, CloudService, Bool) -> Void - var didTapService: (CloudService, UIViewController) -> Void + var didTapBackupNow: () -> Void + var didChooseWifiOnly: (Bool) -> Void + var didChooseAutomatic: (Bool) -> Void + var didToggleService: (UIViewController, CloudService, Bool) -> Void + var didTapService: (CloudService, UIViewController) -> Void - var wifiOnly: () -> AnyPublisher<Bool, Never> - var automatic: () -> AnyPublisher<Bool, Never> - var lastBackup: () -> AnyPublisher<Backup?, Never> - var actionState: () -> AnyPublisher<BackupActionState, Never> - var enabledService: () -> AnyPublisher<CloudService?, Never> - var connectedServices: () -> AnyPublisher<Set<CloudService>, Never> + var wifiOnly: () -> AnyPublisher<Bool, Never> + var automatic: () -> AnyPublisher<Bool, Never> + var lastBackup: () -> AnyPublisher<Fetch.Metadata?, Never> + var actionState: () -> AnyPublisher<BackupActionState, Never> + var enabledService: () -> AnyPublisher<CloudService?, Never> + var connectedServices: () -> AnyPublisher<Set<CloudService>, Never> } extension BackupConfigViewModel { - static func live() -> Self { - class Context { - @Dependency var hud: HUD - @Dependency var service: BackupService - @Dependency var coordinator: BackupCoordinating - } - - let context = Context() + static func live() -> Self { + class Context { + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.backupService) var service: BackupService + @Dependency(\.app.hudManager) var hudManager: HUDManager + } - return .init( - didTapBackupNow: { - context.service.performBackup() - context.hud.update(with: .on) - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { - context.hud.update(with: .none) - } - }, - didChooseWifiOnly: context.service.setBackupOnlyOnWifi(_:), - didChooseAutomatic: context.service.setBackupAutomatically(_:), - didToggleService: { controller, service, enabling in - guard enabling == true else { - context.service.toggle(service: service, enabling: enabling) - return - } + let context = Context() - context.coordinator.toPassphrase(from: controller, cancelClosure: { - context.service.toggle(service: service, enabling: false) - }, passphraseClosure: { passphrase in - context.service.passphrase = passphrase - context.hud.update(with: .onTitle("Initializing and securing your backup file will take few seconds, please keep the app open.")) - DispatchQueue.global().async { - context.service.toggle(service: service, enabling: enabling) + return .init( + didTapBackupNow: { + context.service.didForceBackup() + context.hudManager.show() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + context.hudManager.hide() + } + }, + didChooseWifiOnly: context.service.didSetWiFiOnly(enabled:), + didChooseAutomatic: context.service.didSetAutomaticBackup(enabled:), + didToggleService: { controller, service, enabling in + guard enabling == true else { + context.service.toggle(service: service, enabling: false) + context.service.stopBackups() + return + } + context.navigator.perform(PresentPassphrase(onCancel: { + context.service.toggle(service: service, enabling: false) + }, onPassphrase: { passphrase in + context.hudManager.show(.init( + content: "Initializing and securing your backup file will take few seconds, please keep the app open." + )) + context.service.toggle(service: service, enabling: enabling) + context.service.initializeBackup(passphrase: passphrase) + context.hudManager.hide() + })) + }, + didTapService: { service, controller in + if service == .sftp { + context.navigator.perform(PresentSFTP(completion: { host, username, password in + context.service.setupSFTP(host: host, username: username, password: password) + }, on: controller.navigationController!)) + return + } - DispatchQueue.main.async { - context.hud.update(with: .none) - } - } - }) - }, - didTapService: context.service.authorize, - wifiOnly: { - context.service.settingsPublisher - .map(\.wifiOnlyBackup) - .eraseToAnyPublisher() - }, - automatic: { - context.service.settingsPublisher - .map(\.automaticBackups) - .eraseToAnyPublisher() - }, - lastBackup: { - context.service.settingsPublisher - .map { - guard let enabledService = $0.enabledService else { return nil } - return $0.backups[enabledService] - }.eraseToAnyPublisher() - }, - actionState: { - context.service.settingsPublisher - .map(\.enabledService) - .map { BackupActionState.backupAllowed($0 != nil) } - .eraseToAnyPublisher() - }, - enabledService: { - context.service.settingsPublisher - .map(\.enabledService) - .eraseToAnyPublisher() - }, - connectedServices: { - context.service.settingsPublisher - .map(\.connectedServices) - .removeDuplicates() - .eraseToAnyPublisher() - } - ) - } + context.service.authorize(service: service, presenting: controller) + }, + wifiOnly: { + context.service.settingsPublisher + .map(\.wifiOnlyBackup) + .eraseToAnyPublisher() + }, + automatic: { + context.service.settingsPublisher + .map(\.automaticBackups) + .eraseToAnyPublisher() + }, + lastBackup: { + context.service.settingsPublisher + .combineLatest(context.service.backupsPublisher) + .map { settings, backups in + guard let enabled = settings.enabledService else { return nil } + return backups[enabled] + }.eraseToAnyPublisher() + }, + actionState: { + context.service.settingsPublisher + .map(\.enabledService) + .map { BackupActionState.backupAllowed($0 != nil) } + .eraseToAnyPublisher() + }, + enabledService: { + context.service.settingsPublisher + .map(\.enabledService) + .eraseToAnyPublisher() + }, + connectedServices: { + context.service.connectedServicesPublisher + .removeDuplicates() + .eraseToAnyPublisher() + } + ) + } } diff --git a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..bd18ee5f294fafcdf2ad8d28eb8465b36429f916 --- /dev/null +++ b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift @@ -0,0 +1,101 @@ +import UIKit +import Shout +import Socket +import Shared +import Combine +import Foundation +import CloudFiles +import CloudFilesSFTP + +import AppCore +import ComposableArchitecture + +struct SFTPViewState { + var host: String = "" + var username: String = "" + var password: String = "" + var isButtonEnabled: Bool = false +} + +final class BackupSFTPViewModel { + @Dependency(\.app.hudManager) var hudManager: HUDManager + + var statePublisher: AnyPublisher<SFTPViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + var authPublisher: AnyPublisher<(String, String, String), Never> { + authSubject.eraseToAnyPublisher() + } + + private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) + private let authSubject = PassthroughSubject<(String, String, String), Never>() + + func didEnterHost(_ string: String) { + stateSubject.value.host = string + validate() + } + + func didEnterUsername(_ string: String) { + stateSubject.value.username = string + validate() + } + + func didEnterPassword(_ string: String) { + stateSubject.value.password = string + validate() + } + + func didTapLogin() { + hudManager.show() + + let host = stateSubject.value.host + let username = stateSubject.value.username + let password = stateSubject.value.password + + let anyController = UIViewController() + + DispatchQueue.global().async { [weak self] in + guard let self else { return } + do { + try CloudFilesManager.sftp( + host: host, + username: username, + password: password, + fileName: "backup.xxm" + ).link(anyController) { + switch $0 { + case .success: + self.hudManager.hide() + self.authSubject.send((host, username, password)) + case .failure(let error): + var message = "An error occurred while trying to link SFTP: " + + if case let CloudFilesSFTP.SFTP.SFTPError.link(linkError) = error { + if let sshError = linkError as? SSHError { + message.append(sshError.message) + } else if let socketError = linkError as? Socket.Error, let reason = socketError.errorReason { + message.append(reason) + } else { + message.append(error.localizedDescription) + } + } else { + message.append(error.localizedDescription) + } + + self.hudManager.show(.init(content: message)) + } + } + } catch { + self.hudManager.show(.init(error: error)) + } + } + } + + private func validate() { + stateSubject.value.isButtonEnabled = + !stateSubject.value.host.isEmpty && + !stateSubject.value.username.isEmpty && + !stateSubject.value.password.isEmpty + } +} diff --git a/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift index cc647d9aaecc7507eed9d517909dbf3229b6901f..a470e6c647c987e1af9668fe3a57807075377980 100644 --- a/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift @@ -1,21 +1,20 @@ import UIKit -import Models import Shared import Combine -import GoogleDriveFeature -import DependencyInjection +import CloudFiles +import ComposableArchitecture struct BackupSetupViewModel { - var didTapService: (CloudService, UIViewController) -> Void + var didTapService: (CloudService, UIViewController) -> Void } extension BackupSetupViewModel { - static func live() -> Self { - class Context { - @Dependency var service: BackupService - } - - let context = Context() - return .init(didTapService: context.service.authorize) + static func live() -> Self { + class Context { + @Dependency(\.backupService) var service: BackupService } + + let context = Context() + return .init(didTapService: context.service.authorize) + } } diff --git a/Sources/BackupFeature/ViewModels/BackupViewModel.swift b/Sources/BackupFeature/ViewModels/BackupViewModel.swift index 6522ee1215a4ee84000c85600389bf53d52aa9bc..378590c3edadce9b7036e2f262fcedc6f36c9626 100644 --- a/Sources/BackupFeature/ViewModels/BackupViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupViewModel.swift @@ -1,35 +1,34 @@ import Combine -import DependencyInjection +import ComposableArchitecture 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(\.backupService) var service: BackupService + } - let context = Context() + let context = Context() - return .init( - setupViewModel: { BackupSetupViewModel.live() }, - configViewModel: { BackupConfigViewModel.live() }, - state: { - context.service.settingsPublisher - .map(\.connectedServices) - .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 new file mode 100644 index 0000000000000000000000000000000000000000..3d12ccbcf180961a0bdc989404b2571aa944b841 --- /dev/null +++ b/Sources/BackupFeature/Views/RestoreSFTPView.swift @@ -0,0 +1,84 @@ +import UIKit +import Shared +import InputField +import AppResources + +final class BackupSFTPView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let hostField = OutlinedInputField() + let usernameField = OutlinedInputField() + let passwordField = OutlinedInputField() + let loginButton = CapsuleButton() + let stackView = UIStackView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.neutralDark.color + titleLabel.text = Localized.AccountRestore.Sftp.title + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSMutableAttributedString( + string: Localized.AccountRestore.Sftp.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + attString.setAttributes( + attributes: [ + .foregroundColor: Asset.neutralDark.color, + .font: Fonts.Mulish.bold.font(size: 12.0) as Any, + .paragraphStyle: paragraph + ], betweenCharacters: "*") + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = attString + + hostField.setup(title: Localized.AccountRestore.Sftp.host) + usernameField.setup(title: Localized.AccountRestore.Sftp.username) + passwordField.setup(title: Localized.AccountRestore.Sftp.password, sensitive: true) + + loginButton.set(style: .brandColored, title: Localized.AccountRestore.Sftp.login) + + stackView.spacing = 30 + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.addArrangedSubview(hostField) + stackView.addArrangedSubview(usernameField) + stackView.addArrangedSubview(passwordField) + stackView.addArrangedSubview(loginButton) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).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().offset(38) + $0.right.equalToSuperview().offset(-38) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/ChatFeature/CellFactory.swift b/Sources/ChatFeature/CellFactory.swift new file mode 100644 index 0000000000000000000000000000000000000000..2f7767ebe4902933f0615d000ca48e6ebf9ed8ee --- /dev/null +++ b/Sources/ChatFeature/CellFactory.swift @@ -0,0 +1,76 @@ +//import UIKit +//import XCTestDynamicOverlay +// +//public struct CellFactory<Model> { +// public struct Registrar { +// public init(register: @escaping (UICollectionView) -> Void) { +// self.register = register +// } +// +// public var register: (UICollectionView) -> Void +// +// public func callAsFunction(in view: UICollectionView) { +// register(view) +// } +// } +// +// public struct Builder { +// public init(build: @escaping (Model, UICollectionView, IndexPath) -> UICollectionViewCell?) { +// self.build = build +// } +// +// public var build: (Model, UICollectionView, IndexPath) -> UICollectionViewCell? +// +// public func callAsFunction( +// for model: Model, +// in view: UICollectionView, +// at indexPath: IndexPath +// ) -> UICollectionViewCell? { +// build(model, view, indexPath) +// } +// } +// +// public init( +// register: Registrar, +// build: Builder +// ) { +// self.register = register +// self.build = build +// } +// +// public var register: Registrar +// public var build: Builder +//} +// +//extension CellFactory { +// public static func combined(_ factories: CellFactory...) -> CellFactory { +// combined(factories) +// } +// +// public static func combined(_ factories: [CellFactory]) -> CellFactory { +// CellFactory( +// register: .init { collectionView in +// factories.forEach { $0.register(in: collectionView) } +// }, +// build: .init { model, collectionView, indexPath in +// for factory in factories { +// if let cell = factory.build(for: model, in: collectionView, at: indexPath) { +// return cell +// } +// } +// return nil +// } +// ) +// } +//} +// +//#if DEBUG +//extension CellFactory { +// public static func unimplemented() -> CellFactory { +// CellFactory( +// register: .init(register: XCTUnimplemented("\(Self.self).Registrar")), +// build: .init(build: XCTUnimplemented("\(Self.self).Builder")) +// ) +// } +//} +//#endif diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift index 5cae4a063a3969e806e1b7c9089d888d345357f3..43ed50008506da291cc8d9233bd468423e4ad8b3 100644 --- a/Sources/ChatFeature/Controllers/GroupChatController.swift +++ b/Sources/ChatFeature/Controllers/GroupChatController.swift @@ -1,18 +1,17 @@ -import HUD import UIKit -import Theme -import Models import Shared import Combine +import AppCore import XXModels import Voxophone import ChatLayout -import Integration +import Dependencies +import AppResources +import AppNavigation import DrawerFeature import DifferenceKit import ReportingFeature import ChatInputFeature -import DependencyInjection typealias OutgoingGroupTextCell = CollectionCell<FlexibleSpace, StackMessageView> typealias IncomingGroupTextCell = CollectionCell<StackMessageView, FlexibleSpace> @@ -22,602 +21,670 @@ typealias OutgoingFailedGroupTextCell = CollectionCell<FlexibleSpace, StackMessa typealias OutgoingFailedGroupReplyCell = CollectionCell<FlexibleSpace, ReplyStackMessageView> public final class GroupChatController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var session: SessionType - @Dependency private var coordinator: ChatCoordinating - @Dependency private var reportingStatus: ReportingStatus - @Dependency private var makeReportDrawer: MakeReportDrawer - @Dependency private var makeAppScreenshot: MakeAppScreenshot - @Dependency private var statusBarController: StatusBarStyleControlling - - private let members: MembersController - private var collectionView: UICollectionView! - lazy private var header = GroupHeaderView() - private let inputComponent: ChatInputView - - private let chatLayout = ChatLayout() - private var animator: ManualAnimator? - private let viewModel: GroupChatViewModel - private let layoutDelegate = LayoutDelegate() - private var cancellables = Set<AnyCancellable>() - private var sections = [ArraySection<ChatSection, Message>]() - private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>() - - public override var canBecomeFirstResponder: Bool { true } - public override var inputAccessoryView: UIView? { inputComponent } - - public init(_ info: GroupInfo) { - let viewModel = GroupChatViewModel(info) - self.viewModel = viewModel - self.members = .init(with: info.members) - - self.inputComponent = ChatInputView(store: .init( - initialState: .init(canAddAttachments: false), - reducer: chatInputReducer, - environment: .init( - voxophone: try! DependencyInjection.Container.shared.resolve() as Voxophone, - sendAudio: { _ in }, - didTapCamera: {}, - didTapLibrary: {}, - sendText: { viewModel.send($0) }, - didTapAbortReply: { viewModel.abortReply() }, - didTapMicrophone: { false } - ) - )) - - super.init(nibName: nil, bundle: nil) - - let memberList = info.members.map { - Member( - title: ($0.nickname ?? $0.username) ?? "Fetching username...", - photo: $0.photo - ) - } - - header.setup(title: info.group.name, memberList: memberList) - } - - public required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize( - backgroundColor: Asset.neutralWhite.color, - shadowColor: Asset.neutralDisabled.color - ) - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - collectionView.collectionViewLayout.invalidateLayout() - becomeFirstResponder() + @Dependency(\.navigator) var navigator + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.statusBar) var statusBar + @Dependency(\.reportingStatus) var reportingStatus + +// @Dependency var makeReportDrawer: MakeReportDrawer +// @Dependency var makeAppScreenshot: MakeAppScreenshot + + private var collectionView: UICollectionView! + private lazy var header = GroupHeaderView() + private let inputComponent: ChatInputView + + private var animator: ManualAnimator? + private let viewModel: GroupChatViewModel + private let layoutDelegate = LayoutDelegate() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + private let chatLayout = CollectionViewChatLayout() + private var sections = [ArraySection<ChatSection, Message>]() + private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>() + + public override var canBecomeFirstResponder: Bool { true } + public override var inputAccessoryView: UIView? { inputComponent } + + public init(_ info: GroupInfo) { + let viewModel = GroupChatViewModel(info) + self.viewModel = viewModel + + self.inputComponent = ChatInputView(store: .init( + initialState: .init(canAddAttachments: false), + reducer: chatInputReducer, + environment: .init( + voxophone: Voxophone(), //try! DI.Container.shared.resolve() as Voxophone, + sendAudio: { _ in }, + didTapCamera: {}, + didTapLibrary: {}, + sendText: { viewModel.send($0) }, + didTapAbortReply: { viewModel.abortReply() }, + didTapMicrophone: { false } + ) + )) + + super.init(nibName: nil, bundle: nil) + + let memberList = info.members.map { + Member( + title: ($0.nickname ?? $0.username) ?? "Fetching username...", + photo: $0.photo + ) } - private var isFirstAppearance = true - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - if isFirstAppearance { - isFirstAppearance = false - let insets = UIEdgeInsets( - top: 0, - left: 0, - bottom: inputComponent.bounds.height - view.safeAreaInsets.bottom, - right: 0 - ) - collectionView.contentInset = insets - collectionView.scrollIndicatorInsets = insets - } + header.setup(title: info.group.name, memberList: memberList) + } + + public required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + navigationController?.navigationBar.customize( + backgroundColor: Asset.neutralWhite.color, + shadowColor: Asset.neutralDisabled.color + ) + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + collectionView.collectionViewLayout.invalidateLayout() + becomeFirstResponder() + } + + private var isFirstAppearance = true + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if isFirstAppearance { + isFirstAppearance = false + let insets = UIEdgeInsets( + top: 0, + left: 0, + bottom: inputComponent.bounds.height - view.safeAreaInsets.bottom, + right: 0 + ) + collectionView.contentInset = insets + collectionView.scrollIndicatorInsets = insets } - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - viewModel.readAll() + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.readAll() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + setupNavigationBar() + setupCollectionView() + setupInputController() + setupBindings() + + KeyboardListener.shared.add(delegate: self) + } + + private func setupNavigationBar() { + let more = UIButton() + more.setImage(Asset.chatMore.image, for: .normal) + more.addTarget(self, action: #selector(didTapDots), for: .touchUpInside) + + navigationItem.titleView = header + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: more) + } + + private func setupCollectionView() { + chatLayout.configure(layoutDelegate) + collectionView = .init(on: view, with: chatLayout) + collectionView.register(IncomingGroupTextCell.self) + collectionView.register(OutgoingGroupTextCell.self) + collectionView.register(IncomingGroupReplyCell.self) + collectionView.register(OutgoingGroupReplyCell.self) + collectionView.register(OutgoingFailedGroupTextCell.self) + collectionView.register(OutgoingFailedGroupReplyCell.self) + collectionView.registerSectionHeader(SectionHeaderView.self) + collectionView.dataSource = self + collectionView.delegate = self + } + + private func setupInputController() { + inputComponent.setMaxHeight { [weak self] in + guard let self else { return 150 } + + let maxHeight = self.collectionView.frame.height + - self.collectionView.adjustedContentInset.top + - self.collectionView.adjustedContentInset.bottom + + self.inputComponent.bounds.height + + return maxHeight * 0.9 } - public override func viewDidLoad() { - super.viewDidLoad() + viewModel.replyPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] senderTitle, messageText in + inputComponent.setupReply(message: messageText, sender: senderTitle) + } + .store(in: &cancellables) + } + + private func setupBindings() { + viewModel + .routesPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch $0 { + case .waitingRound: + let button = DrawerCapsuleButton(model: .init( + title: Localized.Chat.RoundDrawer.action, + style: .brandColored + )) - setupNavigationBar() - setupCollectionView() - setupInputController() - setupBindings() + 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 + ], isDismissable: false, from: self)) + + case .webview(let urlString): + navigator.perform(PresentWebsite(urlString: urlString, from: self)) + } + }.store(in: &cancellables) + + viewModel + .reportPopupPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] contact 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) + + reportButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() +// let screenshot = try! self.makeAppScreenshot() +// self.viewModel.report(contact: contact, screenshot: screenshot) { +// self.collectionView.reloadData() +// } + } + }.store(in: &drawerCancellables) + + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Chat.Report.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.Report.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + axis: .vertical, + spacing: 20.0, + views: [reportButton, cancelButton] + ) + ], isDismissable: true, from: self)) + }.store(in: &cancellables) + + viewModel + .messages + .receive(on: DispatchQueue.main) + .sink { [unowned self] sections in + func process() { + let changeSet = StagedChangeset(source: self.sections, target: sections).flattenIfPossible() + collectionView.reload( + using: changeSet, + interrupt: { changeSet in + guard !self.sections.isEmpty else { return true } + return false + }, onInterruptedReload: { + guard let lastSection = self.sections.last else { return } + let positionSnapshot = ChatLayoutPositionSnapshot( + indexPath: IndexPath( + item: lastSection.elements.count - 1, + section: self.sections.count - 1 + ), + kind: .cell, + edge: .bottom + ) + + self.collectionView.reloadData() + self.chatLayout.restoreContentOffset(with: positionSnapshot) + }, + completion: nil, + setData: { self.sections = $0 } + ) + } - KeyboardListener.shared.add(delegate: self) - } + guard currentInterfaceActions.options.isEmpty else { + let reaction = SetActor<Set<InterfaceActions>, ReactionTypes>.Reaction( + type: .delayedUpdate, + action: .onEmpty, + executionType: .once, + actionBlock: { [weak self] in + guard let _ = self else { return } + process() + } + ) - private func setupNavigationBar() { - let more = UIButton() - more.setImage(Asset.chatMore.image, for: .normal) - more.addTarget(self, action: #selector(didTapDots), for: .touchUpInside) + currentInterfaceActions.add(reaction: reaction) + return + } - navigationItem.titleView = header - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: more) + process() + } + .store(in: &cancellables) + } + + @objc private func didTapDots() { + navigator.perform(PresentMemberList(members: viewModel.info.members)) + } + + func scrollToBottom(completion: (() -> Void)? = nil) { + let contentOffsetAtBottom = CGPoint( + x: collectionView.contentOffset.x, + y: chatLayout.collectionViewContentSize.height + - collectionView.frame.height + collectionView.adjustedContentInset.bottom + ) + + guard contentOffsetAtBottom.y > collectionView.contentOffset.y else { completion?(); return } + + let initialOffset = collectionView.contentOffset.y + let delta = contentOffsetAtBottom.y - initialOffset + + if abs(delta) > chatLayout.visibleBounds.height { + animator = ManualAnimator() + animator?.animate(duration: TimeInterval(0.25), curve: .easeInOut) { [weak self] percentage in + guard let self else { return } + + self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: initialOffset + (delta * percentage)) + if percentage == 1.0 { + self.animator = nil + let positionSnapshot = ChatLayoutPositionSnapshot(indexPath: IndexPath(item: 0, section: 0), kind: .footer, edge: .bottom) + self.chatLayout.restoreContentOffset(with: positionSnapshot) + self.currentInterfaceActions.options.remove(.scrollingToBottom) + completion?() + } + } + } else { + currentInterfaceActions.options.insert(.scrollingToBottom) + UIView.animate(withDuration: 0.25, animations: { + self.collectionView.setContentOffset(contentOffsetAtBottom, animated: true) + }, completion: { [weak self] _ in + self?.currentInterfaceActions.options.remove(.scrollingToBottom) + completion?() + }) } + } +} - private func setupCollectionView() { - chatLayout.configure(layoutDelegate) - collectionView = .init(on: view, with: chatLayout) - collectionView.register(IncomingGroupTextCell.self) - collectionView.register(OutgoingGroupTextCell.self) - collectionView.register(IncomingGroupReplyCell.self) - collectionView.register(OutgoingGroupReplyCell.self) - collectionView.register(OutgoingFailedGroupTextCell.self) - collectionView.register(OutgoingFailedGroupReplyCell.self) - collectionView.registerSectionHeader(SectionHeaderView.self) - collectionView.dataSource = self - collectionView.delegate = self +extension GroupChatController: UICollectionViewDataSource { + public func numberOfSections(in collectionView: UICollectionView) -> Int { + sections.count + } + + public func collectionView(_ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath) -> UICollectionReusableView { + let sectionHeader: SectionHeaderView = collectionView.dequeueSupplementaryView(forIndexPath: indexPath) + sectionHeader.title.text = sections[indexPath.section].model.date.asDayOfMonth() + return sectionHeader + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + sections[section].elements.count + } + + public func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + + var item = sections[indexPath.section].elements[indexPath.item] + let canReply: () -> Bool = { + (item.status == .sent || item.status == .received) && item.networkId != nil } - private func setupInputController() { - inputComponent.setMaxHeight { [weak self] in - guard let self = self else { return 150 } - - let maxHeight = self.collectionView.frame.height - - self.collectionView.adjustedContentInset.top - - self.collectionView.adjustedContentInset.bottom - + self.inputComponent.bounds.height - - return maxHeight * 0.9 - } - - viewModel.replyPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] senderTitle, messageText in - inputComponent.setupReply(message: messageText, sender: senderTitle) - } - .store(in: &cancellables) + let performReply: () -> Void = { [weak self] in + self?.viewModel.didRequestReply(item) } - private func setupBindings() { - viewModel.routesPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch $0 { - case .waitingRound: - coordinator.toDrawer(makeWaitingRoundDrawer(), from: self) - case .webview(let urlString): - coordinator.toWebview(with: urlString, from: self) - } - }.store(in: &cancellables) - - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) + let name: (Data) -> String = viewModel.getName(from:) + let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) + let replyContent: (Data) -> (String, String) = viewModel.getReplyContent(for:) - viewModel.reportPopupPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] contact in - presentReportDrawer(contact) - }.store(in: &cancellables) + var isSenderBanned = false - viewModel.messages - .receive(on: DispatchQueue.main) - .sink { [unowned self] sections in - func process() { - let changeSet = StagedChangeset(source: self.sections, target: sections).flattenIfPossible() - collectionView.reload( - using: changeSet, - interrupt: { changeSet in - guard !self.sections.isEmpty else { return true } - return false - }, onInterruptedReload: { - guard let lastSection = self.sections.last else { return } - let positionSnapshot = ChatLayoutPositionSnapshot( - indexPath: IndexPath( - item: lastSection.elements.count - 1, - section: self.sections.count - 1 - ), - kind: .cell, - edge: .bottom - ) - - self.collectionView.reloadData() - self.chatLayout.restoreContentOffset(with: positionSnapshot) - }, - completion: nil, - setData: { self.sections = $0 } - ) - } - - guard currentInterfaceActions.options.isEmpty else { - let reaction = SetActor<Set<InterfaceActions>, ReactionTypes>.Reaction( - type: .delayedUpdate, - action: .onEmpty, - executionType: .once, - actionBlock: { [weak self] in - guard let _ = self else { return } - process() - } - ) - - currentInterfaceActions.add(reaction: reaction) - return - } - - process() - } - .store(in: &cancellables) + if let sender = try? dbManager.getDB().fetchContacts(.init(id: [item.senderId])).first { + isSenderBanned = sender.isBanned } - @objc private func didTapDots() { - coordinator.toMembersList(members, from: self) - } + if item.status == .received { + guard isSenderBanned == false else { + item.text = "This user has been banned" - private func presentReportDrawer(_ contact: Contact) { - var config = MakeReportDrawer.Config() - config.onReport = { [weak self] in - guard let self = self else { return } - let screenshot = try! self.makeAppScreenshot() - self.viewModel.report(contact: contact, screenshot: screenshot) { - self.collectionView.reloadData() - } - } - let drawer = makeReportDrawer(config) - coordinator.toDrawer(drawer, from: self) - } - - private func makeWaitingRoundDrawer() -> UIViewController { - let text = DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Chat.RoundDrawer.title, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 + let cell: IncomingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + Bubbler.buildGroup( + bubble: cell.leftView, + with: item, + with: "Banned user" ) - let button = DrawerCapsuleButton(model: .init( - title: Localized.Chat.RoundDrawer.action, - style: .brandColored - )) + cell.canReply = false + cell.performReply = {} + cell.leftView.didTapShowRound = {} - let drawer = DrawerController(with: [text, button]) + return cell + } - button.action - .receive(on: DispatchQueue.main) - .sink { [weak drawer] in - drawer?.dismiss(animated: true) - }.store(in: &drawer.cancellables) + if let replyMessageId = item.replyMessageId { + let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - return drawer - } + Bubbler.buildReplyGroup( + bubble: cell.leftView, + with: item, + reply: replyContent(replyMessageId), + sender: name(item.senderId) + ) - func scrollToBottom(completion: (() -> Void)? = nil) { - let contentOffsetAtBottom = CGPoint( - x: collectionView.contentOffset.x, - y: chatLayout.collectionViewContentSize.height - - collectionView.frame.height + collectionView.adjustedContentInset.bottom + cell.canReply = canReply() + cell.performReply = performReply + cell.leftView.didTapShowRound = { showRound(item.roundURL) } + + return cell + } else { + let cell: IncomingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + Bubbler.buildGroup( + bubble: cell.leftView, + with: item, + with: name(item.senderId) ) - guard contentOffsetAtBottom.y > collectionView.contentOffset.y else { completion?(); return } + cell.canReply = canReply() + cell.performReply = performReply + cell.leftView.didTapShowRound = { showRound(item.roundURL) } + + return cell + } + } else if item.status == .sendingFailed { + if let replyMessageId = item.replyMessageId { + let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.buildReplyGroup( + bubble: cell.rightView, + with: item, + reply: replyContent(replyMessageId), + sender: name(item.senderId) + ) - let initialOffset = collectionView.contentOffset.y - let delta = contentOffsetAtBottom.y - initialOffset + cell.canReply = canReply() + cell.performReply = performReply - if abs(delta) > chatLayout.visibleBounds.height { - animator = ManualAnimator() - animator?.animate(duration: TimeInterval(0.25), curve: .easeInOut) { [weak self] percentage in - guard let self = self else { return } + return cell + } else { + let cell: OutgoingFailedGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: initialOffset + (delta * percentage)) - if percentage == 1.0 { - self.animator = nil - let positionSnapshot = ChatLayoutPositionSnapshot(indexPath: IndexPath(item: 0, section: 0), kind: .footer, edge: .bottom) - self.chatLayout.restoreContentOffset(with: positionSnapshot) - self.currentInterfaceActions.options.remove(.scrollingToBottom) - completion?() - } - } - } else { - currentInterfaceActions.options.insert(.scrollingToBottom) - UIView.animate(withDuration: 0.25, animations: { - self.collectionView.setContentOffset(contentOffsetAtBottom, animated: true) - }, completion: { [weak self] _ in - self?.currentInterfaceActions.options.remove(.scrollingToBottom) - completion?() - }) - } - } -} + Bubbler.buildGroup( + bubble: cell.rightView, + with: item, + with: name(item.senderId) + ) -extension GroupChatController: UICollectionViewDataSource { - public func numberOfSections(in collectionView: UICollectionView) -> Int { - sections.count - } + cell.canReply = canReply() + cell.performReply = performReply - public func collectionView(_ collectionView: UICollectionView, - viewForSupplementaryElementOfKind kind: String, - at indexPath: IndexPath) -> UICollectionReusableView { - let sectionHeader: SectionHeaderView = collectionView.dequeueSupplementaryView(forIndexPath: indexPath) - sectionHeader.title.text = sections[indexPath.section].model.date.asDayOfMonth() - return sectionHeader - } + return cell + } + } else if item.status == .sendingTimedOut { + if let replyMessageId = item.replyMessageId { + let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - sections[section].elements.count - } + Bubbler.buildReplyGroup( + bubble: cell.rightView, + with: item, + reply: replyContent(replyMessageId), + sender: name(item.senderId) + ) - public func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { + cell.canReply = false + cell.performReply = performReply - var item = sections[indexPath.section].elements[indexPath.item] - let canReply: () -> Bool = { - (item.status == .sent || item.status == .received) && item.networkId != nil - } + return cell + } else { + let cell: OutgoingFailedGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - let performReply: () -> Void = { [weak self] in - self?.viewModel.didRequestReply(item) - } + Bubbler.buildGroup( + bubble: cell.rightView, + with: item, + with: name(item.senderId) + ) - let name: (Data) -> String = viewModel.getName(from:) - let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) - let replyContent: (Data) -> (String, String) = viewModel.getReplyContent(for:) + cell.canReply = false + cell.performReply = performReply - var isSenderBanned = false + return cell + } + } else { + if let replyMessageId = item.replyMessageId { + let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - if let sender = try? session.dbManager.fetchContacts(.init(id: [item.senderId])).first { - isSenderBanned = sender.isBanned - } + Bubbler.buildReplyGroup( + bubble: cell.rightView, + with: item, + reply: replyContent(replyMessageId), + sender: name(item.senderId) + ) - if item.status == .received { - guard isSenderBanned == false else { - item.text = "This user has been banned" + cell.canReply = canReply() + cell.performReply = performReply + cell.rightView.didTapShowRound = { showRound(item.roundURL) } - let cell: IncomingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - Bubbler.buildGroup( - bubble: cell.leftView, - with: item, - with: "Banned user" - ) + return cell + } else { + let cell: OutgoingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - cell.canReply = false - cell.performReply = {} - cell.leftView.didTapShowRound = {} + Bubbler.buildGroup( + bubble: cell.rightView, + with: item, + with: name(item.senderId) + ) - return cell - } + cell.canReply = canReply() + cell.performReply = performReply + cell.rightView.didTapShowRound = { showRound(item.roundURL) } - if let replyMessageId = item.replyMessageId { - let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildReplyGroup( - bubble: cell.leftView, - with: item, - reply: replyContent(replyMessageId), - sender: name(item.senderId) - ) - - cell.canReply = canReply() - cell.performReply = performReply - cell.leftView.didTapShowRound = { showRound(item.roundURL) } - - return cell - } else { - let cell: IncomingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - Bubbler.buildGroup( - bubble: cell.leftView, - with: item, - with: name(item.senderId) - ) - - cell.canReply = canReply() - cell.performReply = performReply - cell.leftView.didTapShowRound = { showRound(item.roundURL) } - - return cell - } - } else if item.status == .sendingFailed { - if let replyMessageId = item.replyMessageId { - let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildReplyGroup( - bubble: cell.rightView, - with: item, - reply: replyContent(replyMessageId), - sender: name(item.senderId) - ) - - cell.canReply = canReply() - cell.performReply = performReply - - return cell - } else { - let cell: OutgoingFailedGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildGroup( - bubble: cell.rightView, - with: item, - with: name(item.senderId) - ) - - cell.canReply = canReply() - cell.performReply = performReply - - return cell - } - } else { - if let replyMessageId = item.replyMessageId { - let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildReplyGroup( - bubble: cell.rightView, - with: item, - reply: replyContent(replyMessageId), - sender: name(item.senderId) - ) - - cell.canReply = canReply() - cell.performReply = performReply - cell.rightView.didTapShowRound = { showRound(item.roundURL) } - - return cell - } else { - let cell: OutgoingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildGroup( - bubble: cell.rightView, - with: item, - with: name(item.senderId) - ) - - cell.canReply = canReply() - cell.performReply = performReply - cell.rightView.didTapShowRound = { showRound(item.roundURL) } - - return cell - } - } + return cell + } } + } } extension GroupChatController: KeyboardListenerDelegate { - fileprivate var isUserInitiatedScrolling: Bool { - return collectionView.isDragging || collectionView.isDecelerating + fileprivate var isUserInitiatedScrolling: Bool { + return collectionView.isDragging || collectionView.isDecelerating + } + + func keyboardWillChangeFrame(info: KeyboardInfo) { + let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first + + guard !currentInterfaceActions.options.contains(.changingFrameSize), + collectionView.contentInsetAdjustmentBehavior != .never, + let keyboardFrame = keyWindow?.convert(info.frameEnd, to: view), + collectionView.convert(collectionView.bounds, to: keyWindow).maxY > info.frameEnd.minY else { + return } - - func keyboardWillChangeFrame(info: KeyboardInfo) { - let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first - - guard !currentInterfaceActions.options.contains(.changingFrameSize), - collectionView.contentInsetAdjustmentBehavior != .never, - let keyboardFrame = keyWindow?.convert(info.frameEnd, to: view), - collectionView.convert(collectionView.bounds, to: keyWindow).maxY > info.frameEnd.minY else { - return - } - currentInterfaceActions.options.insert(.changingKeyboardFrame) - let newBottomInset = collectionView.frame.minY + collectionView.frame.size.height - keyboardFrame.minY - collectionView.safeAreaInsets.bottom - if newBottomInset > 0, - collectionView.contentInset.bottom != newBottomInset { - let positionSnapshot = chatLayout.getContentOffsetSnapshot(from: .bottom) - - currentInterfaceActions.options.insert(.changingContentInsets) - UIView.animate(withDuration: info.animationDuration, animations: { - self.collectionView.performBatchUpdates({ - self.collectionView.contentInset.bottom = newBottomInset - self.collectionView.verticalScrollIndicatorInsets.bottom = newBottomInset - }, completion: nil) - - if let positionSnapshot = positionSnapshot, !self.isUserInitiatedScrolling { - self.chatLayout.restoreContentOffset(with: positionSnapshot) - } - }, completion: { _ in - self.currentInterfaceActions.options.remove(.changingContentInsets) - }) + currentInterfaceActions.options.insert(.changingKeyboardFrame) + let newBottomInset = collectionView.frame.minY + collectionView.frame.size.height - keyboardFrame.minY - collectionView.safeAreaInsets.bottom + if newBottomInset > 0, + collectionView.contentInset.bottom != newBottomInset { + let positionSnapshot = chatLayout.getContentOffsetSnapshot(from: .bottom) + + currentInterfaceActions.options.insert(.changingContentInsets) + UIView.animate(withDuration: info.animationDuration, animations: { + self.collectionView.performBatchUpdates({ + self.collectionView.contentInset.bottom = newBottomInset + self.collectionView.verticalScrollIndicatorInsets.bottom = newBottomInset + }, completion: nil) + + if let positionSnapshot = positionSnapshot, !self.isUserInitiatedScrolling { + self.chatLayout.restoreContentOffset(with: positionSnapshot) } + }, completion: { _ in + self.currentInterfaceActions.options.remove(.changingContentInsets) + }) } + } - func keyboardDidChangeFrame(info: KeyboardInfo) { - guard currentInterfaceActions.options.contains(.changingKeyboardFrame) else { return } - currentInterfaceActions.options.remove(.changingKeyboardFrame) - } + func keyboardDidChangeFrame(info: KeyboardInfo) { + guard currentInterfaceActions.options.contains(.changingKeyboardFrame) else { return } + currentInterfaceActions.options.remove(.changingKeyboardFrame) + } } extension GroupChatController: UICollectionViewDelegate { - private func makeTargetedPreview(for configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - guard let identifier = configuration.identifier as? String, - let first = identifier.components(separatedBy: "|").first, - let last = identifier.components(separatedBy: "|").last, - let item = Int(first), let section = Int(last), - let cell = collectionView.cellForItem(at: IndexPath(item: item, section: section)) else { - return nil - } - - let parameters = UIPreviewParameters() - parameters.backgroundColor = .clear - - if sections[section].elements[item].status == .received { - var leftView: UIView! - - if let cell = cell as? IncomingGroupReplyCell { - leftView = cell.leftView - } else if let cell = cell as? IncomingGroupTextCell { - leftView = cell.leftView - } + private func makeTargetedPreview(for configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + guard let identifier = configuration.identifier as? String, + let first = identifier.components(separatedBy: "|").first, + let last = identifier.components(separatedBy: "|").last, + let item = Int(first), let section = Int(last), + let cell = collectionView.cellForItem(at: IndexPath(item: item, section: section)) else { + return nil + } - parameters.visiblePath = UIBezierPath(roundedRect: leftView.bounds, cornerRadius: 13) - return UITargetedPreview(view: leftView, parameters: parameters) - } + let parameters = UIPreviewParameters() + parameters.backgroundColor = .clear - var rightView: UIView! + if sections[section].elements[item].status == .received { + var leftView: UIView! - if let cell = cell as? OutgoingGroupTextCell { - rightView = cell.rightView - } else if let cell = cell as? OutgoingGroupReplyCell { - rightView = cell.rightView - } + if let cell = cell as? IncomingGroupReplyCell { + leftView = cell.leftView + } else if let cell = cell as? IncomingGroupTextCell { + leftView = cell.leftView + } - parameters.visiblePath = UIBezierPath(roundedRect: rightView.bounds, cornerRadius: 13) - return UITargetedPreview(view: rightView, parameters: parameters) + parameters.visiblePath = UIBezierPath(roundedRect: leftView.bounds, cornerRadius: 13) + return UITargetedPreview(view: leftView, parameters: parameters) } - public func collectionView( - _ collectionView: UICollectionView, - previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration - ) -> UITargetedPreview? { - makeTargetedPreview(for: configuration) - } + var rightView: UIView! - public func collectionView( - _ collectionView: UICollectionView, - previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration - ) -> UITargetedPreview? { - makeTargetedPreview(for: configuration) + if let cell = cell as? OutgoingGroupTextCell { + rightView = cell.rightView + } else if let cell = cell as? OutgoingGroupReplyCell { + rightView = cell.rightView } - public func collectionView( - _ collectionView: UICollectionView, - contextMenuConfigurationForItemAt indexPath: IndexPath, - point: CGPoint - ) -> UIContextMenuConfiguration? { - UIContextMenuConfiguration( - identifier: "\(indexPath.item)|\(indexPath.section)" as NSCopying, - previewProvider: nil - ) { [weak self] suggestedActions in - - guard let self = self else { return nil } - - let item = self.sections[indexPath.section].elements[indexPath.item] - - let copy = UIAction(title: Localized.Chat.BubbleMenu.copy, state: .off) { _ in - UIPasteboard.general.string = item.text - } - - let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in - self?.viewModel.didRequestReply(item) - } - - let delete = UIAction(title: Localized.Chat.BubbleMenu.delete, state: .off) { [weak self] _ in - self?.viewModel.didRequestDelete([item]) - } - - let report = UIAction(title: Localized.Chat.BubbleMenu.report, state: .off) { [weak self] _ in - self?.viewModel.didRequestReport(item) - } - - let retry = UIAction(title: Localized.Chat.BubbleMenu.retry, state: .off) { [weak self] _ in - self?.viewModel.retry(item) - } - - var children = [UIAction]() - - if item.status == .sendingFailed { - children = [copy, retry, delete] - } else if item.status == .sending { - children = [copy] - } else { - children = [copy, reply, delete] - - if self.reportingStatus.isEnabled() { - children.append(report) - } - } - - return UIMenu(title: "", children: children) + parameters.visiblePath = UIBezierPath(roundedRect: rightView.bounds, cornerRadius: 13) + return UITargetedPreview(view: rightView, parameters: parameters) + } + + public func collectionView( + _ collectionView: UICollectionView, + previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration + ) -> UITargetedPreview? { + makeTargetedPreview(for: configuration) + } + + public func collectionView( + _ collectionView: UICollectionView, + previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration + ) -> UITargetedPreview? { + makeTargetedPreview(for: configuration) + } + + public func collectionView( + _ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint + ) -> UIContextMenuConfiguration? { + UIContextMenuConfiguration( + identifier: "\(indexPath.item)|\(indexPath.section)" as NSCopying, + previewProvider: nil + ) { [weak self] suggestedActions in + + guard let self else { + fatalError() + //return nil + } + + let item = self.sections[indexPath.section].elements[indexPath.item] + + let copy = UIAction(title: Localized.Chat.BubbleMenu.copy, state: .off) { _ in + UIPasteboard.general.string = item.text + } + + let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in + self?.viewModel.didRequestReply(item) + } + + let delete = UIAction(title: Localized.Chat.BubbleMenu.delete, state: .off) { [weak self] _ in + self?.viewModel.didRequestDelete([item]) + } + + let report = UIAction(title: Localized.Chat.BubbleMenu.report, state: .off) { [weak self] _ in + self?.viewModel.didRequestReport(item) + } + + let retry = UIAction(title: Localized.Chat.BubbleMenu.retry, state: .off) { [weak self] _ in + self?.viewModel.retry(item) + } + + var children = [UIAction]() + + if item.status == .sendingFailed { + children = [copy, retry, delete] + } else if item.status == .sending { + children = [copy] + } else { + children = [copy, reply, delete] + + if self.reportingStatus.isEnabled() { + children.append(report) } + } + + return UIMenu(title: "", children: children) } + } } diff --git a/Sources/ChatFeature/Controllers/MembersController.swift b/Sources/ChatFeature/Controllers/MembersController.swift index e7bf2c3ece345ace155ed7babbf5c9ec05bf1caf..086114eaafc2da90ceec2c80a61d926d255d62c9 100644 --- a/Sources/ChatFeature/Controllers/MembersController.swift +++ b/Sources/ChatFeature/Controllers/MembersController.swift @@ -1,83 +1,83 @@ import UIKit -import Models import Shared import XXModels +import AppResources final class MembersController: UIViewController { - lazy private var stackView = UIStackView() - - private let members: [Contact] - - init(with members: [Contact]) { - self.members = members - super.init(nibName: nil, bundle: nil) + private lazy var stackView = UIStackView() + + private let members: [Contact] + + init(with members: [Contact]) { + self.members = members + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewDidLoad() { + super.viewDidLoad() + + view.layer.cornerRadius = 15 + view.layer.masksToBounds = true + view.backgroundColor = Asset.neutralWhite.color + + stackView.axis = .vertical + stackView.distribution = .fillEqually + view.addSubview(stackView) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.left.right.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide) } - - required init?(coder: NSCoder) { nil } - - public override func viewDidLoad() { - super.viewDidLoad() - - view.layer.cornerRadius = 15 - view.layer.masksToBounds = true - view.backgroundColor = Asset.neutralWhite.color - - stackView.axis = .vertical - stackView.distribution = .fillEqually - view.addSubview(stackView) - - stackView.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.left.right.equalToSuperview() - $0.bottom.equalTo(view.safeAreaLayoutGuide) - } - - members.forEach { - let memberView = MemberView() - let assignedTitle = ($0.nickname ?? $0.username) ?? "Fetching username..." - memberView.titleLabel.text = assignedTitle - memberView.avatarView.setupProfile(title: assignedTitle, image: $0.photo, size: .small) - stackView.addArrangedSubview(memberView) - } + + members.forEach { + let memberView = MemberView() + let assignedTitle = ($0.nickname ?? $0.username) ?? "Fetching username..." + memberView.titleLabel.text = assignedTitle + memberView.avatarView.setupProfile(title: assignedTitle, image: $0.photo, size: .small) + stackView.addArrangedSubview(memberView) } + } } private final class MemberView: UIView { - let titleLabel = UILabel() - let avatarView = AvatarView() - let separatorView = UIView() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - titleLabel.textColor = Asset.neutralBody.color - titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) - separatorView.backgroundColor = Asset.neutralLine.color - - addSubview(titleLabel) - addSubview(avatarView) - addSubview(separatorView) - - avatarView.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.width.height.equalTo(30) - $0.left.equalToSuperview().offset(25) - $0.centerY.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.centerY.equalTo(avatarView) - $0.left.equalTo(avatarView.snp.right).offset(14) - $0.right.lessThanOrEqualToSuperview().offset(-10) - } - - separatorView.snp.makeConstraints { - $0.height.equalTo(1) - $0.left.equalToSuperview().offset(25) - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + let titleLabel = UILabel() + let avatarView = AvatarView() + let separatorView = UIView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + titleLabel.textColor = Asset.neutralBody.color + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + separatorView.backgroundColor = Asset.neutralLine.color + + addSubview(titleLabel) + addSubview(avatarView) + addSubview(separatorView) + + avatarView.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.width.height.equalTo(30) + $0.left.equalToSuperview().offset(25) + $0.centerY.equalToSuperview() } - - required init?(coder: NSCoder) { nil } + + titleLabel.snp.makeConstraints { + $0.centerY.equalTo(avatarView) + $0.left.equalTo(avatarView.snp.right).offset(14) + $0.right.lessThanOrEqualToSuperview().offset(-10) + } + + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview().offset(25) + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/ChatFeature/Controllers/RetrySheetController.swift b/Sources/ChatFeature/Controllers/RetrySheetController.swift index 48424f434d189d76a29a29c278e3ba34fe659ca5..4605017e1368171f2b705c45412831c89f8d229d 100644 --- a/Sources/ChatFeature/Controllers/RetrySheetController.swift +++ b/Sources/ChatFeature/Controllers/RetrySheetController.swift @@ -10,7 +10,7 @@ public final class RetrySheetController: UIViewController { // MARK: UI - lazy private var screenView = RetrySheetView() + private lazy var screenView = RetrySheetView() // MARK: Properties diff --git a/Sources/ChatFeature/Controllers/SheetController.swift b/Sources/ChatFeature/Controllers/SheetController.swift index 974f086d5df232b982c0ab101b0f04f48f1bc578..12dd2e5a2a0e4b888f64e9277973109f19a8cadf 100644 --- a/Sources/ChatFeature/Controllers/SheetController.swift +++ b/Sources/ChatFeature/Controllers/SheetController.swift @@ -2,50 +2,51 @@ import UIKit import Combine final class SheetController: UIViewController { - enum Action { - case clear - case details - case report - } - - lazy private var screenView = SheetView() - - var actionPublisher: AnyPublisher<Action, Never> { - actionRelay.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let actionRelay = PassthroughSubject<Action, Never>() - - public override func loadView() { - view = screenView - } - - public override func viewDidLoad() { - super.viewDidLoad() - - screenView.clearButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { [weak actionRelay] in - actionRelay?.send(.clear) - } - }.store(in: &cancellables) - - screenView.detailsButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { [weak actionRelay] in - actionRelay?.send(.details) - } - }.store(in: &cancellables) - - screenView.reportButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true) { [weak actionRelay] in - actionRelay?.send(.report) - } - }.store(in: &cancellables) - } + enum Action { + case clear + case details + case report + } + + private lazy var screenView = SheetView() + + var actionPublisher: AnyPublisher<Action, Never> { + actionRelay.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let actionRelay = PassthroughSubject<Action, Never>() + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + screenView + .clearButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak actionRelay] in + actionRelay?.send(.clear) + } + }.store(in: &cancellables) + + screenView.detailsButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak actionRelay] in + actionRelay?.send(.details) + } + }.store(in: &cancellables) + + screenView.reportButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak actionRelay] in + actionRelay?.send(.report) + } + }.store(in: &cancellables) + } } diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index b8d8b7703b180178f4c39ab6f04a73c0e7e7b4d6..4ae089ad95fab266d3300d0f73c1b19345e59c81 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -1,711 +1,759 @@ -import HUD import UIKit -import Theme -import Models import Shared import Combine -import XXLogger -import QuickLook +import AppCore import XXModels +import QuickLook import Voxophone import ChatLayout +import Dependencies +import AppResources import DrawerFeature +import AppNavigation import DifferenceKit import ChatInputFeature import ReportingFeature -import DependencyInjection import ScrollViewController extension FlexibleSpace: CollectionCellContent { - func prepareForReuse() {} + func prepareForReuse() {} } extension Message: Differentiable { - public var differenceIdentifier: Int64 { id! } + public var differenceIdentifier: Int64 { id! } } public final class SingleChatController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var logger: XXLogger - @Dependency private var voxophone: Voxophone - @Dependency private var coordinator: ChatCoordinating - @Dependency private var reportingStatus: ReportingStatus - @Dependency private var makeReportDrawer: MakeReportDrawer - @Dependency private var makeAppScreenshot: MakeAppScreenshot - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var infoView = UIControl() - lazy private var nameLabel = UILabel() - lazy private var avatarView = AvatarView() - - lazy private var moreButton = UIButton() - lazy private var screenView = ChatView() - lazy private var sheet = SheetController() - - private let inputComponent: ChatInputView - private var collectionView: UICollectionView! - - private let chatLayout = ChatLayout() - private var animator: ManualAnimator? - private let viewModel: SingleChatViewModel - private let layoutDelegate = LayoutDelegate() - private var cancellables = Set<AnyCancellable>() - private var sections = [ArraySection<ChatSection, Message>]() - private var currentInterfaceActions: SetActor<Set<InterfaceActions>, ReactionTypes> = SetActor() - - var fileURL: URL? - - public override func loadView() { view = screenView } - public override var canBecomeFirstResponder: Bool { true } - public override var inputAccessoryView: UIView? { inputComponent } - - public init(_ contact: Contact) { - let viewModel = SingleChatViewModel(contact) - self.viewModel = viewModel - - self.inputComponent = ChatInputView(store: .init( - initialState: .init(canAddAttachments: true), - reducer: chatInputReducer, - environment: .init( - voxophone: try! DependencyInjection.Container.shared.resolve() as Voxophone, - sendAudio: { viewModel.didSendAudio(url: $0) }, - didTapCamera: { viewModel.didTest(permission: .camera) }, - didTapLibrary: { viewModel.didTest(permission: .library) }, - sendText: { viewModel.send($0) }, - didTapAbortReply: { viewModel.abortReply() }, - didTapMicrophone: { viewModel.didTest(permission: .microphone) } - ) - )) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize( - backgroundColor: Asset.neutralWhite.color, - shadowColor: Asset.neutralDisabled.color - ) - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - collectionView.collectionViewLayout.invalidateLayout() - becomeFirstResponder() - viewModel.viewDidAppear() - } - - private var isFirstAppearance = true - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - if isFirstAppearance { - isFirstAppearance = false - let insets = UIEdgeInsets( - top: 0, - left: 0, - bottom: inputComponent.bounds.height - view.safeAreaInsets.bottom, - right: 0 - ) - collectionView.contentInset = insets - collectionView.scrollIndicatorInsets = insets - } - } - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - viewModel.readAll() - } - - public override func viewDidLoad() { - super.viewDidLoad() +// @Dependency var voxophone: Voxophone +// @Dependency var makeReportDrawer: MakeReportDrawer +// @Dependency var makeAppScreenshot: MakeAppScreenshot + + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + @Dependency(\.reportingStatus) var reportingStatus: ReportingStatus + + let voxophone = Voxophone() + + private lazy var infoView = UIControl() + private lazy var nameLabel = UILabel() + private lazy var avatarView = AvatarView() + + private lazy var moreButton = UIButton() + private lazy var screenView = ChatView() + private lazy var sheet = SheetController() + + private let inputComponent: ChatInputView + private var collectionView: UICollectionView! + + private var animator: ManualAnimator? + private let viewModel: SingleChatViewModel + private let layoutDelegate = LayoutDelegate() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + private let chatLayout = CollectionViewChatLayout() + private var sections = [ArraySection<ChatSection, Message>]() + private var currentInterfaceActions: SetActor<Set<InterfaceActions>, ReactionTypes> = SetActor() + + var fileURL: URL? + + public override func loadView() { view = screenView } + public override var canBecomeFirstResponder: Bool { true } + public override var inputAccessoryView: UIView? { inputComponent } + + public init(_ contact: Contact) { + let viewModel = SingleChatViewModel(contact) + self.viewModel = viewModel + + self.inputComponent = ChatInputView(store: .init( + initialState: .init(canAddAttachments: true), + reducer: chatInputReducer, + environment: .init( + voxophone: Voxophone(), //try! DI.Container.shared.resolve() as Voxophone, + sendAudio: { viewModel.didSendAudio(url: $0) }, + didTapCamera: { viewModel.didTest(permission: .camera) }, + didTapLibrary: { viewModel.didTest(permission: .library) }, + sendText: { viewModel.send($0) }, + didTapAbortReply: { viewModel.abortReply() }, + didTapMicrophone: { viewModel.didTest(permission: .microphone) } + ) + )) + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + navigationController?.navigationBar.customize( + backgroundColor: Asset.neutralWhite.color, + shadowColor: Asset.neutralDisabled.color + ) + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + collectionView.collectionViewLayout.invalidateLayout() + becomeFirstResponder() + viewModel.viewDidAppear() + } + + private var isFirstAppearance = true + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if isFirstAppearance { + isFirstAppearance = false + let insets = UIEdgeInsets( + top: 0, + left: 0, + bottom: inputComponent.bounds.height - view.safeAreaInsets.bottom, + right: 0 + ) + collectionView.contentInset = insets + collectionView.scrollIndicatorInsets = insets + } + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.readAll() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + viewModel + .contactPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in setupNavigationBar(contact: $0) } + .store(in: &cancellables) + + setupCollectionView() + setupInputController() + setupBindings() + + KeyboardListener.shared.add(delegate: self) + screenView.bringSubviewToFront(screenView.snackBar) + } + + private func setupCollectionView() { + chatLayout.configure(layoutDelegate) + collectionView = .init(on: screenView, with: chatLayout) + collectionView.delegate = self + collectionView.dataSource = self + + collectionView.register(OutgoingTextCell.self) + collectionView.register(IncomingTextCell.self) + collectionView.register(IncomingAudioCell.self) + collectionView.register(OutgoingAudioCell.self) + collectionView.register(IncomingImageCell.self) + collectionView.register(IncomingReplyCell.self) + collectionView.register(OutgoingImageCell.self) + collectionView.register(OutgoingReplyCell.self) + collectionView.register(OutgoingFailedTextCell.self) + collectionView.register(OutgoingFailedReplyCell.self) + + collectionView.registerSectionHeader(SectionHeaderView.self) + } + + private func setupNavigationBar(contact: Contact) { + screenView.set(name: contact.nickname ?? contact.username!) + avatarView.snp.makeConstraints { $0.width.height.equalTo(35) } + + let title = (contact.nickname ?? contact.username) ?? "" + avatarView.setupProfile(title: title, image: contact.photo, size: .small) + + nameLabel.text = title + nameLabel.textColor = Asset.neutralActive.color + nameLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) + + moreButton.setImage(Asset.chatMore.image, for: .normal) + moreButton.addTarget(self, action: #selector(didTapDots), for: .touchUpInside) + + infoView.addTarget(self, action: #selector(didTapInfo), for: .touchUpInside) + + infoView.addSubview(avatarView) + infoView.addSubview(nameLabel) + + avatarView.snp.makeConstraints { + $0.top.left.bottom.equalToSuperview() + } + + nameLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.left.equalTo(avatarView.snp.right).offset(13) + $0.right.lessThanOrEqualToSuperview() + } + + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: moreButton) + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: infoView) + navigationItem.leftItemsSupplementBackButton = true + } + + private func setupInputController() { + inputComponent.setMaxHeight { [weak self] in + guard let self else { return 150 } + + let maxHeight = self.collectionView.frame.height + - self.collectionView.adjustedContentInset.top + - self.collectionView.adjustedContentInset.bottom + + self.inputComponent.bounds.height + + return maxHeight * 0.9 + } + + viewModel.replyPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] senderTitle, messageText in + inputComponent.setupReply(message: messageText, sender: senderTitle) + } + .store(in: &cancellables) + + viewModel.navigation + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + switch $0 { + case .library: + navigator.perform(PresentPhotoLibrary(from: self)) + case .camera: + navigator.perform(PresentCamera(from: self)) + case .cameraPermission: + navigator.perform(PresentPermissionRequest(type: .camera, from: self)) + case .microphonePermission: + navigator.perform(PresentPermissionRequest(type: .microphone, from: self)) + case .libraryPermission: + navigator.perform(PresentPermissionRequest(type: .library, from: self)) + case .webview(let urlString): + navigator.perform(PresentWebsite(urlString: urlString, from: self)) + case .waitingRound: + let button = DrawerCapsuleButton(model: .init( + title: Localized.Chat.RoundDrawer.action, + style: .brandColored + )) - viewModel - .contactPublisher + button + .action .receive(on: DispatchQueue.main) - .sink { [unowned self] in setupNavigationBar(contact: $0) } - .store(in: &cancellables) - - setupCollectionView() - setupInputController() - setupBindings() - - KeyboardListener.shared.add(delegate: self) - } - - // MARK: Private - - private func setupCollectionView() { - chatLayout.configure(layoutDelegate) - collectionView = .init(on: screenView, with: chatLayout) - collectionView.delegate = self - collectionView.dataSource = self - - collectionView.register(OutgoingTextCell.self) - collectionView.register(IncomingTextCell.self) - collectionView.register(IncomingAudioCell.self) - collectionView.register(OutgoingAudioCell.self) - collectionView.register(IncomingImageCell.self) - collectionView.register(IncomingReplyCell.self) - collectionView.register(OutgoingImageCell.self) - collectionView.register(OutgoingReplyCell.self) - collectionView.register(OutgoingFailedTextCell.self) - collectionView.register(OutgoingFailedReplyCell.self) - - collectionView.registerSectionHeader(SectionHeaderView.self) - } - - private func setupNavigationBar(contact: Contact) { - screenView.set(name: contact.nickname ?? contact.username!) - avatarView.snp.makeConstraints { $0.width.height.equalTo(35) } - - let title = (contact.nickname ?? contact.username) ?? "" - avatarView.setupProfile(title: title, image: contact.photo, size: .small) - - nameLabel.text = title - nameLabel.textColor = Asset.neutralActive.color - nameLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) - - moreButton.setImage(Asset.chatMore.image, for: .normal) - moreButton.addTarget(self, action: #selector(didTapDots), for: .touchUpInside) - - infoView.addTarget(self, action: #selector(didTapInfo), for: .touchUpInside) - - infoView.addSubview(avatarView) - infoView.addSubview(nameLabel) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - avatarView.snp.makeConstraints { - $0.top.left.bottom.equalToSuperview() + 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 + ], isDismissable: true, from: self)) + case .none: + break } - nameLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.left.equalTo(avatarView.snp.right).offset(13) - $0.right.lessThanOrEqualToSuperview() + viewModel.didNavigateSomewhere() + }.store(in: &cancellables) + } + + private func setupBindings() { + sheet + .actionPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch $0 { + case .clear: + presentDeleteAllDrawer() + case .details: + navigator.perform(PresentContact( + contact: viewModel.contact, + on: navigationController! + )) + case .report: + presentReportDrawer() } + }.store(in: &cancellables) - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: moreButton) - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: infoView) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupInputController() { - inputComponent.setMaxHeight { [weak self] in - guard let self = self else { return 150 } - - let maxHeight = self.collectionView.frame.height - - self.collectionView.adjustedContentInset.top - - self.collectionView.adjustedContentInset.bottom - + self.inputComponent.bounds.height + viewModel + .shouldDisplayEmptyView + .removeDuplicates() + .sink { [unowned self] in + screenView.titleLabel.isHidden = !$0 - return maxHeight * 0.9 + if $0 == true { + screenView.bringSubviewToFront(screenView.titleLabel) + } + }.store(in: &cancellables) + + viewModel + .reportPopupPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentReportDrawer() + }.store(in: &cancellables) + + viewModel + .isOnline + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak screenView] in + screenView?.displayNetworkIssue(!$0) + }.store(in: &cancellables) + + viewModel + .messages + .receive(on: DispatchQueue.main) + .sink { [unowned self] sections in + func process() { + let changeSet = StagedChangeset(source: self.sections, target: sections).flattenIfPossible() + collectionView.reload( + using: changeSet, + interrupt: { changeSet in + guard !self.sections.isEmpty else { return true } + return false + }, onInterruptedReload: { + guard let lastSection = self.sections.last else { return } + let positionSnapshot = ChatLayoutPositionSnapshot( + indexPath: IndexPath( + item: lastSection.elements.count - 1, + section: self.sections.count - 1 + ), + kind: .cell, + edge: .bottom + ) + + self.collectionView.reloadData() + self.chatLayout.restoreContentOffset(with: positionSnapshot) + }, + completion: nil, + setData: { self.sections = $0 } + ) } - viewModel.replyPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] senderTitle, messageText in - inputComponent.setupReply(message: messageText, sender: senderTitle) - } - .store(in: &cancellables) - - viewModel.navigation - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [unowned self] in - switch $0 { - case .library: - coordinator.toLibrary(from: self) - case .camera: - coordinator.toCamera(from: self) - case .cameraPermission: - coordinator.toPermission(type: .camera, from: self) - case .microphonePermission: - coordinator.toPermission(type: .microphone, from: self) - case .libraryPermission: - coordinator.toPermission(type: .library, from: self) - case .webview(let urlString): - coordinator.toWebview(with: urlString, from: self) - case .waitingRound: - coordinator.toDrawer(makeWaitingRoundDrawer(), from: self) - case .none: - break - } - - viewModel.didNavigateSomewhere() - }.store(in: &cancellables) - } - - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - sheet.actionPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch $0 { - case .clear: - presentDeleteAllDrawer() - case .details: - coordinator.toContact(viewModel.contact, from: self) - case .report: - presentReportDrawer() - } - }.store(in: &cancellables) - - viewModel - .shouldDisplayEmptyView - .removeDuplicates() - .sink { [unowned self] in - screenView.titleLabel.isHidden = !$0 - - if $0 == true { - screenView.bringSubviewToFront(screenView.titleLabel) - } - }.store(in: &cancellables) - - viewModel.reportPopupPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - presentReportDrawer() - }.store(in: &cancellables) - - viewModel.isOnline - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.displayNetworkIssue(!$0) } - .store(in: &cancellables) - - viewModel.messages - .receive(on: DispatchQueue.main) - .sink { [unowned self] sections in - func process() { - let changeSet = StagedChangeset(source: self.sections, target: sections).flattenIfPossible() - collectionView.reload( - using: changeSet, - interrupt: { changeSet in - guard !self.sections.isEmpty else { return true } - return false - }, onInterruptedReload: { - guard let lastSection = self.sections.last else { return } - let positionSnapshot = ChatLayoutPositionSnapshot( - indexPath: IndexPath( - item: lastSection.elements.count - 1, - section: self.sections.count - 1 - ), - kind: .cell, - edge: .bottom - ) - - self.collectionView.reloadData() - self.chatLayout.restoreContentOffset(with: positionSnapshot) - }, - completion: nil, - setData: { self.sections = $0 } - ) - } - - guard currentInterfaceActions.options.isEmpty else { - let reaction = SetActor<Set<InterfaceActions>, ReactionTypes>.Reaction( - type: .delayedUpdate, - action: .onEmpty, - executionType: .once, - actionBlock: { [weak self] in - guard let _ = self else { return } - process() - } - ) - - currentInterfaceActions.add(reaction: reaction) - return - } - - process() + guard currentInterfaceActions.options.isEmpty else { + let reaction = SetActor<Set<InterfaceActions>, ReactionTypes>.Reaction( + type: .delayedUpdate, + action: .onEmpty, + executionType: .once, + actionBlock: { [weak self] in + guard let _ = self else { return } + process() } - .store(in: &cancellables) - } - - func scrollToBottom(completion: (() -> Void)? = nil) { - let contentOffsetAtBottom = CGPoint( - x: collectionView.contentOffset.x, - y: chatLayout.collectionViewContentSize.height - - collectionView.frame.height + collectionView.adjustedContentInset.bottom - ) - - guard contentOffsetAtBottom.y > collectionView.contentOffset.y else { completion?(); return } - - let initialOffset = collectionView.contentOffset.y - let delta = contentOffsetAtBottom.y - initialOffset + ) - if abs(delta) > chatLayout.visibleBounds.height { - animator = ManualAnimator() - animator?.animate(duration: TimeInterval(0.25), curve: .easeInOut) { [weak self] percentage in - guard let self = self else { return } - - self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: initialOffset + (delta * percentage)) - if percentage == 1.0 { - self.animator = nil - let positionSnapshot = ChatLayoutPositionSnapshot(indexPath: IndexPath(item: 0, section: 0), kind: .footer, edge: .bottom) - self.chatLayout.restoreContentOffset(with: positionSnapshot) - self.currentInterfaceActions.options.remove(.scrollingToBottom) - completion?() - } - } - } else { - currentInterfaceActions.options.insert(.scrollingToBottom) - UIView.animate(withDuration: 0.25, animations: { - self.collectionView.setContentOffset(contentOffsetAtBottom, animated: true) - }, completion: { [weak self] _ in - self?.currentInterfaceActions.options.remove(.scrollingToBottom) - completion?() - }) + currentInterfaceActions.add(reaction: reaction) + return } - } - - private func makeWaitingRoundDrawer() -> UIViewController { - let text = DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Chat.RoundDrawer.title, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ) - - let button = DrawerCapsuleButton(model: .init( - title: Localized.Chat.RoundDrawer.action, - style: .brandColored - )) - let drawer = DrawerController(with: [text, button]) - - button.action - .receive(on: DispatchQueue.main) - .sink { [unowned drawer] in drawer.dismiss(animated: true) } - .store(in: &drawer.cancellables) - - return drawer - } - - private func presentReportDrawer() { - var config = MakeReportDrawer.Config() - config.onReport = { [weak self] in - guard let self = self else { return } - let screenshot = try! self.makeAppScreenshot() - self.viewModel.report(screenshot: screenshot) { success in - guard success else { return } - self.navigationController?.popViewController(animated: true) - } + process() + } + .store(in: &cancellables) + } + + func scrollToBottom(completion: (() -> Void)? = nil) { + let contentOffsetAtBottom = CGPoint( + x: collectionView.contentOffset.x, + y: chatLayout.collectionViewContentSize.height + - collectionView.frame.height + collectionView.adjustedContentInset.bottom + ) + + guard contentOffsetAtBottom.y > collectionView.contentOffset.y else { completion?(); return } + + let initialOffset = collectionView.contentOffset.y + let delta = contentOffsetAtBottom.y - initialOffset + + if abs(delta) > chatLayout.visibleBounds.height { + animator = ManualAnimator() + animator?.animate(duration: TimeInterval(0.25), curve: .easeInOut) { [weak self] percentage in + guard let self else { return } + + self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: initialOffset + (delta * percentage)) + if percentage == 1.0 { + self.animator = nil + let positionSnapshot = ChatLayoutPositionSnapshot(indexPath: IndexPath(item: 0, section: 0), kind: .footer, edge: .bottom) + self.chatLayout.restoreContentOffset(with: positionSnapshot) + self.currentInterfaceActions.options.remove(.scrollingToBottom) + completion?() } - let drawer = makeReportDrawer(config) - coordinator.toDrawer(drawer, from: self) - } - - private func presentDeleteAllDrawer() { - let clearButton = CapsuleButton() - clearButton.setStyle(.red) - clearButton.setTitle(Localized.Chat.Clear.action, for: .normal) - - let cancelButton = CapsuleButton() - cancelButton.setStyle(.seeThrough) - cancelButton.setTitle(Localized.Chat.Clear.cancel, for: .normal) - - let drawer = DrawerController(with: [ - DrawerImage( - image: Asset.drawerNegative.image - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 18.0), - text: Localized.Chat.Clear.title, - color: Asset.neutralActive.color - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Chat.Clear.subtitle, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ), - DrawerStack( - spacing: 20.0, - views: [clearButton, cancelButton] - ) - ]) - - clearButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned drawer, weak self] in - drawer.dismiss(animated: true) { - self?.viewModel.didRequestDeleteAll() - } - } - .store(in: &drawer.cancellables) - - cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned drawer] in drawer.dismiss(animated: true) } - .store(in: &drawer.cancellables) - - coordinator.toDrawer(drawer, from: self) - } - - private func previewItemAt(_ indexPath: IndexPath) { - let item = sections[indexPath.section].elements[indexPath.item] - guard let ftid = item.fileTransferId, - item.status != .receiving, - item.status != .receivingFailed else { return } - - let ft = viewModel.getFileTransferWith(id: ftid) - fileURL = FileManager.url(for: "\(ft.name).\(ft.type)") - coordinator.toPreview(from: self) - } - - // MARK: Selectors - - @objc private func didTapDots() { - coordinator.toMenuSheet(sheet, from: self) - } - - @objc private func didTapInfo() { - coordinator.toContact(viewModel.contact, from: self) - } + } + } else { + currentInterfaceActions.options.insert(.scrollingToBottom) + UIView.animate(withDuration: 0.25, animations: { + self.collectionView.setContentOffset(contentOffsetAtBottom, animated: true) + }, completion: { [weak self] _ in + self?.currentInterfaceActions.options.remove(.scrollingToBottom) + completion?() + }) + } + } + + private func presentReportDrawer() { + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Chat.Report.cancel, for: .normal) + + let reportButton = CapsuleButton() + reportButton.setStyle(.red) + reportButton.setTitle(Localized.Chat.Report.action, for: .normal) + + reportButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() +// let screenshot = try! self.makeAppScreenshot() +// self.viewModel.report(screenshot: screenshot) { success in +// guard success else { return } +// self.navigationController?.popViewController(animated: true) +// } + } + }.store(in: &drawerCancellables) + + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Chat.Report.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.Report.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + axis: .vertical, + spacing: 20.0, + views: [reportButton, cancelButton] + ) + ], isDismissable: true, from: self)) + } + + private func presentDeleteAllDrawer() { + let clearButton = CapsuleButton() + clearButton.setStyle(.red) + clearButton.setTitle(Localized.Chat.Clear.action, for: .normal) + + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Chat.Clear.cancel, for: .normal) + + clearButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestDeleteAll() + } + }.store(in: &drawerCancellables) + + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Chat.Clear.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.Clear.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + spacing: 20.0, + views: [clearButton, cancelButton] + ) + ], isDismissable: true, from: self)) + } + + private func previewItemAt(_ indexPath: IndexPath) { + let item = sections[indexPath.section].elements[indexPath.item] + guard let ftid = item.fileTransferId, + item.status != .receiving, + item.status != .receivingFailed else { return } + + let ft = viewModel.getFileTransferWith(id: ftid) + fileURL = FileManager.url(for: "\(ft.name).\(ft.type)") + //coordinator.toPreview(from: self) + } + + @objc private func didTapDots() { + //coordinator.toMenuSheet(sheet, from: self) + } + + @objc private func didTapInfo() { + navigator.perform(PresentContact( + contact: viewModel.contact, + on: navigationController! + )) + } } extension SingleChatController: UICollectionViewDataSource { - public func numberOfSections(in collectionView: UICollectionView) -> Int { - sections.count - } - - public func collectionView( - _ collectionView: UICollectionView, - viewForSupplementaryElementOfKind kind: String, - at indexPath: IndexPath - ) -> UICollectionReusableView { - let sectionHeader: SectionHeaderView = collectionView.dequeueSupplementaryView(forIndexPath: indexPath) - sectionHeader.title.text = sections[indexPath.section].model.date.asDayOfMonth() - return sectionHeader - } - - public func collectionView( - _ collectionView: UICollectionView, - numberOfItemsInSection section: Int - ) -> Int { - sections[section].elements.count - } - - public func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - - let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) - let item = sections[indexPath.section].elements[indexPath.item] - let replyContent: (Data) -> (String, String) = viewModel.getReplyContent(for:) - let performReply: () -> Void = { [weak self] in self?.viewModel.didRequestReply(item) } - - let factory = CellFactory.combined(factories: [ - .incomingImage(transfer: viewModel.getFileTransferWith(id:)), - .outgoingImage(transfer: viewModel.getFileTransferWith(id:)), - .incomingAudio(voxophone: voxophone, transfer: viewModel.getFileTransferWith(id:)), - .outgoingAudio(voxophone: voxophone, transfer: viewModel.getFileTransferWith(id:)), - .incomingText(performReply: performReply, showRound: showRound), - .outgoingText(performReply: performReply, showRound: showRound), - .outgoingFailedText(performReply: performReply), - .incomingReply(performReply: performReply, replyContent: replyContent, showRound: showRound), - .outgoingReply(performReply: performReply, replyContent: replyContent, showRound: showRound), - .outgoingFailedReply(performReply: performReply, replyContent: replyContent) - ]) - - return factory(item: item, collectionView: collectionView, indexPath: indexPath) - } + public func numberOfSections(in collectionView: UICollectionView) -> Int { + sections.count + } + + public func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath + ) -> UICollectionReusableView { + let sectionHeader: SectionHeaderView = collectionView.dequeueSupplementaryView(forIndexPath: indexPath) + sectionHeader.title.text = sections[indexPath.section].model.date.asDayOfMonth() + return sectionHeader + } + + public func collectionView( + _ collectionView: UICollectionView, + numberOfItemsInSection section: Int + ) -> Int { + sections[section].elements.count + } + + public func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + + let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) + let item = sections[indexPath.section].elements[indexPath.item] + let replyContent: (Data) -> (String, String) = viewModel.getReplyContent(for:) + let performReply: () -> Void = { [weak self] in self?.viewModel.didRequestReply(item) } + + let factory = CellFactory.combined(factories: [ + .incomingImage(transfer: viewModel.getFileTransferWith(id:)), + .outgoingImage(transfer: viewModel.getFileTransferWith(id:)), + .incomingAudio(voxophone: voxophone, transfer: viewModel.getFileTransferWith(id:)), + .outgoingAudio(voxophone: voxophone, transfer: viewModel.getFileTransferWith(id:)), + .incomingText(performReply: performReply, showRound: showRound), + .outgoingText(performReply: performReply, showRound: showRound), + .outgoingFailedText(performReply: performReply), + .incomingReply(performReply: performReply, replyContent: replyContent, showRound: showRound), + .outgoingReply(performReply: performReply, replyContent: replyContent, showRound: showRound), + .outgoingFailedReply(performReply: performReply, replyContent: replyContent) + ]) + + return factory(item: item, collectionView: collectionView, indexPath: indexPath) + } } extension SingleChatController: KeyboardListenerDelegate { - fileprivate var isUserInitiatedScrolling: Bool { - collectionView.isDragging || collectionView.isDecelerating - } + fileprivate var isUserInitiatedScrolling: Bool { + collectionView.isDragging || collectionView.isDecelerating + } - func keyboardWillChangeFrame(info: KeyboardInfo) { - let keyWindow = UIApplication.shared - .connectedScenes - .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } - .first { $0.isKeyWindow } + func keyboardWillChangeFrame(info: KeyboardInfo) { + let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first - guard let keyWindow = keyWindow else { - fatalError("[keyboardWillChangeFrame]: Couldn't get key window") - } - - let keyboardFrame = keyWindow.convert(info.frameEnd, to: view) - - guard !currentInterfaceActions.options.contains(.changingFrameSize), - collectionView.contentInsetAdjustmentBehavior != .never, - collectionView.convert(collectionView.bounds, to: keyWindow).maxY > info.frameEnd.minY else { return } - - currentInterfaceActions.options.insert(.changingKeyboardFrame) - let newBottomInset = collectionView.frame.minY + collectionView.frame.size.height - keyboardFrame.minY - collectionView.safeAreaInsets.bottom - if newBottomInset > 0, - collectionView.contentInset.bottom != newBottomInset { - let positionSnapshot = chatLayout.getContentOffsetSnapshot(from: .bottom) - - currentInterfaceActions.options.insert(.changingContentInsets) - UIView.animate(withDuration: info.animationDuration, animations: { - self.collectionView.performBatchUpdates({ - self.collectionView.contentInset.bottom = newBottomInset - self.collectionView.verticalScrollIndicatorInsets.bottom = newBottomInset - }, completion: nil) - - if let positionSnapshot = positionSnapshot, !self.isUserInitiatedScrolling { - self.chatLayout.restoreContentOffset(with: positionSnapshot) - } - }, completion: { _ in - self.currentInterfaceActions.options.remove(.changingContentInsets) - }) - } - } - - func keyboardDidChangeFrame(info: KeyboardInfo) { - guard currentInterfaceActions.options.contains(.changingKeyboardFrame) else { return } - currentInterfaceActions.options.remove(.changingKeyboardFrame) + guard let keyWindow = keyWindow else { + fatalError("[keyboardWillChangeFrame]: Couldn't get key window") } -} - -extension SingleChatController: UICollectionViewDelegate { - private func makeTargetedPreview(for configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - guard let identifier = configuration.identifier as? String, - let first = identifier.components(separatedBy: "|").first, - let last = identifier.components(separatedBy: "|").last, - let item = Int(first), let section = Int(last), - let cell = collectionView.cellForItem(at: IndexPath(item: item, section: section)) else { - return nil - } - let parameters = UIPreviewParameters() - parameters.backgroundColor = .clear + let keyboardFrame = keyWindow.convert(info.frameEnd, to: view) - let status = sections[section].elements[item].status + guard !currentInterfaceActions.options.contains(.changingFrameSize), + collectionView.contentInsetAdjustmentBehavior != .never, + collectionView.convert(collectionView.bounds, to: keyWindow).maxY > info.frameEnd.minY else { return } - if status == .received || status == .receiving { - var leftView: UIView! + currentInterfaceActions.options.insert(.changingKeyboardFrame) + let newBottomInset = collectionView.frame.minY + collectionView.frame.size.height - keyboardFrame.minY - collectionView.safeAreaInsets.bottom + if newBottomInset > 0, + collectionView.contentInset.bottom != newBottomInset { + let positionSnapshot = chatLayout.getContentOffsetSnapshot(from: .bottom) - if let cell = cell as? IncomingReplyCell { - leftView = cell.leftView - } else if let cell = cell as? IncomingAudioCell { - leftView = cell.leftView - } else if let cell = cell as? IncomingTextCell { - leftView = cell.leftView - } else if let cell = cell as? IncomingImageCell { - leftView = cell.leftView - } - - parameters.visiblePath = UIBezierPath(roundedRect: leftView.bounds, cornerRadius: 13) - return UITargetedPreview(view: leftView, parameters: parameters) - } + currentInterfaceActions.options.insert(.changingContentInsets) + UIView.animate(withDuration: info.animationDuration, animations: { + self.collectionView.performBatchUpdates({ + self.collectionView.contentInset.bottom = newBottomInset + self.collectionView.verticalScrollIndicatorInsets.bottom = newBottomInset + }, completion: nil) - #warning("TODO: Refactor") - - var rightView: UIView! - - if let cell = cell as? OutgoingTextCell { - rightView = cell.rightView - } else if let cell = cell as? OutgoingAudioCell { - rightView = cell.rightView - } else if let cell = cell as? OutgoingReplyCell { - rightView = cell.rightView - } else if let cell = cell as? OutgoingImageCell { - rightView = cell.rightView - } else if let cell = cell as? OutgoingFailedTextCell { - rightView = cell.rightView - } else if let cell = cell as? OutgoingFailedReplyCell { - rightView = cell.rightView + if let positionSnapshot = positionSnapshot, !self.isUserInitiatedScrolling { + self.chatLayout.restoreContentOffset(with: positionSnapshot) } - - parameters.visiblePath = UIBezierPath(roundedRect: rightView.bounds, cornerRadius: 13) - return UITargetedPreview(view: rightView, parameters: parameters) + }, completion: { _ in + self.currentInterfaceActions.options.remove(.changingContentInsets) + }) } + } - public func collectionView( - _ collectionView: UICollectionView, - previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration - ) -> UITargetedPreview? { - makeTargetedPreview(for: configuration) - } - - public func collectionView( - _ collectionView: UICollectionView, - previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration - ) -> UITargetedPreview? { - makeTargetedPreview(for: configuration) - } + func keyboardDidChangeFrame(info: KeyboardInfo) { + guard currentInterfaceActions.options.contains(.changingKeyboardFrame) else { return } + currentInterfaceActions.options.remove(.changingKeyboardFrame) + } +} - public func collectionView( - _ collectionView: UICollectionView, - contextMenuConfigurationForItemAt indexPath: IndexPath, - point: CGPoint - ) -> UIContextMenuConfiguration? { - UIContextMenuConfiguration( - identifier: "\(indexPath.item)|\(indexPath.section)" as NSCopying, - previewProvider: nil - ) { [weak self] _ in - - guard let self = self else { return nil } - let item = self.sections[indexPath.section].elements[indexPath.item] - - var children = [ - ActionFactory.build(from: item, action: .copy, closure: self.viewModel.didRequestCopy(_:)), - ActionFactory.build(from: item, action: .retry, closure: self.viewModel.didRequestRetry(_:)), - ActionFactory.build(from: item, action: .reply, closure: self.viewModel.didRequestReply(_:)), - ActionFactory.build(from: item, action: .delete, closure: self.viewModel.didRequestDeleteSingle(_:)) - ] - - if self.reportingStatus.isEnabled() { - children.append( - ActionFactory.build(from: item, action: .report, closure: self.viewModel.didRequestReport(_:)) - ) - } +extension SingleChatController: UICollectionViewDelegate { + private func makeTargetedPreview(for configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + guard let identifier = configuration.identifier as? String, + let first = identifier.components(separatedBy: "|").first, + let last = identifier.components(separatedBy: "|").last, + let item = Int(first), let section = Int(last), + let cell = collectionView.cellForItem(at: IndexPath(item: item, section: section)) else { + return nil + } + + let parameters = UIPreviewParameters() + parameters.backgroundColor = .clear + + let status = sections[section].elements[item].status + + if status == .received || status == .receiving { + var leftView: UIView! + + if let cell = cell as? IncomingReplyCell { + leftView = cell.leftView + } else if let cell = cell as? IncomingAudioCell { + leftView = cell.leftView + } else if let cell = cell as? IncomingTextCell { + leftView = cell.leftView + } else if let cell = cell as? IncomingImageCell { + leftView = cell.leftView + } + + parameters.visiblePath = UIBezierPath(roundedRect: leftView.bounds, cornerRadius: 13) + return UITargetedPreview(view: leftView, parameters: parameters) + } + + var rightView: UIView! + + if let cell = cell as? OutgoingTextCell { + rightView = cell.rightView + } else if let cell = cell as? OutgoingAudioCell { + rightView = cell.rightView + } else if let cell = cell as? OutgoingReplyCell { + rightView = cell.rightView + } else if let cell = cell as? OutgoingImageCell { + rightView = cell.rightView + } else if let cell = cell as? OutgoingFailedTextCell { + rightView = cell.rightView + } else if let cell = cell as? OutgoingFailedReplyCell { + rightView = cell.rightView + } + + parameters.visiblePath = UIBezierPath(roundedRect: rightView.bounds, cornerRadius: 13) + return UITargetedPreview(view: rightView, parameters: parameters) + } + + public func collectionView( + _ collectionView: UICollectionView, + previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration + ) -> UITargetedPreview? { + makeTargetedPreview(for: configuration) + } + + public func collectionView( + _ collectionView: UICollectionView, + previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration + ) -> UITargetedPreview? { + makeTargetedPreview(for: configuration) + } + + public func collectionView( + _ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint + ) -> UIContextMenuConfiguration? { + UIContextMenuConfiguration( + identifier: "\(indexPath.item)|\(indexPath.section)" as NSCopying, + previewProvider: nil + ) { [weak self] _ in + + guard let self else { return nil } + let item = self.sections[indexPath.section].elements[indexPath.item] + + var children = [ + ActionFactory.build(from: item, action: .copy, closure: self.viewModel.didRequestCopy(_:)), + ActionFactory.build(from: item, action: .retry, closure: self.viewModel.didRequestRetry(_:)), + ActionFactory.build(from: item, action: .reply, closure: self.viewModel.didRequestReply(_:)), + ActionFactory.build(from: item, action: .delete, closure: self.viewModel.didRequestDeleteSingle(_:)) + ] + + if self.reportingStatus.isEnabled() { + children.append( + ActionFactory.build(from: item, action: .report, closure: self.viewModel.didRequestReport(_:)) + ) + } - return UIMenu(title: "", children: children.compactMap { $0 }) - } + return UIMenu(title: "", children: children.compactMap { $0 }) } + } - public func collectionView( - _ collectionView: UICollectionView, - didSelectItemAt indexPath: IndexPath - ) { - previewItemAt(indexPath) - } + public func collectionView( + _ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath + ) { + previewItemAt(indexPath) + } } extension SingleChatController: UIImagePickerControllerDelegate { - public func imagePickerController( - _ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] - ) { - picker.delegate = nil - picker.dismiss(animated: true) - guard let image = info[.originalImage] as? UIImage else { return } - - DispatchQueue.global().async { [weak self] in - self?.viewModel.didSend(image: image) - } - } + public func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] + ) { + picker.delegate = nil + picker.dismiss(animated: true) + guard let image = info[.originalImage] as? UIImage else { return } + + DispatchQueue.global().async { [weak self] in + self?.viewModel.didSend(image: image) + } + } } extension SingleChatController: UINavigationControllerDelegate {} extension SingleChatController: QLPreviewControllerDataSource { - public func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 } + public func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 } - public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { - fileURL! as QLPreviewItem - } + public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + fileURL! as QLPreviewItem + } } extension SingleChatController: QLPreviewControllerDelegate { - public func previewControllerDidDismiss(_ controller: QLPreviewController) { - fileURL = nil - } + public func previewControllerDidDismiss(_ controller: QLPreviewController) { + fileURL = nil + } } diff --git a/Sources/ChatFeature/Controllers/WebController.swift b/Sources/ChatFeature/Controllers/WebController.swift deleted file mode 100644 index b093af5ef2232a2b23c9499a16e68b2ed21e2874..0000000000000000000000000000000000000000 --- a/Sources/ChatFeature/Controllers/WebController.swift +++ /dev/null @@ -1,69 +0,0 @@ -import UIKit -import WebKit - -public final class WebScreen: UIViewController { - lazy private(set) var webView = WebView() - - private var url: String! - - public init(url: String) { - self.url = url - super.init(nibName: nil, bundle: nil) - } - - public required init?(coder: NSCoder) { nil } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScreen() - } - - private func setupScreen() { - view.addSubview(webView) - webView.snp.makeConstraints { $0.edges.equalToSuperview() } - - webView.webView.load(URLRequest(url: URL(string: url)!)) - webView.closeButton = UIBarButtonItem(title: "Close", style: .done, target: self, - action: #selector(didTappedClose)) - } - - @objc private func didTappedClose() { - dismiss(animated: true) - } -} - -final class WebView: UIView { - - let webView = WKWebView() - let navBar = UINavigationBar() - var closeButton: UIBarButtonItem! { - didSet { navBar.topItem?.leftBarButtonItem = closeButton } - } - - init() { - super.init(frame: .zero) - setupLayout() - } - - required init?(coder: NSCoder) { nil } - - private func setupLayout() { - backgroundColor = .white - navBar.items = [UINavigationItem(title: "")] - addSubview(webView) - addSubview(navBar) - - navBar.snp.makeConstraints { make -> Void in - make.top.equalTo(safeAreaLayoutGuide) - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - webView.snp.makeConstraints { make -> Void in - make.bottom.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.top.equalTo(navBar.snp.bottom) - } - } -} diff --git a/Sources/ChatFeature/Coordinator/ChatCoordinator.swift b/Sources/ChatFeature/Coordinator/ChatCoordinator.swift deleted file mode 100644 index 64a8e46de2eada72bba4b371bed2b1d441a2b846..0000000000000000000000000000000000000000 --- a/Sources/ChatFeature/Coordinator/ChatCoordinator.swift +++ /dev/null @@ -1,106 +0,0 @@ -import UIKit -import Models -import Shared -import QuickLook -import Permissions -import Presentation -import XXModels - -public protocol ChatCoordinating { - func toCamera(from: UIViewController) - func toLibrary(from: UIViewController) - func toPreview(from: UIViewController) - func toRetrySheet(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toWebview(with: String, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toMenuSheet(_: UIViewController, from: UIViewController) - func toPermission(type: PermissionType, from: UIViewController) - func toMembersList(_: UIViewController, from: UIViewController) -} - -public struct ChatCoordinator: ChatCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var retryFactory: () -> UIViewController - var webFactory: (String) -> UIViewController - var previewFactory: () -> QLPreviewController - var contactFactory: (Contact) -> UIViewController - var imagePickerFactory: () -> UIImagePickerController - var permissionFactory: () -> RequestPermissionController - - public init( - retryFactory: @escaping () -> UIViewController, - webFactory: @escaping (String) -> UIViewController, - previewFactory: @escaping () -> QLPreviewController, - contactFactory: @escaping (Contact) -> UIViewController, - imagePickerFactory: @escaping () -> UIImagePickerController, - permissionFactory: @escaping () -> RequestPermissionController - ) { - self.webFactory = webFactory - self.retryFactory = retryFactory - self.previewFactory = previewFactory - self.contactFactory = contactFactory - self.permissionFactory = permissionFactory - self.imagePickerFactory = imagePickerFactory - } -} - -public extension ChatCoordinator { - func toPreview(from parent: UIViewController) { - let screen = previewFactory() - screen.delegate = (parent as? QLPreviewControllerDelegate) - screen.dataSource = (parent as? QLPreviewControllerDataSource) - pushPresenter.present(screen, from: parent) - } - - func toLibrary(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = false - modalPresenter.present(screen, from: parent) - } - - func toCamera(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.sourceType = .camera - screen.allowsEditing = false - modalPresenter.present(screen, from: parent) - } - - func toRetrySheet(from parent: UIViewController) { - let screen = retryFactory() - bottomPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toWebview(with urlString: String, from parent: UIViewController) { - let screen = webFactory(urlString) - modalPresenter.present(screen, from: parent) - } - - func toPermission(type: PermissionType, from parent: UIViewController) { - let screen = permissionFactory() - screen.setup(type: type) - pushPresenter.present(screen, from: parent) - } - - func toMembersList(_ screen: UIViewController, from parent: UIViewController) { - bottomPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toMenuSheet(_ screen: UIViewController, from parent: UIViewController) { - bottomPresenter.present(screen, from: parent) - } -} diff --git a/Sources/ChatFeature/Helpers/BubbleBuilder.swift b/Sources/ChatFeature/Helpers/BubbleBuilder.swift index ae33fd01e60bfaf501583779e11eb59575444b4f..e9e53d9355f380032cf7e835c7c8531a85c8233c 100644 --- a/Sources/ChatFeature/Helpers/BubbleBuilder.swift +++ b/Sources/ChatFeature/Helpers/BubbleBuilder.swift @@ -1,6 +1,7 @@ import UIKit import Shared import XXModels +import AppResources final class Bubbler { static func build( @@ -235,7 +236,15 @@ final class Bubbler { bubble.replyView.space.backgroundColor = Asset.brandPrimary.color bubble.lockerImageView.removeFromSuperview() bubble.revertBottomStackOrder() - case .sendingFailed, .sendingTimedOut: + case .sendingTimedOut: + bubble.senderLabel.removeFromSuperview() + bubble.backgroundColor = Asset.accentWarning.color + bubble.textView.textColor = Asset.neutralWhite.color + bubble.dateLabel.textColor = Asset.neutralWhite.color + roundButtonColor = Asset.neutralWhite.color + bubble.replyView.space.backgroundColor = Asset.neutralWhite.color + bubble.replyView.container.backgroundColor = Asset.brandLight.color + case .sendingFailed: bubble.senderLabel.removeFromSuperview() bubble.backgroundColor = Asset.accentDanger.color bubble.textView.textColor = Asset.neutralWhite.color @@ -293,7 +302,13 @@ final class Bubbler { roundButtonColor = Asset.neutralDisabled.color bubble.lockerImageView.removeFromSuperview() bubble.revertBottomStackOrder() - case .sendingFailed, .sendingTimedOut: + case .sendingTimedOut: + bubble.senderLabel.removeFromSuperview() + bubble.backgroundColor = Asset.accentWarning.color + bubble.textView.textColor = Asset.neutralWhite.color + bubble.dateLabel.textColor = Asset.neutralWhite.color + roundButtonColor = Asset.neutralWhite.color + case .sendingFailed: bubble.senderLabel.removeFromSuperview() bubble.backgroundColor = Asset.accentDanger.color bubble.textView.textColor = Asset.neutralWhite.color diff --git a/Sources/ChatFeature/Helpers/CellConfigurator.swift b/Sources/ChatFeature/Helpers/CellConfigurator.swift index d9b61fd9d3be46405ab170259dbe82aa0b1961a0..0abf3cca2b166b5fc63da479a23a019dbfb87046 100644 --- a/Sources/ChatFeature/Helpers/CellConfigurator.swift +++ b/Sources/ChatFeature/Helpers/CellConfigurator.swift @@ -3,442 +3,443 @@ import Shared import Combine import XXModels import Voxophone +import AppResources import AVFoundation struct CellFactory { - var canBuild: (Message) -> Bool - - var build: (Message, UICollectionView, IndexPath) -> UICollectionViewCell - - func callAsFunction( - item: Message, - collectionView: UICollectionView, - indexPath: IndexPath - ) -> UICollectionViewCell { - build(item, collectionView, indexPath) - } + var canBuild: (Message) -> Bool + + var build: (Message, UICollectionView, IndexPath) -> UICollectionViewCell + + func callAsFunction( + item: Message, + collectionView: UICollectionView, + indexPath: IndexPath + ) -> UICollectionViewCell { + build(item, collectionView, indexPath) + } } extension CellFactory { - static func combined(factories: [CellFactory]) -> Self { - .init( - canBuild: { _ in true }, - build: { item, collectionView, indexPath in - guard let factory = factories.first(where: { $0.canBuild(item)}) else { - fatalError("Couldn't find a factory for \(item). Did you forget to implement?") - } - - return factory( - item: item, - collectionView: collectionView, - indexPath: indexPath - ) - } + static func combined(factories: [CellFactory]) -> Self { + .init( + canBuild: { _ in true }, + build: { item, collectionView, indexPath in + guard let factory = factories.first(where: { $0.canBuild(item)}) else { + fatalError("Couldn't find a factory for \(item). Did you forget to implement?") + } + + return factory( + item: item, + collectionView: collectionView, + indexPath: indexPath ) - } + } + ) + } } extension CellFactory { - static func incomingAudio( - voxophone: Voxophone, - transfer: @escaping (Data) -> FileTransfer - ) -> Self { - .init( - canBuild: { item in - guard (item.status == .received || item.status == .receiving), - item.replyMessageId == nil, - item.fileTransferId != nil else { return false } - - return transfer(item.fileTransferId!).type == "m4a" - - }, build: { item, collectionView, indexPath in - let ft = transfer(item.fileTransferId!) - let cell: IncomingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - let url = FileManager.url(for: "\(ft.name).\(ft.type)")! - - var model = AudioMessageCellState( - date: item.date, - audioURL: url, - isPlaying: false, - transferProgress: ft.progress, - isLoudspeaker: false, - duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, - playbackTime: 0.0 - ) - - cell.leftView.setup(with: model) - cell.canReply = false - cell.performReply = {} - - Bubbler.build(audioBubble: cell.leftView, with: item) - - voxophone.$state - .sink { - switch $0 { - case .playing(url, _, time: let time, _): - model.isPlaying = true - model.playbackTime = time - default: - model.isPlaying = false - model.playbackTime = 0.0 - } - - model.isLoudspeaker = $0.isLoudspeaker - - cell.leftView.setup(with: model) - }.store(in: &cell.leftView.cancellables) - - cell.leftView.didTapRight = { - guard item.status != .receiving else { return } - - voxophone.toggleLoudspeaker() - } - - cell.leftView.didTapLeft = { - guard item.status != .receiving else { return } - - if case .playing(url, _, _, _) = voxophone.state { - voxophone.reset() - } else { - voxophone.load(url) - voxophone.play() - } - } - - return cell - } + static func incomingAudio( + voxophone: Voxophone, + transfer: @escaping (Data) -> FileTransfer + ) -> Self { + .init( + canBuild: { item in + guard (item.status == .received || item.status == .receiving), + item.replyMessageId == nil, + item.fileTransferId != nil else { return false } + + return transfer(item.fileTransferId!).type == "m4a" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: IncomingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + let url = FileManager.url(for: "\(ft.name).\(ft.type)")! + + var model = AudioMessageCellState( + date: item.date, + audioURL: url, + isPlaying: false, + transferProgress: ft.progress, + isLoudspeaker: false, + duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, + playbackTime: 0.0 ) - } - - static func outgoingAudio( - voxophone: Voxophone, - transfer: @escaping (Data) -> FileTransfer - ) -> Self { - .init( - canBuild: { item in - guard (item.status == .sent || - item.status == .sending || - item.status == .sendingFailed || - item.status == .sendingTimedOut) - && item.replyMessageId == nil - && item.fileTransferId != nil else { - return false - } - - return transfer(item.fileTransferId!).type == "m4a" - - }, build: { item, collectionView, indexPath in - let ft = transfer(item.fileTransferId!) - let cell: OutgoingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - let url = FileManager.url(for: "\(ft.name).\(ft.type)")! - var model = AudioMessageCellState( - date: item.date, - audioURL: url, - isPlaying: false, - transferProgress: ft.progress, - isLoudspeaker: false, - duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, - playbackTime: 0.0 - ) - - cell.rightView.setup(with: model) - cell.canReply = false - cell.performReply = {} - - Bubbler.build(audioBubble: cell.rightView, with: item) - - voxophone.$state - .sink { - switch $0 { - case .playing(url, _, time: let time, _): - model.isPlaying = true - model.playbackTime = time - default: - model.isPlaying = false - model.playbackTime = 0.0 - } - - model.isLoudspeaker = $0.isLoudspeaker - - cell.rightView.setup(with: model) - }.store(in: &cell.rightView.cancellables) - - cell.rightView.didTapRight = { - voxophone.toggleLoudspeaker() - } - - cell.rightView.didTapLeft = { - if case .playing(url, _, _, _) = voxophone.state { - voxophone.reset() - } else { - voxophone.load(url) - voxophone.play() - } - } - - return cell + + cell.leftView.setup(with: model) + cell.canReply = false + cell.performReply = {} + + Bubbler.build(audioBubble: cell.leftView, with: item) + + voxophone.$state + .sink { + switch $0 { + case .playing(url, _, time: let time, _): + model.isPlaying = true + model.playbackTime = time + default: + model.isPlaying = false + model.playbackTime = 0.0 } + + model.isLoudspeaker = $0.isLoudspeaker + + cell.leftView.setup(with: model) + }.store(in: &cell.leftView.cancellables) + + cell.leftView.didTapRight = { + guard item.status != .receiving else { return } + + voxophone.toggleLoudspeaker() + } + + cell.leftView.didTapLeft = { + guard item.status != .receiving else { return } + + if case .playing(url, _, _, _) = voxophone.state { + voxophone.reset() + } else { + voxophone.load(url) + voxophone.play() + } + } + + return cell + } + ) + } + + static func outgoingAudio( + voxophone: Voxophone, + transfer: @escaping (Data) -> FileTransfer + ) -> Self { + .init( + canBuild: { item in + guard (item.status == .sent || + item.status == .sending || + item.status == .sendingFailed || + item.status == .sendingTimedOut) + && item.replyMessageId == nil + && item.fileTransferId != nil else { + return false + } + + return transfer(item.fileTransferId!).type == "m4a" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: OutgoingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + let url = FileManager.url(for: "\(ft.name).\(ft.type)")! + var model = AudioMessageCellState( + date: item.date, + audioURL: url, + isPlaying: false, + transferProgress: ft.progress, + isLoudspeaker: false, + duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, + playbackTime: 0.0 ) - } + + cell.rightView.setup(with: model) + cell.canReply = false + cell.performReply = {} + + Bubbler.build(audioBubble: cell.rightView, with: item) + + voxophone.$state + .sink { + switch $0 { + case .playing(url, _, time: let time, _): + model.isPlaying = true + model.playbackTime = time + default: + model.isPlaying = false + model.playbackTime = 0.0 + } + + model.isLoudspeaker = $0.isLoudspeaker + + cell.rightView.setup(with: model) + }.store(in: &cell.rightView.cancellables) + + cell.rightView.didTapRight = { + voxophone.toggleLoudspeaker() + } + + cell.rightView.didTapLeft = { + if case .playing(url, _, _, _) = voxophone.state { + voxophone.reset() + } else { + voxophone.load(url) + voxophone.play() + } + } + + return cell + } + ) + } } extension CellFactory { - static func outgoingImage( - transfer: @escaping (Data) -> FileTransfer - ) -> Self { - .init( - canBuild: { item in - guard (item.status == .sent || - item.status == .sending || - item.status == .sendingFailed || - item.status == .sendingTimedOut) - && item.replyMessageId == nil - && item.fileTransferId != nil else { - return false - } - - return transfer(item.fileTransferId!).type == "jpeg" - - }, build: { item, collectionView, indexPath in - let ft = transfer(item.fileTransferId!) - let cell: OutgoingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.build(imageBubble: cell.rightView, with: item, with: transfer(item.fileTransferId!)) - cell.canReply = false - cell.performReply = {} - - if let image = UIImage(data: ft.data!) { - cell.rightView.imageView.image = UIImage(cgImage: image.cgImage!, scale: image.scale, orientation: .up) - } - - return cell - } - ) - } - - static func incomingImage( - transfer: @escaping (Data) -> FileTransfer - ) -> Self { - .init( - canBuild: { item in - guard (item.status == .received || item.status == .receiving) - && item.replyMessageId == nil - && item.fileTransferId != nil else { - return false - } - - return transfer(item.fileTransferId!).type == "jpeg" - - }, build: { item, collectionView, indexPath in - let ft = transfer(item.fileTransferId!) - let cell: IncomingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.build(imageBubble: cell.leftView, with: item, with: ft) - cell.canReply = false - cell.performReply = {} - - if let data = ft.data { - cell.leftView.imageView.image = UIImage(data: data) - } else { - cell.leftView.imageView.image = Asset.transferImagePlaceholder.image - } - - return cell - } - ) - } + static func outgoingImage( + transfer: @escaping (Data) -> FileTransfer + ) -> Self { + .init( + canBuild: { item in + guard (item.status == .sent || + item.status == .sending || + item.status == .sendingFailed || + item.status == .sendingTimedOut) + && item.replyMessageId == nil + && item.fileTransferId != nil else { + return false + } + + return transfer(item.fileTransferId!).type == "image" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: OutgoingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.build(imageBubble: cell.rightView, with: item, with: transfer(item.fileTransferId!)) + cell.canReply = false + cell.performReply = {} + + if let image = UIImage(data: ft.data!) { + cell.rightView.imageView.image = UIImage(cgImage: image.cgImage!, scale: image.scale, orientation: .up) + } + + return cell + } + ) + } + + static func incomingImage( + transfer: @escaping (Data) -> FileTransfer + ) -> Self { + .init( + canBuild: { item in + guard (item.status == .received || item.status == .receiving) + && item.replyMessageId == nil + && item.fileTransferId != nil else { + return false + } + + return transfer(item.fileTransferId!).type == "image" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: IncomingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.build(imageBubble: cell.leftView, with: item, with: ft) + cell.canReply = false + cell.performReply = {} + + if let data = ft.data { + cell.leftView.imageView.image = UIImage(data: data) + } else { + cell.leftView.imageView.image = Asset.transferImagePlaceholder.image + } + + return cell + } + ) + } } extension CellFactory { - static func outgoingReply( - performReply: @escaping () -> Void, - replyContent: @escaping (Data) -> (String, String), - showRound: @escaping (String?) -> Void - ) -> Self { - .init( - canBuild: { item in - (item.status == .sent || item.status == .sending) - && item.replyMessageId != nil - - }, build: { item, collectionView, indexPath in - let cell: OutgoingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildReply( - bubble: cell.rightView, - with: item, - reply: replyContent(item.replyMessageId!) - ) - - cell.canReply = item.status == .sent - cell.performReply = performReply - cell.rightView.didTapShowRound = { showRound(item.roundURL) } - return cell - } + static func outgoingReply( + performReply: @escaping () -> Void, + replyContent: @escaping (Data) -> (String, String), + showRound: @escaping (String?) -> Void + ) -> Self { + .init( + canBuild: { item in + (item.status == .sent || item.status == .sending) + && item.replyMessageId != nil + + }, build: { item, collectionView, indexPath in + let cell: OutgoingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.buildReply( + bubble: cell.rightView, + with: item, + reply: replyContent(item.replyMessageId!) ) - } - - static func incomingReply( - performReply: @escaping () -> Void, - replyContent: @escaping (Data) -> (String, String), - showRound: @escaping (String?) -> Void - ) -> Self { - .init( - canBuild: { item in - item.status == .received - && item.replyMessageId != nil - - }, build: { item, collectionView, indexPath in - let cell: IncomingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildReply( - bubble: cell.leftView, - with: item, - reply: replyContent(item.replyMessageId!) - ) - cell.canReply = item.status == .received - cell.performReply = performReply - cell.leftView.didTapShowRound = { showRound(item.roundURL) } - cell.leftView.revertBottomStackOrder() - return cell - } + + cell.canReply = item.status == .sent + cell.performReply = performReply + cell.rightView.didTapShowRound = { showRound(item.roundURL) } + return cell + } + ) + } + + static func incomingReply( + performReply: @escaping () -> Void, + replyContent: @escaping (Data) -> (String, String), + showRound: @escaping (String?) -> Void + ) -> Self { + .init( + canBuild: { item in + item.status == .received + && item.replyMessageId != nil + + }, build: { item, collectionView, indexPath in + let cell: IncomingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.buildReply( + bubble: cell.leftView, + with: item, + reply: replyContent(item.replyMessageId!) ) - } - - static func outgoingFailedReply( - performReply: @escaping () -> Void, - replyContent: @escaping (Data) -> (String, String) - ) -> Self { - .init( - canBuild: { item in - (item.status == .sendingFailed || item.status == .sendingTimedOut) - && item.replyMessageId != nil - - }, build: { item, collectionView, indexPath in - let cell: OutgoingFailedReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.buildReply( - bubble: cell.rightView, - with: item, - reply: replyContent(item.replyMessageId!) - ) - - cell.canReply = false - cell.performReply = performReply - return cell - } + cell.canReply = item.status == .received + cell.performReply = performReply + cell.leftView.didTapShowRound = { showRound(item.roundURL) } + cell.leftView.revertBottomStackOrder() + return cell + } + ) + } + + static func outgoingFailedReply( + performReply: @escaping () -> Void, + replyContent: @escaping (Data) -> (String, String) + ) -> Self { + .init( + canBuild: { item in + (item.status == .sendingFailed || item.status == .sendingTimedOut) + && item.replyMessageId != nil + + }, build: { item, collectionView, indexPath in + let cell: OutgoingFailedReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.buildReply( + bubble: cell.rightView, + with: item, + reply: replyContent(item.replyMessageId!) ) - } + + cell.canReply = false + cell.performReply = performReply + return cell + } + ) + } } extension CellFactory { - static func incomingText( - performReply: @escaping () -> Void, - showRound: @escaping (String?) -> Void - ) -> Self { - .init( - canBuild: { item in - item.status == .received - && item.replyMessageId == nil - - }, build: { item, collectionView, indexPath in - let cell: IncomingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.build(bubble: cell.leftView, with: item) - cell.canReply = item.status == .received - cell.performReply = performReply - cell.leftView.didTapShowRound = { showRound(item.roundURL) } - cell.leftView.revertBottomStackOrder() - return cell - } - ) - } - - static func outgoingText( - performReply: @escaping () -> Void, - showRound: @escaping (String?) -> Void - ) -> Self { - .init( - canBuild: { item in - (item.status == .sending || item.status == .sent) - && item.replyMessageId == nil - - }, build: { item, collectionView, indexPath in - let cell: OutgoingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.build(bubble: cell.rightView, with: item) - cell.canReply = item.status == .sent - cell.performReply = performReply - cell.rightView.didTapShowRound = { showRound(item.roundURL) } - - return cell - } - ) - } - - static func outgoingFailedText(performReply: @escaping () -> Void) -> Self { - .init( - canBuild: { item in - (item.status == .sendingFailed || item.status == .sendingTimedOut) - && item.replyMessageId == nil - - }, build: { item, collectionView, indexPath in - let cell: OutgoingFailedTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - Bubbler.build(bubble: cell.rightView, with: item) - cell.canReply = false - cell.performReply = performReply - return cell - } - ) - } + static func incomingText( + performReply: @escaping () -> Void, + showRound: @escaping (String?) -> Void + ) -> Self { + .init( + canBuild: { item in + item.status == .received + && item.replyMessageId == nil + + }, build: { item, collectionView, indexPath in + let cell: IncomingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.build(bubble: cell.leftView, with: item) + cell.canReply = item.status == .received + cell.performReply = performReply + cell.leftView.didTapShowRound = { showRound(item.roundURL) } + cell.leftView.revertBottomStackOrder() + return cell + } + ) + } + + static func outgoingText( + performReply: @escaping () -> Void, + showRound: @escaping (String?) -> Void + ) -> Self { + .init( + canBuild: { item in + (item.status == .sending || item.status == .sent) + && item.replyMessageId == nil + + }, build: { item, collectionView, indexPath in + let cell: OutgoingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.build(bubble: cell.rightView, with: item) + cell.canReply = item.status == .sent + cell.performReply = performReply + cell.rightView.didTapShowRound = { showRound(item.roundURL) } + + return cell + } + ) + } + + static func outgoingFailedText(performReply: @escaping () -> Void) -> Self { + .init( + canBuild: { item in + (item.status == .sendingFailed || item.status == .sendingTimedOut) + && item.replyMessageId == nil + + }, build: { item, collectionView, indexPath in + let cell: OutgoingFailedTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.build(bubble: cell.rightView, with: item) + cell.canReply = false + cell.performReply = performReply + return cell + } + ) + } } struct ActionFactory { - enum Action { - case copy - case retry - case reply - case delete - case report - - var title: String { - switch self { - - case .copy: - return Localized.Chat.BubbleMenu.copy - case .retry: - return Localized.Chat.BubbleMenu.retry - case .reply: - return Localized.Chat.BubbleMenu.reply - case .delete: - return Localized.Chat.BubbleMenu.delete - case .report: - return Localized.Chat.BubbleMenu.report - } - } + enum Action { + case copy + case retry + case reply + case delete + case report + + var title: String { + switch self { + + case .copy: + return Localized.Chat.BubbleMenu.copy + case .retry: + return Localized.Chat.BubbleMenu.retry + case .reply: + return Localized.Chat.BubbleMenu.reply + case .delete: + return Localized.Chat.BubbleMenu.delete + case .report: + return Localized.Chat.BubbleMenu.report + } } - - static func build( - from item: Message, - action: Action, - closure: @escaping (Message) -> Void - ) -> UIAction? { - - switch action { - case .report: - guard item.status == .received else { return nil } - case .reply: - guard item.status == .received || item.status == .sent else { return nil } - case .retry: - guard item.status == .sendingFailed || item.status == .sendingTimedOut else { return nil } - case .delete, .copy: - break - } - - return UIAction( - title: action.title, - state: .off, - handler: { _ in closure(item) } - ) + } + + static func build( + from item: Message, + action: Action, + closure: @escaping (Message) -> Void + ) -> UIAction? { + + switch action { + case .report: + guard item.status == .received else { return nil } + case .reply: + guard item.status == .received || item.status == .sent else { return nil } + case .retry: + guard item.status == .sendingFailed || item.status == .sendingTimedOut else { return nil } + case .delete, .copy: + break } + + return UIAction( + title: action.title, + state: .off, + handler: { _ in closure(item) } + ) + } } diff --git a/Sources/ChatFeature/Helpers/LayoutDelegate.swift b/Sources/ChatFeature/Helpers/LayoutDelegate.swift index 8bbc4343318a3d7353e28a580c67777663020d9c..4a072358ea503bec6d7c8aa1b52eba20bac9c6a2 100644 --- a/Sources/ChatFeature/Helpers/LayoutDelegate.swift +++ b/Sources/ChatFeature/Helpers/LayoutDelegate.swift @@ -1,7 +1,7 @@ import UIKit import ChatLayout -extension ChatLayout { +extension CollectionViewChatLayout { func configure(_ layoutDelegate: ChatLayoutDelegate) { delegate = layoutDelegate settings.estimatedItemSize = CGSize(width: 100, height: 65) @@ -13,11 +13,11 @@ extension ChatLayout { } final class LayoutDelegate: ChatLayoutDelegate { - public func alignmentForItem(_: ChatLayout, of kind: ItemKind, at: IndexPath) -> ChatItemAlignment { + public func alignmentForItem(_: CollectionViewChatLayout, of kind: ItemKind, at: IndexPath) -> ChatItemAlignment { .fullWidth } - public func shouldPresentHeader(_ chatLayout: ChatLayout, at sectionIndex: Int) -> Bool { + public func shouldPresentHeader(_ chatLayout: CollectionViewChatLayout, at sectionIndex: Int) -> Bool { true } } 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/GroupChatViewModel.swift b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift index 1dbce8a895ebfeabceb5900d3e5d258bcfb31199..e083c9c4abe8c76603091b542bf5e78e8dc00cb5 100644 --- a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift @@ -1,199 +1,263 @@ -import HUD import UIKit -import Models import Shared +import AppCore import Combine import XXModels import Defaults import Foundation -import Integration -import ToastFeature +import AppResources import DifferenceKit import ReportingFeature -import DependencyInjection +import XXMessengerClient +import ComposableArchitecture + +import struct XXModels.Message +import XXClient enum GroupChatNavigationRoutes: Equatable { - case waitingRound - case webview(String) + case waitingRound + case webview(String) } final class GroupChatViewModel { - @Dependency private var session: SessionType - @Dependency private var sendReport: SendReport - @Dependency private var reportingStatus: ReportingStatus - @Dependency private var toastController: ToastController + @Dependency(\.sendReport) var sendReport + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.hudManager) var hudManager + @Dependency(\.app.toastManager) var toastManager + @Dependency(\.reportingStatus) var reportingStatus - @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.username, defaultValue: nil) var username: String? - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } - var reportPopupPublisher: AnyPublisher<Contact, Never> { - reportPopupSubject.eraseToAnyPublisher() - } + var reportPopupPublisher: AnyPublisher<XXModels.Contact, Never> { + reportPopupSubject.eraseToAnyPublisher() + } - var replyPublisher: AnyPublisher<(String, String), Never> { - replySubject.eraseToAnyPublisher() - } + var replyPublisher: AnyPublisher<(String, String), Never> { + replySubject.eraseToAnyPublisher() + } - var routesPublisher: AnyPublisher<GroupChatNavigationRoutes, Never> { - routesSubject.eraseToAnyPublisher() - } + var routesPublisher: AnyPublisher<GroupChatNavigationRoutes, Never> { + routesSubject.eraseToAnyPublisher() + } - let info: GroupInfo - private var stagedReply: Reply? - private var cancellables = Set<AnyCancellable>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let reportPopupSubject = PassthroughSubject<Contact, Never>() - private let replySubject = PassthroughSubject<(String, String), Never>() - private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>() - - var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { - session.dbManager.fetchMessagesPublisher(.init(chat: .group(info.group.id))) - .assertNoFailure() - .map { messages -> [ArraySection<ChatSection, Message>] in - let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in - let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) - return Calendar.current.date(from: components)! - } - - return groupedByDate - .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } - .sorted(by: { $0.model.date < $1.model.date }) - } - .map { sections -> [ArraySection<ChatSection, Message>] in - var snapshot = [ArraySection<ChatSection, Message>]() - sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } - return snapshot - }.eraseToAnyPublisher() - } + let info: GroupInfo + private var stagedReply: Reply? + private var cancellables = Set<AnyCancellable>() + private let reportPopupSubject = PassthroughSubject<XXModels.Contact, Never>() + private let replySubject = PassthroughSubject<(String, String), Never>() + private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>() - init(_ info: GroupInfo) { - self.info = info - } + var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { + try! dbManager.getDB().fetchMessagesPublisher(.init(chat: .group(info.group.id))) + .replaceError(with: []) + .map { messages -> [ArraySection<ChatSection, Message>] in + let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in + let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) + return Calendar.current.date(from: components)! + } - func readAll() { - let assignment = Message.Assignments(isUnread: false) - let query = Message.Query(chat: .group(info.group.id)) - _ = try? session.dbManager.bulkUpdateMessages(query, assignment) - } + return groupedByDate + .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } + .sorted(by: { $0.model.date < $1.model.date }) + } + .map { sections -> [ArraySection<ChatSection, Message>] in + var snapshot = [ArraySection<ChatSection, Message>]() + sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } + return snapshot + }.eraseToAnyPublisher() + } - func didRequestDelete(_ messages: [Message]) { - _ = try? session.dbManager.deleteMessages(.init(id: Set(messages.map(\.id)))) - } + init(_ info: GroupInfo) { + self.info = info + } - func didRequestReport(_ message: Message) { - if let contact = try? session.dbManager.fetchContacts(.init(id: [message.senderId])).first { - reportPopupSubject.send(contact) - } - } + func readAll() { + let assignment = Message.Assignments(isUnread: false) + let query = Message.Query(chat: .group(info.group.id)) + _ = try? dbManager.getDB().bulkUpdateMessages(query, assignment) + } + + func didRequestDelete(_ messages: [Message]) { + _ = try? dbManager.getDB().deleteMessages(.init(id: Set(messages.map(\.id)))) + } - func send(_ text: String) { - session.send(.init( - text: text.trimmingCharacters(in: .whitespacesAndNewlines), - reply: stagedReply - ), toGroup: info.group) - stagedReply = nil + func didRequestReport(_ message: Message) { + if let contact = try? dbManager.getDB().fetchContacts(.init(id: [message.senderId])).first { + reportPopupSubject.send(contact) } + } - func retry(_ message: Message) { - guard let id = message.id else { return } - session.retryMessage(id) + func send(_ text: String) { + do { + var message = Message( + senderId: try messenger.e2e.get()!.getContact().getId(), + recipientId: nil, + groupId: info.group.id, + date: Date(), + status: .sending, + isUnread: false, + text: text.trimmingCharacters(in: .whitespacesAndNewlines), + replyMessageId: stagedReply?.messageId + ) + message = try dbManager.getDB().saveMessage(message) + let report = try messenger.groupChat()!.send( + groupId: info.id, + message: MessagePayload( + text: text.trimmingCharacters(in: .whitespacesAndNewlines), + replyingTo: stagedReply?.messageId + ).encode() + ) + message.networkId = report.messageId + message.date = Date.fromTimestamp(Int(report.timestamp)) + message = try dbManager.getDB().saveMessage(message) + try messenger.cMix.get()?.waitForRoundResult( + roundList: try report.encode(), + timeoutMS: 15_000, + callback: .init(handle: { result in + switch result { + case .delivered: + message.status = .sent + case .notDelivered(timedOut: let timedOut): + message.status = timedOut ? .sendingTimedOut : .sendingFailed + } + _ = try? self.dbManager.getDB().saveMessage(message) + }) + ) + } catch { + print(error.localizedDescription) } + } - func showRoundFrom(_ roundURL: String?) { - if let urlString = roundURL, !urlString.isEmpty { - routesSubject.send(.webview(urlString)) - } else { - routesSubject.send(.waitingRound) - } + func retry(_ message: Message) { + do { + var message = message + message.status = .sending + message = try dbManager.getDB().saveMessage(message) + let report = try messenger.groupChat()!.send( + groupId: info.id, + message: MessagePayload( + text: message.text.trimmingCharacters(in: .whitespacesAndNewlines), + replyingTo: stagedReply?.messageId + ).encode() + ) + message.networkId = report.messageId + message.date = Date.fromTimestamp(Int(report.timestamp)) + message = try dbManager.getDB().saveMessage(message) + try messenger.cMix.get()?.waitForRoundResult( + roundList: try report.encode(), + timeoutMS: 15_000, + callback: .init(handle: { result in + switch result { + case .delivered: + message.status = .sent + case .notDelivered(timedOut: let timedOut): + message.status = timedOut ? .sendingTimedOut : .sendingFailed + } + _ = try? self.dbManager.getDB().saveMessage(message) + }) + ) + } catch { + print(error.localizedDescription) } + } - func abortReply() { - stagedReply = nil + func showRoundFrom(_ roundURL: String?) { + if let urlString = roundURL, !urlString.isEmpty { + routesSubject.send(.webview(urlString)) + } else { + routesSubject.send(.waitingRound) } + } - func getReplyContent(for messageId: Data) -> (String, String) { - guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else { - return ("[DELETED]", "[DELETED]") - } + func abortReply() { + stagedReply = nil + } - return (getName(from: message.senderId), message.text) + func getReplyContent(for messageId: Data) -> (String, String) { + guard let message = try? dbManager.getDB().fetchMessages(.init(networkId: messageId)).first else { + return ("[DELETED]", "[DELETED]") } - func getName(from senderId: Data) -> String { - guard senderId != session.myId else { return "You" } + return (getName(from: message.senderId), message.text) + } - guard let contact = try? session.dbManager.fetchContacts(.init(id: [senderId])).first else { - return "[DELETED]" - } + func getName(from senderId: Data) -> String { + guard senderId != myId else { return "You" } - var name = (contact.nickname ?? contact.username) ?? "Fetching username..." + guard let contact = try? dbManager.getDB().fetchContacts(.init(id: [senderId])).first else { + return "[DELETED]" + } - if contact.isBlocked, reportingStatus.isEnabled() { - name = "\(name) (Blocked)" - } + var name = (contact.nickname ?? contact.username) ?? "Fetching username..." - return name + if contact.isBlocked, reportingStatus.isEnabled() { + name = "\(name) (Blocked)" } - func didRequestReply(_ message: Message) { - guard let networkId = message.networkId else { return } - stagedReply = Reply(messageId: networkId, senderId: message.senderId) - replySubject.send(getReplyContent(for: networkId)) - } + return name + } - func report(contact: Contact, screenshot: UIImage, completion: @escaping () -> Void) { - let report = Report( - sender: .init( - userId: contact.id.base64EncodedString(), - username: contact.username! - ), - recipient: .init( - userId: session.myId.base64EncodedString(), - username: username! - ), - type: .group, - screenshot: screenshot.pngData()!, - partyName: info.group.name, - partyBlob: info.group.id.base64EncodedString(), - partyMembers: info.members.map { Report.ReportUser( - userId: $0.id.base64EncodedString(), - username: $0.username ?? "") - } - ) - - hudSubject.send(.on) - sendReport(report) { result in - switch result { - case .failure(let error): - DispatchQueue.main.async { - self.hudSubject.send(.error(.init(with: error))) - } - - case .success(_): - self.blockContact(contact) - DispatchQueue.main.async { - self.hudSubject.send(.none) - self.presentReportConfirmation(contact: contact) - completion() - } - } + func didRequestReply(_ message: Message) { + guard let networkId = message.networkId else { return } + stagedReply = Reply(messageId: networkId, senderId: message.senderId) + replySubject.send(getReplyContent(for: networkId)) + } + + func report(contact: XXModels.Contact, screenshot: UIImage, completion: @escaping () -> Void) { + let report = Report( + sender: .init( + userId: contact.id.base64EncodedString(), + username: contact.username! + ), + recipient: .init( + userId: myId.base64EncodedString(), + username: username! + ), + type: .group, + screenshot: screenshot.pngData()!, + partyName: info.group.name, + partyBlob: info.group.id.base64EncodedString(), + partyMembers: info.members.map { Report.ReportUser( + userId: $0.id.base64EncodedString(), + username: $0.username ?? "") + } + ) + + hudManager.show() + sendReport(report) { result in + switch result { + case .failure(let error): + DispatchQueue.main.async { + self.hudManager.show(.init(error: error)) } - } - private func blockContact(_ contact: Contact) { - var contact = contact - contact.isBlocked = true - _ = try? session.dbManager.saveContact(contact) + case .success(_): + self.blockContact(contact) + DispatchQueue.main.async { + self.hudManager.hide() + self.presentReportConfirmation(contact: contact) + completion() + } + } } + } - private func presentReportConfirmation(contact: Contact) { - let name = (contact.nickname ?? contact.username) ?? "the contact" - toastController.enqueueToast(model: .init( - title: "Your report has been sent and \(name) is now blocked.", - leftImage: Asset.requestSentToaster.image - )) - } + private func blockContact(_ contact: XXModels.Contact) { + var contact = contact + contact.isBlocked = true + _ = try? dbManager.getDB().saveContact(contact) + } + + private func presentReportConfirmation(contact: XXModels.Contact) { + let name = (contact.nickname ?? contact.username) ?? "the contact" + toastManager.enqueue(.init( + title: "Your report has been sent and \(name) is now blocked.", + leftImage: Asset.requestSentToaster.image + )) + } } diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 8ba933ad52ff63b6beca403e84bd688ccb043ac9..92b93761bcf599f72275369d966ee39e05ef22f7 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -1,305 +1,322 @@ -import HUD import UIKit -import Models import Shared +import AppCore import Combine -import XXLogger import XXModels -import Foundation -import Integration +import XXClient import Defaults -import Permissions -import ToastFeature +import AppResources +import Dependencies +import AppNavigation import DifferenceKit import ReportingFeature -import DependencyInjection +import XXMessengerClient +import PermissionsFeature + +import struct XXModels.Message +import struct XXModels.FileTransfer enum SingleChatNavigationRoutes: Equatable { - case none - case camera - case library - case waitingRound - case cameraPermission - case libraryPermission - case microphonePermission - case webview(String) + case none + case camera + case library + case waitingRound + case cameraPermission + case libraryPermission + case microphonePermission + case webview(String) } final class SingleChatViewModel: NSObject { - @Dependency private var logger: XXLogger - @Dependency private var session: SessionType - @Dependency private var permissions: PermissionHandling - @Dependency private var toastController: ToastController - @Dependency private var sendReport: SendReport - - @KeyObject(.username, defaultValue: nil) var username: String? - - var contact: Contact { contactSubject.value } - private var stagedReply: Reply? - private var cancellables = Set<AnyCancellable>() - private let contactSubject: CurrentValueSubject<Contact, Never> - private let replySubject = PassthroughSubject<(String, String), Never>() - private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() - private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([]) - private let reportPopupSubject = PassthroughSubject<Void, Never>() - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var isOnline: AnyPublisher<Bool, Never> { session.isOnline } - var contactPublisher: AnyPublisher<Contact, Never> { contactSubject.eraseToAnyPublisher() } - var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() } - var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } - var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } - - var reportPopupPublisher: AnyPublisher<Void, Never> { - reportPopupSubject.eraseToAnyPublisher() - } - - var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { - sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in - var snapshot = [ArraySection<ChatSection, Message>]() - sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } - return snapshot - }.eraseToAnyPublisher() - } - - private func updateRecentState(_ contact: Contact) { - if contact.isRecent == true { - var contact = contact - contact.isRecent = false - _ = try? session.dbManager.saveContact(contact) - } - } - - func viewDidAppear() { - updateRecentState(contact) - } - - init(_ contact: Contact) { - self.contactSubject = .init(contact) - super.init() - - updateRecentState(contact) - - session.dbManager.fetchContactsPublisher(Contact.Query(id: [contact.id])) - .assertNoFailure() - .compactMap { $0.first } - .sink { [unowned self] in contactSubject.send($0) } - .store(in: &cancellables) - - session.dbManager.fetchMessagesPublisher(.init(chat: .direct(session.myId, contact.id))) - .assertNoFailure() - .map { - let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in - let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) - return Calendar.current.date(from: components)! - } - - return groupedByDate - .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } - .sorted(by: { $0.model.date < $1.model.date }) - }.receive(on: DispatchQueue.main) - .sink { [unowned self] in sectionsRelay.send($0) } - .store(in: &cancellables) - } - - // MARK: Public - - func getFileTransferWith(id: Data) -> FileTransfer { - guard let transfer = try? session.dbManager.fetchFileTransfers(.init(id: [id])).first else { - fatalError() - } - - return transfer - } - - func didSendAudio(url: URL) { - session.sendFile(url: url, to: contact) - } - - func didSend(image: UIImage) { - guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } - hudRelay.send(.on) - - session.send(imageData: imageData, to: contact) { [weak self] in - switch $0 { - case .success: - self?.hudRelay.send(.none) - case .failure(let error): - self?.hudRelay.send(.error(.init(with: error))) - } - } - } - - func readAll() { - let assignment = Message.Assignments(isUnread: false) - let query = Message.Query(chat: .direct(session.myId, contact.id)) - _ = try? session.dbManager.bulkUpdateMessages(query, assignment) - } - - func didRequestDeleteAll() { - _ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id))) - } - - func didRequestRetry(_ message: Message) { - guard let id = message.id else { return } - session.retryMessage(id) - } - - func didNavigateSomewhere() { - navigationRoutes.send(.none) + @Dependency(\.sendReport) var sendReport + @Dependency(\.permissions) var permissions + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.sendImage) var sendImage + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.hudManager) var hudManager + @Dependency(\.app.sendMessage) var sendMessage + @Dependency(\.app.toastManager) var toastManager + @Dependency(\.app.networkMonitor) var networkMonitor + + @KeyObject(.username, defaultValue: nil) var username: String? + + var contact: XXModels.Contact { contactSubject.value } + private var stagedReply: Reply? + private var cancellables = Set<AnyCancellable>() + private let contactSubject: CurrentValueSubject<XXModels.Contact, Never> + private let replySubject = PassthroughSubject<(String, String), Never>() + private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() + private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([]) + private let reportPopupSubject = PassthroughSubject<Void, Never>() + + private var healthCancellable: XXClient.Cancellable? + + var isOnline: AnyPublisher<Bool, Never> { + networkMonitor + .observeStatus() + .map { $0 == .available } + .eraseToAnyPublisher() + } + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var contactPublisher: AnyPublisher<XXModels.Contact, Never> { contactSubject.eraseToAnyPublisher() } + var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() } + var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } + var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } + + var reportPopupPublisher: AnyPublisher<Void, Never> { + reportPopupSubject.eraseToAnyPublisher() + } + + var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { + sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in + var snapshot = [ArraySection<ChatSection, Message>]() + sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } + return snapshot + }.eraseToAnyPublisher() + } + + private func updateRecentState(_ contact: XXModels.Contact) { + if contact.isRecent == true { + var contact = contact + contact.isRecent = false + _ = try? dbManager.getDB().saveContact(contact) } - - @discardableResult - func didTest(permission: PermissionType) -> Bool { - switch permission { - case .camera: - if permissions.isCameraAllowed { - navigationRoutes.send(.camera) - } else { - navigationRoutes.send(.cameraPermission) - } - case .library: - if permissions.isPhotosAllowed { - navigationRoutes.send(.library) - } else { - navigationRoutes.send(.libraryPermission) - } - case .microphone: - if permissions.isMicrophoneAllowed { - return true - } else { - navigationRoutes.send(.microphonePermission) - } + } + + func viewDidAppear() { + updateRecentState(contact) + } + + init(_ contact: XXModels.Contact) { + self.contactSubject = .init(contact) + super.init() + + updateRecentState(contact) + + try! dbManager.getDB().fetchContactsPublisher(Contact.Query(id: [contact.id])) + .replaceError(with: []) + .compactMap { $0.first } + .sink { [unowned self] in contactSubject.send($0) } + .store(in: &cancellables) + + try! dbManager.getDB().fetchMessagesPublisher(.init(chat: .direct(myId, contact.id))) + .replaceError(with: []) + .map { + let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in + let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) + return Calendar.current.date(from: components)! } - - return false - } - - func didRequestCopy(_ model: Message) { - UIPasteboard.general.string = model.text + + return groupedByDate + .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } + .sorted(by: { $0.model.date < $1.model.date }) + }.receive(on: DispatchQueue.main) + .sink { [unowned self] in sectionsRelay.send($0) } + .store(in: &cancellables) + + healthCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in + guard let self else { return } + self.networkMonitor.update($0) + })) + } + + func getFileTransferWith(id: Data) -> FileTransfer { + guard let transfer = try? dbManager.getDB().fetchFileTransfers(.init(id: [id])).first else { + fatalError() } - - func didRequestDeleteSingle(_ model: Message) { - didRequestDelete([model]) - } - - func didRequestReport(_: Message) { - reportPopupSubject.send() + + return transfer + } + + func didSendAudio(url: URL) {} + + func didSend(image: UIImage) { + guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } + + sendImage(imageData, to: contact.id, onError: { + print("\($0.localizedDescription)") + }) { + print("finished") } - - func abortReply() { - stagedReply = nil + } + + func readAll() { + let assignment = Message.Assignments(isUnread: false) + let query = Message.Query(chat: .direct(myId, contact.id)) + _ = try? dbManager.getDB().bulkUpdateMessages(query, assignment) + } + + func didRequestDeleteAll() { + _ = try? dbManager.getDB().deleteMessages(.init(chat: .direct(myId, contact.id))) + } + + func didRequestRetry(_ message: Message) { + // TODO + } + + func didNavigateSomewhere() { + navigationRoutes.send(.none) + } + + @discardableResult + func didTest(permission: PermissionType) -> Bool { + switch permission { + case .camera: + if permissions.camera.status() { + navigationRoutes.send(.camera) + } else { + navigationRoutes.send(.cameraPermission) + } + case .library: + if permissions.library.status() { + navigationRoutes.send(.library) + } else { + navigationRoutes.send(.libraryPermission) + } + case .microphone: + if permissions.microphone.status() { + return true + } else { + navigationRoutes.send(.microphonePermission) + } } - func send(_ string: String) { - let text = string.trimmingCharacters(in: .whitespacesAndNewlines) - let payload = Payload(text: text, reply: stagedReply) - session.send(payload, toContact: contact) - stagedReply = nil + return false + } + + func didRequestCopy(_ model: Message) { + UIPasteboard.general.string = model.text + } + + func didRequestDeleteSingle(_ model: Message) { + didRequestDelete([model]) + } + + func didRequestReport(_: Message) { + reportPopupSubject.send() + } + + func abortReply() { + stagedReply = nil + } + + func send(_ string: String) { + sendMessage( + text: string.trimmingCharacters(in: .whitespacesAndNewlines), + replyingTo: stagedReply?.messageId, + to: contact.id, + onError: { + print("\($0.localizedDescription)") + }, completion: { + print("completed") + } + ) + } + + func didRequestReply(_ message: Message) { + guard let networkId = message.networkId else { return } + + let senderTitle: String = { + if message.senderId == myId { + return "You" + } else { + return (contact.nickname ?? contact.username) ?? "Fetching username..." + } + }() + + replySubject.send((senderTitle, message.text)) + stagedReply = Reply(messageId: networkId, senderId: message.senderId) + } + + func getReplyContent(for messageId: Data) -> (String, String) { + guard let message = try? dbManager.getDB().fetchMessages(.init(networkId: messageId)).first else { + return ("[DELETED]", "[DELETED]") } - func didRequestReply(_ message: Message) { - guard let networkId = message.networkId else { return } - - let senderTitle: String = { - if message.senderId == session.myId { - return "You" - } else { - return (contact.nickname ?? contact.username) ?? "Fetching username..." - } - }() - - replySubject.send((senderTitle, message.text)) - stagedReply = Reply(messageId: networkId, senderId: message.senderId) + guard let contact = try? dbManager.getDB().fetchContacts(.init(id: [message.senderId])).first else { + fatalError() } - func getReplyContent(for messageId: Data) -> (String, String) { - guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else { - return ("[DELETED]", "[DELETED]") - } + let contactTitle = (contact.nickname ?? contact.username) ?? "You" + return (contactTitle, message.text) + } - guard let contact = try? session.dbManager.fetchContacts(.init(id: [message.senderId])).first else { - fatalError() - } - - let contactTitle = (contact.nickname ?? contact.username) ?? "You" - return (contactTitle, message.text) + func showRoundFrom(_ roundURL: String?) { + if let urlString = roundURL, !urlString.isEmpty { + navigationRoutes.send(.webview(urlString)) + } else { + navigationRoutes.send(.waitingRound) } - - func showRoundFrom(_ roundURL: String?) { - if let urlString = roundURL, !urlString.isEmpty { - navigationRoutes.send(.webview(urlString)) - } else { - navigationRoutes.send(.waitingRound) + } + + func didRequestDelete(_ items: [Message]) { + _ = try? dbManager.getDB().deleteMessages(.init(id: Set(items.compactMap(\.id)))) + } + + func itemWith(id: Int64) -> Message? { + sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id }) + } + + func itemAt(indexPath: IndexPath) -> Message? { + guard sectionsRelay.value.count > indexPath.section else { return nil } + + let items = sectionsRelay.value[indexPath.section].elements + return items.count > indexPath.row ? items[indexPath.row] : nil + } + + func section(at index: Int) -> ChatSection? { + sectionsRelay.value.count > 0 ? sectionsRelay.value[index].model : nil + } + + func report(screenshot: UIImage, completion: @escaping (Bool) -> Void) { + let report = Report( + sender: .init( + userId: contact.id.base64EncodedString(), + username: contact.username! + ), + recipient: .init( + userId: myId.base64EncodedString(), + username: username! + ), + type: .dm, + screenshot: screenshot.pngData()! + ) + + hudManager.show() + sendReport(report) { result in + switch result { + case .failure(let error): + DispatchQueue.main.async { + self.hudManager.show(.init(error: error)) + completion(false) } - } - - func didRequestDelete(_ items: [Message]) { - _ = try? session.dbManager.deleteMessages(.init(id: Set(items.compactMap(\.id)))) - } - - func itemWith(id: Int64) -> Message? { - sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id }) - } - - func itemAt(indexPath: IndexPath) -> Message? { - guard sectionsRelay.value.count > indexPath.section else { return nil } - - let items = sectionsRelay.value[indexPath.section].elements - return items.count > indexPath.row ? items[indexPath.row] : nil - } - - func section(at index: Int) -> ChatSection? { - sectionsRelay.value.count > 0 ? sectionsRelay.value[index].model : nil - } - func report(screenshot: UIImage, completion: @escaping (Bool) -> Void) { - let report = Report( - sender: .init( - userId: contact.id.base64EncodedString(), - username: contact.username! - ), - recipient: .init( - userId: session.myId.base64EncodedString(), - username: username! - ), - type: .dm, - screenshot: screenshot.pngData()! - ) - - hudRelay.send(.on) - sendReport(report) { result in - switch result { - case .failure(let error): - DispatchQueue.main.async { - self.hudRelay.send(.error(.init(with: error))) - completion(false) - } - - case .success(_): - self.blockContact() - DispatchQueue.main.async { - self.hudRelay.send(.none) - self.presentReportConfirmation() - completion(true) - } - } + case .success(_): + self.blockContact() + DispatchQueue.main.async { + self.hudManager.hide() + self.presentReportConfirmation() + completion(true) } + } } - - private func blockContact() { - var contact = contact - contact.isBlocked = true - _ = try? session.dbManager.saveContact(contact) - } - - private func presentReportConfirmation() { - let name = (contact.nickname ?? contact.username) ?? "the contact" - toastController.enqueueToast(model: .init( - title: "Your report has been sent and \(name) is now blocked.", - leftImage: Asset.requestSentToaster.image - )) - } + } + + private func blockContact() { + var contact = contact + contact.isBlocked = true + _ = try? dbManager.getDB().saveContact(contact) + } + + private func presentReportConfirmation() { + let name = (contact.nickname ?? contact.username) ?? "the contact" + toastManager.enqueue(.init( + title: "Your report has been sent and \(name) is now blocked.", + leftImage: Asset.requestSentToaster.image + )) + } } diff --git a/Sources/ChatFeature/Views/Cells/AudioMessageView.swift b/Sources/ChatFeature/Views/Cells/AudioMessageView.swift index 5982ffc49253c7d5b7a21939290b11f0f48e2f20..88b866abe701f4d3af9aa4041616dc1fa1a02d34 100644 --- a/Sources/ChatFeature/Views/Cells/AudioMessageView.swift +++ b/Sources/ChatFeature/Views/Cells/AudioMessageView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import Combine +import AppResources typealias OutgoingAudioCell = CollectionCell<FlexibleSpace, AudioMessageView> typealias IncomingAudioCell = CollectionCell<AudioMessageView, FlexibleSpace> diff --git a/Sources/ChatFeature/Views/Cells/AudioView.swift b/Sources/ChatFeature/Views/Cells/AudioView.swift index 2a46a7a0309bf1f4a184e3d0cbfe88023f7f653d..e3d26bcbd683907b94073cfbea6aa0250e2ec0ef 100644 --- a/Sources/ChatFeature/Views/Cells/AudioView.swift +++ b/Sources/ChatFeature/Views/Cells/AudioView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class AudioView: UIView { // MARK: UI diff --git a/Sources/ChatFeature/Views/Cells/DocumentMessageView.swift b/Sources/ChatFeature/Views/Cells/DocumentMessageView.swift index 2a8cf025f16d524858da87af728cbdd7bcbf1de7..8556277cc8bbe73aabf5511be4fb59333f9d2509 100644 --- a/Sources/ChatFeature/Views/Cells/DocumentMessageView.swift +++ b/Sources/ChatFeature/Views/Cells/DocumentMessageView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources typealias OutgoingDocumentCell = CollectionCell<FlexibleSpace, DocumentMessageView> typealias IncomingDocumentCell = CollectionCell<DocumentMessageView, FlexibleSpace> diff --git a/Sources/ChatFeature/Views/Cells/ImageMessageView.swift b/Sources/ChatFeature/Views/Cells/ImageMessageView.swift index 88efa910ce3ef46a3b1f8012382b1287117b477d..92cfc9f3151cd14dccee4f52aa9bd3f14cebcf81 100644 --- a/Sources/ChatFeature/Views/Cells/ImageMessageView.swift +++ b/Sources/ChatFeature/Views/Cells/ImageMessageView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources typealias OutgoingImageCell = CollectionCell<FlexibleSpace, ImageMessageView> typealias IncomingImageCell = CollectionCell<ImageMessageView, FlexibleSpace> diff --git a/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift b/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift index 20a74276710b492a845ad7b276fc79665a6a1baf..73e063e850229f3ba0f62f6105d6851a6ea4c55c 100644 --- a/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift +++ b/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources typealias IncomingReplyCell = CollectionCell<ReplyStackMessageView, FlexibleSpace> typealias OutgoingReplyCell = CollectionCell<FlexibleSpace, ReplyStackMessageView> diff --git a/Sources/ChatFeature/Views/Cells/ReplyView.swift b/Sources/ChatFeature/Views/Cells/ReplyView.swift index e64def34d02055d356efefd14ce5b6a4e5cab05e..3f8e1f8dcac7bfd7edcc07e62b464d27b4000370 100644 --- a/Sources/ChatFeature/Views/Cells/ReplyView.swift +++ b/Sources/ChatFeature/Views/Cells/ReplyView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class ReplyView: UIView { let space = UIView() diff --git a/Sources/ChatFeature/Views/Cells/StackMessageView.swift b/Sources/ChatFeature/Views/Cells/StackMessageView.swift index 2c5a3c9f3f6c9e86735c28eb4edb64efcfd5d44f..df89a30363624c426e6119942907437a7ad9ef79 100644 --- a/Sources/ChatFeature/Views/Cells/StackMessageView.swift +++ b/Sources/ChatFeature/Views/Cells/StackMessageView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources typealias IncomingTextCell = CollectionCell<StackMessageView, FlexibleSpace> typealias OutgoingTextCell = CollectionCell<FlexibleSpace, StackMessageView> diff --git a/Sources/ChatFeature/Views/ChatMenuView.swift b/Sources/ChatFeature/Views/ChatMenuView.swift index 6b6f06bfe6ad54326de469f45b50ecf928aa2a3c..5ed237dd36f3d3f616337e88ab34f8ca81b2a805 100644 --- a/Sources/ChatFeature/Views/ChatMenuView.swift +++ b/Sources/ChatFeature/Views/ChatMenuView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import Combine +import AppResources final class ChatMenuView: UIToolbar { enum Action { diff --git a/Sources/ChatFeature/Views/ChatView.swift b/Sources/ChatFeature/Views/ChatView.swift index ee3c7d98cdf0e96266cd110a41ac6dc8f8733f9d..34026c0cc6aa77955b0363a12e1f5bc5f6ffa244 100644 --- a/Sources/ChatFeature/Views/ChatView.swift +++ b/Sources/ChatFeature/Views/ChatView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class ChatView: UIView { let titleLabel = UILabel() @@ -28,10 +29,10 @@ final class ChatView: UIView { networkIssueInvisibleConstraint?.isActive = true snackBar.translatesAutoresizingMaskIntoConstraints = false - titleLabel.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide).offset(45) - make.left.equalToSuperview().offset(48) - make.right.equalToSuperview().offset(-61) + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(45) + $0.left.equalToSuperview().offset(48) + $0.right.equalToSuperview().offset(-61) } } diff --git a/Sources/ChatFeature/Views/GroupHeaderView.swift b/Sources/ChatFeature/Views/GroupHeaderView.swift index b33415707304f070f172764a5ee8a49d89a1a10d..fdd4940153806a413e0362ac55d11afde886e31e 100644 --- a/Sources/ChatFeature/Views/GroupHeaderView.swift +++ b/Sources/ChatFeature/Views/GroupHeaderView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources struct Member { let title: String diff --git a/Sources/ChatFeature/Views/RetrySheetView.swift b/Sources/ChatFeature/Views/RetrySheetView.swift index 44c1de62c74e6ab1f19eacb18a9fd22ff02166f1..1b79665bf9acf383edfa3fc7dfa5239447c9b962 100644 --- a/Sources/ChatFeature/Views/RetrySheetView.swift +++ b/Sources/ChatFeature/Views/RetrySheetView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class RetrySheetView: UIView { // MARK: UI diff --git a/Sources/ChatFeature/Views/SectionHeaderView.swift b/Sources/ChatFeature/Views/SectionHeaderView.swift index 564ad98d840772945e8dfcda3a8921d81b23f4cd..9e78f977d123f628f72300350953dcad2aa9f1d0 100644 --- a/Sources/ChatFeature/Views/SectionHeaderView.swift +++ b/Sources/ChatFeature/Views/SectionHeaderView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class SectionHeaderView: UICollectionReusableView { // MARK: UI diff --git a/Sources/ChatFeature/Views/SheetButton.swift b/Sources/ChatFeature/Views/SheetButton.swift index c20ec8931d7b7b303dc35c819f3b6ad7ba09394c..f24e8a06a62e1689957c6e570d08a19d5787faf3 100644 --- a/Sources/ChatFeature/Views/SheetButton.swift +++ b/Sources/ChatFeature/Views/SheetButton.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class SheetButton: UIControl { enum Style { diff --git a/Sources/ChatFeature/Views/SheetView.swift b/Sources/ChatFeature/Views/SheetView.swift index a4cdfeb1348a6cd8b369115d6c34272df7649d87..6e305a6f5b1ee5ce1b47b1b1d1c58fa46fc359b4 100644 --- a/Sources/ChatFeature/Views/SheetView.swift +++ b/Sources/ChatFeature/Views/SheetView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class SheetView: UIView { let stackView = UIStackView() diff --git a/Sources/ChatFeature/Views/TextView.swift b/Sources/ChatFeature/Views/TextView.swift index 1fbc36974b706d916aa6be5b7ce358b322a1cad5..ea7b30e3cfc95e45f677bbf750d8d27eb8c2c275 100644 --- a/Sources/ChatFeature/Views/TextView.swift +++ b/Sources/ChatFeature/Views/TextView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources /// UITextView avoiding selection 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/ChatInputReducer.swift b/Sources/ChatInputFeature/ChatInputReducer.swift index 23bb35bacf48346aaa0623316baee237072bff2b..170647ee55e1fb45bc97e81f6dfd98a8f999514c 100644 --- a/Sources/ChatInputFeature/ChatInputReducer.swift +++ b/Sources/ChatInputFeature/ChatInputReducer.swift @@ -1,3 +1,4 @@ +import Foundation import ComposableArchitecture public let chatInputReducer = Reducer<ChatInputState, ChatInputAction, ChatInputEnvironment> { state, action, env in 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..ba81355868eb9ec1347d9b58f21a3e3edf1a7e88 100644 --- a/Sources/ChatInputFeature/ChatInputView.swift +++ b/Sources/ChatInputFeature/ChatInputView.swift @@ -2,229 +2,228 @@ import UIKit 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/ChatListFeature/Controller/ChatListController.swift b/Sources/ChatListFeature/Controller/ChatListController.swift index 57e744a3b29d7ab15371624fe2ce0932e7f9d772..7383b423c17c6acca5df900204af90d7db50c9d9 100644 --- a/Sources/ChatListFeature/Controller/ChatListController.swift +++ b/Sources/ChatListFeature/Controller/ChatListController.swift @@ -1,236 +1,249 @@ import UIKit -import Theme -import Models import Shared import Combine +import AppCore import XXModels -import MenuFeature -import DependencyInjection +import AppResources +import Dependencies +import AppNavigation public final class ChatListController: UIViewController { - @Dependency private var coordinator: ChatListCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = ChatListView() - lazy private var topLeftView = ChatListTopLeftNavView() - lazy private var topRightView = ChatListTopRightNavView() - lazy private var tableController = ChatListTableController(viewModel) - lazy private var searchTableController = ChatSearchTableController(viewModel) - private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>! - - private let viewModel = ChatListViewModel() - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - private var isEditingSearch = false { - didSet { - screenView.listContainerView - .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents) - } - } - - private var shouldBeShowingRecents = false { - didSet { - screenView.listContainerView - .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents) - } - } - - public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationItem.backButtonTitle = "" - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = ChatListView() + private lazy var topLeftView = ChatListTopLeftNavView() + private lazy var topRightView = ChatListTopRightNavView() + private lazy var tableController = ChatListTableController(viewModel) + private lazy var searchTableController = ChatSearchTableController(viewModel) + private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>! + + private let viewModel = ChatListViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + private var isEditingSearch = false { + didSet { + screenView.listContainerView + .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents) } - - public override func viewDidLoad() { - super.viewDidLoad() - setupChatList() - setupBindings() - setupNavigationBar() - setupRecentContacts() + } + + private var shouldBeShowingRecents = false { + didSet { + screenView.listContainerView + .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents) } - - private func setupNavigationBar() { - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: topLeftView) - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: topRightView) - - topRightView.actionPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch $0 { - case .didTapSearch: - coordinator.toSearch(from: self) - case .didTapNewGroup: - coordinator.toNewGroup(from: self) - } - }.store(in: &cancellables) - - viewModel.badgeCountPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in topLeftView.updateBadge($0) } - .store(in: &cancellables) - - topLeftView.actionPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSideMenu(from: self) } - .store(in: &cancellables) - } - - private func setupChatList() { - addChild(tableController) - addChild(searchTableController) - - screenView.listContainerView.addSubview(tableController.view) - screenView.searchListContainerView.addSubview(searchTableController.view) - - tableController.view.snp.makeConstraints { - $0.top.equalTo(screenView.listContainerView.collectionContainerView.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() + } + + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + navigationItem.backButtonTitle = "" + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupChatList() + setupBindings() + setupNavigationBar() + setupRecentContacts() + } + + private func setupNavigationBar() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: topLeftView) + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: topRightView) + + topRightView + .actionPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch $0 { + case .didTapSearch: + navigator.perform(PresentSearch(on: navigationController!)) + case .didTapNewGroup: + navigator.perform( + PresentGroupDraft(on: navigationController!) + ) } - - searchTableController.view.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - - tableController.didMove(toParent: self) - searchTableController.didMove(toParent: self) + }.store(in: &cancellables) + + viewModel + .badgeCountPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + topLeftView.updateBadge($0) + }.store(in: &cancellables) + + topLeftView + .actionPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentMenu(currentItem: .chats, from: self)) + }.store(in: &cancellables) + } + + private func setupChatList() { + addChild(tableController) + addChild(searchTableController) + screenView.listContainerView.addSubview(tableController.view) + screenView.searchListContainerView.addSubview(searchTableController.view) + + tableController.view.snp.makeConstraints { + $0.top.equalTo(screenView.listContainerView.collectionContainerView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } - - private func setupRecentContacts() { - screenView - .listContainerView - .collectionView - .register(ChatListRecentContactCell.self) - - collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( - collectionView: screenView.listContainerView.collectionView - ) { collectionView, indexPath, contact in - let cell: ChatListRecentContactCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - let title = (contact.nickname ?? contact.username) ?? "" - cell.setup(title: title, image: contact.photo) - return cell - } - - screenView.listContainerView.collectionView.delegate = self - screenView.listContainerView.collectionView.dataSource = collectionDataSource - - viewModel.recentsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - collectionDataSource.apply($0) - shouldBeShowingRecents = $0.numberOfItems > 0 - }.store(in: &cancellables) + searchTableController.view.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } - - private func setupBindings() { - screenView.searchView - .rightPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toScan(from: self) } - .store(in: &cancellables) - - screenView.searchView - .textPublisher - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] query in - viewModel.updateSearch(query: query) - screenView.searchListContainerView.emptyView.updateSearched(content: query) - }.store(in: &cancellables) - - Publishers.CombineLatest( - viewModel.searchPublisher, - screenView.searchView.textPublisher.removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [unowned self] items, query in - guard query.isEmpty == false else { - screenView.searchListContainerView.isHidden = true - screenView.listContainerView.isHidden = false - screenView.bringSubviewToFront(screenView.listContainerView) - return - } - - screenView.listContainerView.isHidden = true - screenView.searchListContainerView.isHidden = false - - guard items.numberOfItems > 0 else { - screenView.searchListContainerView.emptyView.isHidden = false - screenView.bringSubviewToFront(screenView.searchListContainerView) - screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) - return - } - - screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) - screenView.searchListContainerView.emptyView.isHidden = true - } - .store(in: &cancellables) - - screenView.searchView - .isEditingPublisher - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in isEditingSearch = $0 } - .store(in: &cancellables) - - viewModel.chatsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard $0.isEmpty == false else { - screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView) - screenView.listContainerView.emptyView.isHidden = false - return - } - - screenView.listContainerView.bringSubviewToFront(tableController.view) - screenView.listContainerView.emptyView.isHidden = true - } - .store(in: &cancellables) - - screenView.searchListContainerView - .emptyView.searchButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toSearch(from: self) } - .store(in: &cancellables) - - screenView.listContainerView - .emptyView.contactsButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toContacts(from: self) } - .store(in: &cancellables) - - viewModel.isOnline - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak screenView] connected in screenView?.showConnectingBanner(!connected) } - .store(in: &cancellables) + tableController.didMove(toParent: self) + searchTableController.didMove(toParent: self) + } + + private func setupRecentContacts() { + screenView + .listContainerView + .collectionView + .register(ChatListRecentContactCell.self) + + collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( + collectionView: screenView.listContainerView.collectionView + ) { collectionView, indexPath, contact in + let cell: ChatListRecentContactCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + let title = (contact.nickname ?? contact.username) ?? "" + cell.setup(title: title, image: contact.photo) + return cell } + + screenView.listContainerView.collectionView.delegate = self + screenView.listContainerView.collectionView.dataSource = collectionDataSource + + viewModel + .recentsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + collectionDataSource.apply($0) + shouldBeShowingRecents = $0.numberOfItems > 0 + }.store(in: &cancellables) + } + + private func setupBindings() { + screenView + .searchView + .rightPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentScan(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .searchView + .textPublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] query in + viewModel.updateSearch(query: query) + screenView.searchListContainerView.emptyView.updateSearched(content: query) + }.store(in: &cancellables) + + Publishers.CombineLatest( + viewModel.searchPublisher, + screenView.searchView.textPublisher.removeDuplicates() + ) + .receive(on: DispatchQueue.main) + .sink { [unowned self] items, query in + guard query.isEmpty == false else { + screenView.searchListContainerView.isHidden = true + screenView.listContainerView.isHidden = false + screenView.bringSubviewToFront(screenView.listContainerView) + return + } + screenView.listContainerView.isHidden = true + screenView.searchListContainerView.isHidden = false + guard items.numberOfItems > 0 else { + screenView.searchListContainerView.emptyView.isHidden = false + screenView.bringSubviewToFront(screenView.searchListContainerView) + screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) + return + } + screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) + screenView.searchListContainerView.emptyView.isHidden = true + }.store(in: &cancellables) + + screenView + .searchView + .isEditingPublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + isEditingSearch = $0 + }.store(in: &cancellables) + + viewModel + .chatsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard $0.isEmpty == false else { + screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView) + screenView.listContainerView.emptyView.isHidden = false + return + } + screenView.listContainerView.bringSubviewToFront(tableController.view) + screenView.listContainerView.emptyView.isHidden = true + }.store(in: &cancellables) + + screenView + .searchListContainerView + .emptyView + .searchButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentSearch(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .listContainerView + .emptyView + .contactsButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentContactList(on: navigationController!)) + }.store(in: &cancellables) + + viewModel + .isOnline + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak screenView] connected in + screenView?.showConnectingBanner(!connected) + }.store(in: &cancellables) + } } extension ChatListController: UICollectionViewDelegate { - public func collectionView( - _ collectionView: UICollectionView, - didSelectItemAt indexPath: IndexPath - ) { - if let contact = collectionDataSource.itemIdentifier(for: indexPath) { - coordinator.toSingleChat(with: contact, from: self) - } + public func collectionView( + _ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath + ) { + if let contact = collectionDataSource.itemIdentifier(for: indexPath) { + navigator.perform(PresentChat(contact: contact, on: navigationController!)) } + } } diff --git a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift index 46e9c20bed9b21b96d883f0ffc07db81caad0de9..284a571745afde1d577fa0a34afe48be13fd3d76 100644 --- a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift @@ -1,129 +1,127 @@ import UIKit import Shared -import Models import Combine -import DependencyInjection +import AppResources +import Dependencies +import AppNavigation class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch snapshot().sectionIdentifiers[section] { - case .chats: - return "CHATS" - case .connections: - return "CONNECTIONS" - } + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch snapshot().sectionIdentifiers[section] { + case .chats: + return "CHATS" + case .connections: + return "CONNECTIONS" } + } } final class ChatSearchTableController: UITableViewController { - @Dependency private var coordinator: ChatListCoordinating - - private let viewModel: ChatListViewModel - private let cellHeight: CGFloat = 83.0 - private var cancellables = Set<AnyCancellable>() - private var tableDataSource: ChatSearchListTableViewDiffableDataSource? - - init(_ viewModel: ChatListViewModel) { - self.viewModel = viewModel - super.init(style: .grouped) - - tableDataSource = ChatSearchListTableViewDiffableDataSource( - tableView: tableView - ) { table, indexPath, item in - let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) - switch item { - case .chat(let info): - switch info { - case .group(let group): - cell.setupGroup( - name: group.name, - date: group.createdAt, - preview: nil, - unreadCount: 0 - ) - - case .groupChat(let groupChatInfo): - cell.setupGroup( - name: groupChatInfo.group.name, - date: groupChatInfo.lastMessage.date, - preview: groupChatInfo.lastMessage.text, - unreadCount: groupChatInfo.unreadCount - ) - - case .contactChat(let contactChatInfo): - cell.setupContact( - name: (contactChatInfo.contact.nickname ?? contactChatInfo.contact.username) ?? "", - image: contactChatInfo.contact.photo, - date: contactChatInfo.lastMessage.date, - unreadCount: contactChatInfo.unreadCount, - preview: contactChatInfo.lastMessage.text - ) - } - - case .connection(let contact): - cell.setupContact( - name: (contact.nickname ?? contact.username) ?? "", - image: contact.photo, - date: nil, - unreadCount: 0, - preview: contact.username ?? "" - ) - } - - return cell + @Dependency(\.navigator) var navigator: Navigator + + private let viewModel: ChatListViewModel + private let cellHeight: CGFloat = 83.0 + private var cancellables = Set<AnyCancellable>() + private var tableDataSource: ChatSearchListTableViewDiffableDataSource? + + init(_ viewModel: ChatListViewModel) { + self.viewModel = viewModel + super.init(style: .grouped) + + tableDataSource = ChatSearchListTableViewDiffableDataSource( + tableView: tableView + ) { table, indexPath, item in + let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) + switch item { + case .chat(let info): + switch info { + case .group(let group): + cell.setupGroup( + name: group.name, + date: group.createdAt, + preview: nil, + unreadCount: 0 + ) + + case .groupChat(let groupChatInfo): + cell.setupGroup( + name: groupChatInfo.group.name, + date: groupChatInfo.lastMessage.date, + preview: groupChatInfo.lastMessage.text, + unreadCount: groupChatInfo.unreadCount + ) + + case .contactChat(let contactChatInfo): + cell.setupContact( + name: (contactChatInfo.contact.nickname ?? contactChatInfo.contact.username) ?? "", + image: contactChatInfo.contact.photo, + date: contactChatInfo.lastMessage.date, + unreadCount: contactChatInfo.unreadCount, + preview: contactChatInfo.lastMessage.text + ) } + + case .connection(let contact): + cell.setupContact( + name: (contact.nickname ?? contact.username) ?? "", + image: contact.photo, + date: nil, + unreadCount: 0, + preview: contact.username ?? "" + ) + } + + return cell } - - required init?(coder: NSCoder) { nil } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.separatorStyle = .none - tableView.tableFooterView = UIView() - tableView.sectionIndexColor = .blue - tableView.register(ChatListCell.self) - tableView.dataSource = tableDataSource - view.backgroundColor = Asset.neutralWhite.color - - viewModel.searchPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } - .store(in: &cancellables) - } + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + tableView.sectionIndexColor = .blue + tableView.register(ChatListCell.self) + tableView.dataSource = tableDataSource + view.backgroundColor = Asset.neutralWhite.color + + viewModel.searchPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } + .store(in: &cancellables) + } } extension ChatSearchTableController { - override func tableView( - _ tableView: UITableView, - heightForRowAt: IndexPath - ) -> CGFloat { - return cellHeight - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let item = tableDataSource?.itemIdentifier(for: indexPath) { - switch item { - case .chat(let chatInfo): - switch chatInfo { - case .group(let group): - if let groupInfo = viewModel.groupInfo(from: group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .groupChat(let info): - if let groupInfo = viewModel.groupInfo(from: info.group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .contactChat(let info): - guard info.contact.authStatus == .friend else { return } - coordinator.toSingleChat(with: info.contact, from: self) - } - - case .connection(let contact): - coordinator.toContact(contact, from: self) - } + override func tableView( + _ tableView: UITableView, + heightForRowAt: IndexPath + ) -> CGFloat { + return cellHeight + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let item = tableDataSource?.itemIdentifier(for: indexPath) { + switch item { + case .chat(let chatInfo): + switch chatInfo { + case .group(let group): + if let groupInfo = viewModel.groupInfo(from: group) { + navigator.perform(PresentGroupChat(groupInfo: groupInfo, on: navigationController!)) + } + case .groupChat(let info): + if let groupInfo = viewModel.groupInfo(from: info.group) { + navigator.perform(PresentGroupChat(groupInfo: groupInfo, on: navigationController!)) + } + case .contactChat(let info): + guard info.contact.authStatus == .friend else { return } + navigator.perform(PresentChat(contact: info.contact, on: navigationController!)) } + case .connection(let contact): + navigator.perform(PresentContact(contact: contact, on: navigationController!)) + } } + } } diff --git a/Sources/ChatListFeature/Controller/ChatListSheetController.swift b/Sources/ChatListFeature/Controller/ChatListSheetController.swift index cb52ce6a9313f14e4351bcae2c5fdd82d0bee952..6b0d2d1d1c2a1754f02a4efb4b1ac8d6631c7a2a 100644 --- a/Sources/ChatListFeature/Controller/ChatListSheetController.swift +++ b/Sources/ChatListFeature/Controller/ChatListSheetController.swift @@ -2,41 +2,46 @@ import UIKit import Combine public final class ChatListSheetController: UIViewController { - public enum Action { - case delete - case deleteAll - } - - lazy private var screenView = ChatListMenuView() - - var didChooseAction: (Action) -> Void - private var cancellables = Set<AnyCancellable>() - - public init(_ didChooseAction: @escaping ChatListSheetClosure) { - self.didChooseAction = didChooseAction - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupBindings() - } - - private func setupBindings() { - screenView.deleteButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in dismiss(animated: true) { [weak self] in self?.didChooseAction(.delete) }} - .store(in: &cancellables) - - screenView.deleteAllButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in dismiss(animated: true) { [weak self] in self?.didChooseAction(.deleteAll) }} - .store(in: &cancellables) - } + private lazy var screenView = ChatListMenuView() + + private let didTapDelete: () -> Void + private let didTapDeleteAll: () -> Void + private var cancellables = Set<AnyCancellable>() + + public init( + _ didTapDelete: @escaping () -> Void, + _ didTapDeleteAll: @escaping () -> Void + ) { + self.didTapDelete = didTapDelete + self.didTapDeleteAll = didTapDeleteAll + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + screenView + .deleteButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak self] in + self?.didTapDelete() + } + }.store(in: &cancellables) + + screenView + .deleteAllButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak self] in + self?.didTapDeleteAll() + } + }.store(in: &cancellables) + } } diff --git a/Sources/ChatListFeature/Controller/ChatListTableController.swift b/Sources/ChatListFeature/Controller/ChatListTableController.swift index 8527195eee326146909a865a6768e553c10a5e3d..e1c2e06b98954ab0f99d4fa6452fd781a6ede8bd 100644 --- a/Sources/ChatListFeature/Controller/ChatListTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListTableController.swift @@ -1,208 +1,214 @@ import UIKit import Shared -import Models import Combine import XXModels +import AppNavigation import DifferenceKit import DrawerFeature -import DependencyInjection +import Dependencies +import AppResources extension ChatInfo: Differentiable { - public var differenceIdentifier: ChatInfo.ID { id } + public var differenceIdentifier: ChatInfo.ID { id } } final class ChatListTableController: UITableViewController { - @Dependency private var coordinator: ChatListCoordinating - - private var rows = [ChatInfo]() - private let viewModel: ChatListViewModel - private let cellHeight: CGFloat = 83.0 - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - init(_ viewModel: ChatListViewModel) { - self.viewModel = viewModel - super.init(style: .grouped) - } - - required init?(coder: NSCoder) { nil } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - tableView.alwaysBounceVertical = true - tableView.register(ChatListCell.self) - tableView.tableFooterView = UIView() - - viewModel - .chatsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard !self.rows.isEmpty else { - self.rows = $0 - tableView.reloadData() - return - } - - self.tableView.reload( - using: StagedChangeset(source: self.rows, target: $0), - deleteSectionsAnimation: .automatic, - insertSectionsAnimation: .automatic, - reloadSectionsAnimation: .none, - deleteRowsAnimation: .automatic, - insertRowsAnimation: .automatic, - reloadRowsAnimation: .none - ) { [unowned self] in - self.rows = $0 - } - }.store(in: &cancellables) - } + @Dependency(\.navigator) var navigator: Navigator + + private var rows = [ChatInfo]() + private let viewModel: ChatListViewModel + private let cellHeight: CGFloat = 83.0 + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + init(_ viewModel: ChatListViewModel) { + self.viewModel = viewModel + super.init(style: .grouped) + } + + required init?(coder: NSCoder) { nil } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.alwaysBounceVertical = true + tableView.register(ChatListCell.self) + tableView.tableFooterView = UIView() + + viewModel + .chatsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard !self.rows.isEmpty else { + self.rows = $0 + tableView.reloadData() + return + } + + self.tableView.reload( + using: StagedChangeset(source: self.rows, target: $0), + deleteSectionsAnimation: .automatic, + insertSectionsAnimation: .automatic, + reloadSectionsAnimation: .none, + deleteRowsAnimation: .automatic, + insertRowsAnimation: .automatic, + reloadRowsAnimation: .none + ) { [unowned self] in + self.rows = $0 + } + }.store(in: &cancellables) + } } extension ChatListTableController { - override func tableView( - _ tableView: UITableView, - numberOfRowsInSection: Int - ) -> Int { - return rows.count + override func tableView( + _ tableView: UITableView, + numberOfRowsInSection: Int + ) -> Int { + return rows.count + } + + override func tableView( + _ tableView: UITableView, + heightForRowAt: IndexPath + ) -> CGFloat { + return cellHeight + } + + override func tableView( + _ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { + + let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in + guard let self else { return } + self.didRequestDeletionOf(self.rows[indexPath.row]) + complete(true) } - - override func tableView( - _ tableView: UITableView, - heightForRowAt: IndexPath - ) -> CGFloat { - return cellHeight + + delete.image = Asset.chatListDeleteSwipe.image + delete.backgroundColor = Asset.accentDanger.color + return UISwipeActionsConfiguration(actions: [delete]) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch rows[indexPath.row] { + case .group(let group): + if let groupInfo = viewModel.groupInfo(from: group) { + navigator.perform(PresentGroupChat( + groupInfo: groupInfo, + on: navigationController! + )) + } + case .groupChat(let info): + if let groupInfo = viewModel.groupInfo(from: info.group) { + navigator.perform(PresentGroupChat( + groupInfo: groupInfo, + on: navigationController! + )) + } + case .contactChat(let info): + guard info.contact.authStatus == .friend else { return } + navigator.perform(PresentChat(contact: info.contact, on: navigationController!)) } - - override func tableView( - _ tableView: UITableView, - trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath - ) -> UISwipeActionsConfiguration? { - - let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in - guard let self = self else { return } - self.didRequestDeletionOf(self.rows[indexPath.row]) - complete(true) - } - - delete.image = Asset.chatListDeleteSwipe.image - delete.backgroundColor = Asset.accentDanger.color - return UISwipeActionsConfiguration(actions: [delete]) + } + + override func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) + + switch rows[indexPath.row] { + case .group(let group): + cell.setupGroup( + name: group.name, + date: group.createdAt, + preview: nil, + unreadCount: 0 + ) + + case .groupChat(let info): + cell.setupGroup( + name: info.group.name, + date: info.lastMessage.date, + preview: info.lastMessage.text, + unreadCount: info.unreadCount + ) + + case .contactChat(let info): + cell.setupContact( + name: (info.contact.nickname ?? info.contact.username) ?? "", + image: info.contact.photo, + date: info.lastMessage.date, + unreadCount: info.unreadCount, + preview: info.lastMessage.text + ) } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch rows[indexPath.row] { - case .group(let group): - if let groupInfo = viewModel.groupInfo(from: group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .groupChat(let info): - if let groupInfo = viewModel.groupInfo(from: info.group) { - coordinator.toGroupChat(with: groupInfo, from: self) - } - - case .contactChat(let info): - guard info.contact.authStatus == .friend else { return } - coordinator.toSingleChat(with: info.contact, from: self) - } - } - - override func tableView( - _ tableView: UITableView, - cellForRowAt indexPath: IndexPath - ) -> UITableViewCell { - - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) - - switch rows[indexPath.row] { - case .group(let group): - cell.setupGroup( - name: group.name, - date: group.createdAt, - preview: nil, - unreadCount: 0 - ) - - case .groupChat(let info): - cell.setupGroup( - name: info.group.name, - date: info.lastMessage.date, - preview: info.lastMessage.text, - unreadCount: info.unreadCount - ) - - case .contactChat(let info): - cell.setupContact( - name: (info.contact.nickname ?? info.contact.username) ?? "", - image: info.contact.photo, - date: info.lastMessage.date, - unreadCount: info.unreadCount, - preview: info.lastMessage.text - ) - } - - return cell + + return cell + } + + private func didRequestDeletionOf(_ item: ChatInfo) { + let title: String + let subtitle: String + let actionTitle: String + let actionClosure: () -> Void + + switch item { + case .group(let group): + title = Localized.ChatList.DeleteGroup.title + subtitle = Localized.ChatList.DeleteGroup.subtitle + actionTitle = Localized.ChatList.DeleteGroup.action + actionClosure = { [weak viewModel] in viewModel?.leave(group) } + + case .contactChat(let info): + title = Localized.ChatList.Delete.title + subtitle = Localized.ChatList.Delete.subtitle + actionTitle = Localized.ChatList.Delete.delete + actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } + + case .groupChat(let info): + title = Localized.ChatList.DeleteGroup.title + subtitle = Localized.ChatList.DeleteGroup.subtitle + actionTitle = Localized.ChatList.DeleteGroup.action + actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } } - - private func didRequestDeletionOf(_ item: ChatInfo) { - let title: String - let subtitle: String - let actionTitle: String - let actionClosure: () -> Void - - switch item { - case .group(let group): - title = Localized.ChatList.DeleteGroup.title - subtitle = Localized.ChatList.DeleteGroup.subtitle - actionTitle = Localized.ChatList.DeleteGroup.action - actionClosure = { [weak viewModel] in viewModel?.leave(group) } - - case .contactChat(let info): - title = Localized.ChatList.Delete.title - subtitle = Localized.ChatList.Delete.subtitle - actionTitle = Localized.ChatList.Delete.delete - actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } - - case .groupChat(let info): - title = Localized.ChatList.DeleteGroup.title - subtitle = Localized.ChatList.DeleteGroup.subtitle - actionTitle = Localized.ChatList.DeleteGroup.action - actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } + + let actionButton = DrawerCapsuleButton( + model: .init(title: actionTitle, style: .red) + ) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + actionClosure() } - - let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 39 - ), - actionButton - ]) - - actionButton.action.receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - actionClosure() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 39 + ), + actionButton + ], isDismissable: true, from: self)) + } } diff --git a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift deleted file mode 100644 index acd4fbcaecf645611fd30d058a0172f785b03fab..0000000000000000000000000000000000000000 --- a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift +++ /dev/null @@ -1,103 +0,0 @@ -import UIKit -import Shared -import Models -import XXModels -import MenuFeature -import ChatFeature -import Presentation - -public typealias ChatListSheetClosure = (ChatListSheetController.Action) -> Void - -public protocol ChatListCoordinating { - func toScan(from: UIViewController) - func toSearch(from: UIViewController) - func toContacts(from: UIViewController) - func toNewGroup(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toGroupChat(with: GroupInfo, from: UIViewController) -} - -public struct ChatListCoordinator: ChatListCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var scanFactory: () -> UIViewController - var searchFactory: (String?) -> UIViewController - var newGroupFactory: () -> UIViewController - var contactsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupInfo) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - - public init( - scanFactory: @escaping () -> UIViewController, - searchFactory: @escaping (String?) -> UIViewController, - newGroupFactory: @escaping () -> UIViewController, - contactsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupInfo) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController - ) { - self.scanFactory = scanFactory - self.searchFactory = searchFactory - self.contactFactory = contactFactory - self.newGroupFactory = newGroupFactory - self.contactsFactory = contactsFactory - self.sideMenuFactory = sideMenuFactory - self.groupChatFactory = groupChatFactory - self.singleChatFactory = singleChatFactory - } -} - -public extension ChatListCoordinator { - func toSearch(from parent: UIViewController) { - let screen = searchFactory(nil) - pushPresenter.present(screen, from: parent) - } - - func toScan(from parent: UIViewController) { - let screen = scanFactory() - pushPresenter.present(screen, from: parent) - } - - func toContacts(from parent: UIViewController) { - let screen = contactsFactory() - pushPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toSingleChat(with contact: Contact, from parent: UIViewController) { - let screen = singleChatFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toGroupChat(with group: GroupInfo, from parent: UIViewController) { - let screen = groupChatFactory(group) - pushPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.chats, parent) - sidePresenter.present(screen, from: parent) - } - - func toNewGroup(from parent: UIViewController) { - let screen = newGroupFactory() - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } -} diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index af0a8401592ec2715527a3af901846253c20ba80..8fea8877f547bfdf7d205e95412cf2b783f9762d 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -1,198 +1,200 @@ -import HUD import UIKit import Shared -import Models import Combine import XXModels import Defaults -import Integration +import AppCore +import Dependencies +import XXMessengerClient import ReportingFeature -import DependencyInjection + +import struct XXModels.Group +import XXClient enum SearchSection { - case chats - case connections + case chats + case connections } enum SearchItem: Equatable, Hashable { - case chat(ChatInfo) - case connection(Contact) + case chat(ChatInfo) + case connection(XXModels.Contact) } -typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, Contact> +typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, XXModels.Contact> typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> final class ChatListViewModel { - @Dependency private var session: SessionType - @Dependency private var reportingStatus: ReportingStatus - - var isOnline: AnyPublisher<Bool, Never> { - session.isOnline - } - - var chatsPublisher: AnyPublisher<[ChatInfo], Never> { - chatsSubject.eraseToAnyPublisher() - } - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { - let query = Contact.Query( - isRecent: true, - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - return session.dbManager.fetchContactsPublisher(query) - .assertNoFailure() - .map { - let section = SectionId() - var snapshot = RecentsSnapshot() - snapshot.appendSections([section]) - snapshot.appendItems($0, toSection: section) - return snapshot - }.eraseToAnyPublisher() - } - - var searchPublisher: AnyPublisher<SearchSnapshot, Never> { - let contactsQuery = Contact.Query( - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - let contactsStream = session.dbManager - .fetchContactsPublisher(contactsQuery) - .assertNoFailure() - .map { $0.filter { $0.id != self.session.myId }} - - return Publishers.CombineLatest3( - contactsStream, - chatsPublisher, - searchSubject - .removeDuplicates() - .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) - .eraseToAnyPublisher() - ) - .map { (contacts, chats, query) in - let connectionItems = contacts.filter { - let username = $0.username?.lowercased().contains(query.lowercased()) ?? false - let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false - return username || nickname - }.map(SearchItem.connection) - - let chatItems = chats.filter { - switch $0 { - case .group(let group): - return group.name.lowercased().contains(query.lowercased()) - - case .groupChat(let info): - let name = info.group.name.lowercased().contains(query.lowercased()) - let last = info.lastMessage.text.lowercased().contains(query.lowercased()) - return name || last - - case .contactChat(let info): - let username = info.contact.username?.lowercased().contains(query.lowercased()) ?? false - let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false - let lastMessage = info.lastMessage.text.lowercased().contains(query.lowercased()) - return username || nickname || lastMessage - - } - }.map(SearchItem.chat) - - var snapshot = SearchSnapshot() - - if connectionItems.count > 0 { - snapshot.appendSections([.connections]) - snapshot.appendItems(connectionItems, toSection: .connections) - } - - if chatItems.count > 0 { - snapshot.appendSections([.chats]) - snapshot.appendItems(chatItems, toSection: .chats) - } - - return snapshot - }.eraseToAnyPublisher() - } - - var badgeCountPublisher: AnyPublisher<Int, Never> { - let groupQuery = Group.Query(authStatus: [.pending]) - let contactsQuery = Contact.Query( - authStatus: [ - .verified, - .confirming, - .confirmationFailed, - .verificationFailed, - .verificationInProgress - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - return Publishers.CombineLatest( - session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure(), - session.dbManager.fetchGroupsPublisher(groupQuery).assertNoFailure() - ) - .map { $0.0.count + $0.1.count } + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.hudManager) var hudManager + @Dependency(\.reportingStatus) var reportingStatus + + // TO REFACTOR: + var isOnline: AnyPublisher<Bool, Never> { + Just(.init(true)).eraseToAnyPublisher() + } + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var chatsPublisher: AnyPublisher<[ChatInfo], Never> { + chatsSubject.eraseToAnyPublisher() + } + + var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { + let query = Contact.Query( + authStatus: [.friend], + isRecent: true, + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return try! dbManager.getDB().fetchContactsPublisher(query) + .replaceError(with: []) + .map { + let section = SectionId() + var snapshot = RecentsSnapshot() + snapshot.appendSections([section]) + snapshot.appendItems($0, toSection: section) + return snapshot + }.eraseToAnyPublisher() + } + + var searchPublisher: AnyPublisher<SearchSnapshot, Never> { + let contactsQuery = Contact.Query( + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return Publishers.CombineLatest3( + try! dbManager.getDB().fetchContactsPublisher(contactsQuery) + .replaceError(with: []) + .map { $0.filter { $0.id != self.myId }}, + chatsPublisher, + searchSubject + .removeDuplicates() + .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) .eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let searchSubject = CurrentValueSubject<String, Never>("") - private let chatsSubject = CurrentValueSubject<[ChatInfo], Never>([]) - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - - init() { - session.dbManager.fetchChatInfosPublisher( - ChatInfo.Query( - contactChatInfoQuery: .init( - userId: session.myId, - authStatus: [.friend], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ), - groupChatInfoQuery: GroupChatInfo.Query( - authStatus: [.participating], - excludeBannedContactsMessages: reportingStatus.isEnabled() - ), - groupQuery: Group.Query( - withMessages: false, - authStatus: [.participating] - ) - )) - .assertNoFailure() - .sink { [unowned self] in chatsSubject.send($0) } - .store(in: &cancellables) - } - - func updateSearch(query: String) { - searchSubject.send(query) - } - - func leave(_ group: Group) { - hudSubject.send(.on) - - do { - try session.leave(group: group) - try session.dbManager.deleteMessages(.init(chat: .group(group.id))) - hudSubject.send(.none) - } catch { - hudSubject.send(.error(.init(with: error))) + ) + .map { (contacts, chats, query) in + let connectionItems = contacts.filter { + let username = $0.username?.lowercased().contains(query.lowercased()) ?? false + let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false + return username || nickname + }.map(SearchItem.connection) + + let chatItems = chats.filter { + switch $0 { + case .group(let group): + return group.name.lowercased().contains(query.lowercased()) + + case .groupChat(let info): + let name = info.group.name.lowercased().contains(query.lowercased()) + let last = info.lastMessage.text.lowercased().contains(query.lowercased()) + return name || last + + case .contactChat(let info): + let username = info.contact.username?.lowercased().contains(query.lowercased()) ?? false + let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false + let lastMessage = info.lastMessage.text.lowercased().contains(query.lowercased()) + return username || nickname || lastMessage + } + }.map(SearchItem.chat) + + var snapshot = SearchSnapshot() + + if connectionItems.count > 0 { + snapshot.appendSections([.connections]) + snapshot.appendItems(connectionItems, toSection: .connections) + } + + if chatItems.count > 0 { + snapshot.appendSections([.chats]) + snapshot.appendItems(chatItems, toSection: .chats) + } + + return snapshot + }.eraseToAnyPublisher() + } + + var badgeCountPublisher: AnyPublisher<Int, Never> { + let groupQuery = Group.Query(authStatus: [.pending]) + let contactsQuery = Contact.Query( + authStatus: [ + .verified, + .confirming, + .confirmationFailed, + .verificationFailed, + .verificationInProgress + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return Publishers.CombineLatest( + try! dbManager.getDB().fetchContactsPublisher(contactsQuery).replaceError(with: []), + try! dbManager.getDB().fetchGroupsPublisher(groupQuery).replaceError(with: []) + ) + .map { $0.0.count + $0.1.count } + .eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let searchSubject = CurrentValueSubject<String, Never>("") + private let chatsSubject = CurrentValueSubject<[ChatInfo], Never>([]) + + init() { + try! dbManager.getDB().fetchChatInfosPublisher( + ChatInfo.Query( + contactChatInfoQuery: .init( + userId: myId, + authStatus: [.friend], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ), + groupChatInfoQuery: GroupChatInfo.Query( + authStatus: [.participating], + excludeBannedContactsMessages: reportingStatus.isEnabled() + ), + groupQuery: Group.Query( + withMessages: false, + authStatus: [.participating] + ) + )) + .replaceError(with: []) + .sink { [unowned self] in chatsSubject.send($0) } + .store(in: &cancellables) + } + + func updateSearch(query: String) { + searchSubject.send(query) + } + + func leave(_ group: Group) { + hudManager.show() + do { + try messenger.groupChat()!.leaveGroup(groupId: group.id) + try dbManager.getDB().deleteMessages(.init(chat: .group(group.id))) + try dbManager.getDB().deleteGroup(group) + hudManager.hide() + } catch { + hudManager.show(.init(error: error)) } - - func clear(_ contact: Contact) { - _ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id))) - } - - func groupInfo(from group: Group) -> GroupInfo? { - let query = GroupInfo.Query(groupId: group.id) - guard let info = try? session.dbManager.fetchGroupInfos(query).first else { - return nil - } - - return info + } + + func clear(_ contact: XXModels.Contact) { + _ = try? dbManager.getDB().deleteMessages(.init(chat: .direct(myId, contact.id))) + } + + func groupInfo(from group: Group) -> GroupInfo? { + let query = GroupInfo.Query(groupId: group.id) + guard let info = try? dbManager.getDB().fetchGroupInfos(query).first else { + return nil } + + return info + } } diff --git a/Sources/ChatListFeature/Views/ChatListCell.swift b/Sources/ChatListFeature/Views/ChatListCell.swift index bb455395abbc36921e364b27a5ca07e573726ea3..9ee550dad267464a979b43926222b85cfdf467fb 100644 --- a/Sources/ChatListFeature/Views/ChatListCell.swift +++ b/Sources/ChatListFeature/Views/ChatListCell.swift @@ -1,157 +1,158 @@ import UIKit import Shared +import AppResources final class ChatListCell: UITableViewCell { - private let titleLabel = UILabel() - private let unreadView = UIView() - private let unreadCountLabel = UILabel() - private let previewLabel = UILabel() - private let dateLabel = UILabel() - private let avatarView = AvatarView() - private var lastDate: Date? { - didSet { updateTimeAgoLabel() } + let titleLabel = UILabel() + let unreadView = UIView() + let unreadCountLabel = UILabel() + let previewLabel = UILabel() + let dateLabel = UILabel() + let avatarView = AvatarView() + var lastDate: Date? { + didSet { updateTimeAgoLabel() } + } + + private var timer: Timer? + + deinit { timer?.invalidate() } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + previewLabel.numberOfLines = 2 + dateLabel.textAlignment = .right + + unreadView.layer.cornerRadius = 8 + avatarView.layer.cornerRadius = 21 + avatarView.layer.masksToBounds = true + + dateLabel.textAlignment = .right + selectedBackgroundView = UIView() + unreadView.backgroundColor = .clear + backgroundColor = Asset.neutralWhite.color + dateLabel.textColor = Asset.neutralWeak.color + titleLabel.textColor = Asset.neutralActive.color + unreadCountLabel.textColor = Asset.neutralWhite.color + + dateLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + unreadCountLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + + timer = Timer.scheduledTimer(withTimeInterval: 59, repeats: true) { [weak self] _ in + self?.updateTimeAgoLabel() } - - private var timer: Timer? - - deinit { timer?.invalidate() } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - previewLabel.numberOfLines = 2 - dateLabel.textAlignment = .right - - unreadView.layer.cornerRadius = 8 - avatarView.layer.cornerRadius = 21 - avatarView.layer.masksToBounds = true - - dateLabel.textAlignment = .right - selectedBackgroundView = UIView() - unreadView.backgroundColor = .clear - backgroundColor = Asset.neutralWhite.color - dateLabel.textColor = Asset.neutralWeak.color - titleLabel.textColor = Asset.neutralActive.color - unreadCountLabel.textColor = Asset.neutralWhite.color - - dateLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) - unreadCountLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - - timer = Timer.scheduledTimer(withTimeInterval: 59, repeats: true) { [weak self] _ in - self?.updateTimeAgoLabel() - } - - dateLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - contentView.addSubview(titleLabel) - contentView.addSubview(unreadView) - contentView.addSubview(avatarView) - contentView.addSubview(previewLabel) - contentView.addSubview(dateLabel) - unreadView.addSubview(unreadCountLabel) - - avatarView.snp.makeConstraints { - $0.top.equalToSuperview().offset(14) - $0.left.equalToSuperview().offset(24) - $0.width.height.equalTo(48) - } - - unreadCountLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.left.equalTo(avatarView.snp.right).offset(16) - $0.right.lessThanOrEqualTo(dateLabel.snp.left).offset(-10) - } - - dateLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel) - $0.right.equalToSuperview().offset(-25) - } - - previewLabel.snp.makeConstraints { - $0.left.equalTo(titleLabel) - $0.top.equalTo(titleLabel.snp.bottom).offset(2) - $0.right.lessThanOrEqualTo(unreadView.snp.left).offset(-3) - $0.bottom.lessThanOrEqualToSuperview().offset(-10) - } - - unreadView.snp.makeConstraints { - $0.right.equalTo(dateLabel) - $0.centerY.equalTo(previewLabel) - $0.width.height.equalTo(20) - } + + dateLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + contentView.addSubview(titleLabel) + contentView.addSubview(unreadView) + contentView.addSubview(avatarView) + contentView.addSubview(previewLabel) + contentView.addSubview(dateLabel) + unreadView.addSubview(unreadCountLabel) + + avatarView.snp.makeConstraints { + $0.top.equalToSuperview().offset(14) + $0.left.equalToSuperview().offset(24) + $0.width.height.equalTo(48) } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - lastDate = nil - titleLabel.text = nil - unreadCountLabel.text = nil - previewLabel.attributedText = nil - avatarView.prepareForReuse() + + unreadCountLabel.snp.makeConstraints { + $0.center.equalToSuperview() } - - private func updateTimeAgoLabel() { - if let date = lastDate { - dateLabel.text = date.asRelativeFromNow() - } + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.left.equalTo(avatarView.snp.right).offset(16) + $0.right.lessThanOrEqualTo(dateLabel.snp.left).offset(-10) } - - func setupContact( - name: String, - image: Data?, - date: Date?, - unreadCount: Int, - preview: String - ) { - titleLabel.text = name - setPreview(string: preview) - avatarView.setupProfile(title: name, image: image, size: .large) - unreadCountLabel.text = "\(unreadCount)" - unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear - - if let date = date { - lastDate = date - } else { - dateLabel.text = nil - } + + dateLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel) + $0.right.equalToSuperview().offset(-25) } - - func setupGroup( - name: String, - date: Date, - preview: String?, - unreadCount: Int - ) { - lastDate = date - titleLabel.text = name - setPreview(string: preview) - avatarView.setupGroup(size: .large) - unreadCountLabel.text = "\(unreadCount)" - unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear + + previewLabel.snp.makeConstraints { + $0.left.equalTo(titleLabel) + $0.top.equalTo(titleLabel.snp.bottom).offset(2) + $0.right.lessThanOrEqualTo(unreadView.snp.left).offset(-3) + $0.bottom.lessThanOrEqualToSuperview().offset(-10) } - - private func setPreview(string: String?) { - guard let preview = string else { - previewLabel.attributedText = nil - return - } - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.1 - - previewLabel.attributedText = NSAttributedString( - string: preview, - attributes: [ - .paragraphStyle: paragraphStyle, - .font: Fonts.Mulish.regular.font(size: 14.0), - .foregroundColor: Asset.neutralSecondaryAlternative.color - ]) + + unreadView.snp.makeConstraints { + $0.right.equalTo(dateLabel) + $0.centerY.equalTo(previewLabel) + $0.width.height.equalTo(20) + } + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + lastDate = nil + titleLabel.text = nil + unreadCountLabel.text = nil + previewLabel.attributedText = nil + avatarView.prepareForReuse() + } + + private func updateTimeAgoLabel() { + if let date = lastDate { + dateLabel.text = date.asRelativeFromNow() + } + } + + func setupContact( + name: String, + image: Data?, + date: Date?, + unreadCount: Int, + preview: String + ) { + titleLabel.text = name + setPreview(string: preview) + avatarView.setupProfile(title: name, image: image, size: .large) + unreadCountLabel.text = "\(unreadCount)" + unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear + + if let date = date { + lastDate = date + } else { + dateLabel.text = nil + } + } + + func setupGroup( + name: String, + date: Date, + preview: String?, + unreadCount: Int + ) { + lastDate = date + titleLabel.text = name + setPreview(string: preview) + avatarView.setupGroup(size: .large) + unreadCountLabel.text = "\(unreadCount)" + unreadView.backgroundColor = unreadCount > 0 ? Asset.brandPrimary.color : .clear + } + + private func setPreview(string: String?) { + guard let preview = string else { + previewLabel.attributedText = nil + return } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.1 + + previewLabel.attributedText = NSAttributedString( + string: preview, + attributes: [ + .paragraphStyle: paragraphStyle, + .font: Fonts.Mulish.regular.font(size: 14.0), + .foregroundColor: Asset.neutralSecondaryAlternative.color + ]) + } } diff --git a/Sources/ChatListFeature/Views/ChatListContainerView.swift b/Sources/ChatListFeature/Views/ChatListContainerView.swift index 1be2c99cf0e9cc736f0c02acd1b9c3e54f5be688..77f35111eecf168a96a51a56996f91bb5e0d23a1 100644 --- a/Sources/ChatListFeature/Views/ChatListContainerView.swift +++ b/Sources/ChatListFeature/Views/ChatListContainerView.swift @@ -1,114 +1,96 @@ import UIKit import Shared - -final class ChatSearchListContainerView: UIView{ - let emptyView = ChatSearchEmptyView() - - init() { - super.init(frame: .zero) - - addSubview(emptyView) - - emptyView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} +import AppResources final class ChatListContainerView: UIView { - let separatorView = UIView() - let emptyView = ChatListEmptyView() - let collectionContainerView = UIView() - lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - - private let layout: UICollectionViewFlowLayout = { - let layout = UICollectionViewFlowLayout() - layout.minimumLineSpacing = 35 - layout.itemSize = CGSize(width: 56, height: 80) - layout.scrollDirection = .horizontal - return layout - }() - - init() { - super.init(frame: .zero) - - collectionView.showsHorizontalScrollIndicator = false - separatorView.backgroundColor = Asset.neutralLine.color - collectionView.backgroundColor = Asset.neutralWhite.color - collectionView.contentInset = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30) - - addSubview(emptyView) - addSubview(collectionContainerView) - collectionContainerView.addSubview(collectionView) - collectionContainerView.addSubview(separatorView) - - collectionContainerView.snp.makeConstraints { - $0.bottom.equalTo(snp.top) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.height.equalTo(110) - } - - collectionView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - } - - separatorView.snp.makeConstraints { - $0.top.equalTo(collectionView.snp.bottom).offset(20) - $0.height.equalTo(1) - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.equalToSuperview() - } - - emptyView.snp.makeConstraints { - $0.top.equalTo(collectionContainerView.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + let separatorView = UIView() + let emptyView = ChatListEmptyView() + let collectionContainerView = UIView() + lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + + private let layout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 35 + layout.itemSize = CGSize(width: 56, height: 80) + layout.scrollDirection = .horizontal + return layout + }() + + init() { + super.init(frame: .zero) + + collectionView.showsHorizontalScrollIndicator = false + separatorView.backgroundColor = Asset.neutralLine.color + collectionView.backgroundColor = Asset.neutralWhite.color + collectionView.contentInset = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30) + + addSubview(emptyView) + addSubview(collectionContainerView) + collectionContainerView.addSubview(collectionView) + collectionContainerView.addSubview(separatorView) + + collectionContainerView.snp.makeConstraints { + $0.bottom.equalTo(snp.top) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.height.equalTo(110) } - - required init?(coder: NSCoder) { nil } - - func showRecentsCollection(_ show: Bool) { - if show == true && collectionContainerView.alpha != 0.0 || - show == false && collectionContainerView.alpha == 0.0 { - return - } - - if show == true { - collectionContainerView.alpha = 0.0 - collectionContainerView.snp.updateConstraints { - $0.bottom.equalTo(snp.top).offset(collectionContainerView.bounds.height + 20) - } - - UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseInOut) { - self.collectionContainerView.alpha = 1.0 - } - - UIView.animate(withDuration: 0.3, delay: 0.15, options: .curveEaseInOut) { - self.setNeedsLayout() - self.layoutIfNeeded() - } - } else { - collectionContainerView.alpha = 1.0 - collectionContainerView.snp.updateConstraints { - $0.bottom.equalTo(snp.top) - } - - UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut) { - self.collectionContainerView.alpha = 0.0 - } - - UIView.animate(withDuration: 0.2, delay: 0.15, options: .curveEaseInOut) { - self.setNeedsLayout() - self.layoutIfNeeded() - } - } + collectionView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + } + separatorView.snp.makeConstraints { + $0.top.equalTo(collectionView.snp.bottom).offset(20) + $0.height.equalTo(1) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalToSuperview() + } + emptyView.snp.makeConstraints { + $0.top.equalTo(collectionContainerView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func showRecentsCollection(_ show: Bool) { + if show == true && collectionContainerView.alpha != 0.0 || + show == false && collectionContainerView.alpha == 0.0 { + return + } + + if show == true { + collectionContainerView.alpha = 0.0 + collectionContainerView.snp.updateConstraints { + $0.bottom.equalTo(snp.top).offset(collectionContainerView.bounds.height + 20) + } + + UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseInOut) { + self.collectionContainerView.alpha = 1.0 + } + + UIView.animate(withDuration: 0.3, delay: 0.15, options: .curveEaseInOut) { + self.setNeedsLayout() + self.layoutIfNeeded() + } + } else { + collectionContainerView.alpha = 1.0 + collectionContainerView.snp.updateConstraints { + $0.bottom.equalTo(snp.top) + } + + UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut) { + self.collectionContainerView.alpha = 0.0 + } + + UIView.animate(withDuration: 0.2, delay: 0.15, options: .curveEaseInOut) { + self.setNeedsLayout() + self.layoutIfNeeded() + } } + } } diff --git a/Sources/ChatListFeature/Views/ChatListEmptyView.swift b/Sources/ChatListFeature/Views/ChatListEmptyView.swift index 374e184970bcad877ee7ca507369bb219df32265..4004ad713f9fb2e96c43ba4274b61305ce892108 100644 --- a/Sources/ChatListFeature/Views/ChatListEmptyView.swift +++ b/Sources/ChatListFeature/Views/ChatListEmptyView.swift @@ -1,48 +1,49 @@ import UIKit import Shared +import AppResources final class ChatListEmptyView: UIView { - private let titleLabel = UILabel() - private let stackView = UIStackView() - private(set) var contactsButton = CapsuleButton() - - init() { - super.init(frame: .zero) - - backgroundColor = Asset.neutralWhite.color - let paragraph = NSMutableParagraphStyle() - paragraph.lineHeightMultiple = 1.2 - paragraph.alignment = .center - - titleLabel.numberOfLines = 0 - titleLabel.attributedText = NSAttributedString( - string: Localized.ChatList.emptyTitle, - attributes: [ - .paragraphStyle: paragraph, - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.bold.font(size: 24.0) - ] - ) - - contactsButton.setStyle(.brandColored) - contactsButton.setTitle(Localized.ChatList.action, for: .normal) - - stackView.spacing = 24 - stackView.axis = .vertical - stackView.alignment = .center - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(contactsButton) - - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let stackView = UIStackView() + let contactsButton = CapsuleButton() + + init() { + super.init(frame: .zero) + + backgroundColor = Asset.neutralWhite.color + let paragraph = NSMutableParagraphStyle() + paragraph.lineHeightMultiple = 1.2 + paragraph.alignment = .center + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = NSAttributedString( + string: Localized.ChatList.emptyTitle, + attributes: [ + .paragraphStyle: paragraph, + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.bold.font(size: 24.0) + ] + ) + + contactsButton.setStyle(.brandColored) + contactsButton.setTitle(Localized.ChatList.action, for: .normal) + + stackView.spacing = 24 + stackView.axis = .vertical + stackView.alignment = .center + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(contactsButton) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.lessThanOrEqualToSuperview() } - - required init?(coder: NSCoder) { nil } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/ChatListFeature/Views/ChatListMenuView.swift b/Sources/ChatListFeature/Views/ChatListMenuView.swift index 132b38402fd0ebdf08be0d569e45bfc93793275e..94c18bdfca28937a535117f9ad18a90c76d06caf 100644 --- a/Sources/ChatListFeature/Views/ChatListMenuView.swift +++ b/Sources/ChatListFeature/Views/ChatListMenuView.swift @@ -1,69 +1,74 @@ import UIKit import Shared import Combine +import AppResources final class ChatListMenuView: UIToolbar { - enum Action { - case delete - case deleteAll - } - - let stackView = UIStackView() - let deleteButton = UIButton() - let deleteAllButton = UIButton() - - @Published var isDeleteEnabled = false - - var publisher: AnyPublisher<Action, Never> { - actionRelay.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let actionRelay = PassthroughSubject<Action, Never>() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - private func setup() { - clipsToBounds = true - layer.cornerRadius = 15 - barTintColor = Asset.neutralSecondary.color - - deleteButton.setImage(Asset.chatListMenuDelete.image, for: .normal) - deleteAllButton.setTitleColor(Asset.accentDanger.color, for: .normal) - deleteAllButton.setTitle(Localized.ChatList.Menu.deleteAll, for: .normal) - deleteAllButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) - - stackView.spacing = 35 - stackView.addArrangedSubview(deleteButton) - stackView.addArrangedSubview(deleteAllButton) - stackView.distribution = .fillEqually - addSubview(stackView) - - translatesAutoresizingMaskIntoConstraints = false - - $isDeleteEnabled - .assign(to: \.isEnabled, on: deleteButton) - .store(in: &cancellables) - - deleteButton.publisher(for: .touchUpInside) - .sink { [weak actionRelay] in actionRelay?.send(.delete) } - .store(in: &cancellables) - - deleteAllButton.publisher(for: .touchUpInside) - .sink { [weak actionRelay] in actionRelay?.send(.deleteAll) } - .store(in: &cancellables) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-10) - make.height.equalTo(83) - } + enum Action { + case delete + case deleteAll + } + + let stackView = UIStackView() + let deleteButton = UIButton() + let deleteAllButton = UIButton() + + @Published var isDeleteEnabled = false + + var publisher: AnyPublisher<Action, Never> { + actionRelay.eraseToAnyPublisher() + } + + var cancellables = Set<AnyCancellable>() + let actionRelay = PassthroughSubject<Action, Never>() + + init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + private func setup() { + clipsToBounds = true + layer.cornerRadius = 15 + barTintColor = Asset.neutralSecondary.color + + deleteButton.setImage(Asset.chatListMenuDelete.image, for: .normal) + deleteAllButton.setTitleColor(Asset.accentDanger.color, for: .normal) + deleteAllButton.setTitle(Localized.ChatList.Menu.deleteAll, for: .normal) + deleteAllButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) + + stackView.spacing = 35 + stackView.addArrangedSubview(deleteButton) + stackView.addArrangedSubview(deleteAllButton) + stackView.distribution = .fillEqually + addSubview(stackView) + + translatesAutoresizingMaskIntoConstraints = false + + $isDeleteEnabled + .assign(to: \.isEnabled, on: deleteButton) + .store(in: &cancellables) + + deleteButton + .publisher(for: .touchUpInside) + .sink { [weak actionRelay] in + actionRelay?.send(.delete) + }.store(in: &cancellables) + + deleteAllButton + .publisher(for: .touchUpInside) + .sink { [weak actionRelay] in + actionRelay?.send(.deleteAll) + }.store(in: &cancellables) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.equalToSuperview().offset(-10) + $0.height.equalTo(83) } + } } diff --git a/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift b/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift index 4adbdeef783245dcd73a1697f8325654952ab3e2..1174d4019d14daffa80bfc2887962efdb43fde93 100644 --- a/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift +++ b/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift @@ -1,89 +1,86 @@ import UIKit import Shared +import AppResources final class ChatListRecentContactCell: UICollectionViewCell { - private let titleLabel = UILabel() - private let containerView = UIView() - private let avatarView = AvatarView() - - override init(frame: CGRect) { - super.init(frame: frame) - - contentView.backgroundColor = .white - - let newLabel = UILabel() - newLabel.text = "NEW" - newLabel.textColor = Asset.neutralWhite.color - newLabel.font = Fonts.Mulish.bold.font(size: 8.0) - - let newContainerView = UIView() - newContainerView.layer.cornerRadius = 6.0 - newContainerView.layer.masksToBounds = true - newContainerView.backgroundColor = Asset.accentSafe.color - - titleLabel.numberOfLines = 2 - titleLabel.textAlignment = .center - titleLabel.lineBreakMode = .byWordWrapping - titleLabel.textColor = Asset.neutralDark.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - contentView.addSubview(titleLabel) - contentView.addSubview(containerView) - - containerView.addSubview(avatarView) - containerView.addSubview(newContainerView) - - newContainerView.addSubview(newLabel) - - containerView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - } - - newContainerView.snp.makeConstraints { - $0.top.equalTo(containerView.snp.top) - $0.right.equalTo(containerView.snp.right) - } - - newLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(3) - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(3) - } - - avatarView.snp.makeConstraints { - $0.width.equalTo(48) - $0.height.equalTo(48) - $0.top.equalToSuperview().offset(4) - $0.left.equalToSuperview().offset(4) - $0.right.equalToSuperview().offset(-4) - $0.bottom.equalToSuperview().offset(-4) - } - - titleLabel.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.top.equalTo(containerView.snp.bottom).offset(5) - $0.left.greaterThanOrEqualToSuperview() - $0.right.lessThanOrEqualToSuperview() - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let containerView = UIView() + let avatarView = AvatarView() + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.backgroundColor = .white + + let newLabel = UILabel() + newLabel.text = "NEW" + newLabel.textColor = Asset.neutralWhite.color + newLabel.font = Fonts.Mulish.bold.font(size: 8.0) + + let newContainerView = UIView() + newContainerView.layer.cornerRadius = 6.0 + newContainerView.layer.masksToBounds = true + newContainerView.backgroundColor = Asset.accentSafe.color + + titleLabel.numberOfLines = 2 + titleLabel.textAlignment = .center + titleLabel.lineBreakMode = .byWordWrapping + titleLabel.textColor = Asset.neutralDark.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + contentView.addSubview(titleLabel) + contentView.addSubview(containerView) + + containerView.addSubview(avatarView) + containerView.addSubview(newContainerView) + + newContainerView.addSubview(newLabel) + + containerView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - titleLabel.text = nil - avatarView.prepareForReuse() + newContainerView.snp.makeConstraints { + $0.top.equalTo(containerView.snp.top) + $0.right.equalTo(containerView.snp.right) } - - func setup(title: String, image: Data?) { - titleLabel.text = title - avatarView.setupProfile( - title: title, - image: image, - size: .large - ) + newLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(3) + $0.center.equalToSuperview() + $0.left.equalToSuperview().offset(3) + } + avatarView.snp.makeConstraints { + $0.width.equalTo(48) + $0.height.equalTo(48) + $0.top.equalToSuperview().offset(4) + $0.left.equalToSuperview().offset(4) + $0.right.equalToSuperview().offset(-4) + $0.bottom.equalToSuperview().offset(-4) + } + titleLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.top.equalTo(containerView.snp.bottom).offset(5) + $0.left.greaterThanOrEqualToSuperview() + $0.right.lessThanOrEqualToSuperview() + $0.bottom.lessThanOrEqualToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + avatarView.prepareForReuse() + } + + func setup(title: String, image: Data?) { + titleLabel.text = title + avatarView.setupProfile( + title: title, + image: image, + size: .large + ) + } } diff --git a/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift b/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift index 6cc8a78df568a521afbb39b6fe69cd4197aa5e4e..9e35a63d2cabef7ec6561a174a4e39238e6773ca 100644 --- a/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift +++ b/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift @@ -1,72 +1,75 @@ import UIKit import Shared import Combine +import AppResources final class ChatListTopLeftNavView: UIView { - private let titleLabel = UILabel() - private let badgeLabel = UILabel() - private let menuButton = UIButton() - private let stackView = UIStackView() - private let badgeContainerView = UIView() - - var actionPublisher: AnyPublisher<Void, Never> { - actionSubject.eraseToAnyPublisher() + let titleLabel = UILabel() + let badgeLabel = UILabel() + let menuButton = UIButton() + let stackView = UIStackView() + let badgeContainerView = UIView() + + var actionPublisher: AnyPublisher<Void, Never> { + actionSubject.eraseToAnyPublisher() + } + + private let actionSubject = PassthroughSubject<Void, Never>() + + init() { + super.init(frame: .zero) + + titleLabel.text = Localized.ChatList.title + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) + + menuButton.tintColor = Asset.neutralDark.color + menuButton.setImage(Asset.chatListMenu.image, for: .normal) + menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) + + badgeLabel.textColor = Asset.neutralWhite.color + badgeLabel.font = Fonts.Mulish.bold.font(size: 12.0) + + badgeContainerView.layer.cornerRadius = 5 + badgeContainerView.layer.masksToBounds = true + badgeContainerView.backgroundColor = Asset.brandPrimary.color + + badgeContainerView.addSubview(badgeLabel) + menuButton.addSubview(badgeContainerView) + stackView.addArrangedSubview(menuButton) + stackView.addArrangedSubview(titleLabel) + addSubview(stackView) + + badgeLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(3) + $0.center.equalToSuperview() + $0.left.equalToSuperview().offset(3) } - - private let actionSubject = PassthroughSubject<Void, Never>() - - init() { - super.init(frame: .zero) - - titleLabel.text = Localized.ChatList.title - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) - - menuButton.tintColor = Asset.neutralDark.color - menuButton.setImage(Asset.chatListMenu.image, for: .normal) - menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) - - badgeLabel.textColor = Asset.neutralWhite.color - badgeLabel.font = Fonts.Mulish.bold.font(size: 12.0) - - badgeContainerView.layer.cornerRadius = 5 - badgeContainerView.layer.masksToBounds = true - badgeContainerView.backgroundColor = Asset.brandPrimary.color - - badgeContainerView.addSubview(badgeLabel) - menuButton.addSubview(badgeContainerView) - stackView.addArrangedSubview(menuButton) - stackView.addArrangedSubview(titleLabel) - addSubview(stackView) - - badgeLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(3) - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(3) - } - - badgeContainerView.snp.makeConstraints { - $0.centerY.equalTo(menuButton.snp.top) - $0.centerX.equalTo(menuButton.snp.right).multipliedBy(0.8) - } - - menuButton.snp.makeConstraints { $0.width.equalTo(50) } - stackView.snp.makeConstraints { $0.edges.equalToSuperview() } + badgeContainerView.snp.makeConstraints { + $0.centerY.equalTo(menuButton.snp.top) + $0.centerX.equalTo(menuButton.snp.right).multipliedBy(0.8) } - - required init?(coder: NSCoder) { nil } - - @objc private func didTapMenu() { - actionSubject.send() + menuButton.snp.makeConstraints { + $0.width.equalTo(50) } - - func updateBadge(_ count: Int) { - guard count > 0 else { - badgeContainerView.isHidden = true - return - } - - badgeLabel.text = "\(count)" - badgeContainerView.isHidden = false + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + @objc private func didTapMenu() { + actionSubject.send() + } + + func updateBadge(_ count: Int) { + guard count > 0 else { + badgeContainerView.isHidden = true + return } + + badgeLabel.text = "\(count)" + badgeContainerView.isHidden = false + } } diff --git a/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift b/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift index 817893e1a1df3d5f50cf241cfb0d5b60ababe2ae..1e7426425483439a8e884c224aa74e59ef30a65a 100644 --- a/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift +++ b/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift @@ -1,49 +1,56 @@ import UIKit import Shared import Combine +import AppResources final class ChatListTopRightNavView: UIView { - enum Action { - case didTapSearch - case didTapNewGroup + enum Action { + case didTapSearch + case didTapNewGroup + } + + var actionPublisher: AnyPublisher<Action, Never> { + actionSubject.eraseToAnyPublisher() + } + + let stackView = UIStackView() + let searchButton = UIButton() + let newGroupButton = UIButton() + let actionSubject = PassthroughSubject<Action, Never>() + + init() { + super.init(frame: .zero) + + searchButton.tintColor = Asset.neutralDark.color + newGroupButton.tintColor = Asset.neutralDark.color + searchButton.setImage(Asset.chatListUd.image, for: .normal) + newGroupButton.setImage(Asset.chatListNewGroup.image, for: .normal) + searchButton.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside) + newGroupButton.addTarget(self, action: #selector(didTapNewGroup), for: .touchUpInside) + + stackView.spacing = 10 + stackView.addArrangedSubview(newGroupButton) + stackView.addArrangedSubview(searchButton) + addSubview(stackView) + + searchButton.snp.makeConstraints { + $0.width.equalTo(40) } - - var actionPublisher: AnyPublisher<Action, Never> { - actionSubject.eraseToAnyPublisher() + newGroupButton.snp.makeConstraints { + $0.width.equalTo(40) } - - private let stackView = UIStackView() - private let searchButton = UIButton() - private let newGroupButton = UIButton() - private let actionSubject = PassthroughSubject<Action, Never>() - - init() { - super.init(frame: .zero) - - searchButton.tintColor = Asset.neutralDark.color - newGroupButton.tintColor = Asset.neutralDark.color - searchButton.setImage(Asset.chatListUd.image, for: .normal) - newGroupButton.setImage(Asset.chatListNewGroup.image, for: .normal) - searchButton.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside) - newGroupButton.addTarget(self, action: #selector(didTapNewGroup), for: .touchUpInside) - - stackView.spacing = 10 - stackView.addArrangedSubview(newGroupButton) - stackView.addArrangedSubview(searchButton) - addSubview(stackView) - - searchButton.snp.makeConstraints { $0.width.equalTo(40) } - newGroupButton.snp.makeConstraints { $0.width.equalTo(40) } - stackView.snp.makeConstraints { $0.edges.equalToSuperview() } - } - - required init?(coder: NSCoder) { nil } - - @objc private func didTapSearch() { - actionSubject.send(.didTapSearch) - } - - @objc private func didTapNewGroup() { - actionSubject.send(.didTapNewGroup) + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + @objc private func didTapSearch() { + actionSubject.send(.didTapSearch) + } + + @objc private func didTapNewGroup() { + actionSubject.send(.didTapNewGroup) + } } diff --git a/Sources/ChatListFeature/Views/ChatListView.swift b/Sources/ChatListFeature/Views/ChatListView.swift index 03a407980700123d9843d9239e4822b9247dac63..9b90b60a55e5db25a5844ac604162080c969e825 100644 --- a/Sources/ChatListFeature/Views/ChatListView.swift +++ b/Sources/ChatListFeature/Views/ChatListView.swift @@ -1,80 +1,73 @@ import UIKit import Shared +import AppResources final class ChatListView: UIView { - let snackBar = SnackBar() - let containerView = UIView() - let searchView = SearchComponent() - let listContainerView = ChatListContainerView() - let searchListContainerView = ChatSearchListContainerView() + let snackBar = SnackBar() + let containerView = UIView() + let searchView = SearchComponent() + let listContainerView = ChatListContainerView() + let searchListContainerView = ChatSearchListContainerView() + + init() { + super.init(frame: .zero) - init() { - super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + listContainerView.backgroundColor = Asset.neutralWhite.color + searchListContainerView.backgroundColor = Asset.neutralWhite.color + searchView.update(placeholder: Localized.ChatList.Search.title) - backgroundColor = Asset.neutralWhite.color - listContainerView.backgroundColor = Asset.neutralWhite.color - searchListContainerView.backgroundColor = Asset.neutralWhite.color - searchView.update(placeholder: Localized.ChatList.Search.title) + addSubview(snackBar) + addSubview(searchView) + addSubview(containerView) + containerView.addSubview(searchListContainerView) + containerView.addSubview(listContainerView) - addSubview(snackBar) - addSubview(searchView) - addSubview(containerView) - containerView.addSubview(searchListContainerView) - containerView.addSubview(listContainerView) - - setupConstraints() + snackBar.snp.makeConstraints { + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalTo(snp.top) } - - required init?(coder: NSCoder) { nil } - - func showConnectingBanner(_ show: Bool) { - if show == true { - snackBar.alpha = 0.0 - snackBar.snp.updateConstraints { - $0.bottom - .equalTo(snp.top) - .offset(snackBar.bounds.height) - } - } else { - snackBar.alpha = 1.0 - snackBar.snp.updateConstraints { - $0.bottom.equalTo(snp.top) - } - } - - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { - self.setNeedsLayout() - self.layoutIfNeeded() - self.snackBar.alpha = show ? 1.0 : 0.0 - } + searchView.snp.makeConstraints { + $0.top.equalTo(snackBar.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) } - - private func setupConstraints() { - snackBar.snp.makeConstraints { - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalTo(snp.top) - } - - searchView.snp.makeConstraints { - $0.top.equalTo(snackBar.snp.bottom).offset(20) - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - } - - containerView.snp.makeConstraints { - $0.top.equalTo(searchView.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - - listContainerView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - searchListContainerView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + containerView.snp.makeConstraints { + $0.top.equalTo(searchView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + listContainerView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + searchListContainerView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func showConnectingBanner(_ show: Bool) { + if show == true { + snackBar.alpha = 0.0 + snackBar.snp.updateConstraints { + $0.bottom + .equalTo(snp.top) + .offset(snackBar.bounds.height) + } + } else { + snackBar.alpha = 1.0 + snackBar.snp.updateConstraints { + $0.bottom.equalTo(snp.top) + } + } + + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { + self.setNeedsLayout() + self.layoutIfNeeded() + self.snackBar.alpha = show ? 1.0 : 0.0 } + } } diff --git a/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift b/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift index 2fe1471c5d1f7d9d967a2ca24b50a6950c4067c3..7853c79e5eb9e3ad88377e7db9a030f861797b1f 100644 --- a/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift +++ b/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift @@ -1,57 +1,58 @@ import UIKit import Shared +import AppResources final class ChatSearchEmptyView: UIView { - private let titleLabel = UILabel() - private let stackView = UIStackView() - private let descriptionLabel = UILabel() - private(set) var searchButton = CapsuleButton() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - titleLabel.textColor = Asset.brandPrimary.color - titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) - - let paragraph = NSMutableParagraphStyle() - paragraph.lineHeightMultiple = 1.2 - - descriptionLabel.numberOfLines = 0 - descriptionLabel.attributedText = NSAttributedString( - string: "was not found in your connections or in a chat. Click below to search for them as a new connection.", - attributes: [ - .paragraphStyle: paragraph, - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.regular.font(size: 16.0) - ] - ) - - searchButton.setStyle(.brandColored) - searchButton.setTitle("Search for a connection", for: .normal) - - stackView.axis = .vertical - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(descriptionLabel) - stackView.addArrangedSubview(searchButton) - - stackView.setCustomSpacing(10, after: titleLabel) - stackView.setCustomSpacing(30, after: descriptionLabel) - - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.centerY.equalToSuperview().multipliedBy(0.5) - $0.top.greaterThanOrEqualToSuperview() - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.lessThanOrEqualToSuperview() - } + let titleLabel = UILabel() + let stackView = UIStackView() + let descriptionLabel = UILabel() + let searchButton = CapsuleButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.brandPrimary.color + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.lineHeightMultiple = 1.2 + + descriptionLabel.numberOfLines = 0 + descriptionLabel.attributedText = NSAttributedString( + string: "was not found in your connections or in a chat. Click below to search for them as a new connection.", + attributes: [ + .paragraphStyle: paragraph, + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.regular.font(size: 16.0) + ] + ) + + searchButton.setStyle(.brandColored) + searchButton.setTitle("Search for a connection", for: .normal) + + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(descriptionLabel) + stackView.addArrangedSubview(searchButton) + + stackView.setCustomSpacing(10, after: titleLabel) + stackView.setCustomSpacing(30, after: descriptionLabel) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.centerY.equalToSuperview().multipliedBy(0.5) + $0.top.greaterThanOrEqualToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.bottom.lessThanOrEqualToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func updateSearched(content: String) { - titleLabel.text = content - } + func updateSearched(content: String) { + titleLabel.text = content + } } diff --git a/Sources/ChatListFeature/Views/ChatSearchListContainerView.swift b/Sources/ChatListFeature/Views/ChatSearchListContainerView.swift new file mode 100644 index 0000000000000000000000000000000000000000..75c25dc6402403b45afd9c33bc72fb4c2d515a09 --- /dev/null +++ b/Sources/ChatListFeature/Views/ChatSearchListContainerView.swift @@ -0,0 +1,17 @@ +import UIKit + +final class ChatSearchListContainerView: UIView { + let emptyView = ChatSearchEmptyView() + + init() { + super.init(frame: .zero) + + addSubview(emptyView) + + emptyView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/CheckVersion/CheckVersion.swift b/Sources/CheckVersion/CheckVersion.swift new file mode 100644 index 0000000000000000000000000000000000000000..32cd955542ab8311ae679007e73aaa29f8c1819f --- /dev/null +++ b/Sources/CheckVersion/CheckVersion.swift @@ -0,0 +1,62 @@ +import Foundation +import XCTestDynamicOverlay + +public struct CheckVersion { + public enum VersionState { + case updated + case outdated(String) + case wayTooOld(String, String) + } + + public enum Error: Swift.Error { + case noLocalVersion + case failureFetchingRemote(FetchRemoteVersion.Error) + } + + public typealias Completion = (Result<VersionState, Error>) -> Void + + public var run: (@escaping Completion) -> Void + + public func callAsFunction(_ completion: @escaping Completion) -> Void { + run(completion) + } +} + +extension CheckVersion { + public static func live( + local: FetchLocalVersion = .live, + remote: FetchRemoteVersion = .live + ) -> CheckVersion { + .init { completion in + remote { + switch $0 { + case .success(let remoteModel): + guard let localVersion = local() else { + completion(.failure(.noLocalVersion)) + return + } + if localVersion >= remoteModel.details.recommendedVersion { + completion(.success(.updated)) + } else { + if localVersion < remoteModel.details.minimumVersion { + completion(.success(.wayTooOld( + remoteModel.details.appUrl, + remoteModel.details.minimumVersionMessage + ))) + return + } + completion(.success(.outdated(remoteModel.details.appUrl))) + } + case .failure(let error): + completion(.failure(.failureFetchingRemote(error))) + } + } + } + } +} + +extension CheckVersion { + public static let unimplemented = CheckVersion( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/CheckVersion/Dependency.swift b/Sources/CheckVersion/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..636a64c83d37eb9f639076b1ec80dfd6f2e2a7ed --- /dev/null +++ b/Sources/CheckVersion/Dependency.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum CheckVersionDependencyKey: DependencyKey { + static let liveValue: CheckVersion = .live() + static let testValue: CheckVersion = .unimplemented +} + +extension DependencyValues { + public var checkVersion: CheckVersion { + get { self[CheckVersionDependencyKey.self] } + set { self[CheckVersionDependencyKey.self] = newValue } + } +} diff --git a/Sources/CheckVersion/FetchLocalVersion.swift b/Sources/CheckVersion/FetchLocalVersion.swift new file mode 100644 index 0000000000000000000000000000000000000000..1954fe744e5a70f30c6d57c121b4218c73dba992 --- /dev/null +++ b/Sources/CheckVersion/FetchLocalVersion.swift @@ -0,0 +1,22 @@ +import Foundation +import XCTestDynamicOverlay + +public struct FetchLocalVersion { + public var run: () -> String? + + public func callAsFunction() -> String? { + run() + } +} + +extension FetchLocalVersion { + public static let live = FetchLocalVersion { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + } +} + +extension FetchLocalVersion { + public static let unimplemented = FetchLocalVersion( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/CheckVersion/FetchRemoteVersion.swift b/Sources/CheckVersion/FetchRemoteVersion.swift new file mode 100644 index 0000000000000000000000000000000000000000..e3c19f924df47afb64a01887f6ccae05f63824fa --- /dev/null +++ b/Sources/CheckVersion/FetchRemoteVersion.swift @@ -0,0 +1,51 @@ +import Foundation +import XCTestDynamicOverlay + +public struct FetchRemoteVersion { + public enum Error: Swift.Error { + case noData + case requestError + case decodeFailure + } + + public typealias Completion = (Result<Remote, Error>) -> Void + + public var run: (@escaping Completion) -> Void + + public func callAsFunction(_ completion: @escaping Completion) -> Void { + run(completion) + } +} + +extension FetchRemoteVersion { + public static let live = FetchRemoteVersion { completion in + let urlString = "https://elixxir-bins.s3-us-west-1.amazonaws.com/client/dapps/appdb.json" + let request = URLRequest( + url: URL(string: urlString)!, + cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, + timeoutInterval: 5 + ) + URLSession.shared.dataTask(with: request) { data, _, error in + guard error == nil else { + completion(.failure(.requestError)) + return + } + guard let data else { + completion(.failure(.noData)) + return + } + do { + let model = try JSONDecoder().decode(Remote.self, from: data) + completion(.success(model)) + } catch { + completion(.failure(.decodeFailure)) + } + }.resume() + } +} + +extension FetchRemoteVersion { + public static let unimplemented = FetchRemoteVersion( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/CheckVersion/RemoteDetailsModel.swift b/Sources/CheckVersion/RemoteDetailsModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..d549eb6359ed1c38b4403eae55bf5b45384b77fd --- /dev/null +++ b/Sources/CheckVersion/RemoteDetailsModel.swift @@ -0,0 +1,13 @@ +public struct RemoteDetails: Codable { + public var appUrl: String + public var minimumVersion: String + public var recommendedVersion: String + public var minimumVersionMessage: String + + private enum CodingKeys: String, CodingKey { + case appUrl = "new_ios_app_url" + case minimumVersion = "new_ios_min_version" + case minimumVersionMessage = "new_minimum_popup_msg" + case recommendedVersion = "new_ios_recommended_version" + } +} diff --git a/Sources/CheckVersion/RemoteModel.swift b/Sources/CheckVersion/RemoteModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..f2a65522f1fe974fb1f31e618c55b6c5956eba7f --- /dev/null +++ b/Sources/CheckVersion/RemoteModel.swift @@ -0,0 +1,7 @@ +public struct Remote: Codable { + var details: RemoteDetails + + private enum CodingKeys: String, CodingKey { + case details = "dapp-id" + } +} diff --git a/Sources/CollectionView/CellFactory.swift b/Sources/CollectionView/CellFactory.swift deleted file mode 100644 index bc7bc9bec131565d07dc5805865bdcc13e0959dc..0000000000000000000000000000000000000000 --- a/Sources/CollectionView/CellFactory.swift +++ /dev/null @@ -1,76 +0,0 @@ -import UIKit -import XCTestDynamicOverlay - -public struct CellFactory<Model> { - public struct Registrar { - public init(register: @escaping (UICollectionView) -> Void) { - self.register = register - } - - public var register: (UICollectionView) -> Void - - public func callAsFunction(in view: UICollectionView) { - register(view) - } - } - - public struct Builder { - public init(build: @escaping (Model, UICollectionView, IndexPath) -> UICollectionViewCell?) { - self.build = build - } - - public var build: (Model, UICollectionView, IndexPath) -> UICollectionViewCell? - - public func callAsFunction( - for model: Model, - in view: UICollectionView, - at indexPath: IndexPath - ) -> UICollectionViewCell? { - build(model, view, indexPath) - } - } - - public init( - register: Registrar, - build: Builder - ) { - self.register = register - self.build = build - } - - public var register: Registrar - public var build: Builder -} - -extension CellFactory { - public static func combined(_ factories: CellFactory...) -> CellFactory { - combined(factories) - } - - public static func combined(_ factories: [CellFactory]) -> CellFactory { - CellFactory( - register: .init { collectionView in - factories.forEach { $0.register(in: collectionView) } - }, - build: .init { model, collectionView, indexPath in - for factory in factories { - if let cell = factory.build(for: model, in: collectionView, at: indexPath) { - return cell - } - } - return nil - } - ) - } -} - -#if DEBUG -extension CellFactory { - public static func unimplemented() -> CellFactory { - CellFactory( - register: .init(register: XCTUnimplemented("\(Self.self).Registrar")), - build: .init(build: XCTUnimplemented("\(Self.self).Builder")) - ) - } -} -#endif diff --git a/Sources/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift index 30dd3f5e48444176c274a0f6f18502b12e172c17..ee9bb011bd4eff7300dcff42ea5af2524434f6ce 100644 --- a/Sources/ContactFeature/Controllers/ContactController.swift +++ b/Sources/ContactFeature/Controllers/ContactController.swift @@ -1,437 +1,466 @@ -import HUD import UIKit -import Theme import Shared -import Models import Combine import XXModels +import AppCore +import Dependencies +import AppResources +import AppNavigation import DrawerFeature -import DependencyInjection import ScrollViewController public final class ContactController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: ContactCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = ContactView() - lazy private var scrollViewController = ScrollViewController() - - private let viewModel: ContactViewModel - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public init(_ model: Contact) { - self.viewModel = ContactViewModel(model) - super.init(nibName: nil, bundle: nil) + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = ContactView() + private lazy var scrollViewController = ScrollViewController() + + private let viewModel: ContactViewModel + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public init(_ model: Contact) { + self.viewModel = ContactViewModel(model) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + statusBar.set(.lightContent) + navigationController?.navigationBar + .customize( + backgroundColor: Asset.neutralBody.color, + tint: Asset.neutralWhite.color + ) + } + + public override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + screenView.updateTopOffset(-view.safeAreaInsets.top) + screenView.updateBottomOffset(view.safeAreaInsets.bottom) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + + screenView.didTapSend = { [weak self] in + guard let self else { return } + self.navigator.perform(PresentChat( + contact: self.viewModel.contact, + on: self.navigationController! + )) } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.lightContent) - navigationController?.navigationBar - .customize( - backgroundColor: Asset.neutralBody.color, - tint: Asset.neutralWhite.color - ) + screenView.didTapInfo = { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Contact.SendMessage.Info.title, + subtitle: Localized.Contact.SendMessage.Info.subtitle, + urlString: "https://links.xx.network/cmix" + ) } - public override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - screenView.updateTopOffset(-view.safeAreaInsets.top) - screenView.updateBottomOffset(view.safeAreaInsets.bottom) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - - screenView.didTapSend = { [weak self] in - guard let self = self else { return } - self.coordinator.toSingleChat(with: self.viewModel.contact, from: self) + screenView.set(status: viewModel.contact.authStatus) + } + + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.backgroundColor = Asset.neutralWhite.color + scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.bounces = false + } + + private func setupBindings() { + screenView + .cardComponent + .avatarView + .editButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform( + PresentPhotoLibrary(from: self) + ) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.photo) + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.cardComponent.image = $0 + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.title) + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.cardComponent.nameLabel.text = $0 + }.store(in: &cancellables) + + viewModel + .popPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + + viewModel + .popToRootPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigationController?.popToRootViewController(animated: true) + }.store(in: &cancellables) + + viewModel + .successPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.updateToSuccess() + }.store(in: &cancellables) + + setupScannedBindings() + setupReceivedBindings() + setupConfirmedBindings() + setupInProgressBindings() + setupSuccessBindings() + } + + private func setupSuccessBindings() { + screenView + .successView + .keepAdding + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + + screenView + .successView + .sentRequests + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentRequests(on: navigationController!)) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.username) + .removeDuplicates() + .combineLatest( + viewModel.statePublisher.map(\.email).removeDuplicates(), + viewModel.statePublisher.map(\.phone).removeDuplicates() + ) + .sink { [unowned self] in + [Localized.Contact.username: $0.0, + Localized.Contact.email: $0.1, + Localized.Contact.phone: $0.2].forEach { pair in + guard let value = pair.value else { return } + let attributeView = AttributeComponent() + attributeView.set( + title: pair.key, + value: value + ) + screenView.successView.stack.addArrangedSubview(attributeView) } - screenView.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Contact.SendMessage.Info.title, - subtitle: Localized.Contact.SendMessage.Info.subtitle, - urlString: "https://links.xx.network/cmix" - ) + }.store(in: &cancellables) + } + + private func setupScannedBindings() { + screenView + .scannedView.add + .publisher(for: .touchUpInside) + .sink { [unowned self] in + let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" + navigator.perform(PresentNickname(prefilled: nickname, completion: { [weak self] in + guard let self else { return } + self.viewModel.didTapRequest(with: $0) + }, from: self)) + }.store(in: &cancellables) + } + + private func setupReceivedBindings() { + screenView + .receivedView.accept + .publisher(for: .touchUpInside) + .sink { [unowned self] in + let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" + navigator.perform(PresentNickname(prefilled: nickname, completion: { [weak self] in + guard let self else { return } + self.viewModel.didTapAccept($0) + }, from: self)) + }.store(in: &cancellables) + + screenView + .receivedView.reject + .publisher(for: .touchUpInside) + .sink { [weak viewModel] in + viewModel?.didTapReject() + }.store(in: &cancellables) + } + + private func setupInProgressBindings() { + viewModel + .statePublisher + .map(\.username) + .removeDuplicates() + .combineLatest( + viewModel.statePublisher.map(\.email).removeDuplicates(), + viewModel.statePublisher.map(\.phone).removeDuplicates() + ) + .sink { [unowned self] in + [Localized.Contact.username: $0.0, + Localized.Contact.email: $0.1, + Localized.Contact.phone: $0.2].forEach { pair in + guard let value = pair.value else { return } + + let attributeView = AttributeComponent() + attributeView.set( + title: pair.key, + value: value + ) + + screenView.inProgressView.stack.addArrangedSubview(attributeView) } + }.store(in: &cancellables) + + screenView + .inProgressView.feedback + .button.publisher(for: .touchUpInside) + .sink { [weak viewModel] in + viewModel?.didTapResend() + }.store(in: &cancellables) + } + + private func setupConfirmedBindings() { + viewModel.statePublisher + .receive(on: DispatchQueue.main) + .map(\.nickname) + .removeDuplicates() + .combineLatest( + viewModel.statePublisher.map(\.username).removeDuplicates(), + viewModel.statePublisher.map(\.email).removeDuplicates(), + viewModel.statePublisher.map(\.phone).removeDuplicates() + ) + .sink { [unowned self] in + screenView.confirmedView.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + let nicknameAttribute = AttributeComponent() + nicknameAttribute.set(title: Localized.Contact.nickname, value: $0.0, style: .requiredEditable) + screenView.confirmedView.stackView.insertArrangedSubview(nicknameAttribute, at: 0) + + nicknameAttribute + .actionButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + let nickname = (viewModel.contact.nickname ?? viewModel.contact.username) ?? "" + navigator.perform(PresentNickname(prefilled: nickname, completion: { [weak self] in + guard let self else { return } + self.viewModel.didUpdateNickname($0) + }, from: self)) + }.store(in: &cancellables) + + let usernameAttribute = AttributeComponent() + usernameAttribute.set(title: Localized.Contact.username, value: $0.1) + screenView.confirmedView.stackView.addArrangedSubview(usernameAttribute) + + let emailAttribute = AttributeComponent() + emailAttribute.set(title: Localized.Contact.email, value: $0.2) + screenView.confirmedView.stackView.addArrangedSubview(emailAttribute) + + let phoneAttribute = AttributeComponent() + phoneAttribute.set(title: Localized.Contact.phone, value: $0.3) + screenView.confirmedView.stackView.addArrangedSubview(phoneAttribute) + + let deleteButton = RowButton() + deleteButton.setup( + title: Localized.Contact.Delete.Info.title, + icon: Asset.settingsDelete.image, + style: .delete, + separator: false + ) - screenView.set(status: viewModel.contact.authStatus) - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.backgroundColor = Asset.neutralWhite.color - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.bounces = false - } - - private func setupBindings() { - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.cardComponent.avatarView.editButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toPhotos(from: self) } - .store(in: &cancellables) - - viewModel.statePublisher - .map(\.photo) - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.cardComponent.image = $0 } - .store(in: &cancellables) - - viewModel.statePublisher - .map(\.title) - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.cardComponent.nameLabel.text = $0 } - .store(in: &cancellables) - - viewModel.popPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in navigationController?.popViewController(animated: true) } - .store(in: &cancellables) - - viewModel.popToRootPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in navigationController?.popToRootViewController(animated: true) } - .store(in: &cancellables) - - viewModel.successPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.updateToSuccess() } - .store(in: &cancellables) - - setupScannedBindings() - setupReceivedBindings() - setupConfirmedBindings() - setupInProgressBindings() - setupSuccessBindings() - } - - private func setupSuccessBindings() { - screenView.successView.keepAdding - .publisher(for: .touchUpInside) - .sink { [unowned self] in navigationController?.popViewController(animated: true) } - .store(in: &cancellables) - - screenView.successView.sentRequests - .publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toRequests(from: self) } - .store(in: &cancellables) - - viewModel.statePublisher - .map(\.username) - .removeDuplicates() - .combineLatest( - viewModel.statePublisher.map(\.email).removeDuplicates(), - viewModel.statePublisher.map(\.phone).removeDuplicates() - ) - .sink { [unowned self] in - [Localized.Contact.username: $0.0, - Localized.Contact.email: $0.1, - Localized.Contact.phone: $0.2].forEach { pair in - guard let value = pair.value else { return } - - let attributeView = AttributeComponent() - attributeView.set( - title: pair.key, - value: value - ) - - screenView.successView.stack.addArrangedSubview(attributeView) - } - }.store(in: &cancellables) - } - - private func setupScannedBindings() { - screenView.scannedView.add - .publisher(for: .touchUpInside) - .sink { [unowned self] in - coordinator.toNickname( - from: self, - prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", - viewModel.didTapRequest(with:) - ) - }.store(in: &cancellables) - } - - private func setupReceivedBindings() { - screenView.receivedView.accept - .publisher(for: .touchUpInside) - .sink { [unowned self] in - coordinator.toNickname( - from: self, - prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", - viewModel.didTapAccept(_:) - ) - }.store(in: &cancellables) - - screenView.receivedView.reject - .publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapReject() } - .store(in: &cancellables) - } + screenView.confirmedView.stackView.addArrangedSubview(deleteButton) + + deleteButton.publisher(for: .touchUpInside) + .sink { [unowned self] in presentDeleteInfo() } + .store(in: &cancellables) + }.store(in: &cancellables) + + screenView + .confirmedView + .clearButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + presentClearDrawer() + }.store(in: &cancellables) + } + + private func presentClearDrawer() { + let clearButton = CapsuleButton() + clearButton.setStyle(.red) + clearButton.setTitle(Localized.Contact.Clear.action, for: .normal) + + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Contact.Clear.cancel, for: .normal) + + clearButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didTapClear() + } + }.store(in: &drawerCancellables) + + cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Contact.Clear.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Contact.Clear.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + spacing: 20.0, + views: [clearButton, cancelButton] + ) + ], isDismissable: true, from: self)) + } +} - private func setupInProgressBindings() { - viewModel.statePublisher - .map(\.username) - .removeDuplicates() - .combineLatest( - viewModel.statePublisher.map(\.email).removeDuplicates(), - viewModel.statePublisher.map(\.phone).removeDuplicates() - ) - .sink { [unowned self] in - [Localized.Contact.username: $0.0, - Localized.Contact.email: $0.1, - Localized.Contact.phone: $0.2].forEach { pair in - guard let value = pair.value else { return } - - let attributeView = AttributeComponent() - attributeView.set( - title: pair.key, - value: value - ) - - screenView.inProgressView.stack.addArrangedSubview(attributeView) - } - }.store(in: &cancellables) - - screenView.inProgressView.feedback - .button.publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapResend() } - .store(in: &cancellables) +extension ContactController: UIImagePickerControllerDelegate { + public func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] + ) { + var image: UIImage? + + if let originalImage = info[.originalImage] as? UIImage { + image = originalImage } - private func setupConfirmedBindings() { - viewModel.statePublisher - .receive(on: DispatchQueue.main) - .map(\.nickname) - .removeDuplicates() - .combineLatest( - viewModel.statePublisher.map(\.username).removeDuplicates(), - viewModel.statePublisher.map(\.email).removeDuplicates(), - viewModel.statePublisher.map(\.phone).removeDuplicates() - ) - .sink { [unowned self] in - screenView.confirmedView.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - let nicknameAttribute = AttributeComponent() - nicknameAttribute.set(title: Localized.Contact.nickname, value: $0.0, style: .requiredEditable) - screenView.confirmedView.stackView.insertArrangedSubview(nicknameAttribute, at: 0) - - nicknameAttribute.actionButton.publisher(for: .touchUpInside) - .sink { [unowned self] in - coordinator.toNickname( - from: self, - prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", - viewModel.didUpdateNickname(_:) - ) - } - .store(in: &cancellables) - - let usernameAttribute = AttributeComponent() - usernameAttribute.set(title: Localized.Contact.username, value: $0.1) - screenView.confirmedView.stackView.addArrangedSubview(usernameAttribute) - - let emailAttribute = AttributeComponent() - emailAttribute.set(title: Localized.Contact.email, value: $0.2) - screenView.confirmedView.stackView.addArrangedSubview(emailAttribute) - - let phoneAttribute = AttributeComponent() - phoneAttribute.set(title: Localized.Contact.phone, value: $0.3) - screenView.confirmedView.stackView.addArrangedSubview(phoneAttribute) - - let deleteButton = RowButton() - deleteButton.setup( - title: Localized.Contact.Delete.Info.title, - icon: Asset.settingsDelete.image, - style: .delete, - separator: false - ) - - screenView.confirmedView.stackView.addArrangedSubview(deleteButton) - - deleteButton.publisher(for: .touchUpInside) - .sink { [unowned self] in presentDeleteInfo() } - .store(in: &cancellables) - }.store(in: &cancellables) - - screenView.confirmedView.clearButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in presentClearDrawer() } - .store(in: &cancellables) + if let croppedImage = info[.editedImage] as? UIImage { + image = croppedImage } - private func presentClearDrawer() { - let clearButton = CapsuleButton() - clearButton.setStyle(.red) - clearButton.setTitle(Localized.Contact.Clear.action, for: .normal) - - let cancelButton = CapsuleButton() - cancelButton.setStyle(.seeThrough) - cancelButton.setTitle(Localized.Contact.Clear.cancel, for: .normal) - - let drawer = DrawerController(with: [ - DrawerImage( - image: Asset.drawerNegative.image - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 18.0), - text: Localized.Contact.Clear.title, - color: Asset.neutralActive.color - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: Localized.Contact.Clear.subtitle, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ), - DrawerStack( - spacing: 20.0, - views: [clearButton, cancelButton] - ) - ]) - - clearButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - self.viewModel.didTapClear() - } - }.store(in: &drawerCancellables) - - cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) + guard let image = image else { + picker.dismiss(animated: true) + return } -} - -extension ContactController: UIImagePickerControllerDelegate { - public func imagePickerController( - _ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] - ) { - var image: UIImage? - - if let originalImage = info[.originalImage] as? UIImage { - image = originalImage - } - if let croppedImage = info[.editedImage] as? UIImage { - image = croppedImage - } - - guard let image = image else { - picker.dismiss(animated: true) - return - } - - picker.dismiss(animated: true) - viewModel.didChoosePhoto(image) - } + picker.dismiss(animated: true) + viewModel.didChoosePhoto(image) + } } extension ContactController: UINavigationControllerDelegate {} extension ContactController { - private func presentInfo( - title: String, - subtitle: String, - urlString: String = "" - ) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } - - private func presentDeleteInfo() { - let actionButton = DrawerCapsuleButton(model: .init( - title: Localized.Contact.Delete.Info.title, - style: .red - )) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: Localized.Contact.Delete.Drawer.title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - text: Localized.Contact.Delete.Drawer.description(viewModel.contact.username ?? ""), - spacingAfter: 37, - customAttributes: [.font: Fonts.Mulish.bold.font(size: 16.0)] - ), - actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - self.viewModel.didTapDelete() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + private func presentInfo( + title: String, + subtitle: String, + urlString: String = "" + ) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } + + private func presentDeleteInfo() { + let actionButton = DrawerCapsuleButton(model: .init( + title: Localized.Contact.Delete.Info.title, + style: .red + )) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didTapDelete() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.Contact.Delete.Drawer.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + text: Localized.Contact.Delete.Drawer.description(viewModel.contact.username ?? ""), + spacingAfter: 37, + customAttributes: [.font: Fonts.Mulish.bold.font(size: 16.0)] + ), + actionButton + ], isDismissable: true, from: self)) + } } diff --git a/Sources/ContactFeature/Controllers/NicknameController.swift b/Sources/ContactFeature/Controllers/NicknameController.swift index 709a66998b46c59b402adbbaedb8098be57c8ae3..a36ad67dcd7f761d4df829535d17b1d140e2194f 100644 --- a/Sources/ContactFeature/Controllers/NicknameController.swift +++ b/Sources/ContactFeature/Controllers/NicknameController.swift @@ -5,83 +5,83 @@ import InputField import ScrollViewController public final class NicknameController: UIViewController { - lazy private var screenView = NicknameView() - - private let prefilled: String - private let completion: StringClosure - private let viewModel = NicknameViewModel() - private var cancellables = Set<AnyCancellable>() - private let keyboardListener = KeyboardFrameChangeListener(notificationCenter: .default) - - public init(_ prefilled: String, _ completion: @escaping StringClosure) { - self.prefilled = prefilled - self.completion = completion - super.init(nibName: nil, bundle: nil) + private lazy var screenView = NicknameView() + + private let prefilled: String + private let completion: (String) -> Void + private let viewModel = NicknameViewModel() + private var cancellables = Set<AnyCancellable>() + private let keyboardListener = KeyboardFrameChangeListener(notificationCenter: .default) + + public init(_ prefilled: String, _ completion: @escaping (String) -> Void) { + self.prefilled = prefilled + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + let view = UIView() + view.addSubview(screenView) + + screenView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(0) } - required init?(coder: NSCoder) { nil } + self.view = view + } - public override func loadView() { - let view = UIView() - view.addSubview(screenView) + public override func viewDidLoad() { + super.viewDidLoad() + setupKeyboard() + setupBindings() - screenView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview().offset(0) - } + screenView.inputField.update(content: prefilled) + viewModel.didInput(prefilled) + } - self.view = view - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupKeyboard() - setupBindings() - - screenView.inputField.update(content: prefilled) - viewModel.didInput(prefilled) - } + private func setupKeyboard() { + keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in + guard let self else { return } - private func setupKeyboard() { - keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in - guard let self = self else { return } + let inset = self.view.frame.height - self.view.convert(keyboard.frame, from: nil).minY - let inset = self.view.frame.height - self.view.convert(keyboard.frame, from: nil).minY + self.screenView.snp.updateConstraints { + $0.bottom.equalToSuperview().offset(-inset) + } - self.screenView.snp.updateConstraints { - $0.bottom.equalToSuperview().offset(-inset) - } - - self.view.setNeedsLayout() - - UIView.animate(withDuration: keyboard.animationDuration) { - self.view.layoutIfNeeded() - } - } - } + self.view.setNeedsLayout() - private func setupBindings() { - viewModel.state - .map(\.status) - .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.update(status: $0) } - .store(in: &cancellables) - - viewModel.done - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) - completion($0) - }.store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [weak viewModel] in viewModel?.didInput($0) } - .store(in: &cancellables) - - screenView.saveButton.publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapSave() } - .store(in: &cancellables) + UIView.animate(withDuration: keyboard.animationDuration) { + self.view.layoutIfNeeded() + } } + } + + private func setupBindings() { + viewModel.state + .map(\.status) + .receive(on: DispatchQueue.main) + .sink { [weak screenView] in screenView?.update(status: $0) } + .store(in: &cancellables) + + viewModel.done + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dismiss(animated: true) + completion($0) + }.store(in: &cancellables) + + screenView.inputField.textPublisher + .sink { [weak viewModel] in viewModel?.didInput($0) } + .store(in: &cancellables) + + screenView.saveButton.publisher(for: .touchUpInside) + .sink { [weak viewModel] in viewModel?.didTapSave() } + .store(in: &cancellables) + } } diff --git a/Sources/ContactFeature/Coordinator/ContactCoordinator.swift b/Sources/ContactFeature/Coordinator/ContactCoordinator.swift deleted file mode 100644 index eea7f1ca94bbcace7a637abc5a1d094d53fff74b..0000000000000000000000000000000000000000 --- a/Sources/ContactFeature/Coordinator/ContactCoordinator.swift +++ /dev/null @@ -1,70 +0,0 @@ -import UIKit -import Models -import Shared -import XXModels -import ChatFeature -import Presentation - -public protocol ContactCoordinating: AnyObject { - func toPhotos(from: UIViewController) - func toRequests(from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toNickname(from: UIViewController, prefilled: String, _: @escaping StringClosure) -} - -public final class ContactCoordinator: ContactCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter(mode: .replaceBackwards(SingleChatController.self)) - - var requestsFactory: () -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var imagePickerFactory: () -> UIImagePickerController - var nicknameFactory: (String, @escaping StringClosure) -> UIViewController - - public init( - requestsFactory: @escaping () -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - imagePickerFactory: @escaping () -> UIImagePickerController, - nicknameFactory: @escaping (String, @escaping StringClosure) -> UIViewController - ) { - self.requestsFactory = requestsFactory - self.singleChatFactory = singleChatFactory - self.imagePickerFactory = imagePickerFactory - self.nicknameFactory = nicknameFactory - } -} - -public extension ContactCoordinator { - func toPhotos(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = true - modalPresenter.present(screen, from: parent) - } - - func toNickname( - from parent: UIViewController, - prefilled: String, - _ completion: @escaping StringClosure - ) { - let screen = nicknameFactory(prefilled, completion) - bottomPresenter.present(screen, from: parent) - } - - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toSingleChat(with contact: Contact, from parent: UIViewController) { - let screen = singleChatFactory(contact) - replacePresenter.present(screen, from: parent) - } -} diff --git a/Sources/ContactFeature/ViewModels/ContactViewModel.swift b/Sources/ContactFeature/ViewModels/ContactViewModel.swift index 5e6e06698cbfeb359a79ce231dde08f37c03ee6a..6e20c16e65aea219159179fcea4739f8447f858f 100644 --- a/Sources/ContactFeature/ViewModels/ContactViewModel.swift +++ b/Sources/ContactFeature/ViewModels/ContactViewModel.swift @@ -1,142 +1,211 @@ -import HUD import UIKit -import Models +import Shared import Combine +import AppCore import XXModels -import Integration +import Defaults +import XXClient +import Dependencies import CombineSchedulers -import DependencyInjection +import XXMessengerClient struct ContactViewState: Equatable { - var title: String? - var email: String? - var phone: String? - var photo: UIImage? - var username: String? - var nickname: String? + var title: String? + var email: String? + var phone: String? + var photo: UIImage? + var username: String? + var nickname: String? } final class ContactViewModel { - @Dependency private var session: SessionType - - var contact: Contact - - var popToRootPublisher: AnyPublisher<Void, Never> { popToRootRelay.eraseToAnyPublisher() } - var popPublisher: AnyPublisher<Void, Never> { popRelay.eraseToAnyPublisher() } - var hudPublisher: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - var successPublisher: AnyPublisher<Void, Never> { successRelay.eraseToAnyPublisher() } - var statePublisher: AnyPublisher<ContactViewState, Never> { stateRelay.eraseToAnyPublisher() } - - private let popRelay = PassthroughSubject<Void, Never>() - private let popToRootRelay = PassthroughSubject<Void, Never>() - private let successRelay = PassthroughSubject<Void, Never>() - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - private let stateRelay = CurrentValueSubject<ContactViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init(_ contact: Contact) { - self.contact = contact - - do { - let email = try session.extract(fact: .email, from: contact.marshaled!) - let phone = try session.extract(fact: .phone, from: contact.marshaled!) - - stateRelay.value = .init( - title: contact.nickname ?? contact.username, - email: email, - phone: phone, - photo: contact.photo != nil ? UIImage(data: contact.photo!) : nil, - username: contact.username, - nickname: contact.nickname - ) - } catch { - print(error.localizedDescription) - } + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool + + var contact: XXModels.Contact + + var popPublisher: AnyPublisher<Void, Never> { popRelay.eraseToAnyPublisher() } + var successPublisher: AnyPublisher<Void, Never> { successRelay.eraseToAnyPublisher() } + var popToRootPublisher: AnyPublisher<Void, Never> { popToRootRelay.eraseToAnyPublisher() } + var statePublisher: AnyPublisher<ContactViewState, Never> { stateRelay.eraseToAnyPublisher() } + + private let popRelay = PassthroughSubject<Void, Never>() + private let popToRootRelay = PassthroughSubject<Void, Never>() + private let successRelay = PassthroughSubject<Void, Never>() + private let stateRelay = CurrentValueSubject<ContactViewState, Never>(.init()) + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init(_ contact: XXModels.Contact) { + self.contact = contact + + stateRelay.value = .init( + title: contact.nickname ?? contact.username, + email: contact.email, + phone: contact.phone, + photo: contact.photo != nil ? UIImage(data: contact.photo!) : nil, + username: contact.username, + nickname: contact.nickname + ) + } + + func didChoosePhoto(_ photo: UIImage) { + stateRelay.value.photo = photo + contact.photo = photo.jpegData(compressionQuality: 0.0) + _ = try? dbManager.getDB().saveContact(contact) + } + + func didTapDelete() { + hudManager.show() + + do { + try messenger.e2e.get()!.deleteRequest.partnerId(contact.id) + try dbManager.getDB().deleteContact(contact) + + hudManager.hide() + popToRootRelay.send() + } catch { + hudManager.show(.init(error: error)) } - - func didChoosePhoto(_ photo: UIImage) { - stateRelay.value.photo = photo - contact.photo = photo.jpegData(compressionQuality: 0.0) - _ = try? session.dbManager.saveContact(contact) - } - - func didTapDelete() { - hudRelay.send(.on) - - do { - try session.deleteContact(contact) - hudRelay.send(.none) - popToRootRelay.send() - } catch { - hudRelay.send(.error(.init(with: error))) + } + + func didTapReject() { + // TODO: Reject function on the API? + _ = try? dbManager.getDB().deleteContact(contact) + popRelay.send() + } + + func didTapClear() { + _ = try? dbManager.getDB().deleteMessages(.init(chat: .direct(myId, contact.id))) + } + + func didUpdateNickname(_ string: String) { + contact.nickname = string.isEmpty ? nil : string + stateRelay.value.title = string.isEmpty ? contact.username : string + _ = try? dbManager.getDB().saveContact(contact) + + stateRelay.value.nickname = contact.nickname + } + + func didTapResend() { + hudManager.show() + contact.authStatus = .requesting + + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + try self.dbManager.getDB().saveContact(self.contact) + + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() + + if let fact = myFacts.get(.username) { + includedFacts.append(fact) } - } - - func didTapReject() { - try? session.deleteContact(contact) - popRelay.send() - } - - func didTapClear() { - _ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id))) - } - - func didUpdateNickname(_ string: String) { - contact.nickname = string.isEmpty ? nil : string - stateRelay.value.title = string.isEmpty ? contact.username : string - _ = try? session.dbManager.saveContact(contact) - - stateRelay.value.nickname = contact.nickname - } - - func didTapResend() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.add(self.contact) - self.hudRelay.send(.none) - self.popRelay.send() - } catch { - self.hudRelay.send(.error(.init(with: error))) - } + + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) } - } - - func didTapRequest(with nickname: String) { - hudRelay.send(.on) - contact.nickname = nickname - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.add(self.contact) - self.hudRelay.send(.none) - self.successRelay.send() - } catch { - self.hudRelay.send(.error(.init(with: error))) - } + + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) } + + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(self.contact.marshaled!), + myFacts: includedFacts + ) + + self.contact.authStatus = .requested + try self.dbManager.getDB().saveContact(self.contact) + + self.hudManager.hide() + self.popRelay.send() + } catch { + self.contact.authStatus = .requestFailed + _ = try? self.dbManager.getDB().saveContact(self.contact) + self.hudManager.show(.init(error: error)) + } } - - func didTapAccept(_ nickname: String) { - hudRelay.send(.on) - contact.nickname = nickname - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.confirm(self.contact) - self.hudRelay.send(.none) - self.popRelay.send() - } catch { - self.hudRelay.send(.error(.init(with: error))) - } + } + + func didTapRequest(with nickname: String) { + hudManager.show() + contact.nickname = nickname + contact.authStatus = .requesting + + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + try self.dbManager.getDB().saveContact(self.contact) + + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() + + if let fact = myFacts.get(.username) { + includedFacts.append(fact) + } + + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) } + + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) + } + + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(self.contact.marshaled!), + myFacts: includedFacts + ) + + self.contact.authStatus = .requested + try self.dbManager.getDB().saveContact(self.contact) + + self.hudManager.hide() + self.successRelay.send() + } catch { + self.contact.authStatus = .requestFailed + _ = try? self.dbManager.getDB().saveContact(self.contact) + self.hudManager.show(.init(error: error)) + } + } + } + + func didTapAccept(_ nickname: String) { + hudManager.show() + contact.nickname = nickname + contact.authStatus = .confirming + + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + try self.dbManager.getDB().saveContact(self.contact) + + let _ = try self.messenger.e2e.get()!.confirmReceivedRequest(partner: XXClient.Contact.live(self.contact.marshaled!)) + + self.contact.authStatus = .friend + try self.dbManager.getDB().saveContact(self.contact) + + self.hudManager.hide() + self.popRelay.send() + } catch { + self.contact.authStatus = .confirmationFailed + _ = try? self.dbManager.getDB().saveContact(self.contact) + self.hudManager.show(.init(error: error)) + } } + } } diff --git a/Sources/ContactFeature/ViewModels/NicknameViewModel.swift b/Sources/ContactFeature/ViewModels/NicknameViewModel.swift index 25897d83c215e59b6cfd6233bcb28dc503feb616..6f4cd52b331188a2d10fac1ddff5e0ebaf3d5f80 100644 --- a/Sources/ContactFeature/ViewModels/NicknameViewModel.swift +++ b/Sources/ContactFeature/ViewModels/NicknameViewModel.swift @@ -1,6 +1,7 @@ import Shared import Combine import InputField +import AppResources struct NicknameViewState { var nickname: String = "" diff --git a/Sources/ContactFeature/Views/ContactConfirmedView.swift b/Sources/ContactFeature/Views/ContactConfirmedView.swift index 472513842beaf5914e6924bf56f56d1b5f2f5b34..7a99fb9f0be8995158fcf17c0f230eb424a32c2d 100644 --- a/Sources/ContactFeature/Views/ContactConfirmedView.swift +++ b/Sources/ContactFeature/Views/ContactConfirmedView.swift @@ -1,38 +1,39 @@ import UIKit import Shared +import AppResources final class ContactConfirmedView: UIView { - let stackView = UIStackView() - let clearButton = CapsuleButton() - let buttons = SheetCardComponent() - - init() { - super.init(frame: .zero) - - clearButton.setStyle(.seeThrough) - clearButton.setTitle(Localized.Contact.Confirmed.clear, for: .normal) - - buttons.set(buttons: [clearButton]) - - stackView.axis = .vertical - stackView.spacing = 25 - - addSubview(stackView) - addSubview(buttons) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } - - buttons.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(24) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } + let stackView = UIStackView() + let clearButton = CapsuleButton() + let buttons = SheetCardComponent() + + init() { + super.init(frame: .zero) + + clearButton.setStyle(.seeThrough) + clearButton.setTitle(Localized.Contact.Confirmed.clear, for: .normal) + + buttons.set(buttons: [clearButton]) + + stackView.axis = .vertical + stackView.spacing = 25 + + addSubview(stackView) + addSubview(buttons) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + } + + buttons.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(24) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/ContactFeature/Views/ContactInProgressView.swift b/Sources/ContactFeature/Views/ContactInProgressView.swift index 165600f9deb908c47de78f7e4b71e9213d3d847e..2866b6d5d586b1fbc94dfe6479e41d530448980b 100644 --- a/Sources/ContactFeature/Views/ContactInProgressView.swift +++ b/Sources/ContactFeature/Views/ContactInProgressView.swift @@ -1,70 +1,54 @@ import UIKit import Shared -import Models import XXModels +import AppResources final class ContactAlmostView: UIView { - // MARK: UI + let stack = UIStackView() + let feedback = BottomFeedbackComponent() - let stack = UIStackView() - let feedback = BottomFeedbackComponent() + init() { + super.init(frame: .zero) + stack.axis = .vertical + stack.spacing = 25 - // MARK: Lifecycle + addSubview(stack) + addSubview(feedback) - init() { - super.init(frame: .zero) - setup() + stack.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) } - required init?(coder: NSCoder) { nil } - - // MARK: Public - - func set(status: Contact.AuthStatus) { - switch status { - case .requestFailed, .confirmationFailed: - feedback.set( - icon: Asset.contactRequestExclamation.image, - title: Localized.Contact.Inprogress.failed, - style: .danger, - actionTitle: Localized.Contact.Inprogress.resend - ) - - case .confirming, .requested, .requesting: - feedback.set( - icon: Asset.contactRequestExclamation.image, - title: Localized.Contact.Inprogress.pending, - style: .chill - ) - default: - break - } - } - - // MARK: Properties - - private func setup() { - stack.axis = .vertical - stack.spacing = 25 - - addSubview(stack) - addSubview(feedback) - - setupConstraints() + feedback.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(stack.snp.bottom).offset(24) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } - - private func setupConstraints() { - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } - - feedback.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(stack.snp.bottom).offset(24) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } + } + + required init?(coder: NSCoder) { nil } + + func set(status: Contact.AuthStatus) { + switch status { + case .requestFailed, .confirmationFailed: + feedback.set( + icon: Asset.contactRequestExclamation.image, + title: Localized.Contact.Inprogress.failed, + style: .danger, + actionTitle: Localized.Contact.Inprogress.resend + ) + + case .confirming, .requested, .requesting: + feedback.set( + icon: Asset.contactRequestExclamation.image, + title: Localized.Contact.Inprogress.pending, + style: .chill + ) + default: + break } + } } diff --git a/Sources/ContactFeature/Views/ContactReceivedView.swift b/Sources/ContactFeature/Views/ContactReceivedView.swift index 2d29fd46123aab7b37e615fcc31c22cbbd486c06..4678fdb7eb4a2147e18db552fcf2f1705cd9db24 100644 --- a/Sources/ContactFeature/Views/ContactReceivedView.swift +++ b/Sources/ContactFeature/Views/ContactReceivedView.swift @@ -1,67 +1,62 @@ import UIKit import Shared +import AppResources final class ContactReceivedView: UIView { - // MARK: UI + let title = UILabel() + let icon = UIImageView() + let stack = UIStackView() + let accept = CapsuleButton() + let reject = CapsuleButton() - let title = UILabel() - let icon = UIImageView() - let stack = UIStackView() - let accept = CapsuleButton() - let reject = CapsuleButton() + init() { + super.init(frame: .zero) + setup() + } - // MARK: Lifecycle + required init?(coder: NSCoder) { nil } - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } + private func setup() { + icon.contentMode = .center - // MARK: Private + title.textAlignment = .center + title.textColor = Asset.neutralBody.color + title.text = Localized.Contact.Received.title + title.font = Fonts.Mulish.bold.font(size: 24.0) - private func setup() { - icon.contentMode = .center + icon.image = Asset.contactRequestPlaceholder.image - title.textAlignment = .center - title.textColor = Asset.neutralBody.color - title.text = Localized.Contact.Received.title - title.font = Fonts.Mulish.bold.font(size: 24.0) + accept.setStyle(.brandColored) + accept.setTitle(Localized.Contact.Received.accept, for: .normal) - icon.image = Asset.contactRequestPlaceholder.image + reject.setStyle(.seeThrough) + reject.setTitle(Localized.Contact.Received.reject, for: .normal) - accept.setStyle(.brandColored) - accept.setTitle(Localized.Contact.Received.accept, for: .normal) + stack.axis = .vertical + stack.addArrangedSubview(title) + stack.addArrangedSubview(accept) + stack.addArrangedSubview(reject) - reject.setStyle(.seeThrough) - reject.setTitle(Localized.Contact.Received.reject, for: .normal) + stack.setCustomSpacing(24, after: title) + stack.setCustomSpacing(20, after: accept) - stack.axis = .vertical - stack.addArrangedSubview(title) - stack.addArrangedSubview(accept) - stack.addArrangedSubview(reject) + addSubview(icon) + addSubview(stack) - stack.setCustomSpacing(24, after: title) - stack.setCustomSpacing(20, after: accept) + setupConstraints() + } - addSubview(icon) - addSubview(stack) - - setupConstraints() + private func setupConstraints() { + icon.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.bottom.equalTo(stack.snp.top).offset(-30) } - private func setupConstraints() { - icon.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalTo(stack.snp.top).offset(-30) - } - - stack.snp.makeConstraints { make in - make.top.greaterThanOrEqualToSuperview().offset(20) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-34) - } + stack.snp.makeConstraints { make in + make.top.greaterThanOrEqualToSuperview().offset(20) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.bottom.equalToSuperview().offset(-34) } + } } diff --git a/Sources/ContactFeature/Views/ContactScannedView.swift b/Sources/ContactFeature/Views/ContactScannedView.swift index 840e445cacfbc228705a5616c1b198c86bd534fe..8dd6a118c79bf2e1b736d4214d45a90d2450c5b3 100644 --- a/Sources/ContactFeature/Views/ContactScannedView.swift +++ b/Sources/ContactFeature/Views/ContactScannedView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class ContactScannedView: UIView { let title = UILabel() diff --git a/Sources/ContactFeature/Views/ContactSuccessView.swift b/Sources/ContactFeature/Views/ContactSuccessView.swift index 616d1b0621274dc6d7b04fb710cef4d4b28140d6..dc610b3ca85802443e376c37ace40c68900f024f 100644 --- a/Sources/ContactFeature/Views/ContactSuccessView.swift +++ b/Sources/ContactFeature/Views/ContactSuccessView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class ContactSuccessView: UIView { // MARK: UI diff --git a/Sources/ContactFeature/Views/ContactView.swift b/Sources/ContactFeature/Views/ContactView.swift index e5fb03927caf3de9018a382a004b7c89c8a69e99..059686511eaee49e47ccb3fb9b77156fc5a2427b 100644 --- a/Sources/ContactFeature/Views/ContactView.swift +++ b/Sources/ContactFeature/Views/ContactView.swift @@ -1,7 +1,7 @@ import UIKit import Shared -import Models import XXModels +import AppResources final class ContactView: UIView { let container = UIView() diff --git a/Sources/ContactFeature/Views/NicknameView.swift b/Sources/ContactFeature/Views/NicknameView.swift index 731bdd70a67f940d16e570b7f25b78cd8f1e8e1e..637f5a1fa17a451cc2cc5514a6f2acf268febe64 100644 --- a/Sources/ContactFeature/Views/NicknameView.swift +++ b/Sources/ContactFeature/Views/NicknameView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class NicknameView: UIView { let titleLabel = UILabel() diff --git a/Sources/ContactListFeature/ContactListController.swift b/Sources/ContactListFeature/ContactListController.swift new file mode 100644 index 0000000000000000000000000000000000000000..36d8e8b4c65c2801480e0aca20ae60528e706b77 --- /dev/null +++ b/Sources/ContactListFeature/ContactListController.swift @@ -0,0 +1,150 @@ +import UIKit +import Shared +import Combine +import AppCore +import Dependencies +import AppResources +import AppNavigation + +public final class ContactListController: UIViewController { + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = ContactListView() + private lazy var tableController = ContactListTableController(viewModel) + + private let viewModel = ContactListViewModel() + private var cancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupTableView() + setupBindings() + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = " " + + let titleLabel = UILabel() + titleLabel.text = Localized.ContactList.title + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) + + let menuButton = UIButton() + menuButton.tintColor = Asset.neutralDark.color + menuButton.setImage(Asset.chatListMenu.image, for: .normal) + menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) + menuButton.snp.makeConstraints { $0.width.equalTo(50) } + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) + ) + + let search = UIButton() + search.tintColor = Asset.neutralActive.color + search.setImage(Asset.contactListSearch.image, for: .normal) + search.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside) + search.accessibilityIdentifier = Localized.Accessibility.ContactList.search + + let scanButton = UIButton() + scanButton.setImage(Asset.sharedScan.image, for: .normal) + scanButton.addTarget(self, action: #selector(didTapScan), for: .touchUpInside) + + let rightStack = UIStackView() + rightStack.spacing = 15 + rightStack.addArrangedSubview(scanButton) + rightStack.addArrangedSubview(search) + + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightStack) + + search.snp.makeConstraints { + $0.width.equalTo(40) + } + } + + private func setupTableView() { + addChild(tableController) + screenView.addSubview(tableController.view) + tableController.view.snp.makeConstraints { + $0.top.equalTo(screenView.topStackView.snp.bottom) + $0.left.bottom.right.equalToSuperview() + } + tableController.didMove(toParent: self) + } + + private func setupBindings() { + tableController + .didTap + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentChat( + contact: $0, + on: navigationController! + )) + }.store(in: &cancellables) + + screenView + .requestsButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentRequests(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .newGroupButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentGroupDraft(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .searchButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentSearch(on: navigationController!)) + }.store(in: &cancellables) + + viewModel + .requestCount + .receive(on: DispatchQueue.main) + .sink { [weak screenView] in + screenView?.requestsButton.updateNotification($0) + }.store(in: &cancellables) + + viewModel + .contacts + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.stackView.isHidden = !$0.isEmpty + if $0.isEmpty { + screenView.bringSubviewToFront(screenView.stackView) + } + }.store(in: &cancellables) + } + + @objc private func didTapSearch() { + navigator.perform(PresentSearch(on: navigationController!)) + } + + @objc private func didTapScan() { + navigator.perform(PresentScan(on: navigationController!)) + } + + @objc private func didTapMenu() { + navigator.perform(PresentMenu(currentItem: .contacts, from: self)) + } +} diff --git a/Sources/ContactListFeature/ContactListItemButton.swift b/Sources/ContactListFeature/ContactListItemButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..009260c7c3a11e54ca25670b56cd6aa3960c9643 --- /dev/null +++ b/Sources/ContactListFeature/ContactListItemButton.swift @@ -0,0 +1,61 @@ +import UIKit +import Shared +import AppResources + +final class ItemButton: UIControl { + let titleLabel = UILabel() + let iconImageView = UIImageView() + let separatorView = UIView() + let stackView = UIStackView() + let notificationLabel = UILabel() + + init() { + super.init(frame: .zero) + + titleLabel.textColor = Asset.brandPrimary.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + separatorView.backgroundColor = Asset.neutralLine.color + + notificationLabel.isHidden = true + notificationLabel.layer.cornerRadius = 5 + notificationLabel.layer.masksToBounds = true + notificationLabel.textColor = Asset.neutralWhite.color + notificationLabel.backgroundColor = Asset.brandPrimary.color + notificationLabel.font = Fonts.Mulish.bold.font(size: 12.0) + + stackView.spacing = 16 + stackView.addArrangedSubview(iconImageView) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(notificationLabel) + stackView.setCustomSpacing(6, after: titleLabel) + + stackView.isUserInteractionEnabled = false + addSubview(stackView) + addSubview(separatorView) + + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(12) + make.left.equalToSuperview().offset(24) + make.bottom.equalTo(separatorView.snp.top).offset(-12) + } + + separatorView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.bottom.equalToSuperview() + make.height.equalTo(1) + } + } + + required init?(coder: NSCoder) { nil } + + func setup(title: String, image: UIImage) { + titleLabel.text = title + iconImageView.image = image + } + + func updateNotification(_ count: Int) { + notificationLabel.isHidden = count < 1 + notificationLabel.text = " \(count) " + } +} diff --git a/Sources/ContactListFeature/ContactListTableController.swift b/Sources/ContactListFeature/ContactListTableController.swift new file mode 100644 index 0000000000000000000000000000000000000000..8215991d49982bf7f3780636947fcd7adf575adb --- /dev/null +++ b/Sources/ContactListFeature/ContactListTableController.swift @@ -0,0 +1,83 @@ +import UIKit +import Shared +import Combine +import XXModels +import AppResources + +final class ContactListTableController: UITableViewController { + private var collation = UILocalizedIndexedCollation.current() + private var sections: [[Contact]] = [] { + didSet { self.tableView.reloadData() } + } + + private let viewModel: ContactListViewModel + private var cancellables = Set<AnyCancellable>() + private let tapRelay = PassthroughSubject<Contact, Never>() + + var didTap: AnyPublisher<Contact, Never> { tapRelay.eraseToAnyPublisher() } + + override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + } + + init(_ viewModel: ContactListViewModel) { + self.viewModel = viewModel + super.init(style: .grouped) + } + + required init?(coder: NSCoder) { nil } + + private func setupTableView() { + tableView.separatorStyle = .none + tableView.register(AvatarCell.self) + tableView.backgroundColor = Asset.neutralWhite.color + tableView.sectionIndexColor = Asset.neutralDark.color + tableView.contentInset = UIEdgeInsets(top: -20, left: 0, bottom: 0, right: 0) + + viewModel.contacts + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + let results = IndexedListCollator().sectioned(items: $0) + self.collation = results.collation + self.sections = results.sections + }.store(in: &cancellables) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: AvatarCell = tableView.dequeueReusableCell(forIndexPath: indexPath) + let contact = sections[indexPath.section][indexPath.row] + let name = (contact.nickname ?? contact.username) ?? "Fetching username..." + + cell.setup(title: name, image: contact.photo) + return cell + } + + override func numberOfSections(in: UITableView) -> Int { + sections.count + } + + override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + sections[section].count + } + + override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + tapRelay.send(sections[indexPath.section][indexPath.row]) + } + + override func sectionIndexTitles(for: UITableView) -> [String]? { + collation.sectionIndexTitles + } + + override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { + collation.sectionTitles[section] + } + + override func tableView(_: UITableView, sectionForSectionIndexTitle: String, at index: Int) -> Int { + collation.section(forSectionIndexTitle: index) + } + + override func tableView(_: UITableView, heightForRowAt: IndexPath) -> CGFloat { + 64 + } +} diff --git a/Sources/ContactListFeature/ContactListView.swift b/Sources/ContactListFeature/ContactListView.swift new file mode 100644 index 0000000000000000000000000000000000000000..75ce0cb079969051c4fef0598a417dfcf7279ec6 --- /dev/null +++ b/Sources/ContactListFeature/ContactListView.swift @@ -0,0 +1,73 @@ +import UIKit +import Shared +import AppResources + +final class ContactListView: UIView { + let newGroupButton = ItemButton() + let requestsButton = ItemButton() + let topStackView = UIStackView() + let stackView = UIStackView() + let emptyTitleLabel = UILabel() + let searchButton = CapsuleButton() + + init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + private func setup() { + backgroundColor = Asset.neutralWhite.color + + requestsButton.separatorView.isHidden = true + requestsButton.setup(title: "Requests", image: Asset.contactListRequests.image) + newGroupButton.setup(title: Localized.ContactList.newGroup, image: Asset.contactListNewGroup.image) + + let paragraph = NSMutableParagraphStyle() + paragraph.lineHeightMultiple = 1.2 + paragraph.alignment = .center + + emptyTitleLabel.attributedText = NSAttributedString( + string: Localized.ContactList.Empty.title, + attributes: [ + .paragraphStyle: paragraph, + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.bold.font(size: 24.0) as UIFont + ] + ) + emptyTitleLabel.numberOfLines = 0 + + searchButton.setStyle(.brandColored) + searchButton.setTitle(Localized.ContactList.Empty.action, for: .normal) + + stackView.spacing = 24 + stackView.axis = .vertical + stackView.alignment = .center + stackView.addArrangedSubview(emptyTitleLabel) + stackView.addArrangedSubview(searchButton) + + topStackView.axis = .vertical + topStackView.addArrangedSubview(newGroupButton) + topStackView.addArrangedSubview(requestsButton) + + addSubview(topStackView) + addSubview(stackView) + + setupConstraints() + } + + private func setupConstraints() { + topStackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.left.equalToSuperview() + make.right.equalToSuperview() + } + + stackView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + } + } +} diff --git a/Sources/ContactListFeature/ContactListViewModel.swift b/Sources/ContactListFeature/ContactListViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..3e509accc5cfd7c71e76ec7e6dc814da5a63af9a --- /dev/null +++ b/Sources/ContactListFeature/ContactListViewModel.swift @@ -0,0 +1,63 @@ +import Combine +import XXModels +import Defaults +import ReportingFeature +import XXMessengerClient + +import Foundation +import XXClient + +import AppCore +import Dependencies + +final class ContactListViewModel { + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.reportingStatus) var reportingStatus: ReportingStatus + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var contacts: AnyPublisher<[XXModels.Contact], Never> { + let query = Contact.Query( + authStatus: [.friend], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false: nil + ) + + return try! dbManager.getDB().fetchContactsPublisher(query) + .replaceError(with: []) + .map { $0.filter { $0.id != self.myId }} + .eraseToAnyPublisher() + } + + var requestCount: AnyPublisher<Int, Never> { + let groupQuery = Group.Query( + authStatus: [.pending], + isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, + isLeaderBanned: reportingStatus.isEnabled() ? false : nil + ) + + let contactsQuery = Contact.Query( + authStatus: [ + .verified, + .confirming, + .confirmationFailed, + .verificationFailed, + .verificationInProgress + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return Publishers.CombineLatest( + try! dbManager.getDB().fetchContactsPublisher(contactsQuery) + .replaceError(with: []), + try! dbManager.getDB().fetchGroupsPublisher(groupQuery) + .replaceError(with: []) + ) + .map { $0.0.count + $0.1.count } + .eraseToAnyPublisher() + } +} diff --git a/Sources/ContactListFeature/Controllers/ContactListController.swift b/Sources/ContactListFeature/Controllers/ContactListController.swift deleted file mode 100644 index 1e09b9377139e9428ddb539c6ace95164f26bb1c..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Controllers/ContactListController.swift +++ /dev/null @@ -1,135 +0,0 @@ -import UIKit -import Theme -import Shared -import Combine -import DependencyInjection - -public final class ContactListController: UIViewController { - @Dependency private var coordinator: ContactListCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = ContactListView() - lazy private var tableController = ContactListTableController(viewModel) - - private let viewModel = ContactListViewModel() - private var cancellables = Set<AnyCancellable>() - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupTableView() - setupBindings() - } - - private func setupNavigationBar() { - navigationItem.backButtonTitle = " " - - let titleLabel = UILabel() - titleLabel.text = Localized.ContactList.title - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) - - let menuButton = UIButton() - menuButton.tintColor = Asset.neutralDark.color - menuButton.setImage(Asset.chatListMenu.image, for: .normal) - menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) - menuButton.snp.makeConstraints { $0.width.equalTo(50) } - - navigationItem.leftBarButtonItem = UIBarButtonItem( - customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) - ) - - let search = UIButton() - search.tintColor = Asset.neutralActive.color - search.setImage(Asset.contactListSearch.image, for: .normal) - search.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside) - search.accessibilityIdentifier = Localized.Accessibility.ContactList.search - - let scanButton = UIButton() - scanButton.setImage(Asset.sharedScan.image, for: .normal) - scanButton.addTarget(self, action: #selector(didTapScan), for: .touchUpInside) - - let rightStack = UIStackView() - rightStack.spacing = 15 - rightStack.addArrangedSubview(scanButton) - rightStack.addArrangedSubview(search) - - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightStack) - - search.snp.makeConstraints { $0.width.equalTo(40) } - } - - private func setupTableView() { - addChild(tableController) - screenView.addSubview(tableController.view) - - tableController.view.snp.makeConstraints { make in - make.top.equalTo(screenView.topStackView.snp.bottom) - make.left.bottom.right.equalToSuperview() - } - - tableController.didMove(toParent: self) - } - - private func setupBindings() { - tableController.didTap - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSingleChat(with: $0, from: self) } - .store(in: &cancellables) - - screenView.requestsButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toRequests(from: self) } - .store(in: &cancellables) - - screenView.newGroupButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toNewGroup(from: self) } - .store(in: &cancellables) - - screenView.searchButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSearch(from: self) } - .store(in: &cancellables) - - viewModel.requestCount - .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.requestsButton.updateNotification($0) } - .store(in: &cancellables) - - viewModel.contacts - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.stackView.isHidden = !$0.isEmpty - - if $0.isEmpty { - screenView.bringSubviewToFront(screenView.stackView) - } - }.store(in: &cancellables) - } - - @objc private func didTapSearch() { - coordinator.toSearch(from: self) - } - - @objc private func didTapScan() { - coordinator.toScan(from: self) - } - - @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) - } -} diff --git a/Sources/ContactListFeature/Controllers/ContactListTableController.swift b/Sources/ContactListFeature/Controllers/ContactListTableController.swift deleted file mode 100644 index ac7a06628045f199393f1839e2a6f700a7cb194e..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Controllers/ContactListTableController.swift +++ /dev/null @@ -1,83 +0,0 @@ -import UIKit -import Shared -import Models -import Combine -import XXModels - -final class ContactListTableController: UITableViewController { - private var collation = UILocalizedIndexedCollation.current() - private var sections: [[Contact]] = [] { - didSet { self.tableView.reloadData() } - } - - private let viewModel: ContactListViewModel - private var cancellables = Set<AnyCancellable>() - private let tapRelay = PassthroughSubject<Contact, Never>() - - var didTap: AnyPublisher<Contact, Never> { tapRelay.eraseToAnyPublisher() } - - override func viewDidLoad() { - super.viewDidLoad() - setupTableView() - } - - init(_ viewModel: ContactListViewModel) { - self.viewModel = viewModel - super.init(style: .grouped) - } - - required init?(coder: NSCoder) { nil } - - private func setupTableView() { - tableView.separatorStyle = .none - tableView.register(AvatarCell.self) - tableView.backgroundColor = Asset.neutralWhite.color - tableView.sectionIndexColor = Asset.neutralDark.color - tableView.contentInset = UIEdgeInsets(top: -20, left: 0, bottom: 0, right: 0) - - viewModel.contacts - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - let results = IndexedListCollator().sectioned(items: $0) - self.collation = results.collation - self.sections = results.sections - }.store(in: &cancellables) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: AvatarCell = tableView.dequeueReusableCell(forIndexPath: indexPath) - let contact = sections[indexPath.section][indexPath.row] - let name = (contact.nickname ?? contact.username) ?? "Fetching username..." - - cell.setup(title: name, image: contact.photo) - return cell - } - - override func numberOfSections(in: UITableView) -> Int { - sections.count - } - - override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { - sections[section].count - } - - override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { - tapRelay.send(sections[indexPath.section][indexPath.row]) - } - - override func sectionIndexTitles(for: UITableView) -> [String]? { - collation.sectionIndexTitles - } - - override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { - collation.sectionTitles[section] - } - - override func tableView(_: UITableView, sectionForSectionIndexTitle: String, at index: Int) -> Int { - collation.section(forSectionIndexTitle: index) - } - - override func tableView(_: UITableView, heightForRowAt: IndexPath) -> CGFloat { - 64 - } -} diff --git a/Sources/ContactListFeature/Controllers/CreateDrawerController.swift b/Sources/ContactListFeature/Controllers/CreateDrawerController.swift deleted file mode 100644 index 543a9722869b0bf6cbc1b66579f235cfa88daf26..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Controllers/CreateDrawerController.swift +++ /dev/null @@ -1,89 +0,0 @@ -import UIKit -import Shared -import Combine - -public final class CreateDrawerController: UIViewController { - lazy private var screenView = CreateDrawerView() - - private let selectedCount: Int - private let viewModel = CreateDrawerViewModel() - private let completion: (String, String?) -> Void - private var cancellables = Set<AnyCancellable>() - - public init(_ count: Int, _ completion: @escaping (String, String?) -> Void) { - self.selectedCount = count - self.completion = completion - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - let view = UIView() - view.addSubview(screenView) - - screenView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview().offset(0) - } - - self.view = view - } - - public override func viewDidLoad() { - super.viewDidLoad() - screenView.set(count: selectedCount) { - // TODO: âš ï¸ - } - - setupBindings() - } - - private func setupBindings() { - viewModel.statePublisher - .map(\.status) - .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.update(status: $0) } - .store(in: &cancellables) - - viewModel.donePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) - completion($0.0, $0.1) - }.store(in: &cancellables) - - screenView.cancelButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in dismiss(animated: true) } - .store(in: &cancellables) - - screenView.inputField - .textPublisher - .sink { [weak viewModel] in viewModel?.didInput($0) } - .store(in: &cancellables) - - screenView.otherInputField - .textPublisher - .sink { [weak viewModel] in viewModel?.didOtherInput($0) } - .store(in: &cancellables) - - screenView.inputField - .returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) - - screenView.otherInputField - .returnPublisher - .sink { [unowned self] in screenView.otherInputField.endEditing(true) } - .store(in: &cancellables) - - screenView.createButton - .publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapCreate() } - .store(in: &cancellables) - } -} diff --git a/Sources/ContactListFeature/Controllers/CreateGroupController.swift b/Sources/ContactListFeature/Controllers/CreateGroupController.swift deleted file mode 100644 index e55860fc0a48220413dc33373c915e754351ebe5..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Controllers/CreateGroupController.swift +++ /dev/null @@ -1,186 +0,0 @@ -import HUD -import UIKit -import Models -import Shared -import Combine -import XXModels -import DependencyInjection - -public final class CreateGroupController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: ContactListCoordinating - - lazy private var titleLabel = UILabel() - lazy private var createButton = UIButton() - lazy private var screenView = CreateGroupView() - - private var selectedElements = [Contact]() { - didSet { screenView.tableView.reloadData() } - } - private let viewModel = CreateGroupViewModel() - private var cancellables = Set<AnyCancellable>() - private var tableDataSource: UITableViewDiffableDataSource<SectionId, Contact>! - private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>! - - private var count = 0 { - didSet { - createButton.isEnabled = count >= 2 && count <= 10 - - let text = Localized.CreateGroup.title("\(count)") - let attString = NSMutableAttributedString(string: text) - attString.addAttribute(.font, value: Fonts.Mulish.semiBold.font(size: 18.0) as Any) - attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) - attString.addAttribute(name: .foregroundColor, value: Asset.neutralDisabled.color, betweenCharacters: "#") - - titleLabel.attributedText = attString - } - } - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupTableAndCollection() - setupBindings() - - count = 0 - } - - private func setupNavigationBar() { - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: titleLabel) - navigationItem.leftItemsSupplementBackButton = true - - createButton.setTitle(Localized.CreateGroup.create, for: .normal) - createButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - createButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 16.0) - createButton.setTitleColor(Asset.neutralDisabled.color, for: .disabled) - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: createButton) - } - - private func setupTableAndCollection() { - screenView.tableView.rowHeight = 64.0 - screenView.tableView.register(AvatarCell.self) - screenView.collectionView.register(CreateGroupCollectionCell.self) - - collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( - collectionView: screenView.collectionView - ) { [weak viewModel] collectionView, indexPath, contact in - let cell: CreateGroupCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - - let title = (contact.nickname ?? contact.username) ?? "" - cell.setup(title: title, image: contact.photo) - cell.didTapRemove = { viewModel?.didSelect(contact: contact) } - - return cell - } - - tableDataSource = DiffEditableDataSource<SectionId, Contact>( - tableView: screenView.tableView - ) { [weak self] tableView, indexPath, contact in - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) - let title = (contact.nickname ?? contact.username) ?? "" - - cell.setup(title: title, image: contact.photo) - - if let selectedElements = self?.selectedElements, selectedElements.contains(contact) { - tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) - } else { - tableView.deselectRow(at: indexPath, animated: true) - } - - return cell - } - - screenView.tableView.delegate = self - screenView.tableView.dataSource = tableDataSource - screenView.collectionView.dataSource = collectionDataSource - } - - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - let selected = viewModel.selected.share() - - selected - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.collectionView.isHidden = $0.count < 1 - - count = $0.count - selectedElements = $0 - }.store(in: &cancellables) - - selected.map { selectedContacts in - var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() - let sections = [SectionId()] - snapshot.appendSections(sections) - sections.forEach { section in snapshot.appendItems(selectedContacts, toSection: section) } - return snapshot - } - .receive(on: DispatchQueue.main) - .sink { [unowned self] in collectionDataSource.apply($0) } - .store(in: &cancellables) - - viewModel.contacts - .map { contacts in - var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() - let sections = [SectionId()] - snapshot.appendSections(sections) - sections.forEach { section in snapshot.appendItems(contacts, toSection: section) } - return snapshot - } - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - tableDataSource.apply($0, animatingDifferences: tableDataSource.snapshot().numberOfItems > 0) - }.store(in: &cancellables) - - screenView.searchComponent.textPublisher - .removeDuplicates() - .sink { [unowned self] in viewModel.filter($0) } - .store(in: &cancellables) - - viewModel.info - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toGroupChat(with: $0, from: self) } - .store(in: &cancellables) - - createButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toGroupDrawer( - with: count + 1, - from: self, { (name, welcome) in - viewModel.create(name: name, welcome: welcome, members: selectedElements) - } - ) - }.store(in: &cancellables) - } -} - -extension CreateGroupController: UITableViewDelegate { - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let contact = tableDataSource.itemIdentifier(for: indexPath) { - viewModel.didSelect(contact: contact) - } - } - - public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { - if let contact = tableDataSource.itemIdentifier(for: indexPath) { - viewModel.didSelect(contact: contact) - } - } -} diff --git a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift b/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift deleted file mode 100644 index eb967cf04231cb85c203714bb66dd9546bac3209..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift +++ /dev/null @@ -1,129 +0,0 @@ -import UIKit -import Shared -import Models -import XXModels -import MenuFeature -import ChatFeature -import Presentation -import ContactFeature -import ScrollViewController - -public protocol ContactListCoordinating { - func toScan(from: UIViewController) - func toSearch(from: UIViewController) - func toRequests(from: UIViewController) - func toNewGroup(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toGroupChat(with: GroupInfo, from: UIViewController) - func toGroupDrawer(with: Int, from: UIViewController, _: @escaping (String, String?) -> Void) -} - -public struct ContactListCoordinator: ContactListCoordinating { - var pushPresenter: Presenting = PushPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - var replacePresenter: Presenting = ReplacePresenter(mode: .replaceLast) - - var scanFactory: () -> UIViewController - var searchFactory: (String?) -> UIViewController - var newGroupFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupInfo) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var groupDrawerFactory: (Int, @escaping (String, String?) -> Void) -> UIViewController - - public init( - scanFactory: @escaping () -> UIViewController, - searchFactory: @escaping (String?) -> UIViewController, - newGroupFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupInfo) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, - groupDrawerFactory: @escaping (Int, @escaping (String, String?) -> Void) -> UIViewController - ) { - self.scanFactory = scanFactory - self.searchFactory = searchFactory - self.contactFactory = contactFactory - self.newGroupFactory = newGroupFactory - self.requestsFactory = requestsFactory - self.sideMenuFactory = sideMenuFactory - self.groupChatFactory = groupChatFactory - self.singleChatFactory = singleChatFactory - self.groupDrawerFactory = groupDrawerFactory - } -} - -public extension ContactListCoordinator { - func toGroupDrawer( - with count: Int, - from parent: UIViewController, - _ completion: @escaping (String, String?) -> Void - ) { - let screen = ScrollViewController.embedding(groupDrawerFactory(count, completion)) - fullscreenPresenter.present(screen, from: parent) - } - - func toSingleChat( - with contact: Contact, - from parent: UIViewController - ) { - let screen = singleChatFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toScan(from parent: UIViewController) { - let screen = scanFactory() - pushPresenter.present(screen, from: parent) - } - - func toSearch(from parent: UIViewController) { - let screen = searchFactory(nil) - pushPresenter.present(screen, from: parent) - } - - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - pushPresenter.present(screen, from: parent) - } - - func toNewGroup(from parent: UIViewController) { - let screen = newGroupFactory() - pushPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toGroupChat(with info: GroupInfo, from parent: UIViewController) { - let screen = groupChatFactory(info) - replacePresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.contacts, parent) - sidePresenter.present(screen, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift deleted file mode 100644 index 830172f3b9bd298be2c8c8200e2c96f89d264325..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Models -import Combine -import XXModels -import Defaults -import Integration -import ReportingFeature -import DependencyInjection - -final class ContactListViewModel { - @Dependency private var session: SessionType - @Dependency private var reportingStatus: ReportingStatus - - var contacts: AnyPublisher<[Contact], Never> { - let query = Contact.Query( - authStatus: [.friend], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false: nil - ) - - return session.dbManager.fetchContactsPublisher(query) - .assertNoFailure() - .map { $0.filter { $0.id != self.session.myId }} - .eraseToAnyPublisher() - } - - var requestCount: AnyPublisher<Int, Never> { - let groupQuery = Group.Query( - authStatus: [.pending], - isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, - isLeaderBanned: reportingStatus.isEnabled() ? false : nil - ) - - let contactsQuery = Contact.Query( - authStatus: [ - .verified, - .confirming, - .confirmationFailed, - .verificationFailed, - .verificationInProgress - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - return Publishers.CombineLatest( - session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure(), - session.dbManager.fetchGroupsPublisher(groupQuery).assertNoFailure() - ) - .map { $0.0.count + $0.1.count } - .eraseToAnyPublisher() - } -} diff --git a/Sources/ContactListFeature/ViewModels/CreateDrawerViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateDrawerViewModel.swift deleted file mode 100644 index 7369fe9d06625d5e59aa7666d184ea0ae4fc90dc..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/ViewModels/CreateDrawerViewModel.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Shared -import Combine -import InputField - -struct CreateDrawerViewState { - var welcome: String? - var groupName: String = "" - var status: InputField.ValidationStatus = .unknown(nil) -} - -final class CreateDrawerViewModel { - var statePublisher: AnyPublisher<CreateDrawerViewState, Never> { - stateSubject.eraseToAnyPublisher() - } - - var donePublisher: AnyPublisher<(String, String?), Never> { - doneSubject.eraseToAnyPublisher() - } - - private let doneSubject = PassthroughSubject<(String, String?), Never>() - private let stateSubject = CurrentValueSubject<CreateDrawerViewState, Never>(.init()) - - func didInput(_ string: String) { - stateSubject.value.groupName = string - validate() - } - - func didOtherInput(_ string: String) { - stateSubject.value.welcome = string - } - - func didTapCreate() { - let name = stateSubject.value.groupName.trimmingCharacters(in: .whitespacesAndNewlines) - let welcome = stateSubject.value.welcome - doneSubject.send((name, welcome)) - } - - private func validate() { - let value = stateSubject.value.groupName.trimmingCharacters(in: .whitespacesAndNewlines) - - guard value.count >= 4 else { - stateSubject.value.status = .invalid(Localized.CreateGroup.Drawer.minimum) - return - } - - guard value.count < 32 else { - stateSubject.value.status = .invalid(Localized.CreateGroup.Drawer.maximum) - return - } - - stateSubject.value.status = .valid(nil) - } -} diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift deleted file mode 100644 index 555407535ca52ab7ed00c7d1060d6f98a967030b..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ /dev/null @@ -1,102 +0,0 @@ -import HUD -import UIKit -import Models -import Combine -import XXModels -import Defaults -import Integration -import ReportingFeature -import DependencyInjection - -final class CreateGroupViewModel { - @KeyObject(.username, defaultValue: "") var username: String - - // MARK: Injected - - @Dependency private var session: SessionType - @Dependency private var reportingStatus: ReportingStatus - - // MARK: Properties - - var selected: AnyPublisher<[Contact], Never> { - selectedContactsRelay.eraseToAnyPublisher() - } - - var contacts: AnyPublisher<[Contact], Never> { - contactsRelay.eraseToAnyPublisher() - } - - var hud: AnyPublisher<HUDStatus, Never> { - hudRelay.eraseToAnyPublisher() - } - - var info: AnyPublisher<GroupInfo, Never> { - infoRelay.eraseToAnyPublisher() - } - - private var allContacts = [Contact]() - private var cancellables = Set<AnyCancellable>() - private let infoRelay = PassthroughSubject<GroupInfo, Never>() - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - private let contactsRelay = CurrentValueSubject<[Contact], Never>([]) - private let selectedContactsRelay = CurrentValueSubject<[Contact], Never>([]) - - // MARK: Lifecycle - - init() { - let query = Contact.Query( - authStatus: [.friend], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - session.dbManager.fetchContactsPublisher(query) - .assertNoFailure() - .map { $0.filter { $0.id != self.session.myId }} - .map { $0.sorted(by: { $0.username! < $1.username! })} - .sink { [unowned self] in - allContacts = $0 - contactsRelay.send($0) - }.store(in: &cancellables) - } - - // MARK: Public - - func didSelect(contact: Contact) { - if selectedContactsRelay.value.contains(contact) { - selectedContactsRelay.value.removeAll { $0.username == contact.username } - } else { - selectedContactsRelay.value.append(contact) - } - } - - func filter(_ text: String) { - guard text.isEmpty == false else { - contactsRelay.send(allContacts) - return - } - - contactsRelay.send( - allContacts.filter { - ($0.username ?? "").contains(text.lowercased()) - } - ) - } - - func create(name: String, welcome: String?, members: [Contact]) { - hudRelay.send(.on) - - session.createGroup(name: name, welcome: welcome, members: members) { [weak self] in - guard let self = self else { return } - - self.hudRelay.send(.none) - - switch $0 { - case .success(let info): - self.infoRelay.send(info) - case .failure(let error): - self.hudRelay.send(.error(.init(with: error))) - } - } - } -} diff --git a/Sources/ContactListFeature/Views/ContactListItemButton.swift b/Sources/ContactListFeature/Views/ContactListItemButton.swift deleted file mode 100644 index c498f27ab9429fade4721e95b5fca6f3a73bdca7..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Views/ContactListItemButton.swift +++ /dev/null @@ -1,60 +0,0 @@ -import UIKit -import Shared - -final class ItemButton: UIControl { - let titleLabel = UILabel() - let iconImageView = UIImageView() - let separatorView = UIView() - let stackView = UIStackView() - let notificationLabel = UILabel() - - init() { - super.init(frame: .zero) - - titleLabel.textColor = Asset.brandPrimary.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - separatorView.backgroundColor = Asset.neutralLine.color - - notificationLabel.isHidden = true - notificationLabel.layer.cornerRadius = 5 - notificationLabel.layer.masksToBounds = true - notificationLabel.textColor = Asset.neutralWhite.color - notificationLabel.backgroundColor = Asset.brandPrimary.color - notificationLabel.font = Fonts.Mulish.bold.font(size: 12.0) - - stackView.spacing = 16 - stackView.addArrangedSubview(iconImageView) - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(notificationLabel) - stackView.setCustomSpacing(6, after: titleLabel) - - stackView.isUserInteractionEnabled = false - addSubview(stackView) - addSubview(separatorView) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(12) - make.left.equalToSuperview().offset(24) - make.bottom.equalTo(separatorView.snp.top).offset(-12) - } - - separatorView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview() - make.height.equalTo(1) - } - } - - required init?(coder: NSCoder) { nil } - - func setup(title: String, image: UIImage) { - titleLabel.text = title - iconImageView.image = image - } - - func updateNotification(_ count: Int) { - notificationLabel.isHidden = count < 1 - notificationLabel.text = " \(count) " - } -} diff --git a/Sources/ContactListFeature/Views/ContactListView.swift b/Sources/ContactListFeature/Views/ContactListView.swift deleted file mode 100644 index e7a52a717346d43bb2551eac86839d796a9c78c1..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Views/ContactListView.swift +++ /dev/null @@ -1,72 +0,0 @@ -import UIKit -import Shared - -final class ContactListView: UIView { - let newGroupButton = ItemButton() - let requestsButton = ItemButton() - let topStackView = UIStackView() - let stackView = UIStackView() - let emptyTitleLabel = UILabel() - let searchButton = CapsuleButton() - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - private func setup() { - backgroundColor = Asset.neutralWhite.color - - requestsButton.separatorView.isHidden = true - requestsButton.setup(title: "Requests", image: Asset.contactListRequests.image) - newGroupButton.setup(title: Localized.ContactList.newGroup, image: Asset.contactListNewGroup.image) - - let paragraph = NSMutableParagraphStyle() - paragraph.lineHeightMultiple = 1.2 - paragraph.alignment = .center - - emptyTitleLabel.attributedText = NSAttributedString( - string: Localized.ContactList.Empty.title, - attributes: [ - .paragraphStyle: paragraph, - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.bold.font(size: 24.0) as UIFont - ] - ) - emptyTitleLabel.numberOfLines = 0 - - searchButton.setStyle(.brandColored) - searchButton.setTitle(Localized.ContactList.Empty.action, for: .normal) - - stackView.spacing = 24 - stackView.axis = .vertical - stackView.alignment = .center - stackView.addArrangedSubview(emptyTitleLabel) - stackView.addArrangedSubview(searchButton) - - topStackView.axis = .vertical - topStackView.addArrangedSubview(newGroupButton) - topStackView.addArrangedSubview(requestsButton) - - addSubview(topStackView) - addSubview(stackView) - - setupConstraints() - } - - private func setupConstraints() { - topStackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - stackView.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } - } -} diff --git a/Sources/ContactListFeature/Views/CreateDrawerView.swift b/Sources/ContactListFeature/Views/CreateDrawerView.swift deleted file mode 100644 index 618ce7c94b4ed11109294b82bda47c8e17d98820..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Views/CreateDrawerView.swift +++ /dev/null @@ -1,106 +0,0 @@ -import UIKit -import Shared -import InputField - -final class CreateDrawerView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let inputField = InputField() - let otherInputField = InputField() - let stackView = UIStackView() - let createButton = CapsuleButton() - let cancelButton = CapsuleButton() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - - layer.cornerRadius = 40 - backgroundColor = Asset.neutralWhite.color - layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - - titleLabel.textAlignment = .left - titleLabel.text = Localized.CreateGroup.Drawer.title - titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) - titleLabel.textColor = Asset.neutralActive.color - - inputField.setup( - style: .regular, - title: Localized.CreateGroup.Drawer.input, - placeholder: Localized.CreateGroup.Drawer.placeholder, - leftView: .image(Asset.personGray.image), - accessibility: Localized.Accessibility.CreateGroup.Drawer.input, - subtitleColor: Asset.neutralDisabled.color - ) - - otherInputField.setup( - style: .regular, - title: Localized.CreateGroup.Drawer.otherInput, - placeholder: Localized.CreateGroup.Drawer.otherPlaceholder, - leftView: .image(Asset.balloon.image), - accessibility: Localized.Accessibility.CreateGroup.Drawer.otherInput, - subtitleColor: Asset.neutralDisabled.color - ) - - createButton.set( - style: .brandColored, - title: Localized.CreateGroup.Drawer.action, - accessibility: Localized.Accessibility.CreateGroup.Drawer.create - ) - - cancelButton.set( - style: .seeThrough, - title: Localized.CreateGroup.Drawer.cancel - ) - - stackView.spacing = 20 - stackView.axis = .vertical - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(subtitleView) - stackView.addArrangedSubview(inputField) - stackView.addArrangedSubview(otherInputField) - stackView.addArrangedSubview(createButton) - stackView.addArrangedSubview(cancelButton) - - 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) - } - } - - required init?(coder: NSCoder) { nil } - - func set(count: Int, didTap: @escaping () -> Void) { - self.didTapInfo = didTap - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .left - paragraphStyle.lineHeightMultiple = 1.1 - - subtitleView.setup( - text: Localized.CreateGroup.Drawer.subtitle("\(count)"), - attributes: [ - .paragraphStyle: paragraphStyle, - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.semiBold.font(size: 14.0) as Any - ], - didTapInfo: { [weak self] in self?.didTapInfo?() } - ) - } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - createButton.isEnabled = true - case .invalid, .unknown: - createButton.isEnabled = false - } - } -} diff --git a/Sources/ContactListFeature/Views/CreateGroupCollectionCell.swift b/Sources/ContactListFeature/Views/CreateGroupCollectionCell.swift deleted file mode 100644 index 1717365300ab0ae52380cc47e6073f8118684957..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Views/CreateGroupCollectionCell.swift +++ /dev/null @@ -1,80 +0,0 @@ -import UIKit -import Shared -import Combine - -final class CreateGroupCollectionCell: UICollectionViewCell { - let titleLabel = UILabel() - let removeButton = UIButton() - let upperView = UIView() - let avatarView = AvatarView() - - var didTapRemove: (() -> Void)? - var cancellables = Set<AnyCancellable>() - - override init(frame: CGRect) { - super.init(frame: frame) - - titleLabel.numberOfLines = 2 - titleLabel.lineBreakMode = .byWordWrapping - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralDark.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - removeButton.layer.cornerRadius = 9 - removeButton.backgroundColor = Asset.accentDanger.color - removeButton.setImage(Asset.contactListAvatarRemove.image, for: .normal) - - upperView.addSubview(avatarView) - contentView.addSubview(titleLabel) - contentView.addSubview(upperView) - contentView.addSubview(removeButton) - - upperView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - } - - avatarView.snp.makeConstraints { - $0.width.equalTo(48) - $0.height.equalTo(48) - $0.top.equalToSuperview().offset(4) - $0.left.equalToSuperview().offset(4) - $0.right.equalToSuperview().offset(-4) - $0.bottom.equalToSuperview().offset(-4) - } - - removeButton.snp.makeConstraints { - $0.centerY.equalTo(avatarView.snp.top).offset(5) - $0.centerX.equalTo(avatarView.snp.right).offset(-5) - $0.width.equalTo(18) - $0.height.equalTo(18) - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(upperView.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - titleLabel.text = nil - avatarView.prepareForReuse() - cancellables.removeAll() - } - - func setup(title: String, image: Data?) { - titleLabel.text = title - avatarView.setupProfile(title: title, image: image, size: .large) - cancellables.removeAll() - - removeButton.publisher(for: .touchUpInside) - .sink { [unowned self] in didTapRemove?() } - .store(in: &cancellables) - } -} diff --git a/Sources/ContactListFeature/Views/CreateGroupView.swift b/Sources/ContactListFeature/Views/CreateGroupView.swift deleted file mode 100644 index 0f7bd57962bed1fd6930bb6e1cd62fe2c618b01b..0000000000000000000000000000000000000000 --- a/Sources/ContactListFeature/Views/CreateGroupView.swift +++ /dev/null @@ -1,62 +0,0 @@ -import UIKit -import Shared -import SnapKit - -final class CreateGroupView: UIView { - let stackView = UIStackView() - let tableView = UITableView() - let searchComponent = SearchComponent() - lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - - let layout: UICollectionViewFlowLayout = { - let layout = UICollectionViewFlowLayout() - layout.minimumInteritemSpacing = 45 - layout.itemSize = CGSize(width: 56, height: 100) - layout.scrollDirection = .horizontal - return layout - }() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - tableView.separatorStyle = .none - tableView.tintColor = Asset.brandPrimary.color - tableView.backgroundColor = Asset.neutralWhite.color - tableView.allowsMultipleSelectionDuringEditing = true - tableView.setEditing(true, animated: true) - - searchComponent.set( - placeholder: "Search connections", - imageAtRight: UIImage.color(.clear) - ) - - collectionView.backgroundColor = Asset.neutralWhite.color - collectionView.contentInset = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30) - - stackView.spacing = 31 - stackView.axis = .vertical - stackView.addArrangedSubview(collectionView) - stackView.addArrangedSubview(tableView) - - addSubview(stackView) - addSubview(searchComponent) - - searchComponent.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview().offset(20) - make.right.equalToSuperview().offset(-20) - } - - stackView.snp.makeConstraints { make in - make.top.equalTo(searchComponent.snp.bottom).offset(20) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } - - collectionView.snp.makeConstraints { $0.height.equalTo(100) } - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/Countries/Country.swift b/Sources/Countries/Country.swift deleted file mode 100644 index acb0bae1d468c7cf3abba10f20a59e7c8d803949..0000000000000000000000000000000000000000 --- a/Sources/Countries/Country.swift +++ /dev/null @@ -1,45 +0,0 @@ -import os -import Foundation - -public struct Country { - public var name: String - public var code: String - public var flag: String - public var regex: String - public var prefix: String - public var example: String - public var prefixWithFlag: String { "\(flag) \(prefix)" } - - public static func fromMyPhone() -> Self { - let all = all() - - guard let country = all.filter({ $0.code == Locale.current.regionCode }).first else { - return all.filter { $0.code == "US" }.first! - } - - return country - } - - public static func all() -> [Self] { - guard let url = Bundle.module.url(forResource: "country_codes", withExtension: "json"), - let data = try? Data(contentsOf: url), - let countries = try? JSONDecoder().decode([Country].self, from: data) else { - fatalError("Can't handle country codes json") - } - - return countries - } - - public static func findFrom(_ number: String) -> Self { - all().first { country in - let start = number.index(number.startIndex, offsetBy: number.count - 2) - let end = number.index(start, offsetBy: number.count - (number.count - 2)) - - return country.code == String(number[start ..< end]) - }! - } -} - -extension Country: Hashable {} -extension Country: Equatable {} -extension Country: Decodable {} diff --git a/Sources/Countries/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/CountryListController.swift b/Sources/Countries/CountryListController.swift deleted file mode 100644 index a11c0e5724696bc172f58cfdc7b4333288639b7f..0000000000000000000000000000000000000000 --- a/Sources/Countries/CountryListController.swift +++ /dev/null @@ -1,93 +0,0 @@ -import os -import Theme -import UIKit -import Shared -import Combine -import DependencyInjection - -public final class CountryListController: UIViewController { - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = CountryListView() - - private var didChoose: ((Country) -> Void)! - private let viewModel = CountryListViewModel() - private var cancellables = Set<AnyCancellable>() - private var dataSource: UITableViewDiffableDataSource<SectionId, Country>! - - public init(_ didChoose: @escaping (Country) -> Void) { - self.didChoose = didChoose - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - - navigationController?.navigationBar.customize( - backgroundColor: Asset.neutralWhite.color, - shadowColor: Asset.neutralDisabled.color - ) - } - - public override func loadView() { - view = screenView - } - - public override func viewDidLoad() { - super.viewDidLoad() - screenView.tableView.register(CountryListCell.self) - setupNavigationBar() - setupBindings() - - viewModel.fetchCountryList() - } - - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.Countries.title - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) - - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupBindings() { - viewModel.countries - .receive(on: DispatchQueue.main) - .sink { [unowned self] in dataSource.apply($0, animatingDifferences: false) } - .store(in: &cancellables) - - dataSource = UITableViewDiffableDataSource<SectionId, Country>( - tableView: screenView.tableView - ) { tableView, indexPath, country in - let cell: CountryListCell = tableView.dequeueReusableCell(forIndexPath: indexPath) - cell.flagLabel.text = country.flag - cell.nameLabel.text = country.name - cell.prefixLabel.text = country.prefix - return cell - } - - screenView.searchComponent - .textPublisher - .removeDuplicates() - .sink { [unowned self] in viewModel.didSearchFor($0) } - .store(in: &cancellables) - - screenView.tableView.delegate = self - screenView.tableView.dataSource = dataSource - } - - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let country = dataSource.itemIdentifier(for: indexPath) { - didChoose(country) - navigationController?.popViewController(animated: true) - } - } -} - -extension CountryListController: UITableViewDelegate {} 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/CountryListFeature/CountryListController.swift b/Sources/CountryListFeature/CountryListController.swift new file mode 100644 index 0000000000000000000000000000000000000000..dd6ad4e65985464e7ae3c323a30df35da6033ab9 --- /dev/null +++ b/Sources/CountryListFeature/CountryListController.swift @@ -0,0 +1,76 @@ +import UIKit +import Shared +import Combine +import AppCore +import AppResources +import Dependencies + +public final class CountryListController: UIViewController, UITableViewDelegate { + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = CountryListView() + + private let completion: (Country) -> Void + private let viewModel = CountryListViewModel() + private var cancellables = Set<AnyCancellable>() + private var dataSource: UITableViewDiffableDataSource<SectionId, Country>! + + public init(_ completion: @escaping (Country) -> Void) { + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + } + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + screenView + .tableView + .register(CountryListCell.self) + + viewModel + .countries + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dataSource.apply($0, animatingDifferences: false) + }.store(in: &cancellables) + + dataSource = UITableViewDiffableDataSource<SectionId, Country>( + tableView: screenView.tableView + ) { tableView, indexPath, country in + let cell: CountryListCell = tableView.dequeueReusableCell(forIndexPath: indexPath) + cell.flagLabel.text = country.flag + cell.nameLabel.text = country.name + cell.prefixLabel.text = country.prefix + return cell + } + + screenView + .searchComponent + .textPublisher + .removeDuplicates() + .sink { [unowned self] in + viewModel.didSearchFor($0) + }.store(in: &cancellables) + + screenView.tableView.delegate = self + screenView.tableView.dataSource = dataSource + viewModel.fetchCountryList() + } + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let country = dataSource.itemIdentifier(for: indexPath) { + completion(country) + 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/CrashReport/CrashReport.swift b/Sources/CrashReport/CrashReport.swift new file mode 100644 index 0000000000000000000000000000000000000000..8e6d2b7e4e793c70f16dfed720def386c783f1d0 --- /dev/null +++ b/Sources/CrashReport/CrashReport.swift @@ -0,0 +1,25 @@ +import Firebase +import FirebaseCrashlytics +import XCTestDynamicOverlay + +public struct CrashReport { + public var configure: () -> Void + public var sendError: (NSError) -> Void + public var setEnabled: (Bool) -> Void +} + +extension CrashReport { + public static let live = CrashReport( + configure: FirebaseApp.configure, + sendError: Crashlytics.crashlytics().record(error:), + setEnabled: Crashlytics.crashlytics().setCrashlyticsCollectionEnabled + ) +} + +extension CrashReport { + public static let unimplemented = CrashReport( + configure: XCTUnimplemented("\(Self.self)"), + sendError: XCTUnimplemented("\(Self.self)"), + setEnabled: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/CrashReport/Dependency.swift b/Sources/CrashReport/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..ee81581b77eac6a15c8ca9e4d5044a644a7b4c13 --- /dev/null +++ b/Sources/CrashReport/Dependency.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum CrashReportDependencyKey: DependencyKey { + static let liveValue: CrashReport = .live + static let testValue: CrashReport = .unimplemented +} + +extension DependencyValues { + public var crashReport: CrashReport { + get { self[CrashReportDependencyKey.self] } + set { self[CrashReportDependencyKey.self] = newValue } + } +} diff --git a/Sources/CrashReporting/CrashReporter.swift b/Sources/CrashReporting/CrashReporter.swift deleted file mode 100644 index f249d741c7baa42218d77487a26e717866ea2f9a..0000000000000000000000000000000000000000 --- a/Sources/CrashReporting/CrashReporter.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -public struct CrashReporter { - 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 extension CrashReporter { - static let noop = Self( - configure: {}, - sendError: { _ in }, - setEnabled: { _ in } - ) -} diff --git a/Sources/CrashService/CrashService.swift b/Sources/CrashService/CrashService.swift deleted file mode 100644 index 4815ed483b3177262b90089ecc2af62973208a5d..0000000000000000000000000000000000000000 --- a/Sources/CrashService/CrashService.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Firebase -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) } - ) -} diff --git a/Sources/CreateGroupFeature/CreateGroupController.swift b/Sources/CreateGroupFeature/CreateGroupController.swift new file mode 100644 index 0000000000000000000000000000000000000000..ad7303364f56d0dbd7b840f3803cb066f8be2578 --- /dev/null +++ b/Sources/CreateGroupFeature/CreateGroupController.swift @@ -0,0 +1,87 @@ +import UIKit +import Combine +import XXModels + +public final class CreateGroupController: UIViewController { + private lazy var screenView = CreateGroupView() + + private let groupMembers: [Contact] + private let viewModel = CreateGroupViewModel() + private var cancellables = Set<AnyCancellable>() + + public init(_ groupMembers: [Contact]) { + self.groupMembers = groupMembers + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + screenView.set(count: groupMembers.count, didTap: {}) + + viewModel + .statePublisher + .map(\.status) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.shouldDismiss) + .filter { $0 == true } + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in + dismiss(animated: true) + }.store(in: &cancellables) + + screenView + .cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dismiss(animated: true) + }.store(in: &cancellables) + + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) + + screenView + .otherInputField + .textPublisher + .sink { [unowned self] in + viewModel.didOtherInput($0) + }.store(in: &cancellables) + + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) + + screenView + .otherInputField + .returnPublisher + .sink { [unowned self] in + screenView.otherInputField.endEditing(true) + }.store(in: &cancellables) + + screenView + .createButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapCreate(groupMembers) + }.store(in: &cancellables) + } +} diff --git a/Sources/CreateGroupFeature/CreateGroupView.swift b/Sources/CreateGroupFeature/CreateGroupView.swift new file mode 100644 index 0000000000000000000000000000000000000000..8d38493433607e66a2ac836e8ae8e07d473be134 --- /dev/null +++ b/Sources/CreateGroupFeature/CreateGroupView.swift @@ -0,0 +1,107 @@ +import UIKit +import Shared +import InputField +import AppResources + +final class CreateGroupView: UIView { + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let inputField = InputField() + let otherInputField = InputField() + let stackView = UIStackView() + let createButton = CapsuleButton() + let cancelButton = CapsuleButton() + + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + + layer.cornerRadius = 40 + backgroundColor = Asset.neutralWhite.color + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + titleLabel.textAlignment = .left + titleLabel.text = Localized.CreateGroup.Drawer.title + titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + titleLabel.textColor = Asset.neutralActive.color + + inputField.setup( + style: .regular, + title: Localized.CreateGroup.Drawer.input, + placeholder: Localized.CreateGroup.Drawer.placeholder, + leftView: .image(Asset.personGray.image), + accessibility: Localized.Accessibility.CreateGroup.Drawer.input, + subtitleColor: Asset.neutralDisabled.color + ) + + otherInputField.setup( + style: .regular, + title: Localized.CreateGroup.Drawer.otherInput, + placeholder: Localized.CreateGroup.Drawer.otherPlaceholder, + leftView: .image(Asset.balloon.image), + accessibility: Localized.Accessibility.CreateGroup.Drawer.otherInput, + subtitleColor: Asset.neutralDisabled.color + ) + + createButton.set( + style: .brandColored, + title: Localized.CreateGroup.Drawer.action, + accessibility: Localized.Accessibility.CreateGroup.Drawer.create + ) + + cancelButton.set( + style: .seeThrough, + title: Localized.CreateGroup.Drawer.cancel + ) + + stackView.spacing = 20 + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleView) + stackView.addArrangedSubview(inputField) + stackView.addArrangedSubview(otherInputField) + stackView.addArrangedSubview(createButton) + stackView.addArrangedSubview(cancelButton) + + 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) + } + } + + required init?(coder: NSCoder) { nil } + + func set(count: Int, didTap: @escaping () -> Void) { + self.didTapInfo = didTap + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + paragraphStyle.lineHeightMultiple = 1.1 + + subtitleView.setup( + text: Localized.CreateGroup.Drawer.subtitle("\(count)"), + attributes: [ + .paragraphStyle: paragraphStyle, + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.semiBold.font(size: 14.0) as Any + ], + didTapInfo: { [weak self] in self?.didTapInfo?() } + ) + } + + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + + switch status { + case .valid: + createButton.isEnabled = true + case .invalid, .unknown: + createButton.isEnabled = false + } + } +} diff --git a/Sources/CreateGroupFeature/CreateGroupViewModel.swift b/Sources/CreateGroupFeature/CreateGroupViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..25ef6090123ff307f2f6555d56bade868c5f5ce8 --- /dev/null +++ b/Sources/CreateGroupFeature/CreateGroupViewModel.swift @@ -0,0 +1,99 @@ +import Shared +import Combine +import AppCore +import XXModels +import InputField +import AppResources +import Dependencies + +import Foundation // ? + +struct CreateGroupViewModel { + struct ViewState { + var welcome: String? + var groupName: String = "" + var status: InputField.ValidationStatus = .unknown(nil) + var shouldDismiss: Bool = false + } + + @Dependency(\.app.bgQueue) var bgQueue + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.hudManager) var hudManager + + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) + + func didInput(_ string: String) { + stateSubject.value.groupName = string + validate() + } + + func didOtherInput(_ string: String) { + stateSubject.value.welcome = string + } + + func didTapCreate(_ members: [Contact]) { + hudManager.show() + let welcome = stateSubject.value.welcome + let name = stateSubject.value.groupName.trimmingCharacters(in: .whitespacesAndNewlines) + + bgQueue.schedule { + do { + let report = try messenger.groupChat()!.makeGroup( + membership: members.map(\.id), + message: welcome?.data(using: .utf8), + name: name.data(using: .utf8) + ) + let group = Group( + id: report.id, + name: name, + leaderId: try messenger.e2e.get()!.getContact().getId(), + createdAt: Date(), + authStatus: .participating, + serialized: try report.encode() + ) + try dbManager.getDB().saveGroup(group) + if let welcome { + try dbManager.getDB().saveMessage(.init( + senderId: try messenger.e2e.get()!.getContact().getId(), + recipientId: nil, + groupId: group.id, + date: group.createdAt, + status: .sent, + isUnread: false, + text: welcome + )) + } + try members.map { + GroupMember(groupId: group.id, contactId: $0.id) + }.forEach { + try dbManager.getDB().saveGroupMember($0) + } + _ = try dbManager.getDB().fetchGroupInfos( + .init(groupId: group.id) + ).first + hudManager.hide() + stateSubject.value.shouldDismiss = true + } catch { + hudManager.show(.init(error: error)) + } + } + } + + private func validate() { + let value = stateSubject.value.groupName.trimmingCharacters(in: .whitespacesAndNewlines) + guard value.count >= 4 else { + stateSubject.value.status = .invalid(Localized.CreateGroup.Drawer.minimum) + return + } + guard value.count < 21 else { + stateSubject.value.status = .invalid(Localized.CreateGroup.Drawer.maximum) + return + } + stateSubject.value.status = .valid(nil) + } +} diff --git a/Sources/Defaults/Dependencies.swift b/Sources/Defaults/Dependencies.swift new file mode 100644 index 0000000000000000000000000000000000000000..e0446cf6f3a8c8d952b419df86a95cb7043d52fd --- /dev/null +++ b/Sources/Defaults/Dependencies.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum KeyObjectStoreDependencyKey: DependencyKey { + static let liveValue: KeyObjectStore = .live + static let testValue: KeyObjectStore = .unimplemented +} + +extension DependencyValues { + public var store: KeyObjectStore { + get { self[KeyObjectStoreDependencyKey.self] } + set { self[KeyObjectStoreDependencyKey.self] = newValue } + } +} diff --git a/Sources/Defaults/KeyObject.swift b/Sources/Defaults/KeyObject.swift index 0ade4e83639f54a5181b4292936a2d9dee049f60..99635cdab25184351ea1b3421562e185d9628870 100644 --- a/Sources/Defaults/KeyObject.swift +++ b/Sources/Defaults/KeyObject.swift @@ -1,110 +1,60 @@ import Foundation -import DependencyInjection +import Dependencies 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 theme - 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 -} - -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 - } -} - -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:) - ) + 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 } @propertyWrapper public struct KeyObject<T> { - let key: String - let defaultValue: T + let key: String + let defaultValue: T - @Dependency var store: KeyObjectStore + @Dependency(\.store) 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.get(key) as? T ?? defaultValue + } + set { + if let value = newValue as? OptionalProtocol, value.isNil() { + store.remove(key) + } else { + store.set(newValue, for: 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/Defaults/KeyObjectStore.swift b/Sources/Defaults/KeyObjectStore.swift new file mode 100644 index 0000000000000000000000000000000000000000..38588f92da0ba9f28ed3c389afb8f9c9469a049a --- /dev/null +++ b/Sources/Defaults/KeyObjectStore.swift @@ -0,0 +1,21 @@ +public struct KeyObjectStore { + public var get: ObjectForKey + public var set: SetObjectForKey + public var remove: RemoveObjectForKey +} + +extension KeyObjectStore { + public static let live = KeyObjectStore( + get: .live, + set: .live, + remove: .live + ) +} + +extension KeyObjectStore { + public static let unimplemented = KeyObjectStore( + get: .unimplemented, + set: .unimplemented, + remove: .unimplemented + ) +} diff --git a/Sources/Defaults/ObjectForKey.swift b/Sources/Defaults/ObjectForKey.swift new file mode 100644 index 0000000000000000000000000000000000000000..e2c51f0699a95fd32f636dc58d099f2f4afe0c2c --- /dev/null +++ b/Sources/Defaults/ObjectForKey.swift @@ -0,0 +1,22 @@ +import Foundation +import XCTestDynamicOverlay + +public struct ObjectForKey { + public var run: (String) -> Any? + + public func callAsFunction(_ key: String) -> Any? { + run(key) + } +} + +extension ObjectForKey { + public static let live = ObjectForKey { + UserDefaults.standard.object(forKey: $0) + } +} + +extension ObjectForKey { + public static let unimplemented = ObjectForKey( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/Defaults/RemoveObjectForKey.swift b/Sources/Defaults/RemoveObjectForKey.swift new file mode 100644 index 0000000000000000000000000000000000000000..f8108bcffc0d978558f38b017e2cc9f0de848cc3 --- /dev/null +++ b/Sources/Defaults/RemoveObjectForKey.swift @@ -0,0 +1,22 @@ +import Foundation +import XCTestDynamicOverlay + +public struct RemoveObjectForKey { + public var run: (String) -> Void + + public func callAsFunction(_ key: String) -> Void { + run(key) + } +} + +extension RemoveObjectForKey { + public static let live = RemoveObjectForKey { + UserDefaults.standard.removeObject(forKey: $0) + } +} + +extension RemoveObjectForKey { + public static let unimplemented = RemoveObjectForKey( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/Defaults/SetObjectForKey.swift b/Sources/Defaults/SetObjectForKey.swift new file mode 100644 index 0000000000000000000000000000000000000000..c2f7d625d607474cdb4f7ba673fe9ead94673651 --- /dev/null +++ b/Sources/Defaults/SetObjectForKey.swift @@ -0,0 +1,22 @@ +import Foundation +import XCTestDynamicOverlay + +public struct SetObjectForKey { + public var run: (Any?, String) -> Void + + public func callAsFunction(_ value: Any?, for key: String) -> Void { + run(value, key) + } +} + +extension SetObjectForKey { + public static let live = SetObjectForKey { value, key in + UserDefaults.standard.set(value, forKey: key) + } +} + +extension SetObjectForKey { + public static let unimplemented = SetObjectForKey( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/DependencyInjection/Container.swift b/Sources/DependencyInjection/Container.swift deleted file mode 100644 index cd7bf2fcb3ce6515f63355f71163b30d83f67dd5..0000000000000000000000000000000000000000 --- a/Sources/DependencyInjection/Container.swift +++ /dev/null @@ -1,27 +0,0 @@ -public final class Container { - public static let shared = Container() - - public init() {} - - public func register<T>(_ dependency: T) { - dependencies[key(for: T.self)] = dependency - } - - public func unregister<T>(_ dependencyType: T.Type) { - dependencies.removeValue(forKey: String(describing: dependencyType)) - } - - public func resolve<T>() throws -> T { - let key = self.key(for: T.self) - guard let dependency = dependencies[key] as? T else { - throw UnregisteredDependencyError(type: key) - } - return dependency - } - - var dependencies = [String: Any]() - - func key<T>(for dependencyType: T.Type) -> String { - String(describing: dependencyType) - } -} diff --git a/Sources/DependencyInjection/DependencyPropertyWrapper.swift b/Sources/DependencyInjection/DependencyPropertyWrapper.swift deleted file mode 100644 index ef7067accdd81d40f96cc862aab22d213efafda1..0000000000000000000000000000000000000000 --- a/Sources/DependencyInjection/DependencyPropertyWrapper.swift +++ /dev/null @@ -1,20 +0,0 @@ -@propertyWrapper -public struct Dependency<T> { - public init(container: Container = .shared, file: StaticString = #file, line: UInt = #line) { - self.container = container - self.file = file - self.line = line - } - - public var wrappedValue: T { - do { - return try container.resolve() - } catch { - fatalError(error.localizedDescription, file: file, line: line) - } - } - - let container: Container - let file: StaticString - let line: UInt -} diff --git a/Sources/DependencyInjection/UnregisteredDependencyError.swift b/Sources/DependencyInjection/UnregisteredDependencyError.swift deleted file mode 100644 index 8ad955e63840a51fbd4da4df87dc2970728e3824..0000000000000000000000000000000000000000 --- a/Sources/DependencyInjection/UnregisteredDependencyError.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -public struct UnregisteredDependencyError: Error, Equatable { - public var type: String -} - -extension UnregisteredDependencyError: LocalizedError { - public var errorDescription: String? { - "Resolving unregistered dependency <\(type)>" - } -} diff --git a/Sources/DrawerFeature/DrawerController.swift b/Sources/DrawerFeature/DrawerController.swift index d907c26ba78956a919bcf58034b6e50720c0456a..c7503f8c68382be1494104ff483abcacb11ca542 100644 --- a/Sources/DrawerFeature/DrawerController.swift +++ b/Sources/DrawerFeature/DrawerController.swift @@ -2,26 +2,26 @@ import UIKit import Combine public final class DrawerController: UIViewController { - lazy private var screenView = DrawerView() - private let content: [DrawerItem] - public var cancellables = Set<AnyCancellable>() + private lazy var screenView = DrawerView() + private let content: [DrawerItem] + public var cancellables = Set<AnyCancellable>() - public init(with content: [DrawerItem]) { - self.content = content - super.init(nibName: nil, bundle: nil) + public init(_ items: [Any]) { + self.content = items as! [DrawerItem] + super.init(nibName: nil, bundle: nil) - let views = content.map { $0.makeView() } - views.forEach { screenView.stackView.addArrangedSubview($0) } + let views = content.map { $0.makeView() } + views.forEach { screenView.stackView.addArrangedSubview($0) } - content.enumerated().forEach { item in - guard let spacing = item.element.spacingAfter else { return } - screenView.stackView.setCustomSpacing(spacing, after: views[item.offset]) - } + content.enumerated().forEach { item in + guard let spacing = item.element.spacingAfter else { return } + screenView.stackView.setCustomSpacing(spacing, after: views[item.offset]) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - public override func loadView() { - view = screenView - } + public override func loadView() { + view = screenView + } } diff --git a/Sources/DrawerFeature/DrawerView.swift b/Sources/DrawerFeature/DrawerView.swift index e03282d5306ef66399d5ee0e96915f38d3e895de..95b711c3280ccdd40502d6cc4421eb98f058c672 100644 --- a/Sources/DrawerFeature/DrawerView.swift +++ b/Sources/DrawerFeature/DrawerView.swift @@ -1,26 +1,27 @@ import UIKit import Shared +import AppResources final class DrawerView: UIView { - let stackView = UIStackView() + let stackView = UIStackView() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - layer.cornerRadius = 40 - backgroundColor = Asset.neutralWhite.color - layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + layer.cornerRadius = 40 + backgroundColor = Asset.neutralWhite.color + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - stackView.axis = .vertical - addSubview(stackView) + stackView.axis = .vertical + addSubview(stackView) - stackView.snp.makeConstraints { - $0.top.equalToSuperview().offset(40) - $0.left.equalToSuperview().offset(50) - $0.right.equalToSuperview().offset(-50) - $0.bottom.equalToSuperview().offset(-50) - } + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(40) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + $0.bottom.equalToSuperview().offset(-50) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } 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 726dae9fe28ccd5720167a7b4ed1944eebf419e5..78f2b9299ca078c20fdbf426159d732a1af3366e 100644 --- a/Sources/DrawerFeature/Items/DrawerTable.swift +++ b/Sources/DrawerFeature/Items/DrawerTable.swift @@ -1,146 +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 + 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/DropboxFeature/DropboxInterface.swift b/Sources/DropboxFeature/DropboxInterface.swift deleted file mode 100644 index 1a5aa02a054ad29816530447c3ed5d725573160b..0000000000000000000000000000000000000000 --- a/Sources/DropboxFeature/DropboxInterface.swift +++ /dev/null @@ -1,18 +0,0 @@ -import UIKit -import Combine - -public protocol DropboxInterface { - func isAuthorized() -> Bool - - func unlink() - - func handleOpenUrl(_ url: URL) -> Bool - - func downloadBackup(_: String, _: @escaping (Result<Data, Error>) -> Void) - - func uploadBackup(_: URL, _: @escaping (Result<DropboxMetadata, Error>) -> Void) - - func downloadMetadata(_: @escaping (Result<DropboxMetadata?, Error>) -> Void) - - func authorize(presenting: UIViewController) -> AnyPublisher<Result<Bool, Error>, Never> -} diff --git a/Sources/DropboxFeature/DropboxMetadata.swift b/Sources/DropboxFeature/DropboxMetadata.swift deleted file mode 100644 index ceb4fe195242bf13ffb4e2a65a48d58aea282756..0000000000000000000000000000000000000000 --- a/Sources/DropboxFeature/DropboxMetadata.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import SwiftyDropbox - -public struct DropboxMetadata: Equatable { - public var size: Float - public var path: String - public var modifiedDate: Date - - public init( - size: Float, - path: String, - modifiedDate: Date - ) { - self.size = size - self.path = path - self.modifiedDate = modifiedDate - } -} diff --git a/Sources/DropboxFeature/DropboxService.swift b/Sources/DropboxFeature/DropboxService.swift deleted file mode 100644 index e90ef79721443c67ae452e1a0105ab0b86c2e6ca..0000000000000000000000000000000000000000 --- a/Sources/DropboxFeature/DropboxService.swift +++ /dev/null @@ -1,209 +0,0 @@ -import UIKit -import Combine -import SwiftyDropbox - -public struct DropboxService: DropboxInterface { - private let didAuthorizeSubject = PassthroughSubject<Result<Bool, Error>, Never>() - - public init() { - let path = Bundle.module.path(forResource: "Dropbox-Keys", ofType: "plist") - let url = URL(fileURLWithPath: path!) - let keys = try! NSDictionary(contentsOf: url, error: ()) - - DropboxClientsManager.setupWithAppKey(keys["DROPBOX_APP_KEY"] as! String) - } - - public func unlink() { - DropboxClientsManager.unlinkClients() - } - - public func isAuthorized() -> Bool { - DropboxClientsManager.authorizedClient != nil - } - - public func authorize(presenting controller: UIViewController) -> AnyPublisher<Result<Bool, Error>, Never> { - let scopes = ["files.metadata.read", "files.content.read", "files.content.write"] - - return didAuthorizeSubject.handleEvents(receiveSubscription: { _ in - let scopeRequest = ScopeRequest(scopeType: .user, scopes: scopes, includeGrantedScopes: false) - - DropboxClientsManager.authorizeFromControllerV2( - UIApplication.shared, - controller: controller, - loadingStatusDelegate: nil, - openURL: { (url: URL) -> Void in UIApplication.shared.open(url, options: [:], completionHandler: nil) }, - scopeRequest: scopeRequest - ) - }).first().eraseToAnyPublisher() - } - - public func handleOpenUrl(_ url: URL) -> Bool { - DropboxClientsManager.handleRedirectURL(url) { - switch $0 { - case .none: - didAuthorizeSubject.send(.success(false)) - case .error(let oAuthError, _): - didAuthorizeSubject.send(.failure(oAuthError)) - case .success: - didAuthorizeSubject.send(.success(true)) - case .cancel: - didAuthorizeSubject.send(.success(false)) - } - } - } - - public func downloadBackup(_ path: String, _ completion: @escaping (Result<Data, Error>) -> Void) { - Task { - do { - guard try await folderExists() else { fatalError() } - - let data = try await fetchBackup() - completion(.success(data)) - } catch { - completion(.failure(error)) - } - } - } - - public func uploadBackup(_ url: URL, _ completion: @escaping (Result<DropboxMetadata, Error>) -> Void) { - Task { - do { - if try await !folderExists() { - try await createFolder() - } - - let data = try Data(contentsOf: url) - let metadata = try await upload(data: data) - completion(.success(metadata)) - } catch { - completion(.failure(error)) - } - } - } - - public func downloadMetadata(_ completion: @escaping (Result<DropboxMetadata?, Error>) -> Void) { - Task { - do { - guard try await folderExists() else { - completion(.success(nil)) - return - } - - let metadata = try await fetchMetadata() - completion(.success(metadata)) - } catch { - completion(.failure(error)) - } - } - } -} - -extension DropboxService { - private func folderExists() async throws -> Bool { - guard let client = DropboxClientsManager.authorizedClient else { fatalError() } - - return try await withCheckedThrowingContinuation { continuation in - client.files.listFolder(path: "/backup") - .response { result, error in - if let error = error { - if case .routeError(_, _, _, _) = error as CallError { - continuation.resume(returning: false) - return - } - - let err = NSError(domain: error.description, code: 0) - continuation.resume(throwing: err) - return - } - - continuation.resume(returning: result != nil) - } - } - } - - private func createFolder() async throws { - guard let client = DropboxClientsManager.authorizedClient else { fatalError() } - - return try await withCheckedThrowingContinuation { continuation in - client.files.createFolderV2(path: "/backup") - .response { _, error in - if let error = error { - let err = NSError(domain: error.description, code: 0) - continuation.resume(throwing: err) - return - } - - continuation.resume(returning: ()) - } - } - } - - private func fetchMetadata() async throws -> DropboxMetadata? { - guard let client = DropboxClientsManager.authorizedClient else { fatalError() } - - return try await withCheckedThrowingContinuation { continuation in - client.files.getMetadata(path: "/backup/backup.xxm") - .response { response, error in - if let error = error { - let err = NSError(domain: error.description, code: 0) - continuation.resume(throwing: err) - return - } - - if let result = response as? Files.FileMetadata { - let size = Float(result.size) - let modifiedDate = result.serverModified - continuation.resume(returning: .init( - size: size, - path: "/backup/backup.xxm", - modifiedDate: modifiedDate - )) - } else { - continuation.resume(returning: nil) - } - } - } - } - - private func fetchBackup() async throws -> Data { - guard let client = DropboxClientsManager.authorizedClient else { fatalError() } - - return try await withCheckedThrowingContinuation { continuation in - client.files.download(path: "/backup/backup.xxm") - .response(completionHandler: { response, error in - if let error = error { - let err = NSError(domain: error.description, code: 0) - continuation.resume(throwing: err) - return - } - - if let response = response { - continuation.resume(returning: response.1) - } - }) - } - } - - private func upload(data: Data) async throws -> DropboxMetadata { - guard let client = DropboxClientsManager.authorizedClient else { fatalError() } - - return try await withCheckedThrowingContinuation { continuation in - client.files.upload(path: "/backup/backup.xxm", mode: .overwrite, input: data) - .response { response, error in - if let error = error { - let err = NSError(domain: error.description, code: 0) - continuation.resume(throwing: err) - return - } - - if let response = response { - continuation.resume(returning: .init( - size: Float(response.size), - path: response.pathLower!, - modifiedDate: response.serverModified - )) - } - } - } - } -} diff --git a/Sources/DropboxFeature/DropboxServiceMock.swift b/Sources/DropboxFeature/DropboxServiceMock.swift deleted file mode 100644 index ba9654b69f870fb28b49cd8414676375fcb866ba..0000000000000000000000000000000000000000 --- a/Sources/DropboxFeature/DropboxServiceMock.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit -import Combine - -public struct DropboxServiceMock: DropboxInterface { - public init() {} - - public func unlink() {} - - public func isAuthorized() -> Bool { true } - - public func handleOpenUrl(_ url: URL) -> Bool { true } - - public func didFinishAuthFlow(withError: String?) {} - - public func downloadBackup(_: String, _: @escaping (Result<Data, Error>) -> Void) {} - - public func uploadBackup(_: URL, _: @escaping (Result<DropboxMetadata, Error>) -> Void) {} - - public func downloadMetadata(_: @escaping (Result<DropboxMetadata?, Error>) -> Void) {} - - public func authorize(presenting: UIViewController) -> AnyPublisher<Result<Bool, Error>, Never> { fatalError() } -} diff --git a/Sources/DropboxFeature/Resources/Dropbox-Keys.plist b/Sources/DropboxFeature/Resources/Dropbox-Keys.plist deleted file mode 100644 index ecf15d0188386cac204a2e243e59320620a6c9c5..0000000000000000000000000000000000000000 --- a/Sources/DropboxFeature/Resources/Dropbox-Keys.plist +++ /dev/null @@ -1,8 +0,0 @@ -<?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>DROPBOX_APP_KEY</key> - <string></string> -</dict> -</plist> diff --git a/Sources/FetchBannedList/Dependency.swift b/Sources/FetchBannedList/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..8eb9ad1696a663a47bcb257e3b71d09fc6225e11 --- /dev/null +++ b/Sources/FetchBannedList/Dependency.swift @@ -0,0 +1,14 @@ +import Dependencies + +private enum FetchBannedListDependencyKey: DependencyKey { + static let liveValue: FetchBannedList = .live + static let testValue: FetchBannedList = .unimplemented +} + +extension DependencyValues { + public var fetchBannedList: FetchBannedList { + get { self[FetchBannedListDependencyKey.self] } + set { self[FetchBannedListDependencyKey.self] = newValue } + } +} + diff --git a/Sources/FetchBannedList/FetchBannedList.swift b/Sources/FetchBannedList/FetchBannedList.swift new file mode 100644 index 0000000000000000000000000000000000000000..9315cd589c431e3ed010d34e7c321f2dc9c5c787 --- /dev/null +++ b/Sources/FetchBannedList/FetchBannedList.swift @@ -0,0 +1,46 @@ +import Foundation +import XCTestDynamicOverlay + +public struct FetchBannedList { + public enum Error: Swift.Error, Equatable { + case network(URLError) + case invalidResponse + } + + public typealias Completion = (Result<Data, Error>) -> Void + + public var run: (@escaping Completion) -> Void + + public func callAsFunction(completion: @escaping Completion) { + run(completion) + } +} + +extension FetchBannedList { + public static let live = FetchBannedList { completion in + let url = URL(string: "https://elixxir-bins.s3.us-west-1.amazonaws.com/client/bannedUsers/banned.csv")! + let session = URLSession.shared + let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData) + let task = session.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(.network(error as! URLError))) + return + } + guard let response = response as? HTTPURLResponse, + (200..<300).contains(response.statusCode), + let data = data + else { + completion(.failure(.invalidResponse)) + return + } + completion(.success(data)) + } + task.resume() + } +} + +extension FetchBannedList { + public static let unimplemented = FetchBannedList( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/GoogleDriveFeature/GoogleDriveInterface.swift b/Sources/GoogleDriveFeature/GoogleDriveInterface.swift deleted file mode 100644 index c6710b8610fc9081e211fe0363f10f40584ea0e6..0000000000000000000000000000000000000000 --- a/Sources/GoogleDriveFeature/GoogleDriveInterface.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit - -public protocol GoogleDriveInterface { - func isAuthorized(_: @escaping (Bool) -> Void) - - func downloadMetadata(_: @escaping (Result<GoogleDriveMetadata?, Error>) -> Void) - - func uploadBackup(_: URL, _: @escaping (Result<GoogleDriveMetadata, Error>) -> Void) - - func authorize(presenting: UIViewController, _: @escaping (Result<Void, Error>) -> Void) - - func downloadBackup(_: String, progressCallback: @escaping (Float) -> Void, _: @escaping (Result<Data, Error>) -> Void) -} diff --git a/Sources/GoogleDriveFeature/GoogleDriveMetadata.swift b/Sources/GoogleDriveFeature/GoogleDriveMetadata.swift deleted file mode 100644 index f4179db86b943af99a0eaacdcae32ea987bdd73f..0000000000000000000000000000000000000000 --- a/Sources/GoogleDriveFeature/GoogleDriveMetadata.swift +++ /dev/null @@ -1,27 +0,0 @@ -import GoogleAPIClientForREST_Drive - -public struct GoogleDriveMetadata: Equatable { - public var size: Float - public var identifier: String - public var modifiedDate: Date - - public init( - size: Float, - identifier: String, - modifiedDate: Date - ) { - self.size = size - self.identifier = identifier - self.modifiedDate = modifiedDate - } -} - -extension GoogleDriveMetadata { - init?(withDriveFile file: GTLRDrive_File) { - guard let size = file.size?.floatValue, - let identifier = file.identifier, - let modifiedDate = file.modifiedTime?.date else { return nil } - - self.init(size: size, identifier: identifier, modifiedDate: modifiedDate) - } -} diff --git a/Sources/GoogleDriveFeature/GoogleDriveService.swift b/Sources/GoogleDriveFeature/GoogleDriveService.swift deleted file mode 100644 index 4ae4ac7457058d11ab3777cd71a4e8843f33458c..0000000000000000000000000000000000000000 --- a/Sources/GoogleDriveFeature/GoogleDriveService.swift +++ /dev/null @@ -1,335 +0,0 @@ -import UIKit -import GoogleSignIn -import GTMSessionFetcherFull -import GTMSessionFetcherCore -import GoogleAPIClientForREST_Drive - -public final class GoogleDriveService: GoogleDriveInterface { - private static let scopeFile = "https://www.googleapis.com/auth/drive.file" - private static let scopeAppData = "https://www.googleapis.com/auth/drive.appdata" - - var user: GIDGoogleUser? - - let service: GTLRDriveService = { - let service = GTLRDriveService() - - let path = Bundle.module.path(forResource: "GoogleDrive-Keys", ofType: "plist") - let url = URL(fileURLWithPath: path!) - let keys = try! NSDictionary(contentsOf: url, error: ()) - - service.apiKey = keys["DRIVE_API_KEY"] as? String - return service - }() - - public init() {} - - public func isAuthorized(_ completion: @escaping (Bool) -> Void) { - guard GIDSignIn.sharedInstance.hasPreviousSignIn() else { - return completion(false) - } - - GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in - guard let user = user, let scopes = user.grantedScopes, error == nil else { - return completion(false) - } - - self.user = user - self.service.authorizer = user.authentication.fetcherAuthorizer() - completion(scopes.contains(GoogleDriveService.scopeFile) && scopes.contains(GoogleDriveService.scopeAppData)) - } - } - - public func authorize( - presenting controller: UIViewController, - _ completion: @escaping (Result<Void, Error>) -> Void - ) { - GIDSignIn.sharedInstance.restorePreviousSignIn { [weak self] user, error in - guard let self = self else { return } - - guard error == nil else { - self.signIn(presenting: controller) { - switch $0 { - case .success: - self.authorizeDrive(controller: controller, completion: completion) - case .failure(let error): - completion(.failure(error)) - } - } - - return - } - - guard let user = user else { fatalError() } - - self.user = user - self.service.authorizer = user.authentication.fetcherAuthorizer() - self.authorizeDrive(controller: controller, completion: completion) - } - } - - public func downloadMetadata(_ completion: @escaping (Result<GoogleDriveMetadata?, Error>) -> Void) { - Task { - do { - guard let folder = try await fetchFolder() else { - completion(.success(nil)) - return - } - - _ = try await listFiles(on: folder) - - let backup = try await fetchBackup(at: folder) - completion(.success(backup)) - } catch { - completion(.failure(error)) - } - } - } - - public func downloadBackup( - _ backup: String, - progressCallback: @escaping (Float) -> Void, - _ completion: @escaping (Result<Data, Error>) -> Void - ) { - let query = GTLRDriveQuery_FilesGet.queryForMedia(withFileId: backup) - service.executeQuery(query) { _, file, error in - guard error == nil else { - print("Error on line #\(#line): \(error!.localizedDescription)") - return completion(.failure(error!)) - } - - guard let data = (file as? GTLRDataObject)?.data else { - print("Error on line #\(#line)") - return completion(.failure(NSError())) - } - - completion(.success(data)) - } - } - - public func uploadBackup( - _ file: URL, - _ completion: @escaping (Result<GoogleDriveMetadata, Error>) -> Void - ) { - Task { - do { - var folder = try await fetchFolder() - if folder == nil { folder = try await createFolder() } - let metadata = try await uploadFile(file, to: folder!) - let listMetadata = try await listFiles(on: folder!) - try await cleanup(listMetadata) - completion(.success(metadata)) - } catch { - print("Error on line #\(#line): \(error.localizedDescription)") - completion(.failure(error)) - } - } - } -} - -extension GoogleDriveService { - private func authorizeDrive( - controller: UIViewController, - completion: @escaping (Result<Void, Error>) -> Void - ) { - if let user = user, - let scopes = user.grantedScopes, - scopes.contains(GoogleDriveService.scopeFile), - scopes.contains(GoogleDriveService.scopeAppData) { - return completion(.success(())) - } - - GIDSignIn.sharedInstance.addScopes( - [GoogleDriveService.scopeFile, GoogleDriveService.scopeAppData], - presenting: controller, callback: { user, error in - guard error == nil else { - print("Error on line #\(#line): \(error!.localizedDescription)") - return completion(.failure(error!)) - } - - guard let user = user else { fatalError() } - self.user = user - completion(.success(())) - } - ) - } - - private func signIn( - presenting controller: UIViewController, - completion: @escaping (Result<Void, Error>) -> Void - ) { - GIDSignIn.sharedInstance.signIn( - with: GIDConfiguration(clientID: "662236151640-30i07ubg6ukodg15u0bnpk322p030u3j.apps.googleusercontent.com"), - presenting: controller, - callback: { user, error in - guard error == nil else { - print("Error on line #\(#line): \(error!.localizedDescription)") - return completion(.failure(error!)) - } - - guard let user = user else { fatalError() } - - self.user = user - self.service.authorizer = user.authentication.fetcherAuthorizer() - completion(.success(())) - } - ) - } - - private func fetchFolder() async throws -> String? { - let query = GTLRDriveQuery_FilesList.query() - query.q = "mimeType = 'application/vnd.google-apps.folder' and name = 'backup'" - query.spaces = "appDataFolder" - query.fields = "nextPageToken, files(id, name)" - - return try await withCheckedThrowingContinuation { continuation in - service.executeQuery(query) { _, result, error in - if let error = error { - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - let item = (result as? GTLRDrive_FileList)?.files?.first - continuation.resume(returning: item?.identifier) - } - } - } - - private func fetchBackup(at folder: String) async throws -> GoogleDriveMetadata? { - let query = GTLRDriveQuery_FilesList.query() - query.q = "'\(folder)' in parents and name = 'backup.xxm'" - query.spaces = "appDataFolder" - query.fields = "nextPageToken, files(id, size, name, modifiedTime)" - - return try await withCheckedThrowingContinuation { continuation in - service.executeQuery(query) { _, result, error in - if let error = error { - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - var metadata: GoogleDriveMetadata? = nil - - if let file = (result as? GTLRDrive_FileList)?.files?.first, - let size = file.size, - let id = file.identifier, - let date = file.modifiedTime?.date { - metadata = GoogleDriveMetadata(size: size.floatValue, identifier: id, modifiedDate: date) - } - - continuation.resume(returning: metadata) - } - } - } - - private func createFolder() async throws -> String { - let file = GTLRDrive_File() - file.name = "backup" - file.parents = ["appDataFolder"] - file.mimeType = "application/vnd.google-apps.folder" - - let query = GTLRDriveQuery_FilesCreate.query(withObject: file, uploadParameters: nil) - - return try await withCheckedThrowingContinuation { continuation in - service.executeQuery(query) { _, result, error in - if let error = error { - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - guard let identifier = (result as? GTLRDrive_File)?.identifier else { - let errorTitle = "Couldn't create backup folder but no error was passed (?)" - let error = NSError(domain: errorTitle, code: 0, userInfo: [NSLocalizedDescriptionKey: errorTitle]) - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - continuation.resume(returning: identifier) - } - } - } - - private func uploadFile( - _ fileURL: URL, - to folder: String - ) async throws -> GoogleDriveMetadata { - - let file = GTLRDrive_File() - file.name = "backup.xxm" - file.parents = [folder] - file.mimeType = "application/octet-stream" - - let params = GTLRUploadParameters(fileURL: fileURL, mimeType: file.mimeType!) - let query = GTLRDriveQuery_FilesCreate.query(withObject: file, uploadParameters: params) - query.fields = "id, size, modifiedTime" - - return try await withCheckedThrowingContinuation { continuation in - service.executeQuery(query) { _, result, error in - if let error = error { - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - guard let driveFile = (result as? GTLRDrive_File), - let size = driveFile.size, - let id = driveFile.identifier, - let date = driveFile.modifiedTime?.date else { - let errorTitle = "Couldn't upload file but no error was passed (?)" - let error = NSError(domain: errorTitle, code: 0, userInfo: [NSLocalizedDescriptionKey: errorTitle]) - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - continuation.resume(returning: .init(size: size.floatValue, identifier: id, modifiedDate: date)) - } - } - } - - private func listFiles(on folder: String) async throws -> [GoogleDriveMetadata] { - let query = GTLRDriveQuery_FilesList.query() - query.q = "'\(folder)' in parents" - query.spaces = "appDataFolder" - query.fields = "nextPageToken, files(id, modifiedTime, size, name)" - - return try await withCheckedThrowingContinuation { continuation in - service.executeQuery(query) { _, result, error in - if let error = error { - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - guard let files = (result as? GTLRDrive_FileList)?.files else { - continuation.resume(returning: []) - return - } - - let metadataList = files.compactMap(GoogleDriveMetadata.init(withDriveFile:)) - continuation.resume(returning: metadataList) - } - } - } - - private func cleanup(_ files: [GoogleDriveMetadata]) async throws { - let latestBackup = files.max { $0.modifiedDate < $1.modifiedDate } - let identifiers = files.filter { $0 != latestBackup }.map(\.identifier) - let query = GTLRBatchQuery(queries: identifiers.map(GTLRDriveQuery_FilesDelete.query(withFileId:))) - - return try await withCheckedThrowingContinuation { continuation in - service.executeQuery(query) { _, _, error in - if let error = error { - print("Error on line #\(#line): \(error.localizedDescription)") - continuation.resume(throwing: error) - return - } - - continuation.resume(returning: ()) - } - } - } -} diff --git a/Sources/GoogleDriveFeature/GoogleDriveServiceMock.swift b/Sources/GoogleDriveFeature/GoogleDriveServiceMock.swift deleted file mode 100644 index cf7625355cbc778fca29eb245586079b1601db6f..0000000000000000000000000000000000000000 --- a/Sources/GoogleDriveFeature/GoogleDriveServiceMock.swift +++ /dev/null @@ -1,40 +0,0 @@ -import UIKit - -public final class GoogleDriveServiceMock: GoogleDriveInterface { - public init() {} - - public func isAuthorized(_ completion: @escaping (Bool) -> Void) { - completion(true) - } - - public func uploadBackup(_: URL, _ completion: @escaping (Result<GoogleDriveMetadata, Error>) -> Void) { - completion(.success(.init(size: 23.toBytes(), identifier: "", modifiedDate: Date()))) - } - - public func downloadMetadata(_ completion: @escaping (Result<GoogleDriveMetadata?, Error>) -> Void) { - completion(.success(.init(size: 23.toBytes(), identifier: "", modifiedDate: Date()))) - } - - public func authorize(presenting: UIViewController, _ completion: @escaping (Result<Void, Error>) -> Void) { - completion(.success(())) - } - - public func downloadBackup( - _: String, - progressCallback: @escaping (Float) -> Void, - _ completion: @escaping (Result<Data, Error>) -> Void - ) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { progressCallback(3.toBytes()) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { progressCallback(7.toBytes()) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) { progressCallback(12.toBytes()) } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { progressCallback(15.toBytes()) } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { progressCallback(16.toBytes()) } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { progressCallback(19.toBytes()) } - DispatchQueue.main.asyncAfter(deadline: .now() + 2.1) { progressCallback(22.toBytes()) } - DispatchQueue.main.asyncAfter(deadline: .now() + 2.4) { completion(.success(Data())) } - } -} - -private extension Int { - func toBytes() -> Float { Float(self) * 1000000.0 } -} diff --git a/Sources/GoogleDriveFeature/Resources/GoogleDrive-Keys.plist b/Sources/GoogleDriveFeature/Resources/GoogleDrive-Keys.plist deleted file mode 100644 index 614bac60d3fd5014a33613ee3e83c72656d0854d..0000000000000000000000000000000000000000 --- a/Sources/GoogleDriveFeature/Resources/GoogleDrive-Keys.plist +++ /dev/null @@ -1,8 +0,0 @@ -<?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>DRIVE_API_KEY</key> - <string></string> -</dict> -</plist> diff --git a/Sources/GroupDraftFeature/GroupDraftCollectionCell.swift b/Sources/GroupDraftFeature/GroupDraftCollectionCell.swift new file mode 100644 index 0000000000000000000000000000000000000000..cdcb1393c7460df0410e352ca7ab3347d359dbaf --- /dev/null +++ b/Sources/GroupDraftFeature/GroupDraftCollectionCell.swift @@ -0,0 +1,83 @@ +import UIKit +import Shared +import Combine +import AppResources + +final class GroupDraftCollectionCell: UICollectionViewCell { + let titleLabel = UILabel() + let removeButton = UIButton() + let upperView = UIView() + let avatarView = AvatarView() + + var didTapRemove: (() -> Void)? + var cancellables = Set<AnyCancellable>() + + override init(frame: CGRect) { + super.init(frame: frame) + + titleLabel.numberOfLines = 2 + titleLabel.lineBreakMode = .byWordWrapping + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralDark.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + removeButton.layer.cornerRadius = 9 + removeButton.backgroundColor = Asset.accentDanger.color + removeButton.setImage(Asset.contactListAvatarRemove.image, for: .normal) + + upperView.addSubview(avatarView) + contentView.addSubview(titleLabel) + contentView.addSubview(upperView) + contentView.addSubview(removeButton) + + upperView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + } + + avatarView.snp.makeConstraints { + $0.width.equalTo(48) + $0.height.equalTo(48) + $0.top.equalToSuperview().offset(4) + $0.left.equalToSuperview().offset(4) + $0.right.equalToSuperview().offset(-4) + $0.bottom.equalToSuperview().offset(-4) + } + + removeButton.snp.makeConstraints { + $0.centerY.equalTo(avatarView.snp.top).offset(5) + $0.centerX.equalTo(avatarView.snp.right).offset(-5) + $0.width.equalTo(18) + $0.height.equalTo(18) + } + + titleLabel.snp.makeConstraints { + $0.top.equalTo(upperView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + avatarView.prepareForReuse() + cancellables.removeAll() + } + + func setup(title: String, image: Data?) { + titleLabel.text = title + avatarView.setupProfile(title: title, image: image, size: .large) + cancellables.removeAll() + + removeButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + didTapRemove?() + }.store(in: &cancellables) + } +} diff --git a/Sources/GroupDraftFeature/GroupDraftController.swift b/Sources/GroupDraftFeature/GroupDraftController.swift new file mode 100644 index 0000000000000000000000000000000000000000..27dd858fa3abc733e288f4495d53f0aaad629741 --- /dev/null +++ b/Sources/GroupDraftFeature/GroupDraftController.swift @@ -0,0 +1,191 @@ +import UIKit +import Shared +import Combine +import XXModels +import AppResources +import Dependencies +import AppNavigation + +public final class GroupDraftController: UIViewController { + @Dependency(\.navigator) var navigator: Navigator + + private lazy var titleLabel = UILabel() + private lazy var createButton = UIButton() + private lazy var screenView = GroupDraftView() + + private var selectedElements = [Contact]() { + didSet { screenView.tableView.reloadData() } + } + private let viewModel = GroupDraftViewModel() + private var cancellables = Set<AnyCancellable>() + private var tableDataSource: UITableViewDiffableDataSource<SectionId, Contact>! + private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>! + + private var count = 0 { + didSet { + createButton.isEnabled = count >= 2 && count <= 10 + + let text = Localized.CreateGroup.title("\(count)") + let attString = NSMutableAttributedString(string: text) + attString.addAttribute(.font, value: Fonts.Mulish.semiBold.font(size: 18.0) as Any) + attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) + attString.addAttributes(attributes: [ + .foregroundColor: Asset.neutralDisabled.color, + .font: Fonts.Mulish.regular.font(size: 14.0) as Any + ], betweenCharacters: "#") + + titleLabel.attributedText = attString + titleLabel.sizeToFit() + } + } + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupTableAndCollection() + setupBindings() + + count = 0 + } + + private func setupNavigationBar() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: titleLabel) + navigationItem.leftItemsSupplementBackButton = true + + createButton.setTitle(Localized.CreateGroup.create, for: .normal) + createButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + createButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 16.0) + createButton.setTitleColor(Asset.neutralDisabled.color, for: .disabled) + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: createButton) + } + + private func setupTableAndCollection() { + screenView.tableView.rowHeight = 64.0 + screenView.tableView.register(AvatarCell.self) + screenView.collectionView.register(GroupDraftCollectionCell.self) + + collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( + collectionView: screenView.collectionView + ) { [weak viewModel] collectionView, indexPath, contact in + let cell: GroupDraftCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + let title = (contact.nickname ?? contact.username) ?? "" + cell.setup(title: title, image: contact.photo) + cell.didTapRemove = { viewModel?.didSelect(contact: contact) } + + return cell + } + + tableDataSource = DiffEditableDataSource<SectionId, Contact>( + tableView: screenView.tableView + ) { [weak self] tableView, indexPath, contact in + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) + let title = (contact.nickname ?? contact.username) ?? "" + + cell.setup(title: title, image: contact.photo) + + if let selectedElements = self?.selectedElements, selectedElements.contains(contact) { + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) + } else { + tableView.deselectRow(at: indexPath, animated: true) + } + + return cell + } + + screenView.tableView.delegate = self + screenView.tableView.dataSource = tableDataSource + screenView.collectionView.dataSource = collectionDataSource + } + + private func setupBindings() { + let selected = viewModel.selected.share() + + selected + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.collectionView.isHidden = $0.count < 1 + + count = $0.count + selectedElements = $0 + }.store(in: &cancellables) + + selected.map { selectedContacts in + var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() + let sections = [SectionId()] + snapshot.appendSections(sections) + sections.forEach { section in snapshot.appendItems(selectedContacts, toSection: section) } + return snapshot + } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in collectionDataSource.apply($0) } + .store(in: &cancellables) + + viewModel + .contacts + .map { contacts in + var snapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>() + let sections = [SectionId()] + snapshot.appendSections(sections) + sections.forEach { section in snapshot.appendItems(contacts, toSection: section) } + return snapshot + } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + tableDataSource.apply($0, animatingDifferences: tableDataSource.snapshot().numberOfItems > 0) + }.store(in: &cancellables) + + screenView + .searchComponent + .textPublisher + .removeDuplicates() + .sink { [unowned self] in + viewModel.filter($0) + }.store(in: &cancellables) + + viewModel + .info + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentGroupChat( + groupInfo: $0, + on: navigationController! + )) + }.store(in: &cancellables) + + createButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentCreateGroup( + members: selectedElements, + from: self + )) + }.store(in: &cancellables) + } +} + +extension GroupDraftController: UITableViewDelegate { + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let contact = tableDataSource.itemIdentifier(for: indexPath) { + viewModel.didSelect(contact: contact) + } + } + + public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + if let contact = tableDataSource.itemIdentifier(for: indexPath) { + viewModel.didSelect(contact: contact) + } + } +} diff --git a/Sources/GroupDraftFeature/GroupDraftView.swift b/Sources/GroupDraftFeature/GroupDraftView.swift new file mode 100644 index 0000000000000000000000000000000000000000..972cd50602ddac171f203a01b4fef82d04ab8a37 --- /dev/null +++ b/Sources/GroupDraftFeature/GroupDraftView.swift @@ -0,0 +1,65 @@ +import UIKit +import Shared +import SnapKit +import AppResources + +final class GroupDraftView: UIView { + let stackView = UIStackView() + let tableView = UITableView() + let searchComponent = SearchComponent() + lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + + let layout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.minimumInteritemSpacing = 45 + layout.itemSize = CGSize(width: 56, height: 100) + layout.scrollDirection = .horizontal + return layout + }() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + tableView.separatorStyle = .none + tableView.tintColor = Asset.brandPrimary.color + tableView.backgroundColor = Asset.neutralWhite.color + tableView.allowsMultipleSelectionDuringEditing = true + tableView.setEditing(true, animated: true) + + searchComponent.set( + placeholder: "Search connections", + imageAtRight: UIImage.color(.clear) + ) + + collectionView.backgroundColor = Asset.neutralWhite.color + collectionView.contentInset = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30) + + stackView.spacing = 31 + stackView.axis = .vertical + stackView.addArrangedSubview(collectionView) + stackView.addArrangedSubview(tableView) + + addSubview(stackView) + addSubview(searchComponent) + + searchComponent.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + } + + stackView.snp.makeConstraints { + $0.top.equalTo(searchComponent.snp.bottom).offset(20) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + + collectionView.snp.makeConstraints { + $0.height.equalTo(100) + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/GroupDraftFeature/GroupDraftViewModel.swift b/Sources/GroupDraftFeature/GroupDraftViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..476dd69ad26eaab2de5a17ed501289f830799374 --- /dev/null +++ b/Sources/GroupDraftFeature/GroupDraftViewModel.swift @@ -0,0 +1,76 @@ +import Combine +import AppCore +import XXModels +import Defaults +import Foundation +import Dependencies +import ReportingFeature + +final class GroupDraftViewModel { + @Dependency(\.app.bgQueue) var bgQueue + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.messenger) var messenger + @Dependency(\.reportingStatus) var reportingStatus + + @KeyObject(.username, defaultValue: "") var username: String + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var selected: AnyPublisher<[XXModels.Contact], Never> { + selectedContactsRelay.eraseToAnyPublisher() + } + + var contacts: AnyPublisher<[XXModels.Contact], Never> { + contactsRelay.eraseToAnyPublisher() + } + + var info: AnyPublisher<GroupInfo, Never> { + infoRelay.eraseToAnyPublisher() + } + + private var allContacts = [XXModels.Contact]() + private var cancellables = Set<AnyCancellable>() + private let infoRelay = PassthroughSubject<GroupInfo, Never>() + private let contactsRelay = CurrentValueSubject<[XXModels.Contact], Never>([]) + private let selectedContactsRelay = CurrentValueSubject<[XXModels.Contact], Never>([]) + + init() { + let query = Contact.Query( + authStatus: [.friend], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + try! dbManager.getDB().fetchContactsPublisher(query) + .replaceError(with: []) + .map { $0.filter { $0.id != self.myId }} + .map { $0.sorted(by: { $0.username! < $1.username! })} + .sink { [unowned self] in + allContacts = $0 + contactsRelay.send($0) + }.store(in: &cancellables) + } + + func didSelect(contact: XXModels.Contact) { + if selectedContactsRelay.value.contains(contact) { + selectedContactsRelay.value.removeAll { $0.username == contact.username } + } else { + selectedContactsRelay.value.append(contact) + } + } + + func filter(_ text: String) { + guard text.isEmpty == false else { + contactsRelay.send(allContacts) + return + } + + contactsRelay.send( + allContacts.filter { + ($0.username ?? "").contains(text.lowercased()) + } + ) + } +} diff --git a/Sources/HUD/DotAnimation.swift b/Sources/HUD/DotAnimation.swift deleted file mode 100644 index f7bfae046b167ba1a0974723b10c359189ec5de0..0000000000000000000000000000000000000000 --- a/Sources/HUD/DotAnimation.swift +++ /dev/null @@ -1,94 +0,0 @@ -import UIKit - -final class DotAnimation: UIView { - let leftDot = UIView() - let middleDot = UIView() - let rightDot = UIView() - - var leftInvert = false - var middleInvert = false - var rightInvert = false - - var leftValue: CGFloat = 20 - var middleValue: CGFloat = 45 - var rightValue: CGFloat = 70 - - var displayLink: CADisplayLink? - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - func setColor( - _ color: UIColor = UIColor( - red: 0, - green: 188/255, - blue: 206/255, - alpha: 1.0 - ) - ) { - leftDot.backgroundColor = color - middleDot.backgroundColor = color - rightDot.backgroundColor = color - } - - private func setup() { - setupCornerRadius() - setColor() - addSubviews() - setupConstraints() - - displayLink = CADisplayLink(target: self, selector: #selector(handleAnimations)) - displayLink!.add(to: RunLoop.main, forMode: .default) - } - - private func setupCornerRadius() { - leftDot.layer.cornerRadius = 7.5 - middleDot.layer.cornerRadius = 7.5 - rightDot.layer.cornerRadius = 7.5 - } - - private func addSubviews() { - addSubview(leftDot) - addSubview(middleDot) - addSubview(rightDot) - } - - private func setupConstraints() { - leftDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.right.equalTo(middleDot.snp.left).offset(-5) - make.width.height.equalTo(15) - } - - middleDot.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.height.equalTo(15) - } - - rightDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.left.equalTo(middleDot.snp.right).offset(5) - make.width.height.equalTo(15) - } - } - - @objc private func handleAnimations() { - let factor: CGFloat = 70 - - leftInvert ? (leftValue -= 1) : (leftValue += 1) - middleInvert ? (middleValue -= 1) : (middleValue += 1) - rightInvert ? (rightValue -= 1) : (rightValue += 1) - - leftDot.layer.transform = CATransform3DMakeScale(leftValue/factor, leftValue/factor, 1) - middleDot.layer.transform = CATransform3DMakeScale(middleValue/factor, middleValue/factor, 1) - rightDot.layer.transform = CATransform3DMakeScale(rightValue/factor, rightValue/factor, 1) - - if leftValue > factor || leftValue < 10 { leftInvert.toggle() } - if middleValue > factor || middleValue < 10 { middleInvert.toggle() } - if rightValue > factor || rightValue < 10 { rightInvert.toggle() } - } -} diff --git a/Sources/HUD/ErrorView.swift b/Sources/HUD/ErrorView.swift deleted file mode 100644 index 2692ab2a53274cbb2e3d63303ad830885f4a8116..0000000000000000000000000000000000000000 --- a/Sources/HUD/ErrorView.swift +++ /dev/null @@ -1,58 +0,0 @@ -import UIKit -import Shared -import SnapKit - -final class ErrorView: UIView { - let title = UILabel() - let content = UILabel() - let stack = UIStackView() - let button = CapsuleButton() - - init(with model: HUDError) { - super.init(frame: .zero) - setup(with: model) - } - - required init?(coder: NSCoder) { nil } - - private func setup(with model: HUDError) { - layer.cornerRadius = 6 - backgroundColor = Asset.neutralWhite.color - - title.text = model.title - title.textColor = Asset.neutralBody.color - title.font = Fonts.Mulish.bold.font(size: 35.0) - title.textAlignment = .center - title.numberOfLines = 0 - - content.text = model.content - content.textColor = Asset.neutralBody.color - content.numberOfLines = 0 - content.font = Fonts.Mulish.regular.font(size: 14.0) - content.textAlignment = .center - - button.setTitle(model.buttonTitle, for: .normal) - button.setStyle(.brandColored) - - stack.axis = .vertical - - stack.addArrangedSubview(title) - stack.addArrangedSubview(content) - - if model.dismissable { - stack.addArrangedSubview(button) - } - - stack.setCustomSpacing(25, after: title) - stack.setCustomSpacing(59, after: content) - - addSubview(stack) - - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(60) - make.left.equalToSuperview().offset(57) - make.right.equalToSuperview().offset(-57) - make.bottom.equalToSuperview().offset(-35) - } - } -} diff --git a/Sources/HUD/HUD.swift b/Sources/HUD/HUD.swift deleted file mode 100644 index 207f3473c9173a3a64c8636a8373d734d207807b..0000000000000000000000000000000000000000 --- a/Sources/HUD/HUD.swift +++ /dev/null @@ -1,195 +0,0 @@ -import UIKit -import Theme -import Shared -import Combine -import SnapKit - -private enum Constants { - static let title = Localized.Hud.Error.title - static let action = Localized.Hud.Error.action -} - -public enum HUDStatus: Equatable { - case none - case on - case onTitle(String) - case onAction(String) - case error(HUDError) - - var isPresented: Bool { - switch self { - case .none: - return false - case .on, .error, .onTitle, .onAction: - return true - } - } -} - -public struct HUDError: Equatable { - var title: String - var content: String - var buttonTitle: String - var dismissable: Bool - - public init( - content: String, - title: String? = nil, - buttonTitle: String? = nil, - dismissable: Bool = true - ) { - self.content = content - self.title = title ?? Constants.title - self.buttonTitle = buttonTitle ?? Constants.action - self.dismissable = dismissable - } - - public init(with error: Error) { - self.title = Constants.title - self.buttonTitle = Constants.action - self.content = error.localizedDescription - self.dismissable = true - } -} - -public final class HUD { - private(set) var window: UIWindow? - private(set) var errorView: ErrorView? - private(set) var titleLabel: UILabel? - private(set) var animation: DotAnimation? - public var actionButton: CapsuleButton? - private var cancellables = Set<AnyCancellable>() - - private var status: HUDStatus = .none { - didSet { - if oldValue.isPresented == true && status.isPresented == true { - self.errorView = nil - self.animation = nil - self.window = nil - self.actionButton = nil - self.titleLabel = nil - - switch status { - case .on: - animation = DotAnimation() - - case .onTitle(let text): - animation = DotAnimation() - titleLabel = UILabel() - titleLabel!.text = text - - case .onAction(let title): - animation = DotAnimation() - actionButton = CapsuleButton() - actionButton!.set(style: .seeThroughWhite, title: title) - - case .error(let error): - errorView = ErrorView(with: error) - case .none: - break - } - - showWindow() - } - - if oldValue.isPresented == false && status.isPresented == true { - switch status { - case .on: - animation = DotAnimation() - - case .onTitle(let text): - animation = DotAnimation() - titleLabel = UILabel() - titleLabel!.text = text - - case .onAction(let title): - animation = DotAnimation() - actionButton = CapsuleButton() - actionButton!.set(style: .seeThroughWhite, title: title) - - case .error(let error): - errorView = ErrorView(with: error) - case .none: - break - } - - showWindow() - } - - if oldValue.isPresented == true && status.isPresented == false { - hideWindow() - } - } - } - - public init() {} - - public func update(with status: HUDStatus) { - self.status = status - } - - private func showWindow() { - window = Window() - window?.backgroundColor = UIColor.black.withAlphaComponent(0.8) - window?.rootViewController = StatusBarViewController(nil) - - if let animation = animation { - window?.addSubview(animation) - animation.setColor(.white) - animation.snp.makeConstraints { $0.center.equalToSuperview() } - } - - if let titleLabel = titleLabel { - window?.addSubview(titleLabel) - titleLabel.textAlignment = .center - titleLabel.numberOfLines = 0 - titleLabel.snp.makeConstraints { make in - make.left.equalToSuperview().offset(18) - make.center.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-18) - } - } - - if let actionButton = actionButton { - window?.addSubview(actionButton) - actionButton.snp.makeConstraints { - $0.left.equalToSuperview().offset(18) - $0.right.equalToSuperview().offset(-18) - $0.bottom.equalToSuperview().offset(-50) - } - } - - if let errorView = errorView { - window?.addSubview(errorView) - errorView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(18) - make.center.equalToSuperview() - make.right.equalToSuperview().offset(-18) - } - - errorView.button - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in hideWindow() } - .store(in: &cancellables) - } - - window?.alpha = 0.0 - window?.makeKeyAndVisible() - - UIView.animate(withDuration: 0.3) { self.window?.alpha = 1.0 } - } - - private func hideWindow() { - UIView.animate(withDuration: 0.3) { - self.window?.alpha = 0.0 - } completion: { _ in - self.cancellables.removeAll() - self.errorView = nil - self.animation = nil - self.actionButton = nil - self.titleLabel = nil - self.window = nil - } - } -} 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 042916f36061b3b111e3d49cd1deb9e6079c2ed3..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 @@ -58,7 +59,7 @@ public extension Validator where T == String { return .failure("") } - let regex = try? NSRegularExpression(pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d!@#$%^&*]{8,}$") + let regex = try? NSRegularExpression(pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d\\W_]{8,}$") guard let regex = regex, regex.firstMatch(in: passphrase, options: [], range: passphrase.fullRange()) != nil else { return .failure("") diff --git a/Sources/Integration/Callbacks.swift b/Sources/Integration/Callbacks.swift deleted file mode 100644 index ed9d917cf8ff0469015a0dfa4db869a450666bfe..0000000000000000000000000000000000000000 --- a/Sources/Integration/Callbacks.swift +++ /dev/null @@ -1,295 +0,0 @@ -import Bindings - -final class TextListener: NSObject, BindingsListenerProtocol { - let callback: (BindingsMessage?) -> () - - init(_ callback: @escaping (BindingsMessage?) -> Void) { - self.callback = callback - super.init() - } - - func hear(_ message: BindingsMessage?) { - callback(message) - } - - func name() -> String { "TEXT_LISTENER" } -} - -final class ConfirmationCallback: NSObject, BindingsAuthConfirmCallbackProtocol { - let callback: (_ partner: BindingsContact) -> () - - init(_ callback: @escaping (_ partner: BindingsContact) -> ()) { - self.callback = callback - super.init() - } - - func callback(_ partner: BindingsContact?) { - guard let partner = partner else { return } - callback(partner) - } -} - -final class RequestCallback: NSObject, BindingsAuthRequestCallbackProtocol { - let callback: (_ requestor: BindingsContact) -> () - - init(_ callback: @escaping (_ requestor: BindingsContact) -> ()) { - self.callback = callback - super.init() - } - - func callback(_ requestor: BindingsContact?) { - guard let requestor = requestor else { return } - callback(requestor) - } -} - -final class HealthCallback: NSObject, BindingsNetworkHealthCallbackProtocol { - let callback: (Bool) -> Void - - init(_ callback: @escaping (Bool) -> Void) { - self.callback = callback - super.init() - } - - func callback(_ p0: Bool) { - callback(p0) - } -} - -final class LogCallback: NSObject, BindingsLogWriterProtocol { - let callback: (String?) -> Void - - init(_ callback: @escaping (String?) -> Void) { - self.callback = callback - super.init() - } - - func log(_ p0: String?) { - callback(p0) - } -} - -final class DeliveryCallback: NSObject, BindingsMessageDeliveryCallbackProtocol { - let callback: (DeliveryResult) -> Void - - init(_ callback: @escaping (DeliveryResult) -> Void) { - self.callback = callback - super.init() - } - - func eventCallback(_ msgID: Data?, delivered: Bool, timedOut: Bool, roundResults: Data?) { - - let content = - """ - "Delivery Callback: - - Timed out: \(timedOut) - - Delivered: \(delivered) - - Message ID in base64: \(String(describing: msgID?.base64EncodedString())) - - Round results in base64: \(String(describing: roundResults?.base64EncodedString()))" - """ - - log(string: content, type: .info) - callback((msgID, delivered, timedOut, roundResults)) - } -} - -final class RoundCallback: NSObject, BindingsRoundCompletionCallbackProtocol { - let callback: (Bool) -> Void - - init(_ callback: @escaping (Bool) -> Void) { - self.callback = callback - super.init() - } - - func eventCallback(_ rid: Int, success: Bool, timedOut: Bool) { - log(string: ">>> Add/Confirm RoundCallback:\nid: \(rid)\nSuccessfull: \(success)\nTimed out: \(timedOut)", type: .info) - callback(success && !timedOut) - } -} - -final class SearchCallback: NSObject, BindingsSingleSearchCallbackProtocol { - let callback: (Result<BindingsContact, Error>) -> Void - - init(_ callback: @escaping (Result<BindingsContact, Error>) -> Void) { - self.callback = callback - super.init() - } - - func callback(_ contact: BindingsContact?, error: String?) { - if let error = error, error.count > 0 { - callback(.failure(NSError.create(error).friendly())) - return - } - - if let contact = contact { - callback(.success(contact)) - } - } -} - -final class EventCallback: NSObject, BindingsEventCallbackFunctionObjectProtocol { - let callback: (BackendEvent) -> Void - - init(_ callback: @escaping (BackendEvent) -> Void) { - self.callback = callback - super.init() - } - - func reportEvent(_ priority: Int, category: String?, evtType: String?, details: String?) { - callback((priority, category, evtType, details)) - } -} - -final class GroupRequestCallback: NSObject, BindingsGroupRequestFuncProtocol { - let callback: (BindingsGroup) -> Void - - init(_ callback: @escaping (BindingsGroup) -> Void) { - self.callback = callback - super.init() - } - - func groupRequestCallback(_ g: BindingsGroup?) { - guard let group = g else { return } - callback(group) - } -} - -final class GroupMessageCallback: NSObject, BindingsGroupReceiveFuncProtocol { - let callback: (BindingsGroupMessageReceive) -> Void - - init(_ callback: @escaping (BindingsGroupMessageReceive) -> Void) { - self.callback = callback - super.init() - } - - func groupReceiveCallback(_ msg: BindingsGroupMessageReceive?) { - guard let message = msg else { return } - callback(message) - } -} - -final class MultiLookupCallback: NSObject, BindingsMultiLookupCallbackProtocol { - let thisCallback: (BindingsContactList?, BindingsIdList?, String?) -> Void - - init(_ callback: @escaping (BindingsContactList?, BindingsIdList?, String?) -> Void) { - self.thisCallback = callback - super.init() - } - - func callback(_ Succeeded: BindingsContactList?, failed: BindingsIdList?, errors: String?) { - thisCallback(Succeeded, failed, errors) - } -} - -final class PreImageCallback: NSObject, BindingsPreimageNotificationProtocol { - let callback: (Data?, Bool) -> Void - - init(_ callback: @escaping (Data?, Bool) -> Void) { - self.callback = callback - super.init() - } - - func notify(_ identity: Data?, deleted: Bool) { - callback(identity, deleted) - } -} - -final class LookupCallback: NSObject, BindingsLookupCallbackProtocol { - let callback: (Result<BindingsContact, Error>) -> Void - - init(_ callback: @escaping (Result<BindingsContact, Error>) -> Void) { - self.callback = callback - super.init() - } - - func callback(_ contact: BindingsContact?, error: String?) { - if let error = error, !error.isEmpty { - callback(.failure(NSError.create(error).friendly())) - return - } - - if let contact = contact { - callback(.success(contact)) - } - } -} - -final class IncomingTransferCallback: NSObject, BindingsFileTransferReceiveFuncProtocol { - let callback: (Data?, String?, String?, Data?, Int, Data?) -> Void - - init(_ callback: @escaping (Data?, String?, String?, Data?, Int, Data?) -> Void) { - self.callback = callback - super.init() - } - - func receiveCallback(_ tid: Data?, fileName: String?, fileType: String?, sender: Data?, size: Int, preview: Data?) { - callback(tid, fileName, fileType, sender, size, preview) - } -} - -final class IncomingTransferProgressCallback: NSObject, BindingsFileTransferReceivedProgressFuncProtocol { - let callback: (Bool, Int, Int, Error?) -> Void - - init(_ callback: @escaping (Bool, Int, Int, Error?) -> Void) { - self.callback = callback - super.init() - } - - func receivedProgressCallback(_ completed: Bool, received: Int, total: Int, t: BindingsFilePartTracker?, err: Error?) { - callback(completed, received, total, err) - } -} - -final class OutgoingTransferProgressCallback: NSObject, BindingsFileTransferSentProgressFuncProtocol { - let callback: (Bool, Int, Int, Int, Error?) -> Void - - init(_ callback: @escaping (Bool, Int, Int, Int, Error?) -> Void) { - self.callback = callback - super.init() - } - - func sentProgressCallback(_ completed: Bool, sent: Int, arrived: Int, total: Int, t: BindingsFilePartTracker?, err: Error?) { - callback(completed, sent, arrived, total, err) - } -} - -final class UpdateBackupCallback: NSObject, BindingsUpdateBackupFuncProtocol { - let callback: (Data) -> Void - - init(_ callback: @escaping (Data) -> Void) { - self.callback = callback - super.init() - } - - func updateBackup(_ encryptedBackup: Data?) { - guard let data = encryptedBackup else { return } - callback(data) - } -} - -final class ResetCallback: NSObject, BindingsAuthResetNotificationCallbackProtocol { - let callback: (BindingsContact) -> Void - - init(_ callback: @escaping (BindingsContact) -> Void) { - self.callback = callback - super.init() - } - - func callback(_ requestor: BindingsContact?) { - guard let requestor = requestor else { return } - callback(requestor) - } -} - -final class RestoreContactsCallback: NSObject, BindingsRestoreContactsUpdaterProtocol { - let callback: (Int, Int, Int, String?) -> Void - - init(_ callback: @escaping (Int, Int, Int, String?) -> Void) { - self.callback = callback - super.init() - } - - func restoreContactsCallback(_ numFound: Int, numRestored: Int, total: Int, err: String?) { - callback(numFound, numRestored, total, err) - } -} diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift deleted file mode 100644 index 8fc201af93fe681852c550f62965029316e00811..0000000000000000000000000000000000000000 --- a/Sources/Integration/Client.swift +++ /dev/null @@ -1,236 +0,0 @@ -import Retry -import Models -import Combine -import Defaults -import Bindings -import XXModels -import Foundation - -public class Client { - @KeyObject(.inappnotifications, defaultValue: true) var inappnotifications: Bool - - let bindings: BindingsInterface - var backupManager: BackupInterface? - var dummyManager: DummyTrafficManaging? - var groupManager: GroupManagerInterface? - var userDiscovery: UserDiscoveryInterface? - var transferManager: TransferManagerInterface? - - var backup: AnyPublisher<Data, Never> { backupSubject.eraseToAnyPublisher() } - var network: AnyPublisher<Bool, Never> { networkSubject.eraseToAnyPublisher() } - var resets: AnyPublisher<Contact, Never> { resetsSubject.eraseToAnyPublisher() } - var messages: AnyPublisher<Message, Never> { messagesSubject.eraseToAnyPublisher() } - var requests: AnyPublisher<Contact, Never> { requestsSubject.eraseToAnyPublisher() } - var events: AnyPublisher<BackendEvent, Never> { eventsSubject.eraseToAnyPublisher() } - var transfers: AnyPublisher<FileTransfer, Never> { transfersSubject.eraseToAnyPublisher() } - var requestsSent: AnyPublisher<Contact, Never> { requestsSentSubject.eraseToAnyPublisher() } - var confirmations: AnyPublisher<Contact, Never> { confirmationsSubject.eraseToAnyPublisher() } - var groupRequests: AnyPublisher<(Group, [Data], String?), Never> { groupRequestsSubject.eraseToAnyPublisher() } - - private let backupSubject = PassthroughSubject<Data, Never>() - private let networkSubject = PassthroughSubject<Bool, Never>() - private let resetsSubject = PassthroughSubject<Contact, Never>() - private let requestsSubject = PassthroughSubject<Contact, Never>() - private let messagesSubject = PassthroughSubject<Message, Never>() - private let eventsSubject = PassthroughSubject<BackendEvent, Never>() - private let requestsSentSubject = PassthroughSubject<Contact, Never>() - private let confirmationsSubject = PassthroughSubject<Contact, Never>() - private let transfersSubject = PassthroughSubject<FileTransfer, Never>() - private let groupRequestsSubject = PassthroughSubject<(Group, [Data], String?), Never>() - - private var isBackupInitialization = false - private var isBackupInitializationCompleted = false - - // MARK: Lifecycle - - init( - _ bindings: BindingsInterface, - fromBackup: Bool, - email: String?, - phone: String? - ) { - self.bindings = bindings - self.isBackupInitialization = fromBackup - - do { - try registerListenersAndStart() - - if fromBackup { - try instantiateUserDiscoveryFromBackup(email: email, phone: phone) - } else { - try instantiateUserDiscovery() - } - - try instantiateTransferManager() - try instantiateDummyTrafficManager() - updatePreImage() - } catch { - log(string: error.localizedDescription, type: .error) - } - } - - public func initializeBackup(passphrase: String) { - backupManager = nil - backupManager = bindings.initializeBackup(passphrase: passphrase) { [weak backupSubject] in - backupSubject?.send($0) - } - } - - public func resumeBackup() { - backupManager = nil - backupManager = bindings.resumeBackup { [weak backupSubject] in - backupSubject?.send($0) - } - } - - // public func isBackupRunning() -> Bool { - // guard let backupManager = backupManager else { return false } - // return backupManager.isBackupRunning() - // } - - public func addJson(_ string: String) { - guard let backupManager = backupManager else { - fatalError("Trying to add json parameters to backup but no backup manager created yet") - } - - backupManager.addJson(string) - } - - public func stopListeningBackup() { - guard let backupManager = backupManager else { return } - try? backupManager.stop() - self.backupManager = nil - } - - public func restoreContacts(fromBackup backup: Data) { - var totalPendingRestoration: Int = 0 - - let report = bindings.restore( - ids: backup, - using: userDiscovery!) { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(var contact): - contact.authStatus = .requested - self.requestsSentSubject.send(contact) - print(">>> Restored \(contact.username). Setting status as requested") - case .failure(let error): - print(">>> \(error.localizedDescription)") - } - } restoreCallback: { numFound, numRestored, total, errorString in - totalPendingRestoration = total - let results = - """ - >>> Results from within closure of RestoreContacts: - - numFound: \(numFound) - - numRestored: \(numRestored) - - total: \(total) - - errorString: \(errorString) - """ - print(results) - } - - guard totalPendingRestoration > 0 else { fatalError("Total is zero, why called restore contacts?") } - - guard report.lenRestored() == totalPendingRestoration else { - print(">>> numRestored \(report.lenRestored()) is != than the total (\(totalPendingRestoration)). Going on recursion...\nnumFailed: \(report.lenFailed())\n\(report.getRestoreContactsError())") - restoreContacts(fromBackup: backup) - return - } - - isBackupInitializationCompleted = true - } - - private func registerListenersAndStart() throws { - bindings.listenNetworkUpdates { [weak networkSubject] in networkSubject?.send($0) } - - bindings.listenRequests { [weak self] in - guard let self = self else { return } - - if self.isBackupInitialization { - if self.isBackupInitializationCompleted { - self.requestsSubject.send($0) - } - } else { - self.requestsSubject.send($0) - } - } _: { [weak confirmationsSubject] in - confirmationsSubject?.send($0) - } _: { [weak resetsSubject] in - resetsSubject?.send($0) - } - - bindings.listenEvents { [weak eventsSubject] in - eventsSubject?.send($0) - } - - groupManager = try bindings.listenGroupRequests { [weak groupRequestsSubject] request, members, welcome in - groupRequestsSubject?.send((request, members, welcome)) - } groupMessages: { [weak messagesSubject] in - messagesSubject?.send($0) - } - - bindings.listenPreImageUpdates() - - try bindings.listenMessages { [weak messagesSubject] in - messagesSubject?.send($0) - } - - bindings.startNetwork() - } - - private func instantiateTransferManager() throws { - transferManager = try bindings.generateTransferManager { [weak transfersSubject] tid, name, type, sender in - - /// Someone transfered something to me - /// but I haven't received yet. I'll store an - /// IncomingTransfer object so later on I can - /// pull up whatever this contact has sent me. - /// - guard let name = name, - let type = type, - let contactId = sender else { - log(string: "Transfer of \(name ?? "nil").\(type ?? "nil") is being dismissed", type: .error) - return - } - - transfersSubject?.send( - FileTransfer( - id: tid, - contactId: contactId, - name: name, - type: type, - data: nil, - progress: 0.0, - isIncoming: true, - createdAt: Date() - ) - ) - } - } - - private func instantiateUserDiscovery() throws { - retry(max: 4, retryStrategy: .delay(seconds: 1)) { [weak self] in - guard let self = self else { return } - self.userDiscovery = try self.bindings.generateUD() - } - } - - private func instantiateUserDiscoveryFromBackup(email: String?, phone: String?) throws { - retry(max: 4, retryStrategy: .delay(seconds: 1)) { [weak self] in - guard let self = self else { return } - self.userDiscovery = try self.bindings.generateUDFromBackup(email: email, phone: phone) - } - } - - private func instantiateDummyTrafficManager() throws { - dummyManager = try bindings.generateDummyTraficManager() - } - - private func updatePreImage() { - if let defaults = UserDefaults(suiteName: "group.elixxir.messenger") { - defaults.set(bindings.getPreImages(), forKey: "preImage") - } - } -} diff --git a/Sources/Integration/Extensions.swift b/Sources/Integration/Extensions.swift deleted file mode 100644 index f2afdcb99e46f87141081f94e657b7ef5e14721e..0000000000000000000000000000000000000000 --- a/Sources/Integration/Extensions.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Models -import XXModels -import Bindings - -extension Contact { - init(with contact: BindingsContact, status: Contact.AuthStatus) { - self.init( - id: contact.getID()!, - marshaled: try! contact.marshal(), - username: contact.retrieve(fact: .username) ?? "", - email: contact.retrieve(fact: .email), - phone: contact.retrieve(fact: .phone), - nickname: nil, - photo: nil, - authStatus: status, - isRecent: false, - createdAt: Date() - ) - } -} - -extension Message { - init(with message: BindingsMessage, myId: Data) { - guard let payload = try? Payload(with: message.getPayload()!) else { fatalError() } - - self.init( - networkId: message.getID()!, - senderId: message.getSender()!, - recipientId: myId, - groupId: nil, - date: Date.fromTimestamp(Int(message.getTimestampNano())), - status: .received, - isUnread: true, - text: payload.text, - replyMessageId: payload.reply?.messageId, - roundURL: message.getRoundURL(), - fileTransferId: nil - ) - } - - init(with message: BindingsGroupMessageReceive) { - guard let payload = try? Payload(with: message.getPayload()!) else { fatalError() } - - self.init( - networkId: message.getMessageID()!, - senderId: message.getSenderID()!, - recipientId: nil, - groupId: message.getGroupID()!, - date: Date.fromTimestamp(Int(message.getTimestampNano())), - status: .received, - isUnread: true, - text: payload.text, - replyMessageId: payload.reply?.messageId, - roundURL: message.getRoundURL(), - fileTransferId: nil - ) - } -} diff --git a/Sources/Integration/FeedbackPlayer.swift b/Sources/Integration/FeedbackPlayer.swift deleted file mode 100644 index 09d4773d68576b4fd625bdc741e9e086c9a5d7ff..0000000000000000000000000000000000000000 --- a/Sources/Integration/FeedbackPlayer.swift +++ /dev/null @@ -1,38 +0,0 @@ -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) - } -} diff --git a/Sources/Integration/Implementations/Bindings.swift b/Sources/Integration/Implementations/Bindings.swift deleted file mode 100644 index a05387dda2653644a26a3bc28b40e011c6eb5e03..0000000000000000000000000000000000000000 --- a/Sources/Integration/Implementations/Bindings.swift +++ /dev/null @@ -1,613 +0,0 @@ -import Shared -import Models -import Bindings -import XXModels -import Foundation -import DependencyInjection - -public let evaluateNotification: NotificationEvaluation = BindingsNotificationsForMe - -public protocol NotificationReportProtocol { - func forMe() -> Bool - func type() -> String - func source() -> Data? -} - -public protocol NotificationManyReportProtocol { - func len() -> Int - func get(index: Int) throws -> NotificationReportProtocol -} - -extension BindingsNotificationForMeReport: NotificationReportProtocol {} - -extension BindingsManyNotificationForMeReport: NotificationManyReportProtocol { - public func get(index: Int) throws -> NotificationReportProtocol { - try get(index) - } -} - -extension BindingsClient: BindingsInterface { - public func removeContact(_ data: Data) throws { - do { - try deleteContact(data) - log(string: "Deleted a contact", type: .info) - } catch { - log(string: "Failed to delete a contact: \(error.localizedDescription)", type: .error) - throw error.friendly() - } - } - - func dumpThreads() { - log(type: .crumbs) - - var error: NSError? - let string = BindingsDumpStack(&error) - - if let error = error { - log(string: error.localizedDescription, type: .error) - return - } - - log(string: string, type: .bindings) - } - - public func resetSessionWith(_ recipient: Data) { - var int: Int = 0 - - do { - try resetSession(recipient, meMarshaled: meMarshalled, message: "", ret0_: &int) - } catch { - print(">>> \(error.localizedDescription)") - } - } - - public func verify(marshaled: Data, verifiedMarshaled: Data) throws -> Bool { - var bool: ObjCBool = false - try verifyOwnership(marshaled, verifiedMarshaled: verifiedMarshaled, ret0_: &bool) - log(string: "Onwership verification: \(bool.boolValue)", type: bool.boolValue ? .info : .error) - return bool.boolValue - } - - public func compress( - image: Data, - _ completion: @escaping(Result<Data, Error>) -> Void - ) { - var error: NSError? - let compressed = BindingsCompressJpeg(image, &error) - - guard error == nil else { - log(string: "Error when compressing jpeg: \(error!.localizedDescription)", type: .error) - completion(.failure(error!.friendly())) - return - } - - guard let compressed = compressed else { - completion(.failure(NSError.create("Image compression failed without error"))) - return - } - - let compressionRate = String(format: "%.4f", Float(compressed.count)/Float(image.count)) - log(string: "Compressed image x\(compressionRate) (\(image.count) -> \(compressed.count))", type: .info) - completion(.success(compressed)) - } - - public var hasRunningTasks: Bool { - hasRunningProcessies() - } - - public var myId: Data { - guard let user = getUser(), let contact = user.getContact(), let id = contact.getID() else { - fatalError("Couldn't get my ID") - } - - return id - } - - public var meMarshalled: Data { - guard let user = getUser(), let contact = user.getContact(), let marshal = try? contact.marshal() else { - fatalError("Couldn't get my own contact marshalled") - } - - return marshal - } - - public func getPreImages() -> String { - getPreimages(receptionId) - } - - public func meMarshalled(_ username: String, email: String?, phone: String?) -> Data { - guard let user = getUser(), - let contact = user.getContact(), - let factList = contact.getFactList() else { fatalError() } - - try! factList.add(username, factType: FactType.username.rawValue) - - if let email = email { - try! factList.add(email, factType: FactType.email.rawValue) - } - - if let phone = phone { - try! factList.add(phone, factType: FactType.phone.rawValue) - } - - return try! contact.marshal() - } - - public var receptionId: Data { - guard let user = getUser(), let recId = user.getReceptionID() else { fatalError() } - return recId - } - - public static let version: String = { - return BindingsGetVersion() - }() - - public static let new: ClientNew = BindingsNewClient - - public static let fromBackup: ClientFromBackup = BindingsNewClientFromBackup - - public static let secret: (Int) -> Data? = BindingsGenerateSecret - - public static let login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? = BindingsLogin - - public static func updateNDF( - for env: NetworkEnvironment, - _ completion: @escaping (Result<Data?, Error>) -> Void - ) { - var error: NSError? - let ndf = BindingsDownloadAndVerifySignedNdfWithUrl(env.url, env.cert, &error) - - guard error == nil else { - Self.updateNDF(for: env, completion) - return - } - - completion(.success(ndf)) - } - - /// Fetches a JSON with up-to-date error descriptions - /// then passes it to the bindings that will emit cleaner - /// errors - /// - /// - ToDo: Request status codes for errors - /// - public static func updateErrors() { - log(type: .crumbs) - - var error: NSError? - if let dbErrors = BindingsDownloadErrorDB(&error) { - var otherError: NSError? - BindingsUpdateCommonErrors(String(data: dbErrors, encoding: .utf8), &otherError) - - if let otherError = otherError { - log(string: otherError.localizedDescription, type: .error) - } - } - - if let error = error { - log(string: error.localizedDescription, type: .error) - } - } - - /// Starts the network - /// - /// If network status is != 0 it means the network is - /// not ready yet or the device is not ready. A recursion was applied - /// as a temporary solution in order to retry indefinitely - /// - /// - ToDo: Split function into smaller functions - /// - public func startNetwork() { - log(type: .crumbs) - - var error: NSError? - let status = networkFollowerStatus() - - BindingsLogLevel(1, &error) - registerErrorCallback(BindingsError()) - - guard status == 0 else { - log(string: ">>> Network is not ready yet. Let's give it a second...", type: .error) - sleep(1) - startNetwork() - return - } - - try! startNetworkFollower(10000) - log(string: ">>> Starting the network...", type: .info) - } - - /// (Tries) to stop the network - /// - /// - Warning: This function tries to stop several - /// threads and it may take some time. - /// That's why we register a background - /// task on AppDelegate.swift - /// - public func stopNetwork() { - log(type: .crumbs) - - try! stopNetworkFollower() - log(string: "Stopping the network...", type: .info) - } - - /// Extracts *user id* from a contact - /// - /// - Parameters: - /// - from: Byte array containing contact object - /// - /// - Returns: Optional byte array, if *user id* could be retrieved - /// - public func getId(from marshaled: Data) -> Data? { - log(type: .crumbs) - - var error: NSError? - let contact = BindingsUnmarshalContact(marshaled, &error) - - if let error = error { - log(string: error.localizedDescription, type: .error) - return nil - } - - return contact?.getID() - } - - public func add(_ contact: Data, from me: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) { - log(type: .crumbs) - - do { - var roundId = Int() - try requestAuthenticatedChannel(contact, meMarshaled: me, message: nil, ret0_: &roundId) - completion(.success(true)) - } catch { - log(string: error.localizedDescription, type: .error) - completion(.failure(error.friendly())) - } - } - - /// Confirms a contact request - /// - /// - Parameters: - /// - contact: Byte array containing *contact object* - /// - completion: Result callback with associated - /// values *boolean* = success && - /// !timedOut or *Error* upon throwing - /// - public func confirm(_ contact: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) { - log(type: .crumbs) - - do { - var roundId = Int() - try confirmAuthenticatedChannel(contact, ret0_: &roundId) - completion(.success(true)) - } catch { - log(string: error.localizedDescription, type: .error) - completion(.failure(error.friendly())) - } - } - - /// Sends a message over CMIX - /// - /// - Parameters: - /// - recipient: Byte array containing *user id* - /// - payload: Byte array containing *message payload* - /// - /// - Returns: Result w/ associated values - /// byte array containing *SentReport* - /// or *Error* upon throwing - /// - public func send(_ payload: Data, to recipient: Data) -> Result<E2ESendReportType, Error> { - log(type: .crumbs) - - do { - let report = try sendE2E(recipient, payload: payload, messageType: 2, parameters: nil) - - var roundIds = [Int]() - - if let roundList = report.getRoundList(), let payloadUnwrapped = try? Payload(with: payload) { - let length = roundList.len() - for index in 0..<length { - var integer: Int = 0 - do { - try roundList.get(index, ret0_: &integer) - roundIds.append(integer) - } catch { - log(string: "Error trying to inspect round list: \(error.localizedDescription)", type: .error) - } - } - - log(string: "Round ids for \(payloadUnwrapped.text.prefix(5))... = \(roundIds)", type: .info) - } - - return .success(report) - } catch { - log(string: error.localizedDescription, type: .error) - return .failure(error) - } - } - - /// Listens to the delivery of a message through a report - /// - /// - Note: Delivery actually refers to the - /// gateway, not necessarily the other end - /// received/read this message yet. - /// - /// - Parameters: - /// - report: SentReport marshalled - /// - completion: Result callback w/ associated - /// values *completed* or *Error* - /// upon throwing - /// - public func listen(report: Data, _ completion: @escaping (Result<MessageDeliveryStatus, Error>) -> Void) { - do { - try listenDelivery(of: report) { msgId, delivered, timedOut, roundResults in - let status: MessageDeliveryStatus - - if delivered == false { - let extendedLogs = - """ - Round delivery callback from wait(forMessageDelivery:) - - timedOut = \(timedOut) - - delivered = \(delivered) - """ - log(string: extendedLogs, type: .error) - log(string: extendedLogs, type: .error) - - if timedOut == true { - status = .timedout - } else { - status = .failed - } - } else { - status = .sent - } - - completion(.success(status)) - } - } catch { - completion(.failure(error)) - } - } - - public func registerNotifications(_ token: Data) throws { - let tokenString = token.map { String(format: "%02hhx", $0) }.joined() - - do { - try register(forNotifications: tokenString) - } catch { - throw error.friendly() - } - } - - /// Unregisters device token on backend - /// - /// - Throws: If when trying to unregister - /// some exception come up such as - /// timing out or user is not registered - /// - public func unregisterNotifications() throws { - log(type: .crumbs) - - do { - try unregisterForNotifications() - log(string: "Unregistered notifications", type: .info) - } catch { - log(string: error.localizedDescription, type: .error) - throw error.friendly() - } - } - - /// Checks if number of nodes already registered is enough - /// - /// Whenever the user wants to do an operation that involves - /// *User Discovery*, the app should make sure that a minimum - /// amount of nodes already know about this user - /// - /// - Throws: `NodeRegistrationError.amountIsTooLow` if - /// the ratio is below minimum (currently 85%). - /// `NodeRegistrationError.networkIsNotHealthyYet` - /// when trying to fetch registration status and - /// network is not healthy yet - /// - public func nodeRegistrationStatus() throws { - log(type: .crumbs) - - enum NodeRegistrationError: Error { - case amountIsTooLow - } - - var shortRatio: String? - - do { - let status = try getNodeRegistrationStatus() - let registered = Float(status.getRegistered()) - let total = Float(status.getTotal()) - let ratio = Float(registered/total) - - let nf = NumberFormatter() - nf.roundingMode = .down - nf.maximumFractionDigits = 2 - nf.numberStyle = .percent - shortRatio = nf.string(from: NSNumber(value: ratio)) - - guard ratio >= 0.85 else { throw NodeRegistrationError.amountIsTooLow } - log(string: "Node registration rate: \(shortRatio ?? "")", type: .info) - } catch NodeRegistrationError.amountIsTooLow { - - let string = "Node registration rate is still below 85% (\(shortRatio ?? ""))" - log(string: string, type: .error) - - let userError = "We are still establishing a secure registration with the decentralized network. Please try again in a few seconds." - - throw NSError.create(userError) - } catch { - log(string: error.localizedDescription, type: .error) - throw error - } - } - - /// Instantiates a transfer manager - /// - /// - Returns: An instance of *BindingsFileTransfer (TransferManager)* - /// - /// - Throws: `FTError.noInstance` if no error was thrown - /// but also no instance was created - /// - public func generateTransferManager( - _ callback: @escaping (Data, String?, String?, Data?) -> Void - ) throws -> TransferManagerInterface { - log(type: .crumbs) - - let incomingTransferCallback = IncomingTransferCallback { tid, name, type, sender, size, preview in - guard let tid = tid else { fatalError("An incoming transfer has no TID?") } - - callback(tid, name, type, sender) - } - - var error: NSError? - let manager = BindingsNewFileTransferManager(self, incomingTransferCallback, "", &error) - - guard let error = error else { return manager! } - throw error.friendly() - } - - public func generateDummyTraficManager() throws -> DummyTrafficManaging { - var error: NSError? - let manager = BindingsNewDummyTrafficManager(self, 5, 30000, 25000, &error) - - guard let error = error else { return manager! } - throw error.friendly() - } - - public func generateUDFromBackup(email: String?, phone: String?) throws -> UserDiscoveryInterface { - var error: NSError? - - let paramEmail = email != nil ? "E\(email!)" : nil - let paramPhone = phone != nil ? "P\(phone!)" : nil - - let udb = BindingsNewUserDiscoveryFromBackup(self, paramEmail, paramPhone, &error) - - /// Alternate udb - - guard let certPath = Bundle.module.path(forResource: "ud.elixxir.io", ofType: "crt") else { - fatalError("Couldn't retrieve cert.") - } - - guard let contactFilePath = Bundle.module.path(forResource: "udContact-test", ofType: "bin") else { - fatalError("Couldn't retrieve cert.") - } - -// try! udb!.setAlternative( -// "18.198.117.203:11420".data(using: .utf8), -// cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), -// contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) -// ) - - guard let error = error else { return udb! } - throw error.friendly() - } - - public func generateUD() throws -> UserDiscoveryInterface { - log(type: .crumbs) - - var error: NSError? - let udb = BindingsNewUserDiscovery(self, &error) - - /// Alternate udb - - guard let certPath = Bundle.module.path(forResource: "ud.elixxir.io", ofType: "crt") else { - fatalError("Couldn't retrieve cert.") - } - - guard let contactFilePath = Bundle.module.path(forResource: "udContact-test", ofType: "bin") else { - fatalError("Couldn't retrieve cert.") - } - -// try! udb!.setAlternative( -// "18.198.117.203:11420".data(using: .utf8), -// cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), -// contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) -// ) - - guard let error = error else { return udb! } - throw error.friendly() - } - - public func restore( - ids: Data, - using ud: UserDiscoveryInterface, - lookupCallback: @escaping (Result<Contact, Error>) -> Void, - restoreCallback: @escaping (Int, Int, Int, String?) -> Void - ) -> RestoreReportType { - let restoreCb = RestoreContactsCallback(restoreCallback) - - let lookupCb = LookupCallback { - switch $0 { - case .success(let contact): - lookupCallback(.success(.init(with: contact, status: .stranger))) - case .failure(let error): - lookupCallback(.failure(error)) - } - } - - return BindingsRestoreContactsFromBackup(ids, self, ud as? BindingsUserDiscovery, lookupCb, restoreCb)! - } -} - -extension BindingsContact { - - /// Scans the contact instance for a specified fact - /// - /// - Parameters: - /// - fact: enum defined in ```FactType``` - /// that specifies the type we're - /// searching - /// - /// - Note: Since GoLang does not support collections - /// We need to do this workaround *length* and - /// *get* instead of subscripting as in Swift. - /// - /// - Returns: Optional string in case we find the the fact - /// - /// - ToDo: Return a struct that contains all possible facts (?) - /// - func retrieve(fact: FactType) -> String? { - log(type: .crumbs) - - guard let factList = getFactList() else { return nil } - for index in 0..<factList.num() { - if let actualFact = factList.get(index) { - if actualFact.type() == fact.rawValue { - return String(actualFact.stringify().dropFirst()) - } - } - } - return nil - } -} - -extension BindingsSendReport: E2ESendReportType { - public var marshalled: Data { try! marshal() } - public var timestamp: Int64 { getTimestampNano() } - public var uniqueId: Data? { getMessageID() } - public var roundURL: String { getRoundURL() } -} - -public protocol DummyTrafficManaging { - var status: Bool { get } - func setStatus(status: Bool) -} - -extension BindingsDummyTraffic: DummyTrafficManaging { - public var status: Bool { - getStatus() - } - - public func setStatus(status: Bool) { - try? setStatus(status) - } -} - -extension BindingsBackup: BackupInterface {} - -extension BindingsRestoreContactsReport: RestoreReportType {} diff --git a/Sources/Integration/Implementations/GroupManager.swift b/Sources/Integration/Implementations/GroupManager.swift deleted file mode 100644 index 4b3dc4ef2e03abff18cea433fc30a1f2ed424b33..0000000000000000000000000000000000000000 --- a/Sources/Integration/Implementations/GroupManager.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Models -import XXModels -import Bindings - -extension BindingsGroupChat: GroupManagerInterface { - public func send(_ payload: Data, to group: Data) -> Result<(Int64, Data?, String), Error> { - log(type: .crumbs) - - do { - let report = try send(group, message: payload) - return .success(( - report.getRoundID(), - report.getMessageID(), - report.getRoundURL() - )) - } catch { - return .failure(error) - } - } - - public func create( - me: Data, - name: String, - welcome: String?, - with ids: [Data], - _ completion: @escaping (Result<Group, Error>) -> Void - ) { - log(type: .crumbs) - - let list = BindingsIdList() - ids.forEach { try? list.add($0) } - - var welcomeData: Data? - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - if let welcome = welcome { - welcomeData = welcome.data(using: .utf8) - } - - let report = self.makeGroup(list, name: name.data(using: .utf8), message: welcomeData) - - if let status = report?.getStatus() { - switch status { - case 0: - completion(.failure(NSError.create("An error occurred before any requests could be sent"))) - return - case 1, 2: - // 1. All requests failed to send - // 2. Some requests failed and some succeeded - - if let id = report?.getGroup()?.getID() { - do { - try self.resendRequest(id) - fallthrough - } catch { - completion(.failure(error)) - return - } - } - case 3: - // All good - guard let group = report?.getGroup() else { - let errorContent = "Couldn't get report from group, although status was 3." - completion(.failure(NSError.create(errorContent))) - log(string: errorContent, type: .error) - return - } - - completion(.success(.init( - id: group.getID()!, - name: name, - leaderId: me, - createdAt: Date(), - authStatus: .participating, - serialized: group.serialize()! - ))) - return - default: - break - } - } - } - } - - public func join(_ serializedGroup: Data) throws { - try joinGroup(serializedGroup) - } - - public func leave(_ groupId: Data) throws { - try leaveGroup(groupId) - } -} diff --git a/Sources/Integration/Implementations/TransferManager.swift b/Sources/Integration/Implementations/TransferManager.swift deleted file mode 100644 index 8c38a91fcc20eb289d83d0a62d9efb71592f2554..0000000000000000000000000000000000000000 --- a/Sources/Integration/Implementations/TransferManager.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Models -import Bindings -import Foundation - -extension BindingsFileTransfer: TransferManagerInterface { - - public func endTransferUpload( - with TID: Data - ) throws { - try closeSend(TID) - } - - public func listenUploadFromTransfer( - with id: Data, - _ callback: @escaping (Bool, Int, Int, Int, Error?) -> Void - ) throws { - let cb = OutgoingTransferProgressCallback { completed, sent, arrived, total, error in - callback(completed, sent, arrived, total, error) - } - - try registerSendProgressCallback(id, progressFunc: cb, periodMS: 1000) - } - - public func listenDownloadFromTransfer( - with id: Data, - _ callback: @escaping (Bool, Int, Int, Error?) -> Void - ) throws { - let cb = IncomingTransferProgressCallback { completed, received, total, error in - callback(completed, received, total, error) - } - - try registerReceiveProgressCallback(id, progressFunc: cb, periodMS: 1000) - } - - public func downloadFileFromTransfer( - with id: Data - ) throws -> Data { - try receive(id) - } - - public func uploadFile( - url: URL, - to recipient: Data, - _ callback: @escaping (Bool, Int, Int, Int, Error?) -> Void - ) throws -> Data { - let cb = OutgoingTransferProgressCallback { completed, sent, arrived, total, error in - callback(completed, sent, arrived, total, error) - } - - guard let file = try? Data(contentsOf: url) else { fatalError() } - - return try send( - url.lastPathComponent, - fileType: url.pathExtension, - fileData: file, - recipientID: recipient, - retry: 1, - preview: nil, - progressFunc: cb, - periodMS: 1000 - ) - } -} diff --git a/Sources/Integration/Implementations/UserDiscovery.swift b/Sources/Integration/Implementations/UserDiscovery.swift deleted file mode 100644 index 56c9b4de349c6cc807e5a1e7e78a2755ffe7715e..0000000000000000000000000000000000000000 --- a/Sources/Integration/Implementations/UserDiscovery.swift +++ /dev/null @@ -1,166 +0,0 @@ -import Retry -import Models -import XXModels -import Bindings -import Foundation - -extension BindingsUserDiscovery: UserDiscoveryInterface { - public func lookup(forUserId: Data, _ completion: @escaping (Result<Contact, Error>) -> Void) { - let callback = LookupCallback { - switch $0 { - case .success(let contact): - completion(.success(.init(with: contact, status: .stranger))) - case .failure(let error): - completion(.failure(error)) - } - } - - retry(max: 10, retryStrategy: .delay(seconds: 1)) { [weak self] in - guard let self = self else { return } - try self.lookup(forUserId, callback: callback, timeoutMS: 20000) - }.finalCatch { error in - log(string: "UD.lookup 4E2E failed:\n\(error.localizedDescription)", type: .error) - completion(.failure(error.friendly())) - } - } - - public func lookup(idList: [Data], _ completion: @escaping (Result<[Contact], Error>) -> Void) { - let list = BindingsIdList() - idList.forEach { try? list.add($0) } - - let callback = MultiLookupCallback { [weak self] contactList, idList, error in - guard let self = self else { return } - - if let error = error, error.count > 2 { - log(string: "UD.lookup group failed: \(error)", type: .error) - completion(.failure(NSError.create(error).friendly())) - return - } - - guard let contacts = contactList else { return } - let count = contacts.len() - var results = [Contact]() - - for index in 0..<count { - guard let contact = try? contacts.get(index), - let marshal = try? contact.marshal(), - ((try? self.retrieve(from: marshal, fact: .username) != nil) != nil) else { - log(string: "Skipping", type: .error); continue - } - - results.append(Contact(with: contact, status: .stranger)) - } - - completion(.success(results)) - } - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - do { - try self.multiLookup(list, callback: callback, timeoutMS: 30000) - } catch { - log(string: "UD.lookup group failed: \(error.localizedDescription)", type: .error) - completion(.failure(error.friendly())) - } - } - } - - public func deleteMyself(_ username: String) throws { - log(type: .crumbs) - - do { - try removeUser("U\(username)") - } catch { - throw error.friendly() - } - } - - public func register(_ fact: FactType, value: String, _ completion: @escaping (Result<String?, Error>) -> Void) { - log(type: .crumbs) - - if fact == .username { - do { - try register(value) - completion(.success(value)) - return - } catch { - completion(.failure(error.friendly())) - return - } - } - - var error: NSError? - let bindingsFact = BindingsNewFact(fact.rawValue, value, &error) - - if let error = error { - completion(.failure(error.friendly())) - return - } - - var otherError: NSError? - let confirmationId = addFact(bindingsFact?.stringify(), error: &otherError) - - if let otherError = otherError { - completion(.failure(otherError)) - return - } - - completion(.success(confirmationId)) - } - - public func confirm(code: String, id: String) throws { - log(type: .crumbs) - - do { - try confirmFact(id, code: code) - } catch { - throw error.friendly() - } - } - - public func retrieve( - from marshaled: Data, - fact: FactType - ) throws -> String? { - - log(type: .crumbs) - - var error: NSError? - let contact = BindingsUnmarshalContact(marshaled, &error) - if let err = error { - throw err.friendly() - } - - return contact?.retrieve(fact: fact) - } - - public func remove(_ fact: String) throws { - log(type: .crumbs) - - do { - try removeFact(fact) - } catch { - throw error.friendly() - } - } - - public func search(fact: String, _ completion: @escaping (Result<Contact, Error>) -> Void) throws { - log(type: .crumbs) - - let callback = SearchCallback { - switch $0 { - case .success(let contact): - completion(.success(Contact(with: contact, status: .stranger))) - case .failure(let error): - completion(.failure(error)) - } - } - - do { - try searchSingle(fact, callback: callback, timeoutMS: 50000) - } catch { - throw error.friendly() - } - } -} diff --git a/Sources/Integration/Interfaces/BindingsInterface.swift b/Sources/Integration/Interfaces/BindingsInterface.swift deleted file mode 100644 index b2eef8f94ba3283557530f35ca68e83a2b8dba62..0000000000000000000000000000000000000000 --- a/Sources/Integration/Interfaces/BindingsInterface.swift +++ /dev/null @@ -1,174 +0,0 @@ -import Models -import Combine -import XXModels -import Foundation - -public enum MessageDeliveryStatus { - case sent - case failed - case timedout -} - -public typealias DeliveryResult = (Data?, Bool, Bool, Data?) - -public typealias BackendEvent = (Int, String?, String?, String?) - -public typealias ClientNew = (String?, String?, Data?, String?, NSErrorPointer) -> Bool - -public typealias ClientFromBackup = (String?, String?, Data?, Data?, Data?, NSErrorPointer) -> Data? - -public typealias NotificationEvaluation = (String?, String?, NSErrorPointer) -> NotificationManyReportProtocol? - -public protocol E2ESendReportType { - var timestamp: Int64 { get } - var uniqueId: Data? { get } - var marshalled: Data { get } - var roundURL: String { get } -} - -public protocol BackupInterface { - func stop() throws - func addJson(_: String?) - func isBackupRunning() -> Bool -} - -public protocol RestoreReportType { - func lenFailed() -> Int - func lenRestored() -> Int - func getErrorAt(_: Int) -> String - func getFailedAt(_: Int) -> Data? - func getRestoreContactsError() -> String - func getRestoredAt(_: Int) -> Data? -} - -public protocol BindingsInterface { - - // MARK: Properties - - var myId: Data { get } - - var hasRunningTasks: Bool { get } - - var receptionId: Data { get } - - var meMarshalled: Data { get } - - func meMarshalled(_: String, email: String?, phone: String?) -> Data - - func verify(marshaled: Data, verifiedMarshaled: Data) throws -> Bool - - func nodeRegistrationStatus() throws - - // MARK: Static - - static func updateErrors() - - static var version: String { get } - - static var secret: (Int) -> Data? { get } - - static var login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? { get } - - static var new: ClientNew { get } - - static var fromBackup: ClientFromBackup { get } - - static func updateNDF(for: NetworkEnvironment, _: @escaping (Result<Data?, Error>) -> Void) - - // MARK: Network - - func startNetwork() - - func stopNetwork() - - func replayRequests() - - // MARK: Contacts - - func getId(from: Data) -> Data? - - func confirm(_: Data, _: @escaping (Result<Bool, Error>) -> Void) - - func add(_: Data, from: Data, _: @escaping (Result<Bool, Error>) -> Void) - - // MARK: Messages - - func send(_ payload: Data, to recipient: Data) -> Result<E2ESendReportType, Error> - - func compress(image: Data, _: @escaping(Result<Data, Error>) -> Void) - - func resetSessionWith(_: Data) - - func listen( - report: Data, - _: @escaping (Result<MessageDeliveryStatus, Error>) -> Void - ) - - func listenRound( - id: Int, - _: @escaping (Result<Bool, Error>) -> Void - ) - - // MARK: Notifications - - func getPreImages() -> String - - func registerNotifications(_ token: Data) throws - - func unregisterNotifications() throws - - func generateDummyTraficManager() throws -> DummyTrafficManaging - - // MARK: UD - - func generateUD() throws -> UserDiscoveryInterface - - func generateUDFromBackup(email: String?, phone: String?) throws -> UserDiscoveryInterface - - // MARK: FileTransfer - - func generateTransferManager( - _: @escaping (Data, String?, String?, Data?) -> Void - ) throws -> TransferManagerInterface - - // MARK: Listeners - - static func listenLogs() - - func listenEvents(_: @escaping (BackendEvent) -> Void) - - func listenMessages(_: @escaping (Message) -> Void) throws - - func initializeBackup( - passphrase: String, - callback: @escaping (Data) -> Void - ) -> BackupInterface - - func resumeBackup( - callback: @escaping (Data) -> Void - ) -> BackupInterface - - func listenRequests( - _ requests: @escaping (Contact) -> Void, - _ confirmations: @escaping (Contact) -> Void, - _ resets: @escaping (Contact) -> Void - ) - - func listenPreImageUpdates() - - func listenGroupRequests( - _: @escaping (Group, [Data], String?) -> Void, - groupMessages: @escaping (Message) -> Void - ) throws -> GroupManagerInterface? - - func listenNetworkUpdates(_: @escaping (Bool) -> Void) - - func removeContact(_ data: Data) throws - - func restore( - ids: Data, - using: UserDiscoveryInterface, - lookupCallback: @escaping (Result<Contact, Error>) -> Void, - restoreCallback: @escaping (Int, Int, Int, String?) -> Void - ) -> RestoreReportType -} diff --git a/Sources/Integration/Interfaces/GroupManagerInterface.swift b/Sources/Integration/Interfaces/GroupManagerInterface.swift deleted file mode 100644 index dcddfa9e39efe1f67d33a172e28c5cf84b595267..0000000000000000000000000000000000000000 --- a/Sources/Integration/Interfaces/GroupManagerInterface.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Models -import XXModels -import Foundation - -public protocol GroupManagerInterface { - - func join(_: Data) throws - - func leave(_: Data) throws - - func send(_: Data, to: Data) -> Result<(Int64, Data?, String), Error> - - func create(me: Data, name: String, welcome: String?, with: [Data], _: @escaping (Result<Group, Error>) -> Void) -} diff --git a/Sources/Integration/Interfaces/TransferManagerInterface.swift b/Sources/Integration/Interfaces/TransferManagerInterface.swift deleted file mode 100644 index b95d1ee34d2cd154192f74b336a44d6deed9dc69..0000000000000000000000000000000000000000 --- a/Sources/Integration/Interfaces/TransferManagerInterface.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -public protocol TransferManagerInterface { - func endTransferUpload( - with TID: Data - ) throws - - func listenUploadFromTransfer( - with: Data, - _: @escaping (Bool, Int, Int, Int, Error?) -> Void - ) throws - - func listenDownloadFromTransfer( - with: Data, - _: @escaping (Bool, Int, Int, Error?) -> Void - ) throws - - func downloadFileFromTransfer( - with: Data - ) throws -> Data - - func uploadFile( - url: URL, - to: Data, - _: @escaping (Bool, Int, Int, Int, Error?) -> Void - ) throws -> Data -} diff --git a/Sources/Integration/Interfaces/UserDiscoveryInterface.swift b/Sources/Integration/Interfaces/UserDiscoveryInterface.swift deleted file mode 100644 index ded311ecdf19e7b179b2f91d325588f57a33ff2d..0000000000000000000000000000000000000000 --- a/Sources/Integration/Interfaces/UserDiscoveryInterface.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Models -import XXModels -import Foundation - -public struct LookupResult { - public let id: Data - public let username: String -} - -public protocol UserDiscoveryInterface { - - func remove(_: String) throws - - func deleteMyself(_: String) throws - - func confirm(code: String, id: String) throws - - func retrieve(from: Data, fact: FactType) throws -> String? - - func lookup(forUserId: Data, _: @escaping (Result<Contact, Error>) -> Void) - - func search(fact: String, _: @escaping (Result<Contact, Error>) -> Void) throws - - func lookup(idList: [Data], _: @escaping (Result<[Contact], Error>) -> Void) - - func register(_: FactType, value: String, _: @escaping (Result<String?, Error>) -> Void) -} diff --git a/Sources/Integration/Listeners.swift b/Sources/Integration/Listeners.swift deleted file mode 100644 index 7ab877603cb879c6d660384a0a596e259a8856e3..0000000000000000000000000000000000000000 --- a/Sources/Integration/Listeners.swift +++ /dev/null @@ -1,151 +0,0 @@ -import Models -import Shared -import os.log -import Combine -import XXModels -import Bindings -import Foundation - -public extension BindingsClient { - static func listenLogs() { - let callback = LogCallback { log(string: $0 ?? "", type: .bindings) } - BindingsRegisterLogWriter(callback) - } - - func listenPreImageUpdates() { - let callback = PreImageCallback { [weak self] _, _ in - if let defaults = UserDefaults(suiteName: "group.elixxir.messenger") { - let preImage = self?.getPreImages() - defaults.set(preImage, forKey: "preImage") - } - } - - registerPreimageCallback(receptionId, pin: callback) - } - - func initializeBackup(passphrase: String, callback: @escaping (Data) -> Void) -> BackupInterface { - var error: NSError? - os_signpost(.begin, log: logHandler, name: "Encrypting", "Calling BindingsInitializeBackup") - let backup = BindingsInitializeBackup(passphrase, UpdateBackupCallback(callback), self, &error) - os_signpost(.end, log: logHandler, name: "Encrypting", "Finished BindingsInitializeBackup") - return backup! - } - - func resumeBackup(callback: @escaping (Data) -> Void) -> BackupInterface { - var error: NSError? - let backup = BindingsResumeBackup(UpdateBackupCallback(callback), self, &error) - return backup! - } - - func listenMessages(_ callback: @escaping (Message) -> Void) throws { - let zeroBytes = [UInt8](repeating: 0, count: 33) - - let listener = TextListener { bindingsMessage in - guard let message = bindingsMessage else { return } - let domainModel = Message(with: message, myId: self.myId) - callback(domainModel) - } - - _ = try! registerListener(Data(zeroBytes), msgType: 2, listener: listener) - } - - func listenRequests( - _ requests: @escaping (Contact) -> Void, - _ confirmations: @escaping (Contact) -> Void, - _ resets: @escaping (Contact) -> Void - ) { - let resetCallback = ResetCallback { resets(Contact(with: $0, status: .friend)) } - let confirmCallback = ConfirmationCallback { confirmations(Contact(with: $0, status: .friend)) } - let requestCallback = RequestCallback { requests(Contact(with: $0, status: .verificationInProgress)) } - registerAuthCallbacks(requestCallback, confirm: confirmCallback, reset: resetCallback) - } - - func listenNetworkUpdates(_ callback: @escaping (Bool) -> Void) { - registerNetworkHealthCB(HealthCallback(callback)) - } - - func listenEvents(_ completion: @escaping (BackendEvent) -> Void) { - do { - try registerEventCallback("EventListener", myObj: EventCallback(completion)) - } catch { - log(string: ">>> Event listener failed: \(error.localizedDescription)", type: .error) - } - } - - func listenRound(id: Int, _ completion: @escaping (Result<Bool, Error>) -> Void) { - let callback = RoundCallback { completion(.success($0)) } - - do { - try wait(forRoundCompletion: id, rec: callback, timeoutMS: 15000) - } catch { - completion(.failure(error)) - } - } - - func listenDelivery(of report: Data, _ completion: @escaping (DeliveryResult) -> Void) throws { - let callback = DeliveryCallback { completion($0) } - - var roundIds = [Int]() - - var unmarshalError: NSError? - - if let unmarshaled = BindingsUnmarshalSendReport(report, &unmarshalError), - let roundList = unmarshaled.getRoundList() { - let length = roundList.len() - for index in 0..<length { - var integer: Int = 0 - do { - try roundList.get(index, ret0_: &integer) - roundIds.append(integer) - } catch { - log(string: ">>> Error inspecting round list:\n\(error.localizedDescription)", type: .error) - } - } - } - - try! wait(forMessageDelivery: report, mdc: callback, timeoutMS: 30000) - } - - func listenGroupRequests( - _ groupRequests: @escaping (Group, [Data], String?) -> Void, - groupMessages: @escaping (Message) -> Void - ) throws -> GroupManagerInterface? { - var error: NSError? - - let requestCallback = GroupRequestCallback { - guard let id = $0.getID(), - let name = $0.getName(), - let serialize = $0.serialize(), - let memberList = $0.getMembership() else { return } - - var members = [Data]() - - var welcomeMessage: String? - - if let welcomeData = $0.getInitMessage() { - welcomeMessage = String(data: welcomeData, encoding: .utf8) - } - - for index in 0..<memberList.len() { - guard let member = try? memberList.get(index), - let memberId = member.getID() else { continue } - members.append(memberId) - } - - groupRequests(.init( - id: id, - name: String(data: name, encoding: .utf8)!, - leaderId: members.first!, - createdAt: Date(), - authStatus: .pending, - serialized: serialize - ), members, welcomeMessage) - } - - let messageCallback = GroupMessageCallback { groupMessages(Message(with: $0)) } - let groupManager = BindingsNewGroupManager(self, requestCallback, messageCallback, &error) - - guard let error = error else { return groupManager } - fatalError(error.localizedDescription) - } -} diff --git a/Sources/Integration/Logging.swift b/Sources/Integration/Logging.swift deleted file mode 100644 index c37e60b32c1261112930efe2a84248a74290fe3b..0000000000000000000000000000000000000000 --- a/Sources/Integration/Logging.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Bindings -import XXLogger -import CrashReporting -import DependencyInjection -import Foundation -import os - -let oslogger = Logger(subsystem: "logs_xxmessenger", category: "Logging.swift") - -final class BindingsError: NSObject, BindingsClientErrorProtocol { - func report(_ source: String?, message: String?, trace: String?) { - var content = "" - - content += String(describing: source) + "\n" - content += String(describing: message) + "\n" - content += String(describing: trace) - - log(string: content, type: .error) - } -} - -extension Error { - func friendly() -> NSError { - log(string: ">>> Switching to friendly error from: \(localizedDescription)", type: .error) - - let error = BindingsErrorStringToUserFriendlyMessage(localizedDescription) - if error.hasPrefix("UR") { - let crashReporter = try! DependencyInjection.Container.shared.resolve() as CrashReporter - crashReporter.sendError(self as NSError) - return NSError.create("Unexpected error. Please try again") - } else { - return NSError.create(error) - } - } -} - -enum LogType { - case info - case error - case crumbs - case bindings - - var icon: String { - switch self { - case .error: - return "🟥" - case .crumbs: - return "ðŸž" - case .bindings: - return "âš™ï¸" - case .info: - return "✅" - } - } -} - -func log( - string: String? = nil, - type: LogType, - function: String = #function, - file: String = #file, - line: Int = #line -) { - var trimmedFile = "" - if let index = file.lastIndex(of: "/") { - let afterEqualsTo = String(file.suffix(from: index).dropFirst()) - trimmedFile = afterEqualsTo - } - - let content = "\(type.icon) \(function) @\(trimmedFile):\(line) \(string ?? "")" - let logger = try! DependencyInjection.Container.shared.resolve() as XXLogger - - switch type { - case .info: - logger.info(content) - oslogger.info("\(content)") - case .error: - logger.error(content) - oslogger.error("\(content)") - case .crumbs: - logger.debug(content) - case .bindings: - logger.warning(content) - } -} diff --git a/Sources/Integration/Mocks/BindingsMock.swift b/Sources/Integration/Mocks/BindingsMock.swift deleted file mode 100644 index b4eb5c0bdb65e64130ef0f1c6e85e01993fb8bf4..0000000000000000000000000000000000000000 --- a/Sources/Integration/Mocks/BindingsMock.swift +++ /dev/null @@ -1,302 +0,0 @@ -import Models -import Combine -import XXModels -import Foundation - -public final class BindingsMock: BindingsInterface { - private var cancellables = Set<AnyCancellable>() - private let requestsSubject = PassthroughSubject<Contact, Never>() - private let groupRequestsSubject = PassthroughSubject<Group, Never>() - private let confirmationsSubject = PassthroughSubject<Contact, Never>() - - public var hasRunningTasks: Bool { - false - } - - public func replayRequests() {} - - public var myId: Data { - "MOCK_USER".data(using: .utf8)! - } - - public var receptionId: Data { - "RECEPTION_ID".data(using: .utf8)! - } - - public var meMarshalled: Data { - "MOCK_USER_MARSHALLED".data(using: .utf8)! - } - - public static var secret: (Int) -> Data? = { - "\($0)".data(using: .utf8)! - } - - public func verify(marshaled: Data, verifiedMarshaled: Data) throws -> Bool { - true - } - - public static let version: String = "MOCK" - - public static var new: ClientNew = { _,_,_,_,_ in true } - - public static var fromBackup: ClientFromBackup = { _,_,_,_,_,_ in Data() } - - public static var login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? = { _,_,_,_ in BindingsMock() } - - public func meMarshalled(_: String, email: String?, phone: String?) -> Data { - meMarshalled - } - - public func startNetwork() {} - - public func stopNetwork() {} - - public static func listenLogs() {} - - public static func updateErrors() {} - - public func listenPreImageUpdates() {} - - public func getPreImages() -> String { "" } - - public func nodeRegistrationStatus() throws {} - - public func getId(from: Data) -> Data? { from } - - public func unregisterNotifications() throws {} - - public func registerNotifications(_: Data) throws {} - - public func compress(image: Data, _: @escaping(Result<Data, Error>) -> Void) {} - - public func generateUD() throws -> UserDiscoveryInterface { UserDiscoveryMock() } - - public func generateUDFromBackup( - email: String?, - phone: String? - ) throws -> UserDiscoveryInterface { UserDiscoveryMock() } - - public func generateTransferManager( - _: @escaping (Data, String?, String?, Data?) -> Void - ) throws -> TransferManagerInterface { - TransferManagerMock() - } - - public func listenEvents(_: @escaping (BackendEvent) -> Void) {} - - public func listenMessages(_: @escaping (Message) -> Void) throws {} - - - public func initializeBackup( - passphrase: String, - callback: @escaping (Data) -> Void - ) -> BackupInterface { BindingsBackupMock() } - - public func resumeBackup( - callback: @escaping (Data) -> Void - ) -> BackupInterface { BindingsBackupMock() } - - public func listenBackups(_: @escaping (Data) -> Void) -> BackupInterface { fatalError() } - - public func listenNetworkUpdates(_: @escaping (Bool) -> Void) {} - - public func confirm(_: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) { - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { - completion(.success(true)) - } - } - - public func listenRound(id: Int, _: @escaping (Result<Bool, Error>) -> Void) {} - - public func add(_ contact: Data, from: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) { - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in - if contact == Contact.georgeDiscovered.marshaled { - completion(.success(true)) - } else { - completion(.success(false)) - return - } - - self?.requestsSubject.send(.carlRequested) - self?.requestsSubject.send(.angelinaRequested) - self?.requestsSubject.send(.elonRequested) - - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in - self?.confirmationsSubject.send(.georgeDiscovered) - } - } - } - - public func send( - _ payload: Data, - to recipient: Data - ) -> Result<E2ESendReportType, Error> { - .success(MockE2ESendReport()) - } - - public func listen( - report: Data, - _ completion: @escaping (Result<MessageDeliveryStatus, Error>) -> Void - ) { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - completion(.success(.sent)) - } - } - - public func generateDummyTraficManager() throws -> DummyTrafficManaging { - MockDummyManager() - } - - public func removeContact(_ data: Data) throws {} - - public func resetSessionWith(_: Data) {} - - public func listenRequests( - _ requests: @escaping (Contact) -> Void, - _ confirmations: @escaping (Contact) -> Void, - _ resets: @escaping (Contact) -> Void - ) { - requestsSubject.sink(receiveValue: requests).store(in: &cancellables) - confirmationsSubject.sink(receiveValue: confirmations).store(in: &cancellables) - } - - public func listenGroupRequests( - _ groupRequests: @escaping (Group, [Data], String?) -> Void, - groupMessages: @escaping (Message) -> Void - ) throws -> GroupManagerInterface? { - groupRequestsSubject - .sink { groupRequests($0, [], nil) } - .store(in: &cancellables) - - return GroupManagerMock() - } - - public func restore( - ids: Data, - using ud: UserDiscoveryInterface, - lookupCallback: @escaping (Result<Contact, Error>) -> Void, - restoreCallback: @escaping (Int, Int, Int, String?) -> Void - ) -> RestoreReportType { - fatalError() - } - - public static func updateNDF(for: NetworkEnvironment, _ completion: @escaping (Result<Data?, Error>) -> Void) { - completion(.success(Data())) - } -} - -extension Group { - static let mockGroup = Group( - id: "mockGroup".data(using: .utf8)!, - name: "Bruno's birthday 6/1", - leaderId: "mockGroupLeader".data(using: .utf8)!, - createdAt: Date.distantPast, - authStatus: .pending, - serialized: "mockGroup".data(using: .utf8)! - ) -} - -extension Contact { - static func mock(_ count: Int = 1) -> [Contact] { - var mocks = [Contact]() - - for n in 0..<count { - mocks.append( - .init( - id: "brad\(n)".data(using: .utf8)!, - marshaled: "brad\(n)".data(using: .utf8)!, - username: "brad\(n)", - email: "brad\(n)@xx.io", - phone: "819820212\(n)5BR", - nickname: nil, - photo: nil, - authStatus: .verified, - isRecent: false, - createdAt: Date() - )) - } - - return mocks - } - - static let angelinaRequested = Contact( - id: "angelinajolie".data(using: .utf8)!, - marshaled: "angelinajolie".data(using: .utf8)!, - username: "angelinajolie", - email: nil, - phone: nil, - nickname: "Angelica Jolie", - photo: nil, - authStatus: .verificationInProgress, - isRecent: false, - createdAt: Date() - ) - - static let carlRequested = Contact( - id: "carlsagan".data(using: .utf8)!, - marshaled: "carlsagan".data(using: .utf8)!, - username: "carlsagan", - email: "carl@jpl.nasa", - phone: "81982022244BR", - nickname: "Carl Sagan", - photo: nil, - authStatus: .verified, - isRecent: false, - createdAt: Date.distantPast - ) - - static let elonRequested = Contact( - id: "elonmusk".data(using: .utf8)!, - marshaled: "elonmusk".data(using: .utf8)!, - username: "elonmusk", - email: "elon@tesla.com", - phone: nil, - nickname: "Elon Musk", - photo: nil, - authStatus: .verified, - isRecent: false, - createdAt: Date.distantPast - ) - - static let georgeDiscovered = Contact( - id: "georgebenson74".data(using: .utf8)!, - marshaled: "georgebenson74".data(using: .utf8)!, - username: "bruno_muniz74", - email: "george@xx.io", - phone: "81987022255BR", - nickname: "Bruno Muniz", - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - ) -} - -public struct MockE2ESendReport: E2ESendReportType { - public var timestamp: Int64 { 1 } - public var marshalled: Data { Data() } - public var uniqueId: Data? { Data() } - public var roundURL: String { "https://www.google.com.br" } -} - -public struct MockDummyManager: DummyTrafficManaging { - public var status: Bool { true } - - public func setStatus(status: Bool) { - print("Dummy manager status set to \(status)") - } -} - -public struct BindingsBackupMock: BackupInterface { - public func stop() throws { - // TODO - } - - public func addJson(_: String?) { - // TODO - } - - public func isBackupRunning() -> Bool { - return true - } -} diff --git a/Sources/Integration/Mocks/GroupManagerMock.swift b/Sources/Integration/Mocks/GroupManagerMock.swift deleted file mode 100644 index 137cfca69c25a188f20c60af867befc3d79a04cf..0000000000000000000000000000000000000000 --- a/Sources/Integration/Mocks/GroupManagerMock.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Models -import XXModels -import Foundation - -final class GroupManagerMock: GroupManagerInterface { - func join(_: Data) throws {} - - func leave(_: Data) throws {} - - func send(_: Data, to: Data) -> Result<(Int64, Data?, String), Error> { - .success((1, nil, "https://www.google.com.br")) - } - - func create( - me: Data, - name: String, - welcome: String?, - with: [Data], - _: @escaping (Result<Group, Error>) -> Void - ) {} -} diff --git a/Sources/Integration/Mocks/TransferManagerMock.swift b/Sources/Integration/Mocks/TransferManagerMock.swift deleted file mode 100644 index 9b87da5d214d5cf0dcc5d39cdd88a979ff1a6e76..0000000000000000000000000000000000000000 --- a/Sources/Integration/Mocks/TransferManagerMock.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -final class TransferManagerMock: TransferManagerInterface { - func endTransferUpload( - with TID: Data - ) throws {} - - func listenDownloadFromTransfer( - with: Data, - _: @escaping (Bool, Int, Int, Error?) -> Void - ) throws { - fatalError() - } - - func listenUploadFromTransfer( - with: Data, - _: @escaping (Bool, Int, Int, Int, Error?) -> Void - ) throws {} - - func downloadFileFromTransfer( - with: Data - ) throws -> Data { - fatalError() - } - - func uploadFile( - url: URL, - to: Data, - _: @escaping (Bool, Int, Int, Int, Error?) -> Void - ) throws -> Data { - Data() - } -} diff --git a/Sources/Integration/Mocks/UserDiscoveryMock.swift b/Sources/Integration/Mocks/UserDiscoveryMock.swift deleted file mode 100644 index bb1a8f2440ce0baf65bd8eb5dff4ef305babb842..0000000000000000000000000000000000000000 --- a/Sources/Integration/Mocks/UserDiscoveryMock.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Models -import XXModels -import Foundation - -final class UserDiscoveryMock: UserDiscoveryInterface { - - func remove(_ fact: String) throws {} - - func deleteMyself(_ username: String) throws {} - - func confirm(code: String, id: String) throws {} - - func lookup(idList: [Data], _: @escaping (Result<[Contact], Error>) -> Void) {} - - func retrieve(from: Data, fact: FactType) throws -> String? { fact.description } - - func search(fact: String, _ completion: @escaping (Result<Contact, Error>) -> Void) throws { - completion(.success(.georgeDiscovered)) - } - - func register(_: FactType, value: String, _ completion: @escaping (Result<String?, Error>) -> Void) { - completion(.success("#CONFIRMATION_CODE_FOR \(value)")) - } - - func lookup( - forUserId: Data, - _ completion: @escaping (Result<Contact, Error>) -> Void - ) { - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - completion(.success( - .init( - id: "mock_username".data(using: .utf8)!, - marshaled: "mock_username".data(using: .utf8)!, - username: "mock_username", - email: nil, - phone: nil, - nickname: "mock_nickname", - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - ))) - } - } -} diff --git a/Sources/Integration/Resources/cert_mainnet.txt b/Sources/Integration/Resources/cert_mainnet.txt deleted file mode 100644 index 40045d63666dded7450e22eb82c15b62a17d4d68..0000000000000000000000000000000000000000 --- a/Sources/Integration/Resources/cert_mainnet.txt +++ /dev/null @@ -1 +0,0 @@ -[PLACE THE CERTIFICATE CONTENT HERE] diff --git a/Sources/Integration/Session/Session+Chat.swift b/Sources/Integration/Session/Session+Chat.swift deleted file mode 100644 index b8d6e02d45f0cb6681fe206a31c117d7807a70c5..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+Chat.swift +++ /dev/null @@ -1,270 +0,0 @@ -import UIKit -import Models -import Shared -import XXModels -import Foundation - -extension Session { - public func send(imageData: Data, to contact: Contact, completion: @escaping (Result<Void, Error>) -> Void) { - client.bindings.compress(image: imageData) { [weak self] result in - guard let self = self else { - completion(.success(())) - return - } - - switch result { - case .success(let compressedImage): - do { - let url = try FileManager.store( - data: compressedImage, - name: "image_\(Date.asTimestamp)", - type: "jpeg" - ) - - self.sendFile(url: url, to: contact) - completion(.success(())) - } catch { - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(error)) - log(string: "Error when compressing image: \(error.localizedDescription)", type: .error) - } - } - } - - public func sendFile(url: URL, to contact: Contact) { - guard let manager = client.transferManager else { fatalError("A transfer manager was not created") } - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - var tid: Data? - - do { - tid = try manager.uploadFile(url: url, to: contact.id) { completed, send, arrived, total, error in - guard let tid = tid else { return } - - if completed { - self.endTransferWith(tid: tid) - } else { - if error != nil { - self.failTransferWith(tid: tid) - } else { - self.progressTransferWith(tid: tid, arrived: Float(arrived), total: Float(total)) - } - } - } - - guard let tid = tid else { return } - - let content = url.pathExtension == "m4a" ? "a voice message" : "an image" - - let transfer = FileTransfer( - id: tid, - contactId: contact.id, - name: url.deletingPathExtension().lastPathComponent, - type: url.pathExtension, - data: try? Data(contentsOf: url), - progress: 0.0, - isIncoming: false, - createdAt: Date() - ) - - _ = try? self.dbManager.saveFileTransfer(transfer) - - let message = Message( - networkId: nil, - senderId: self.client.bindings.myId, - recipientId: contact.id, - groupId: nil, - date: Date(), - status: .sending, - isUnread: false, - text: "You sent \(content)", - replyMessageId: nil, - roundURL: nil, - fileTransferId: tid - ) - - _ = try? self.dbManager.saveMessage(message) - } catch { - print(error.localizedDescription) - } - } - } - - public func send(_ payload: Payload, toContact contact: Contact) { - var message = Message( - networkId: nil, - senderId: client.bindings.myId, - recipientId: contact.id, - groupId: nil, - date: Date(), - status: .sending, - isUnread: false, - text: payload.text, - replyMessageId: payload.reply?.messageId, - roundURL: nil, - fileTransferId: nil - ) - - do { - message = try dbManager.saveMessage(message) - send(message: message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - - public func retryMessage(_ id: Int64) { - if var message = try? dbManager.fetchMessages(.init(id: [id])).first { - message.status = .sending - message.date = Date() - - if let message = try? dbManager.saveMessage(message) { - if let _ = message.recipientId { - send(message: message) - } else { - send(groupMessage: message) - } - } - } - } - - func send(message: Message) { - var message = message - - var reply: Reply? - if let replyId = message.replyMessageId, - let replyMessage = try? dbManager.fetchMessages(Message.Query(networkId: replyId)).first { - reply = Reply(messageId: replyId, senderId: replyMessage.senderId) - } - - let payloadData = Payload(text: message.text, reply: reply).asData() - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - switch self.client.bindings.send(payloadData, to: message.recipientId!) { - case .success(let report): - message.roundURL = report.roundURL - - self.client.bindings.listen(report: report.marshalled) { result in - switch result { - case .success(let status): - switch status { - case .failed: - message.status = .sendingFailed - case .sent: - message.status = .sent - case .timedout: - message.status = .sendingTimedOut - } - case .failure: - message.status = .sendingFailed - } - - message.networkId = report.uniqueId - message.date = Date.fromTimestamp(Int(report.timestamp)) - DispatchQueue.main.async { - do { - _ = try self.dbManager.saveMessage(message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - case .failure(let error): - message.status = .sendingFailed - log(string: error.localizedDescription, type: .error) - } - - DispatchQueue.main.async { - do { - _ = try self.dbManager.saveMessage(message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - } - - private func endTransferWith(tid: Data) { - guard let manager = client.transferManager else { - fatalError("A transfer manager was not created") - } - - try? manager.endTransferUpload(with: tid) - - if var message = try? dbManager.fetchMessages(.init(fileTransferId: tid)).first { - message.status = .sent - _ = try? dbManager.saveMessage(message) - } - - if var transfer = try? dbManager.fetchFileTransfers(.init(id: [tid])).first { - transfer.progress = 1.0 - _ = try? dbManager.saveFileTransfer(transfer) - } - } - - private func failTransferWith(tid: Data) { - if var message = try? dbManager.fetchMessages(.init(fileTransferId: tid)).first { - message.status = .sendingFailed - _ = try? dbManager.saveMessage(message) - } - } - - private func progressTransferWith(tid: Data, arrived: Float, total: Float) { - if var transfer = try? dbManager.fetchFileTransfers(.init(id: [tid])).first { - transfer.progress = arrived/total - _ = try? dbManager.saveFileTransfer(transfer) - } - } - - func handle(incomingTransfer transfer: FileTransfer) { - guard let manager = client.transferManager else { - fatalError("A transfer manager was not created") - } - - let content = transfer.type == "m4a" ? "a voice message" : "an image" - - var message = try! dbManager.saveMessage( - Message( - networkId: nil, - senderId: transfer.contactId, - recipientId: myId, - groupId: nil, - date: transfer.createdAt, - status: .receiving, - isUnread: true, - text: "Sent you \(content)", - replyMessageId: nil, - roundURL: nil, - fileTransferId: transfer.id - ) - ) - - try! manager.listenDownloadFromTransfer(with: transfer.id) { completed, arrived, total, error in - if let error = error { - print(error.localizedDescription) - return - } - - if completed { - if let data = try? manager.downloadFileFromTransfer(with: transfer.id), - let _ = try? FileManager.store(data: data, name: transfer.name, type: transfer.type) { - var transfer = transfer - transfer.data = data - transfer.progress = 1.0 - message.status = .received - - _ = try? self.dbManager.saveFileTransfer(transfer) - _ = try? self.dbManager.saveMessage(message) - } - } else { - self.progressTransferWith(tid: transfer.id, arrived: Float(arrived), total: Float(total)) - } - } - } -} diff --git a/Sources/Integration/Session/Session+Contacts.swift b/Sources/Integration/Session/Session+Contacts.swift deleted file mode 100644 index d30f78c24e014cb411b0608638904d5664f626aa..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+Contacts.swift +++ /dev/null @@ -1,258 +0,0 @@ -import Retry -import Models -import Shared -import XXModels -import Foundation - -extension Session { - public func getId(from data: Data) -> Data? { - client.bindings.getId(from: data) - } - - public func verify(contact: Contact) { - var contact = contact - contact.authStatus = .verificationInProgress - - do { - contact = try dbManager.saveContact(contact) - } catch { - log(string: "Failed to store contact request upon verification. Returning, request will be abandoned to not crash", type: .error) - } - - retry(max: 4, retryStrategy: .delay(seconds: 1)) { [weak self] in - if self?.networkMonitor.xxStatus != .available { - log(string: "Network is not available yet for ownership. Retrying in 1 second...", type: .error) - throw NSError.create("") - } - }.finalCatch { error in - log(string: "Failed to verify contact cause network wasn't available at all", type: .crumbs) - return - } - - let resultClosure: (Result<Contact, Error>) -> Void = { result in - switch result { - case .success(let mightBe): - guard try! self.client.bindings.verify(marshaled: contact.marshaled!, verifiedMarshaled: mightBe.marshaled!) else { - do { - try self.dbManager.deleteContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - - return - } - - contact.authStatus = .verified - - do { - try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - - case .failure: - contact.authStatus = .verificationFailed - - do { - try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - let ud = client.userDiscovery! - - let hasEmail = contact.email != nil - let hasPhone = contact.phone != nil - - guard hasEmail || hasPhone else { - ud.lookup(forUserId: contact.id, resultClosure) - return - } - - var fact: String - - if hasEmail { - fact = "\(FactType.email.prefix)\(contact.email!)" - } else { - fact = "\(FactType.phone.prefix)\(contact.phone!)" - } - - do { - try ud.search(fact: fact, resultClosure) - } catch { - log(string: error.localizedDescription, type: .error) - contact.authStatus = .verificationFailed - - do { - try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - public func retryRequest(_ contact: Contact) throws { - let name = (contact.nickname ?? contact.username) ?? "" - - client.bindings.add(contact.marshaled!, from: myQR) { [weak self, contact] in - var contact = contact - guard let self = self else { return } - - do { - switch $0 { - case .success: - contact.authStatus = .requested - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resent(name), - leftImage: Asset.sharedSuccess.image - )) - - case .failure: - contact.createdAt = Date() - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resentFailed(name), - color: Asset.accentDanger.color, - leftImage: Asset.requestFailedToaster.image - )) - } - - _ = try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - public func add(_ contact: Contact) throws { - /// Make sure we are not adding ourselves - /// - guard contact.username != username else { - throw NSError.create("You can't add yourself") - } - - var contact = contact - - /// Check if this contact is actually - /// being requested/confirmed after failing - /// - if [.requestFailed, .confirmationFailed].contains(contact.authStatus) { - /// If it is being re-requested or - /// re-confirmed, no need to save again - /// - contact.createdAt = Date() - - if contact.authStatus == .confirmationFailed { - try confirm(contact) - return - } - } else { - /// If its not failed, lets make sure that - /// this is an actual new contact - /// - if let _ = try? dbManager.fetchContacts(.init(id: [contact.id])).first { - /// Found a user w/ that id already stored - /// - throw NSError.create("This user has already been requested") - } - - contact.authStatus = .requesting - } - - contact = try dbManager.saveContact(contact) - - let myself = client.bindings.meMarshalled( - username!, - email: isSharingEmail ? email : nil, - phone: isSharingPhone ? phone : nil - ) - - client.bindings.add(contact.marshaled!, from: myself) { [weak self, contact] in - guard let self = self else { return } - var contact = contact - - do { - switch $0 { - case .success(let success): - contact.authStatus = success ? .requested : .requestFailed - contact = try self.dbManager.saveContact(contact) - - let name = contact.nickname ?? contact.username - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.sent(name ?? ""), - leftImage: Asset.sharedSuccess.image - )) - - case .failure: - contact.createdAt = Date() - contact.authStatus = .requestFailed - contact = try self.dbManager.saveContact(contact) - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Failed.toast(contact.nickname ?? contact.username!), - color: Asset.accentDanger.color, - leftImage: Asset.requestFailedToaster.image - )) - } - } catch { - print(error.localizedDescription) - } - } - } - - public func confirm(_ contact: Contact) throws { - var contact = contact - contact.authStatus = .confirming - contact = try dbManager.saveContact(contact) - - client.bindings.confirm(contact.marshaled!) { [weak self] in - switch $0 { - case .success(let confirmed): - contact.isRecent = true - contact.createdAt = Date() - contact.authStatus = confirmed ? .friend : .confirmationFailed - - case .failure: - contact.authStatus = .confirmationFailed - } - - _ = try? self?.dbManager.saveContact(contact) - } - } - - public func deleteContact(_ contact: Contact) throws { - if !(try dbManager.fetchFileTransfers(.init(contactId: contact.id))).isEmpty { - throw NSError.create("There is an ongoing file transfer with this contact as you are receiving or sending a file, please try again later once it’s done") - } - - try client.bindings.removeContact(contact.marshaled!) - - /// Currently this cascades into deleting - /// all messages w/ contact.id == senderId - /// But this shouldn't be the always the case - /// because if we have a group / this contact - /// the messages will be gone as well. - /// - /// Suggestion: If there's a group where this user belongs to - /// we will just cleanup the contact model stored on the db - /// leaving only username and id which are the equivalent to - /// .stranger contacts. - /// - //try dbManager.deleteContact(contact) - - var contact = contact - contact.email = nil - contact.phone = nil - contact.photo = nil - contact.isRecent = false - contact.marshaled = nil - contact.isBlocked = true - contact.authStatus = .stranger - contact.nickname = contact.username - _ = try! dbManager.saveContact(contact) - } -} diff --git a/Sources/Integration/Session/Session+Group.swift b/Sources/Integration/Session/Session+Group.swift deleted file mode 100644 index 47652ee94cc4ec7d870005ff69d2d21ea3839ce3..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+Group.swift +++ /dev/null @@ -1,243 +0,0 @@ -import Models -import XXModels -import Foundation - -extension Session { - public func join(group: Group) throws { - guard let manager = client.groupManager else { fatalError("A group manager was not created") } - - try manager.join(group.serialized) - var group = group - group.authStatus = .participating - scanStrangers {} - try dbManager.saveGroup(group) - } - - public func leave(group: Group) throws { - guard let manager = client.groupManager else { fatalError("A group manager was not created") } - try manager.leave(group.id) - try dbManager.deleteGroup(group) - } - - public func createGroup( - name: String, - welcome: String?, - members: [Contact], - _ completion: @escaping (Result<GroupInfo, Error>) -> Void - ) { - guard let manager = client.groupManager else { - fatalError("A group manager was not created") - } - - manager.create( - me: myId, - name: name, - welcome: welcome, - with: members.map { $0.id }) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let group): - try! self.dbManager.saveGroup(group) - - members - .map { GroupMember(groupId: group.id, contactId: $0.id) } - .forEach { try! self.dbManager.saveGroupMember($0) } - - // TODO: Add saveBulkGroupMembers to the database - - if let welcome = welcome { - let message = Message( - networkId: nil, - senderId: self.myId, - recipientId: nil, - groupId: group.id, - date: group.createdAt, - status: .sent, - isUnread: false, - text: welcome, - replyMessageId: nil, - roundURL: nil, - fileTransferId: nil - ) - - try! self.dbManager.saveMessage(message) - } - - let query = GroupInfo.Query(groupId: group.id) - let info = try! self.dbManager.fetchGroupInfos(query).first - completion(.success(info!)) - - case .failure(let error): - completion(.failure(error)) - } - } - } - - @discardableResult - func processGroupCreation(_ group: Group, memberIds: [Data], welcome: String?) -> GroupInfo { - /// Save the group - /// - _ = try! dbManager.saveGroup(group) - - /// Which of those members are not my friends? - /// - let friendsParticipating = try! dbManager.fetchContacts(Contact.Query(id: Set(memberIds))) - - /// Save the strangers as contacts - /// - let friendIds = friendsParticipating.map(\.id) - memberIds.forEach { - if !friendIds.contains($0) { - try! dbManager.saveContact(.init( - id: $0, - marshaled: nil, - username: nil, - email: nil, - phone: nil, - nickname: nil, - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - )) - } - } - - /// Save group members relation - /// - memberIds.forEach { - try! dbManager.saveGroupMember(.init(groupId: group.id, contactId: $0)) - } - - /// Save the welcome message (if any) - /// - if let welcome = welcome { - _ = try! dbManager.saveMessage(.init( - networkId: nil, - senderId: group.leaderId, - recipientId: nil, - groupId: group.id, - date: group.createdAt, - status: .received, - isUnread: true, - text: welcome, - replyMessageId: nil, - roundURL: nil, - fileTransferId: nil - )) - } - - - if inappnotifications { - DeviceFeedback.sound(.contactAdded) - DeviceFeedback.shake(.notification) - } - - scanStrangers {} - - let info = try! dbManager.fetchGroupInfos(.init(groupId: group.id)).first - return info! - } -} - -// MARK: - GroupMessages - -extension Session { - public func send(_ payload: Payload, toGroup group: Group) { - var message = Message( - senderId: client.bindings.myId, - recipientId: nil, - groupId: group.id, - date: Date(), - status: .sending, - isUnread: false, - text: payload.text, - replyMessageId: payload.reply?.messageId, - roundURL: nil, - fileTransferId: nil - ) - - do { - message = try dbManager.saveMessage(message) - send(groupMessage: message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - - func send(groupMessage: Message) { - guard let manager = client.groupManager else { fatalError("A group manager was not created") } - var message = groupMessage - - var reply: Reply? - if let replyId = message.replyMessageId, - let replyMessage = try? dbManager.fetchMessages(Message.Query(networkId: replyId)).first { - reply = Reply(messageId: replyId, senderId: replyMessage.senderId) - } - - let payloadData = Payload(text: message.text, reply: reply).asData() - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - switch manager.send(payloadData, to: message.groupId!) { - case .success((let roundId, let uniqueId, let roundURL)): - message.roundURL = roundURL - - self.client.bindings.listenRound(id: Int(roundId)) { result in - switch result { - case .success(let succeeded): - message.networkId = uniqueId - message.status = succeeded ? .sent : .sendingFailed - case .failure: - message.status = .sendingFailed - } - - do { - try self.dbManager.saveMessage(message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - case .failure: - message.status = .sendingFailed - } - - do { - try self.dbManager.saveMessage(message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - public func scanStrangers(_ completion: @escaping () -> Void) { - DispatchQueue.global().async { [weak self] in - guard let self = self, - let ud = self.client.userDiscovery, - let strangers = try? self.dbManager.fetchContacts(.init(username: .some(nil))), - !strangers.isEmpty else { return } - - ud.lookup(idList: strangers.map(\.id)) { result in - switch result { - case .success(let strangersWithUsernames): - let acquaintances = strangers.map { stranger -> Contact in - var exStranger = stranger - exStranger.username = strangersWithUsernames.first(where: { $0.id == stranger.id })?.username - return exStranger - } - - DispatchQueue.main.async { - acquaintances.forEach { _ = try? self.dbManager.saveContact($0) } - } - - completion() - case .failure(let error): - print(error.localizedDescription) - DispatchQueue.main.async { completion() } - } - } - } - } -} diff --git a/Sources/Integration/Session/Session+Network.swift b/Sources/Integration/Session/Session+Network.swift deleted file mode 100644 index 070ea2cddeb608418f2d80ec400a8b7f0d63e24d..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+Network.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -extension Session { - public func start() { - DispatchQueue.global().async { [weak client] in - client?.bindings.startNetwork() - } - } - - public func stop() { - DispatchQueue.global().async { [weak client] in - client?.bindings.stopNetwork() - } - } -} diff --git a/Sources/Integration/Session/Session+Notifications.swift b/Sources/Integration/Session/Session+Notifications.swift deleted file mode 100644 index b4e98e9647d8292051d8df61679d629740c84407..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+Notifications.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -extension Session { - public func registerNotifications(_ token: Data) throws { - try client.bindings.registerNotifications(token) - } - - public func unregisterNotifications() throws { - try client.bindings.unregisterNotifications() - } -} diff --git a/Sources/Integration/Session/Session+UD.swift b/Sources/Integration/Session/Session+UD.swift deleted file mode 100644 index 27add4b13b1839482aefe29badc0f0fbe4e61e8f..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+UD.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Retry -import Models -import Combine -import XXModels -import Foundation - -extension Session { - public func waitForNodes(timeout: Int) -> AnyPublisher<Void, Error> { - Deferred { - Future { promise in - retry(max: timeout, retryStrategy: .delay(seconds: 1)) { [weak self] in - guard let self = self else { return } - try self.client.bindings.nodeRegistrationStatus() - promise(.success(())) - }.finalCatch { - promise(.failure($0)) - } - } - }.eraseToAnyPublisher() - } - - public func search(fact: String) -> AnyPublisher<Contact, Error> { - Deferred { - Future { promise in - guard let ud = self.client.userDiscovery else { - let error = NSError(domain: "", code: 0) - promise(.failure(error)) - return - } - - do { - try self.client.bindings.nodeRegistrationStatus() - try ud.search(fact: fact) { - switch $0 { - case .success(let contact): - promise(.success(contact)) - case .failure(let error): - promise(.failure(error)) - } - } - } catch { - promise(.failure(error)) - } - } - }.eraseToAnyPublisher() - } - - public func search(fact: String, _ completion: @escaping (Result<Contact, Error>) -> Void) throws { - guard let ud = client.userDiscovery else { return } - try client.bindings.nodeRegistrationStatus() - try ud.search(fact: fact, completion) - } - - public func extract(fact: FactType, from marshalled: Data) throws -> String? { - guard let ud = client.userDiscovery else { return nil } - return try ud.retrieve(from: marshalled, fact: fact) - } - - public func unregister(fact: FactType) throws { - guard let ud = client.userDiscovery else { return } - - switch fact { - case .phone: - try ud.remove("P" + phone!) - isSharingPhone = false - phone = nil - case .email: - try ud.remove("E" + email!) - isSharingEmail = false - email = nil - default: - break - } - } - - public func register(_ fact: FactType, value: String, _ completion: @escaping (Result<String?, Error>) -> Void) { - guard let ud = client.userDiscovery else { return } - - switch fact { - case .username: - ud.register(.username, value: value) { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(_): - self.username = value - - if var me = try? self.myContact() { - me.username = value - _ = try? self.dbManager.saveContact(me) - } - - completion(.success(nil)) - case .failure(let error): - completion(.failure(error)) - } - } - default: - ud.register(fact, value: value, completion) - } - } - - public func confirm(code: String, confirmation: AttributeConfirmation) throws { - guard let ud = client.userDiscovery else { return } - - try ud.confirm(code: code, id: confirmation.confirmationId!) - - if confirmation.isEmail { - email = confirmation.content - } else { - phone = confirmation.content - } - - if let _ = client.backupManager { - updateFactsOnBackup() - } - } -} diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift deleted file mode 100644 index 49e1c60e20b3be9765529620893c493d900b9cfd..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session.swift +++ /dev/null @@ -1,507 +0,0 @@ -import Retry -import os.log -import Models -import Shared -import Combine -import Defaults -import XXModels -import XXDatabase -import Foundation -import ToastFeature -import BackupFeature -import NetworkMonitor -import ReportingFeature -import DependencyInjection -import XXLegacyDatabaseMigrator - -let logHandler = OSLog(subsystem: "xx.network", category: "Performance debugging") - -struct BackupParameters: Codable { - var email: String? - var phone: String? - var username: String -} - -struct BackupReport: Codable { - var contactIds: [String] - var parameters: String - - private enum CodingKeys: String, CodingKey { - case parameters = "Params" - case contactIds = "RestoredContacts" - } -} - -public final class Session: SessionType { - @KeyObject(.theme, defaultValue: nil) var theme: String? - @KeyObject(.email, defaultValue: nil) var email: String? - @KeyObject(.phone, defaultValue: nil) var phone: String? - @KeyObject(.avatar, defaultValue: nil) var avatar: Data? - @KeyObject(.username, defaultValue: nil) var username: String? - @KeyObject(.biometrics, defaultValue: false) var biometrics: Bool - @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool - @KeyObject(.requestCounter, defaultValue: 0) var requestCounter: Int - @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool - @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool - @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool - @KeyObject(.crashReporting, defaultValue: true) var crashReporting: Bool - @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard: Bool - @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications: Bool - @KeyObject(.inappnotifications, defaultValue: true) var inappnotifications: Bool - - @Dependency var backupService: BackupService - @Dependency var toastController: ToastController - @Dependency var reportingStatus: ReportingStatus - @Dependency var networkMonitor: NetworkMonitoring - - public let client: Client - public let dbManager: Database - private var cancellables = Set<AnyCancellable>() - - public var myId: Data { client.bindings.myId } - public var version: String { type(of: client.bindings).version } - - public var myQR: Data { - client - .bindings - .meMarshalled( - username!, - email: isSharingEmail ? email : nil, - phone: isSharingPhone ? phone : nil - ) - } - - public var hasRunningTasks: Bool { - client.bindings.hasRunningTasks - } - - public var isOnline: AnyPublisher<Bool, Never> { - networkMonitor.statusPublisher.map { $0 == .available }.eraseToAnyPublisher() - } - - public init( - passphrase: String, - backupFile: Data, - ndf: String - ) throws { - let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking - - os_signpost(.begin, log: logHandler, name: "Decrypting", "Calling newClientFromBackup") - let (client, backupData) = try network.newClientFromBackup(passphrase: passphrase, data: backupFile, ndf: ndf) - os_signpost(.end, log: logHandler, name: "Decrypting", "Finished newClientFromBackup") - - self.client = client - - let legacyOldPath = NSSearchPathForDirectoriesInDomains( - .documentDirectory, .userDomainMask, true - )[0].appending("/xxmessenger.sqlite") - - let legacyPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("database") - .appendingPathExtension("sqlite").path - - let dbExistsInLegacyOldPath = FileManager.default.fileExists(atPath: legacyOldPath) - let dbExistsInLegacyPath = FileManager.default.fileExists(atPath: legacyPath) - - if dbExistsInLegacyOldPath && !dbExistsInLegacyPath { - try? FileManager.default.moveItem(atPath: legacyOldPath, toPath: legacyPath) - } - - let dbPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("xxm_database") - .appendingPathExtension("sqlite").path - - dbManager = try Database.onDisk(path: dbPath) - - if dbExistsInLegacyPath { - try Migrator.live()( - try .init(path: legacyPath), - to: dbManager, - myContactId: client.bindings.myId, - meMarshaled: client.bindings.meMarshalled - ) - - try FileManager.default.moveItem(atPath: legacyPath, toPath: legacyPath.appending("-backup")) - } - - let report = try! JSONDecoder().decode(BackupReport.self, from: backupData!) - - if !report.parameters.isEmpty { - let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8)) - - username = params.username - - if let paramsPhone = params.phone, !paramsPhone.isEmpty { - phone = paramsPhone - } - - if let paramsEmail = params.email, !paramsEmail.isEmpty { - email = paramsEmail - } - } - - guard let username = username, username.isEmpty == false else { - fatalError("Trying to restore an account that has no username") - } - - try continueInitialization() - - if !report.contactIds.isEmpty { - client.restoreContacts(fromBackup: try! JSONSerialization.data(withJSONObject: report.contactIds)) - } - } - - public init(ndf: String) throws { - let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking - self.client = try network.newClient(ndf: ndf) - - let legacyOldPath = NSSearchPathForDirectoriesInDomains( - .documentDirectory, .userDomainMask, true - )[0].appending("/xxmessenger.sqlite") - - let legacyPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("database") - .appendingPathExtension("sqlite").path - - let dbExistsInLegacyOldPath = FileManager.default.fileExists(atPath: legacyOldPath) - let dbExistsInLegacyPath = FileManager.default.fileExists(atPath: legacyPath) - - if dbExistsInLegacyOldPath && !dbExistsInLegacyPath { - try? FileManager.default.moveItem(atPath: legacyOldPath, toPath: legacyPath) - } - - let dbPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("xxm_database") - .appendingPathExtension("sqlite").path - - dbManager = try Database.onDisk(path: dbPath) - - if dbExistsInLegacyPath { - try Migrator.live()( - try .init(path: legacyPath), - to: dbManager, - myContactId: client.bindings.myId, - meMarshaled: client.bindings.meMarshalled - ) - - try FileManager.default.moveItem(atPath: legacyPath, toPath: legacyPath.appending("-backup")) - } - - try continueInitialization() - } - - private func continueInitialization() throws { - var myContact = try self.myContact() - myContact.marshaled = client.bindings.meMarshalled - myContact.username = username - myContact.email = email - myContact.phone = phone - myContact.authStatus = .friend - myContact.isRecent = false - _ = try dbManager.saveContact(myContact) - - setupBindings() - networkMonitor.start() - - networkMonitor.statusPublisher - .filter { $0 == .available }.first() - .sink { [unowned self] _ in - client.bindings.replayRequests() - scanStrangers {} - } - .store(in: &cancellables) - - registerUnfinishedUploadTransfers() - registerUnfinishedDownloadTransfers() - - let query = Contact.Query(authStatus: [.verificationInProgress]) - _ = try? dbManager.bulkUpdateContacts(query, .init(authStatus: .verificationFailed)) - } - - public func setDummyTraffic(status: Bool) { - client.dummyManager?.setStatus(status: status) - } - - public func deleteMyself() throws { - guard let username = username, let ud = client.userDiscovery else { return } - - try? unregisterNotifications() - try ud.deleteMyself(username) - - stop() - cleanUp() - } - - private func cleanUp() { - retry(max: 10, retryStrategy: .delay(seconds: 1)) { [unowned self] in - guard self.hasRunningTasks == false else { throw NSError.create("") } - }.finalCatch { _ in fatalError("Couldn't delete account because network is not stopping") } - - try! dbManager.drop() - FileManager.xxCleanup() - - email = nil - phone = nil - theme = nil - avatar = nil - self.username = nil - isSharingEmail = false - isSharingPhone = false - requestCounter = 0 - biometrics = false - hideAppList = false - recordingLogs = true - crashReporting = true - icognitoKeyboard = false - pushNotifications = false - inappnotifications = true - } - - private func registerUnfinishedDownloadTransfers() { - guard let unfinishedReceivingMessages = try? dbManager.fetchMessages(.init(status: [.receiving])), - let unfinishedReceivingTransfers = try? dbManager.fetchFileTransfers(.init( - id: Set(unfinishedReceivingMessages - .filter { $0.fileTransferId != nil } - .compactMap(\.fileTransferId)))) - else { return } - - let pairs = unfinishedReceivingMessages.compactMap { message -> (Message, FileTransfer)? in - guard let transfer = unfinishedReceivingTransfers.first(where: { ft in - ft.id == message.fileTransferId - }) else { return nil } - - return (message, transfer) - } - - pairs.forEach { message, transfer in - var message = message - var transfer = transfer - - do { - try client.transferManager?.listenDownloadFromTransfer(with: transfer.id) { [weak self] completed, received, total, error in - guard let self = self else { return } - if completed { - transfer.progress = 1.0 - message.status = .received - - if let data = try? self.client.transferManager?.downloadFileFromTransfer(with: transfer.id), - let _ = try? FileManager.store(data: data, name: transfer.name, type: transfer.type) { - transfer.data = data - } - } else { - if error != nil { - message.status = .receivingFailed - } else { - transfer.progress = Float(received)/Float(total) - } - } - - _ = try? self.dbManager.saveFileTransfer(transfer) - _ = try? self.dbManager.saveMessage(message) - } - } catch { - message.status = .receivingFailed - _ = try? self.dbManager.saveMessage(message) - } - } - } - - private func registerUnfinishedUploadTransfers() { - guard let unfinishedSendingMessages = try? dbManager.fetchMessages(.init(status: [.sending])), - let unfinishedSendingTransfers = try? dbManager.fetchFileTransfers(.init( - id: Set(unfinishedSendingMessages - .filter { $0.fileTransferId != nil } - .compactMap(\.fileTransferId)))) - else { return } - - let pairs = unfinishedSendingMessages.compactMap { message -> (Message, FileTransfer)? in - guard let transfer = unfinishedSendingTransfers.first(where: { ft in - ft.id == message.fileTransferId - }) else { return nil } - - return (message, transfer) - } - - pairs.forEach { message, transfer in - var message = message - var transfer = transfer - - do { - try client.transferManager?.listenUploadFromTransfer(with: transfer.id) { [weak self] completed, sent, arrived, total, error in - guard let self = self else { return } - - if completed { - transfer.progress = 1.0 - message.status = .sent - - try? self.client.transferManager?.endTransferUpload(with: transfer.id) - } else { - if error != nil { - message.status = .sendingFailed - } else { - transfer.progress = Float(arrived)/Float(total) - } - } - - _ = try? self.dbManager.saveFileTransfer(transfer) - _ = try? self.dbManager.saveMessage(message) - } - } catch { - message.status = .sendingFailed - _ = try? self.dbManager.saveMessage(message) - } - } - } - - func updateFactsOnBackup() { - struct BackupParameters: Codable { - var email: String? - var phone: String? - var username: String - - var jsonFormat: String { - let data = try! JSONEncoder().encode(self) - let json = String(data: data, encoding: .utf8) - return json! - } - } - - let params = BackupParameters( - email: email, - phone: phone, - username: username! - ).jsonFormat - - client.addJson(params) - - guard username!.isEmpty == false else { - fatalError("Tried to build a backup with my username but an empty string was set to it") - } - - backupService.performBackupIfAutomaticIsEnabled() - } - - private func setupBindings() { - client.requests - .sink { [unowned self] contact in - let query = Contact.Query(id: [contact.id]) - - if let prexistent = try? dbManager.fetchContacts(query).first { - guard prexistent.authStatus == .stranger else { return } - } - - if self.inappnotifications { - DeviceFeedback.sound(.contactAdded) - DeviceFeedback.shake(.notification) - } - - verify(contact: contact) - }.store(in: &cancellables) - - client.requestsSent - .sink { [unowned self] in _ = try? dbManager.saveContact($0) } - .store(in: &cancellables) - - client.backup - .sink { [unowned self] in backupService.updateBackup(data: $0) } - .store(in: &cancellables) - - client.resets - .sink { [unowned self] in - /// This will get called when my contact restore its contact. - /// TODO: Hold a record on the chat that this contact restored. - /// - if var contact = try? dbManager.fetchContacts(.init(id: [$0.id])).first { - contact.authStatus = .friend - _ = try? dbManager.saveContact(contact) - } - }.store(in: &cancellables) - - backupService.settingsPublisher - .map { $0.enabledService != nil } - .removeDuplicates() - .sink { [unowned self] in - if $0 == true { - guard let passphrase = backupService.passphrase else { - client.resumeBackup() - return - } - - client.initializeBackup(passphrase: passphrase) - backupService.passphrase = nil - updateFactsOnBackup() - } else { - backupService.passphrase = nil - client.stopListeningBackup() - } - } - .store(in: &cancellables) - - client.messages - .sink { [unowned self] in - if var contact = try? dbManager.fetchContacts(.init(id: [$0.senderId])).first { - guard contact.isBanned == false else { return } - contact.isRecent = false - _ = try? dbManager.saveContact(contact) - } - - _ = try? dbManager.saveMessage($0) - }.store(in: &cancellables) - - client.network - .sink { [unowned self] in networkMonitor.update($0) } - .store(in: &cancellables) - - client.groupRequests - .sink { [unowned self] request in - if let _ = try? dbManager.fetchGroups(.init(id: [request.0.id])).first { - return - } - - if let contact = try! dbManager.fetchContacts(.init(id: [request.0.leaderId])).first { - if reportingStatus.isEnabled(), (contact.isBlocked || contact.isBanned) { - return - } - } - - DispatchQueue.global().async { [weak self] in - self?.processGroupCreation(request.0, memberIds: request.1, welcome: request.2) - } - }.store(in: &cancellables) - - client.confirmations - .sink { [unowned self] in - if var contact = try? dbManager.fetchContacts(.init(id: [$0.id])).first { - contact.authStatus = .friend - contact.isRecent = true - contact.createdAt = Date() - _ = try? dbManager.saveContact(contact) - - toastController.enqueueToast(model: .init( - title: contact.nickname ?? contact.username!, - subtitle: Localized.Requests.Confirmations.toaster, - leftImage: Asset.sharedSuccess.image - )) - } - }.store(in: &cancellables) - - client.transfers - .sink { [unowned self] in - guard let transfer = try? dbManager.saveFileTransfer($0) else { return } - handle(incomingTransfer: transfer) - } - .store(in: &cancellables) - } - - func myContact() throws -> Contact { - if let contact = try dbManager.fetchContacts(.init(id: [client.bindings.myId])).first { - return contact - } else { - return try dbManager.saveContact(.init(id: client.bindings.myId)) - } - } -} diff --git a/Sources/Integration/Session/SessionType.swift b/Sources/Integration/Session/SessionType.swift deleted file mode 100644 index effd6c96c3239904722b74f2856f2a953f0b986e..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/SessionType.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Models -import Combine -import XXModels -import Foundation - -public protocol SessionType { - var myId: Data { get } - var myQR: Data { get } - var version: String { get } - var hasRunningTasks: Bool { get } - var isOnline: AnyPublisher<Bool, Never> { get } - - var dbManager: Database { get } - - func deleteMyself() throws - func getId(from: Data) -> Data? - - func sendFile(url: URL, to: Contact) - func send(imageData: Data, to: Contact, completion: @escaping (Result<Void, Error>) -> Void) - - func verify(contact: Contact) - - func setDummyTraffic(status: Bool) - - // UserDiscovery - - func unregister(fact: FactType) throws - func extract(fact: FactType, from: Data) throws -> String? - func confirm(code: String, confirmation: AttributeConfirmation) throws - func search(fact: String, _: @escaping (Result<Contact, Error>) -> Void) throws - func register(_: FactType, value: String, _: @escaping (Result<String?, Error>) -> Void) - - // Notifications - - func unregisterNotifications() throws - func registerNotifications(_ token: Data) throws - - // Network - - func start() - func stop() - - // Messages - - func retryMessage(_: Int64) - func send(_: Payload, toContact: Contact) - - // Contacts - - func add(_: Contact) throws - func confirm(_: Contact) throws - func deleteContact(_: Contact) throws - - func retryRequest(_: Contact) throws - func scanStrangers(_: @escaping () -> Void) - - // Groups - - func join(group: Group) throws - func send(_: Payload, toGroup: Group) - func leave(group: Group) throws - - func createGroup( - name: String, - welcome: String?, - members: [Contact], - _ completion: @escaping (Result<GroupInfo, Error>) -> Void - ) - - func search(fact: String) -> AnyPublisher<Contact, Error> - - func waitForNodes(timeout: Int) -> AnyPublisher<Void, Error> -} diff --git a/Sources/Integration/XXNetwork.swift b/Sources/Integration/XXNetwork.swift deleted file mode 100644 index 1273a26a68e4487227c96fe953ecabe23994e450..0000000000000000000000000000000000000000 --- a/Sources/Integration/XXNetwork.swift +++ /dev/null @@ -1,173 +0,0 @@ -import Shared -import XXLogger -import Keychain -import Foundation -import DependencyInjection - -public enum NetworkEnvironment { - case mainnet -} - -public protocol XXNetworking { - var hasClient: Bool { get } - - func writeLogs() - func purgeFiles() - func updateErrors() - func newClient(ndf: String) throws -> Client - - func updateNDF( - _: @escaping (Result<String, Error>) -> Void - ) - - func loadClient( - with: Data, - fromBackup: Bool, - email: String?, - phone: String? - ) throws -> Client - - func newClientFromBackup( - passphrase: String, - data: Data, - ndf: String - ) throws -> (Client, Data?) -} - -public struct XXNetwork<B: BindingsInterface> { - @Dependency private var logger: XXLogger - @Dependency private var keychain: KeychainHandling - - public init() {} -} - -extension XXNetwork: XXNetworking { - public var hasClient: Bool { - guard let files = FileManager.xxContents else { return false } - return files.count > 0 - } - - public func writeLogs() { - B.listenLogs() - } - - public func updateErrors() { - B.updateErrors() - } - - public func updateNDF(_ completion: @escaping (Result<String, Error>) -> Void) { - B.updateNDF(for: .mainnet) { - switch $0 { - case .success(let data): - guard let ndfData = data, let ndf = String(data: ndfData, encoding: .utf8) else { - completion(.failure(NSError.create("NDF is empty (?)"))) - return - } - - completion(.success(ndf)) - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func purgeFiles() { - FileManager.xxCleanup() - } - - public func newClientFromBackup( - passphrase: String, - data: Data, - ndf: String - ) throws -> (Client, Data?) { - var error: NSError? - - let password = B.secret(32)! - try keychain.store(password: password) - - let backupData = B.fromBackup( - ndf, - FileManager.xxPath, - password, - "\(passphrase)".data(using: .utf8), - data, - &error - ) - - if let error = error { throw error } - - var email: String? - var phone: String? - - let report = try! JSONDecoder().decode(BackupReport.self, from: backupData!) - - if !report.parameters.isEmpty { - let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8)) - phone = params.phone - email = params.email - } - - let client = try loadClient(with: password, fromBackup: true, email: email, phone: phone) - return (client, backupData) - } - - public func newClient(ndf: String) throws -> Client { - var password: Data! - - if hasClient == false { - var error: NSError? - - password = B.secret(32) - try keychain.store(password: password) - - _ = B.new(ndf, FileManager.xxPath, password, nil, &error) - if let error = error { throw error } - } else { - guard let secret = try keychain.getPassword() else { - throw NSError.create("Empty stored secret") - } - - password = secret - } - - return try loadClient(with: password, fromBackup: false, email: nil, phone: nil) - } - - public func loadClient( - with secret: Data, - fromBackup: Bool, - email: String?, - phone: String? - ) throws -> Client { - var error: NSError? - let bindings = B.login(FileManager.xxPath, secret, "", &error) - if let error = error { throw error } - - if let defaults = UserDefaults(suiteName: "group.elixxir.messenger") { - defaults.set(bindings!.receptionId.base64EncodedString(), forKey: "receptionId") - } - - return Client(bindings!, fromBackup: fromBackup, email: email, phone: phone) - } -} - -extension NetworkEnvironment { - var url: String { - switch self { - case .mainnet: - return "https://elixxir-bins.s3.us-west-1.amazonaws.com/ndf/mainnet.json" - } - } - - var cert: String { - switch self { - case .mainnet: - guard let filepath = Bundle.module.path(forResource: "cert_mainnet", ofType: "txt"), - let certString = try? String(contentsOfFile: filepath) else { - fatalError("Couldn't retrieve network cert file.") - } - - return certString - } - } -} diff --git a/Sources/Keychain/Dependency.swift b/Sources/Keychain/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..02ca45a9267826025a38dc77c4826eb034113bb1 --- /dev/null +++ b/Sources/Keychain/Dependency.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum KeychainDependencyKey: DependencyKey { + static let liveValue: KeychainManager = .live + static let testValue: KeychainManager = .unimplemented +} + +extension DependencyValues { + public var keychain: KeychainManager { + get { self[KeychainDependencyKey.self] } + set { self[KeychainDependencyKey.self] = newValue } + } +} diff --git a/Sources/Keychain/DestroyKeychain.swift b/Sources/Keychain/DestroyKeychain.swift new file mode 100644 index 0000000000000000000000000000000000000000..8bb24fe094745fa5b65e2c3e9e68b6cfc2fabc08 --- /dev/null +++ b/Sources/Keychain/DestroyKeychain.swift @@ -0,0 +1,22 @@ +import KeychainAccess +import XCTestDynamicOverlay + +public struct DestroyKeychain { + public var run: () throws -> Void + + public func callAsFunction() throws -> Void { + try run() + } +} + +extension DestroyKeychain { + public static let live = DestroyKeychain { + try Keychain(service: "XXM").removeAll() + } +} + +extension DestroyKeychain { + public static let unimplemented = DestroyKeychain( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/Keychain/GetValueForKey.swift b/Sources/Keychain/GetValueForKey.swift new file mode 100644 index 0000000000000000000000000000000000000000..31fe3ca81fc53279bf5f1d0a99ca10704927839e --- /dev/null +++ b/Sources/Keychain/GetValueForKey.swift @@ -0,0 +1,22 @@ +import KeychainAccess +import XCTestDynamicOverlay + +public struct GetValueForKey { + public var run: (String) throws -> String? + + public func callAsFunction(_ key: String) throws -> String? { + try run(key) + } +} + +extension GetValueForKey { + public static let live = GetValueForKey { + try Keychain(service: "XXM").get($0) + } +} + +extension GetValueForKey { + public static let unimplemented = GetValueForKey( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/Keychain/KeychainHandler.swift b/Sources/Keychain/KeychainHandler.swift index 6ac0d645d6def33360ee5ed0d0ab1e973a0487fc..6b55051142ba5937692e491e1620460469cf0b6e 100644 --- a/Sources/Keychain/KeychainHandler.swift +++ b/Sources/Keychain/KeychainHandler.swift @@ -1,46 +1,24 @@ -import Foundation -import KeychainAccess - -public enum KeychainSFTP: String { - case pwd - case host - case username +public struct KeychainManager { + public var set: SetValueForKey + public var get: GetValueForKey + public var remove: RemoveValueForKey + public var destroy: DestroyKeychain } -public protocol KeychainHandling { - func clear() throws - func getPassword() throws -> Data? - func store(password pwd: Data) throws - - func get(key: KeychainSFTP) throws -> String? - func store(key: KeychainSFTP, value: String) throws +extension KeychainManager { + public static let live = KeychainManager( + set: .live, + get: .live, + remove: .live, + destroy: .live + ) } -public struct KeychainHandler: KeychainHandling { - private let keychain: Keychain - private let password = "password" - - public init() { - self.keychain = Keychain(service: "XXM") - } - - public func clear() throws { - try keychain.removeAll() - } - - public func store(password pwd: Data) throws { - try keychain.set(pwd, key: password) - } - - public func getPassword() throws -> Data? { - try keychain.getData(password) - } - - 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) - } +extension KeychainManager { + public static let unimplemented = KeychainManager( + set: .unimplemented, + get: .unimplemented, + remove: .unimplemented, + destroy: .unimplemented + ) } diff --git a/Sources/Keychain/MockKeychainHandler.swift b/Sources/Keychain/MockKeychainHandler.swift deleted file mode 100644 index 39d4a33ddb1e0e6d8a75d8b5a1ea980d8fb189bf..0000000000000000000000000000000000000000 --- a/Sources/Keychain/MockKeychainHandler.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -public struct MockKeychainHandler: KeychainHandling { - public init() {} - - public func clear() throws {} - public func store(password pwd: Data) throws {} - public func getPassword() throws -> Data? { Data() } - public func get(key: KeychainSFTP) throws -> String? { nil } - public func store(key: KeychainSFTP, value: String) throws {} -} diff --git a/Sources/Keychain/RemoveValueForKey.swift b/Sources/Keychain/RemoveValueForKey.swift new file mode 100644 index 0000000000000000000000000000000000000000..9868a6f6d6bed828d64881877a8d082a90d8afe1 --- /dev/null +++ b/Sources/Keychain/RemoveValueForKey.swift @@ -0,0 +1,22 @@ +import KeychainAccess +import XCTestDynamicOverlay + +public struct RemoveValueForKey { + public var run: (String) throws -> Void + + public func callAsFunction(_ key: String) throws -> Void { + try run(key) + } +} + +extension RemoveValueForKey { + public static let live = RemoveValueForKey { + try Keychain(service: "XXM").remove($0) + } +} + +extension RemoveValueForKey { + public static let unimplemented = RemoveValueForKey( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/Keychain/SetValueForKey.swift b/Sources/Keychain/SetValueForKey.swift new file mode 100644 index 0000000000000000000000000000000000000000..c0cc66ba4816f3156798705ef2bfc34275698a58 --- /dev/null +++ b/Sources/Keychain/SetValueForKey.swift @@ -0,0 +1,22 @@ +import KeychainAccess +import XCTestDynamicOverlay + +public struct SetValueForKey { + public var run: (String, String) throws -> Void + + public func callAsFunction(_ value: String, for key: String) throws -> Void { + try run(value, key) + } +} + +extension SetValueForKey { + public static let live = SetValueForKey { value, key in + try Keychain(service: "XXM").set(value, key: key) + } +} + +extension SetValueForKey { + public static let unimplemented = SetValueForKey( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index cf8b093b2131d2becbb7d5e6bb55a3b02fb73c0e..d6d1780b9c64bae58cf3508c0e3be095144cea90 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -1,162 +1,137 @@ -import HUD import UIKit import Shared import Combine -import Defaults -import PushFeature -import DependencyInjection +import Dependencies +import AppResources +import DrawerFeature +import AppNavigation public final class LaunchController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: LaunchCoordinating - - @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool - - lazy private var screenView = LaunchView() - - private let blocker = UpdateBlocker() - private let viewModel = LaunchViewModel() - public var pendingPushRoute: PushRouter.Route? - private var cancellables = Set<AnyCancellable>() - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - viewModel.viewDidAppear() - } - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController? - .navigationBar - .customize(translucent: true) - } - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - screenView.setupGradient() + @Dependency(\.navigator) var navigator: Navigator + + private lazy var screenView = LaunchView() + private let viewModel = LaunchViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public var pendingPushNotificationRoute: PushNotificationRouter.Route? + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + viewModel + .statePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard $0.shouldPushChats == false else { + guard $0.shouldShowTerms == false else { + navigator.perform(PresentTermsAndConditions(replacing: true, on: navigationController!)) + return + } + if let route = pendingPushNotificationRoute { + hasPendingPushRoute(route) + return + } + navigator.perform(PresentChatList(on: navigationController!)) + return + } + guard $0.shouldPushOnboarding == false else { + navigator.perform(PresentOnboardingStart(on: navigationController!)) + return + } + if let update = $0.shouldOfferUpdate { + offerUpdate(model: update) + } + }.store(in: &cancellables) + + viewModel.startLaunch() + } + + private func hasPendingPushRoute(_ route: PushNotificationRouter.Route) { + switch route { + case .requests: + navigator.perform(PresentRequests(on: navigationController!)) + case .search(username: let username): + navigator.perform(PresentSearch( + searching: username, + fromOnboarding: true, + on: navigationController!)) + case .groupChat(id: let groupId): + if let info = viewModel.getGroupInfoWith(groupId: groupId) { + navigator.perform(PresentGroupChat(groupInfo: info, on: navigationController!)) + return + } + navigator.perform(PresentChatList(on: navigationController!)) + case .contactChat(id: let userId): + if let model = viewModel.getContactWith(userId: userId) { + navigator.perform(PresentChat(contact: model, on: navigationController!)) + return + } + navigator.perform(PresentChatList(on: navigationController!)) } - - public override func viewDidLoad() { - super.viewDidLoad() - - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - viewModel.routePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch $0 { - case .chats: - guard didAcceptTerms == true else { - coordinator.toTerms(from: self) - return - } - - if let pushRoute = pendingPushRoute { - switch pushRoute { - case .requests: - coordinator.toRequests(from: self) - - case .search(username: let username): - coordinator.toSearch(searching: username, from: self) - - case .groupChat(id: let groupId): - if let groupInfo = viewModel.getGroupInfoWith(groupId: groupId) { - coordinator.toGroupChat(with: groupInfo, from: self) - return - } - coordinator.toChats(from: self) - - case .contactChat(id: let userId): - if let contact = viewModel.getContactWith(userId: userId) { - coordinator.toSingleChat(with: contact, from: self) - return - } - coordinator.toChats(from: self) - } - - return - } - - coordinator.toChats(from: self) - - case .onboarding(let ndf): - coordinator.toOnboarding(with: ndf, from: self) - - case .update(let model): - offerUpdate(model: model) - } - }.store(in: &cancellables) + } + + private func offerUpdate(model: LaunchViewModel.UpdateModel) { + let updateButton = CapsuleButton() + updateButton.set( + style: .brandColored, + title: model.positiveActionTitle + ) + let notNowButton = CapsuleButton() + if let negativeTitle = model.negativeActionTitle { + notNowButton.set( + style: .red, + title: negativeTitle + ) } - - private func offerUpdate(model: Update) { - let drawerView = UIView() - drawerView.backgroundColor = Asset.neutralSecondary.color - drawerView.layer.cornerRadius = 5 - - let vStack = UIStackView() - vStack.axis = .vertical - vStack.spacing = 10 - drawerView.addSubview(vStack) - - vStack.snp.makeConstraints { - $0.top.equalToSuperview().offset(18) - $0.left.equalToSuperview().offset(18) - $0.right.equalToSuperview().offset(-18) - $0.bottom.equalToSuperview().offset(-18) + updateButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + UIApplication.shared.open(.init(string: model.urlString)!) } - - let title = UILabel() - title.text = "App Update" - title.textAlignment = .center - title.textColor = Asset.neutralDark.color - - let body = UILabel() - body.numberOfLines = 0 - body.textAlignment = .center - body.textColor = Asset.neutralDark.color - - let update = CapsuleButton() - update.publisher(for: .touchUpInside) - .sink { UIApplication.shared.open(.init(string: model.urlString)!, options: [:]) } - .store(in: &cancellables) - - vStack.addArrangedSubview(title) - vStack.addArrangedSubview(body) - vStack.addArrangedSubview(update) - - body.text = model.content - update.set( - style: model.actionStyle, - title: model.positiveActionTitle - ) - - if let negativeTitle = model.negativeActionTitle { - let negativeButton = CapsuleButton() - negativeButton.set(style: .simplestColoredRed, title: negativeTitle) - - negativeButton.publisher(for: .touchUpInside) - .sink { [unowned self] in - blocker.hideWindow() - viewModel.versionApproved() - }.store(in: &cancellables) - - vStack.addArrangedSubview(negativeButton) - } - - blocker.window?.addSubview(drawerView) - drawerView.snp.makeConstraints { - $0.left.equalToSuperview().offset(18) - $0.center.equalToSuperview() - $0.right.equalToSuperview().offset(-18) + }.store(in: &drawerCancellables) + + notNowButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + self.viewModel.didRefuseUpdating() } + }.store(in: &drawerCancellables) - blocker.showWindow() + var actions: [UIView] = [updateButton] + if model.negativeActionTitle != nil { + actions.append(notNowButton) } + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: "App Update", + color: Asset.neutralActive.color, + alignment: .center, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: model.content, + color: Asset.neutralBody.color, + alignment: .center, + spacingAfter: 19 + ), + DrawerStack( + axis: .vertical, + views: actions + ) + ], isDismissable: false, from: self)) + } } diff --git a/Sources/LaunchFeature/LaunchCoordinator.swift b/Sources/LaunchFeature/LaunchCoordinator.swift deleted file mode 100644 index 37035773d6cfd875d5acf6de309b4ac84d72ae6f..0000000000000000000000000000000000000000 --- a/Sources/LaunchFeature/LaunchCoordinator.swift +++ /dev/null @@ -1,84 +0,0 @@ -import UIKit -import Models -import XXModels -import Presentation - -public protocol LaunchCoordinating { - func toChats(from: UIViewController) - func toTerms(from: UIViewController) - func toRequests(from: UIViewController) - func toSearch(searching: String, from: UIViewController) - func toOnboarding(with: String, from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toGroupChat(with: GroupInfo, from: UIViewController) -} - -public struct LaunchCoordinator: LaunchCoordinating { - var replacePresenter: Presenting = ReplacePresenter() - - var termsFactory: (String?) -> UIViewController - var searchFactory: (String) -> UIViewController - var requestsFactory: () -> UIViewController - var chatListFactory: () -> UIViewController - var onboardingFactory: (String) -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupInfo) -> UIViewController - - public init( - termsFactory: @escaping (String?) -> UIViewController, - searchFactory: @escaping (String) -> UIViewController, - requestsFactory: @escaping () -> UIViewController, - chatListFactory: @escaping () -> UIViewController, - onboardingFactory: @escaping (String) -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupInfo) -> UIViewController - ) { - self.termsFactory = termsFactory - self.searchFactory = searchFactory - self.requestsFactory = requestsFactory - self.chatListFactory = chatListFactory - self.groupChatFactory = groupChatFactory - self.onboardingFactory = onboardingFactory - self.singleChatFactory = singleChatFactory - } -} - -public extension LaunchCoordinator { - func toSearch(searching: String, from parent: UIViewController) { - let screen = searchFactory(searching) - let chatListScreen = chatListFactory() - replacePresenter.present(chatListScreen, screen, from: parent) - } - - func toTerms(from parent: UIViewController) { - let screen = termsFactory(nil) - replacePresenter.present(screen, from: parent) - } - - func toChats(from parent: UIViewController) { - let screen = chatListFactory() - replacePresenter.present(screen, from: parent) - } - - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - replacePresenter.present(screen, from: parent) - } - - func toOnboarding(with ndf: String, from parent: UIViewController) { - let screen = onboardingFactory(ndf) - replacePresenter.present(screen, from: parent) - } - - func toSingleChat(with contact: Contact, from parent: UIViewController) { - let chatListScreen = chatListFactory() - let singleChatScreen = singleChatFactory(contact) - replacePresenter.present(chatListScreen, singleChatScreen, from: parent) - } - - func toGroupChat(with group: GroupInfo, from parent: UIViewController) { - let chatListScreen = chatListFactory() - let groupChatScreen = groupChatFactory(group) - replacePresenter.present(chatListScreen, groupChatScreen, from: parent) - } -} diff --git a/Sources/LaunchFeature/LaunchView.swift b/Sources/LaunchFeature/LaunchView.swift index 995c9f8c6b66501798069a736594c3e4a87a5537..40f6fb44b8b23520c062774adca2df350b98a409 100644 --- a/Sources/LaunchFeature/LaunchView.swift +++ b/Sources/LaunchFeature/LaunchView.swift @@ -1,37 +1,37 @@ import UIKit -import Shared +import AppResources final class LaunchView: UIView { - private var imageView = UIImageView() + let imageView = UIImageView() + let gradientLayer = CAGradientLayer() - init() { - super.init(frame: .zero) - imageView.image = Asset.splash.image - imageView.contentMode = .scaleAspectFit - backgroundColor = Asset.neutralWhite.color + init() { + super.init(frame: .zero) - addSubview(imageView) + gradientLayer.colors = [ + UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, + UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, + UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, + UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor + ] + gradientLayer.startPoint = CGPoint(x: 1, y: 0) + gradientLayer.endPoint = CGPoint(x: 0, y: 1) + layer.insertSublayer(gradientLayer, at: 0) + imageView.image = Asset.splash.image + imageView.contentMode = .scaleAspectFit + backgroundColor = Asset.neutralWhite.color - imageView.snp.makeConstraints { - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(100) - } - } + addSubview(imageView) - required init?(coder: NSCoder) { nil } + imageView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.left.equalToSuperview().offset(100) + } + } - func setupGradient() { - let gradient = CAGradientLayer() - gradient.colors = [ - UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, - UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, - UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, - UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor - ] + required init?(coder: NSCoder) { nil } - gradient.frame = bounds - gradient.startPoint = CGPoint(x: 1, y: 0) - gradient.endPoint = CGPoint(x: 0, y: 1) - layer.insertSublayer(gradient, at: 0) - } + override func layoutSubviews() { + gradientLayer.frame = bounds + } } diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 73fa6722f37c0fec119a11866046db37ddb0ae9e..bbb68e5dc47560b8542113565e198eb12c96e1bf 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -1,266 +1,325 @@ -import HUD import Shared -import Models import Combine import Defaults import XXModels import Keychain -import Foundation -import Integration -import Permissions -import ToastFeature -import DropboxFeature -import VersionChecking +import XXClient +import CloudFiles +import CheckVersion +import AppResources +import BackupFeature import ReportingFeature -import CombineSchedulers -import DependencyInjection +import CloudFilesDropbox +import XXMessengerClient + +import UpdateErrors +import FetchBannedList +import ProcessBannedList + +import AppCore +import Foundation +import PermissionsFeature +import ComposableArchitecture + +import XXDatabase +import XXLegacyDatabaseMigrator + +import class XXClient.Cancellable + +import PulseLogHandler -struct Update { +final class LaunchViewModel { + struct UpdateModel { let content: String let urlString: String let positiveActionTitle: String let negativeActionTitle: String? let actionStyle: CapsuleButtonStyle -} - -enum LaunchRoute { - case chats - case update(Update) - case onboarding(String) -} - -final class LaunchViewModel { - @Dependency private var network: XXNetworking - @Dependency private var versionChecker: VersionChecker - @Dependency private var dropboxService: DropboxInterface - @Dependency private var keychainHandler: KeychainHandling - @Dependency private var permissionHandler: PermissionHandling - @Dependency private var fetchBannedList: FetchBannedList - @Dependency private var reportingStatus: ReportingStatus - @Dependency private var processBannedList: ProcessBannedList - @Dependency private var toastController: ToastController - @Dependency private var session: SessionType - - @KeyObject(.username, defaultValue: nil) var username: String? - @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var routePublisher: AnyPublisher<LaunchRoute, Never> { - routeSubject.eraseToAnyPublisher() + } + + struct ViewState { + var shouldShowTerms = false + var shouldPushChats = false + var shouldOfferUpdate: UpdateModel? + var shouldPushOnboarding = false + } + + @Dependency(\.app.log) var log + @Dependency(\.app.bgQueue) var bgQueue + @Dependency(\.permissions) var permissions + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.updateErrors) var updateErrors + @Dependency(\.app.hudManager) var hudManager + @Dependency(\.checkVersion) var checkVersion + @Dependency(\.dummyTraffic) var dummyTraffic + @Dependency(\.app.toastManager) var toastManager + @Dependency(\.fetchBannedList) var fetchBannedList + @Dependency(\.reportingStatus) var reportingStatus + @Dependency(\.app.networkMonitor) var networkMonitor + @Dependency(\.processBannedList) var processBannedList + + @Dependency(\.app.authHandler) var authHandler + @Dependency(\.app.groupRequest) var groupRequest + @Dependency(\.app.backupHandler) var backupHandler + @Dependency(\.app.messageListener) var messageListener + @Dependency(\.app.receiveFileHandler) var receiveFileHandler + @Dependency(\.app.groupMessageHandler) var groupMessageHandler + + var authHandlerCancellable: Cancellable? + var groupRequestCancellable: Cancellable? + var backupHandlerCancellable: Cancellable? + var networkHandlerCancellable: Cancellable? + var receiveFileHandlerCancellable: Cancellable? + var groupMessageHandlerCancellable: Cancellable? + var messageListenerHandlerCancellable: Cancellable? + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool + @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool + + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + let dropboxManager = CloudFilesManager.dropbox( + appKey: "ppx0de5f16p9aq2", + path: "/backup/backup.xxm" + ) + + let sftpManager = CloudFilesManager.sftp( + host: "", + username: "", + password: "", + fileName: "" + ) + + let stateSubject = CurrentValueSubject <ViewState, Never>(.init()) + + func startLaunch() { + if !didAcceptTerms { + stateSubject.value.shouldShowTerms = true } + hudManager.show() + checkVersion { + switch $0 { + case .success(let result): + switch result { + case .updated: + self.didVerifyVersion() + case .outdated(let appUrl): + self.hudManager.hide() + + self.stateSubject.value.shouldOfferUpdate = .init( + content: Localized.Launch.Version.Recommended.title, + urlString: appUrl, + positiveActionTitle: Localized.Launch.Version.Recommended.positive, + negativeActionTitle: Localized.Launch.Version.Recommended.negative, + actionStyle: .simplestColoredRed + ) + case .wayTooOld(let appUrl, let minimumVersionMessage): + self.hudManager.hide() - var mainScheduler: AnySchedulerOf<DispatchQueue> = { - DispatchQueue.main.eraseToAnyScheduler() - }() - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = { - DispatchQueue.global().eraseToAnyScheduler() - }() - - private var cancellables = Set<AnyCancellable>() - private let routeSubject = PassthroughSubject<LaunchRoute, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - - func viewDidAppear() { - mainScheduler.schedule(after: .init(.now() + 1)) { [weak self] in - guard let self = self else { return } - - self.hudSubject.send(.on) - - self.versionChecker().sink { [unowned self] in - switch $0 { - case .upToDate: - self.versionApproved() - case .failure(let error): - self.versionFailed(error: error) - case .updateRequired(let info): - self.versionUpdateRequired(info) - case .updateRecommended(let info): - self.versionUpdateRecommended(info) - } - }.store(in: &self.cancellables) + self.stateSubject.value.shouldOfferUpdate = .init( + content: minimumVersionMessage, + urlString: appUrl, + positiveActionTitle: Localized.Launch.Version.Required.positive, + negativeActionTitle: nil, + actionStyle: .brandColored + ) } + case .failure(let error): + self.hudManager.show(.init( + title: Localized.Launch.Version.failed, + content: error.localizedDescription + )) + } } - - func versionApproved() { - network.writeLogs() - - network.updateNDF { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let ndf): - self.network.updateErrors() - - guard self.network.hasClient else { - self.hudSubject.send(.none) - self.routeSubject.send(.onboarding(ndf)) - self.dropboxService.unlink() - try? self.keychainHandler.clear() - return - } - - guard self.username != nil else { - self.network.purgeFiles() - self.hudSubject.send(.none) - self.routeSubject.send(.onboarding(ndf)) - self.dropboxService.unlink() - try? self.keychainHandler.clear() - return - } - - self.backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - let session = try Session(ndf: ndf) - DependencyInjection.Container.shared.register(session as SessionType) - - self.updateBannedList { - DispatchQueue.main.async { - self.hudSubject.send(.none) - self.checkBiometrics() - } - } - } catch { - DispatchQueue.main.async { - self.hudSubject.send(.error(HUDError(with: error))) - } - } - } - - case .failure(let error): - self.hudSubject.send(.error(HUDError(with: error))) + } + + func didRefuseUpdating() { + hudManager.show() + didVerifyVersion() + } + + private func didVerifyVersion() { + updateBannedList { + self.updateErrors { + switch $0 { + case .success: + do { + if !self.dbManager.hasDB() { + try self.dbManager.makeDB() } + try self.setupMessenger() + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudManager.show(.init(content: xxError)) + } + case .failure(let error): + self.hudManager.show(.init(error: error)) } + } } + } +} - func getContactWith(userId: Data) -> Contact? { - let query = Contact.Query( - id: [userId], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) +extension LaunchViewModel { + func setupMessenger() throws { + _ = try messenger.setLogLevel(.trace) + messenger.startLogging() - return try! session.dbManager.fetchContacts(query).first + authHandlerCancellable = authHandler { [weak self] in + self?.log(.error($0 as NSError)) } - - func getGroupInfoWith(groupId: Data) -> GroupInfo? { - let query = GroupInfo.Query(groupId: groupId) - return try! session.dbManager.fetchGroupInfos(query).first + backupHandlerCancellable = backupHandler { [weak self] in + self?.log(.error($0 as NSError)) } - - private func versionFailed(error: Error) { - let title = Localized.Launch.Version.failed - let content = error.localizedDescription - let hudError = HUDError(content: content, title: title, dismissable: false) - - hudSubject.send(.error(hudError)) + receiveFileHandlerCancellable = receiveFileHandler { [weak self] in + self?.log(.error($0 as NSError)) + } + messageListenerHandlerCancellable = messageListener { [weak self] in + self?.log(.error($0 as NSError)) } - private func versionUpdateRequired(_ info: DappVersionInformation) { - hudSubject.send(.none) + if messenger.isLoaded() == false { + if messenger.isCreated() == false { + try messenger.create() + } + try messenger.load() + } + try messenger.start() + if messenger.isConnected() == false { + try messenger.connect() + try messenger.listenForMessages() + } - let model = Update( - content: info.minimumMessage, - urlString: info.appUrl, - positiveActionTitle: Localized.Launch.Version.Required.positive, - negativeActionTitle: nil, - actionStyle: .brandColored - ) + let dummyTrafficManager = try NewDummyTrafficManager.live( + cMixId: messenger.e2e()!.getId() + ) + dummyTraffic.set(dummyTrafficManager) + + try dummyTrafficManager.setStatus(dummyTrafficOn) + + if messenger.isLoggedIn() == false { + if try messenger.isRegistered() { + try messenger.logIn() + hudManager.hide() + stateSubject.value.shouldPushChats = true + } else { + try? sftpManager.unlink() + try? dropboxManager.unlink() + hudManager.hide() + stateSubject.value.shouldPushOnboarding = true + } + } else { + hudManager.hide() + stateSubject.value.shouldPushChats = true + } + if !messenger.isBackupRunning() { + try? messenger.resumeBackup() + } - routeSubject.send(.update(model)) + groupRequestCancellable = groupRequest { [weak self] in + self?.log(.error($0 as NSError)) } - private func versionUpdateRecommended(_ info: DappVersionInformation) { - hudSubject.send(.none) + groupMessageHandlerCancellable = groupMessageHandler { [weak self] in + self?.log(.error($0 as NSError)) + } - let model = Update( - content: Localized.Launch.Version.Recommended.title, - urlString: info.appUrl, - positiveActionTitle: Localized.Launch.Version.Recommended.positive, - negativeActionTitle: Localized.Launch.Version.Recommended.negative, - actionStyle: .simplestColoredRed - ) + try messenger.startGroupChat() - routeSubject.send(.update(model)) + try messenger.trackServices { [weak self] in + self?.log(.error($0 as NSError)) } - private func checkBiometrics() { - if permissionHandler.isBiometricsAvailable && isBiometricsOn { - permissionHandler.requestBiometrics { [weak self] in - guard let self = self else { return } + try messenger.startFileTransfer() - switch $0 { - case .success(let granted): - guard granted else { return } - self.routeSubject.send(.chats) + networkMonitor.start() + networkHandlerCancellable = messenger.cMix.get()!.addHealthCallback( + HealthCallback { + self.networkMonitor.update($0) + } + ) - case .failure(let error): - self.hudSubject.send(.error(HUDError(with: error))) - } - } - } else { - self.routeSubject.send(.chats) - } - } + try failPendingProcessesFromLastSession() + } +} - private func updateBannedList(completion: @escaping () -> Void) { - fetchBannedList { result in - switch result { - case .failure(_): - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.updateBannedList(completion: completion) - } - case .success(let data): - self.processBannedList(data, completion: completion) - } +extension LaunchViewModel { + func failPendingProcessesFromLastSession() throws { + try dbManager.getDB().bulkUpdateMessages( + .init(status: [.sending]), + .init(status: .sendingFailed) + ) + } + + func updateBannedList(completion: @escaping () -> Void) { + fetchBannedList { result in + switch result { + case .failure(_): + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.updateBannedList(completion: completion) } + case .success(let data): + self.processBannedList(data, completion: completion) + } } - - private func processBannedList(_ data: Data, completion: @escaping () -> Void) { - processBannedList( - data: data, - forEach: { result in - switch result { - case .success(let userId): - let query = Contact.Query(id: [userId]) - if var contact = try! self.session.dbManager.fetchContacts(query).first { - if contact.isBanned == false { - contact.isBanned = true - try! self.session.dbManager.saveContact(contact) - self.enqueueBanWarning(contact: contact) - } - } else { - try! self.session.dbManager.saveContact(.init(id: userId, isBanned: true)) - } - - case .failure(_): - break - } - }, - completion: { result in - switch result { - case .failure(_): - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.updateBannedList(completion: completion) - } - - case .success(_): - completion() - } + } + + func processBannedList(_ data: Data, completion: @escaping () -> Void) { + processBannedList( + data: data, + forEach: { result in + switch result { + case .success(let userId): + let query = Contact.Query(id: [userId]) + if var contact = try! dbManager.getDB().fetchContacts(query).first { + if contact.isBanned == false { + contact.isBanned = true + try! dbManager.getDB().saveContact(contact) + enqueueBanWarning(contact: contact) } - ) - } + } else { + try! dbManager.getDB().saveContact(.init(id: userId, isBanned: true)) + } - private func enqueueBanWarning(contact: Contact) { - let name = (contact.nickname ?? contact.username) ?? "One of your contacts" - toastController.enqueueToast(model: .init( - title: "\(name) has been banned for offensive content.", - leftImage: Asset.requestSentToaster.image - )) - } + case .failure(_): + break + } + }, + completion: { result in + switch result { + case .failure(_): + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.updateBannedList(completion: completion) + } + case .success(_): + completion() + } + } + ) + } + + func enqueueBanWarning(contact: XXModels.Contact) { + let name = (contact.nickname ?? contact.username) ?? "One of your contacts" + toastManager.enqueue(.init( + title: "\(name) has been banned for offensive content.", + leftImage: Asset.requestSentToaster.image + )) + } + + func getContactWith(userId: Data) -> XXModels.Contact? { + try? dbManager.getDB().fetchContacts(.init( + id: [userId], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + )).first + } + + func getGroupInfoWith(groupId: Data) -> GroupInfo? { + try? dbManager.getDB().fetchGroupInfos(.init(groupId: groupId)).first + } } diff --git a/Sources/LaunchFeature/PushNotificationRouter.swift b/Sources/LaunchFeature/PushNotificationRouter.swift new file mode 100644 index 0000000000000000000000000000000000000000..45b7850af57987213fd45b3c6376d1792165c0e5 --- /dev/null +++ b/Sources/LaunchFeature/PushNotificationRouter.swift @@ -0,0 +1,25 @@ +import Foundation +import XCTestDynamicOverlay + +public struct PushNotificationRouter { + public typealias NavigateTo = (Route, @escaping () -> Void) -> Void + + public enum Route { + case requests + case groupChat(id: Data) + case contactChat(id: Data) + case search(username: String) + } + + public var navigateTo: NavigateTo + + public init(navigateTo: @escaping NavigateTo) { + self.navigateTo = navigateTo + } +} + +public extension PushNotificationRouter { + static let unimplemented = PushNotificationRouter( + navigateTo: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/LaunchFeature/UpdateBlocker.swift b/Sources/LaunchFeature/UpdateBlocker.swift deleted file mode 100644 index 571c2a527a33e8411c320cbc7b25fdec6145d399..0000000000000000000000000000000000000000 --- a/Sources/LaunchFeature/UpdateBlocker.swift +++ /dev/null @@ -1,24 +0,0 @@ -import UIKit -import Theme -import Shared - -final class UpdateBlocker { - private(set) var window: Window? = Window() - - func showWindow() { - window?.backgroundColor = UIColor.black.withAlphaComponent(0.5) - window?.rootViewController = StatusBarViewController(nil) - window?.alpha = 0.0 - window?.makeKeyAndVisible() - - UIView.animate(withDuration: 0.3) { self.window?.alpha = 1.0 } - } - - func hideWindow() { - UIView.animate(withDuration: 0.3) { - self.window?.alpha = 0.0 - } completion: { _ in - self.window = nil - } - } -} diff --git a/Sources/MenuFeature/Controllers/MenuController.swift b/Sources/MenuFeature/Controllers/MenuController.swift index 93f1fcf4a328da1f73f077b8f0f8806ceee0c7f5..17a9a73505fc73230b5fad387c0dd9c247621ea1 100644 --- a/Sources/MenuFeature/Controllers/MenuController.swift +++ b/Sources/MenuFeature/Controllers/MenuController.swift @@ -1,235 +1,252 @@ -import Theme import UIKit import Shared import Combine +import AppCore +import Dependencies +import AppResources +import AppNavigation import DrawerFeature -import DependencyInjection - -public enum MenuItem { - case join - case scan - case chats - case share - case profile - case contacts - case requests - case settings - case dashboard -} public final class MenuController: UIViewController { - @Dependency private var coordinator: MenuCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = MenuView() - - private let previousItem: MenuItem - private let viewModel = MenuViewModel() - private let previousController: UIViewController - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public init( - _ previousItem: MenuItem, - _ previousController: UIViewController - ) { - self.previousItem = previousItem - self.previousController = previousController - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - } - - public override func viewDidLoad() { - super.viewDidLoad() - - screenView.headerView.set( - username: viewModel.username, - image: viewModel.avatar - ) - - screenView.select(item: previousItem) - screenView.xxdkVersionLabel.text = "XXDK \(viewModel.xxdk)" - screenView.buildLabel.text = Localized.Menu.build(viewModel.build) - screenView.versionLabel.text = Localized.Menu.version(viewModel.version) - setupBindings() + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = MenuView() + + private let currentItem: MenuItem + private let viewModel = MenuViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + private var navController: UINavigationController? + + public init( + _ currentItem: MenuItem, + _ navController: UINavigationController? = nil + ) { + self.currentItem = currentItem + self.navController = navController + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + screenView.headerView.set( + username: viewModel.username, + image: viewModel.avatar + ) + + switch currentItem { + case .scan: + screenView.scanButton.set(color: Asset.brandPrimary.color) + case .chats: + screenView.chatsButton.set(color: Asset.brandPrimary.color) + case .contacts: + screenView.contactsButton.set(color: Asset.brandPrimary.color) + case .requests: + screenView.requestsButton.set(color: Asset.brandPrimary.color) + case .settings: + screenView.settingsButton.set(color: Asset.brandPrimary.color) + default: + break } - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.lightContent) - } - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - statusBarController.style.send(.darkContent) - } - - private func setupBindings() { - screenView.headerView.scanButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .scan else { return } - self.coordinator.toFlow(.scan, from: self.previousController) - } - }.store(in: &cancellables) - - screenView.headerView.nameButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .profile else { return } - self.coordinator.toFlow(.profile, from: self.previousController) - } - }.store(in: &cancellables) - - screenView.scanButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .scan else { return } - self.coordinator.toFlow(.scan, from: self.previousController) - } - }.store(in: &cancellables) - - screenView.chatsButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .chats else { return } - self.coordinator.toFlow(.chats, from: self.previousController) - } - }.store(in: &cancellables) - - screenView.contactsButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .contacts else { return } - self.coordinator.toFlow(.contacts, from: self.previousController) - } - }.store(in: &cancellables) - - screenView.settingsButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .settings else { return } - self.coordinator.toFlow(.settings, from: self.previousController) - } - }.store(in: &cancellables) - - screenView.dashboardButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .dashboard else { return } - self.presentDrawer( - title: Localized.ChatList.Dashboard.title, - subtitle: Localized.ChatList.Dashboard.subtitle, - actionTitle: Localized.ChatList.Dashboard.open) { - guard let url = URL(string: "https://dashboard.xx.network") else { return } - UIApplication.shared.open(url, options: [:]) - } - } - }.store(in: &cancellables) - - screenView.requestsButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .requests else { return } - self.coordinator.toFlow(.requests, from: self.previousController) - } - }.store(in: &cancellables) - - screenView.joinButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .join else { return } - self.presentDrawer( - title: Localized.ChatList.Join.title, - subtitle: Localized.ChatList.Join.subtitle, - actionTitle: Localized.ChatList.Dashboard.open) { - guard let url = URL(string: "https://xx.network") else { return } - UIApplication.shared.open(url, options: [:]) - } - } - }.store(in: &cancellables) - - screenView.shareButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dismiss(animated: true) { [weak self] in - guard let self = self, self.previousItem != .share else { return } - self.coordinator.toActivityController( - with: [Localized.Menu.shareContent(self.viewModel.referralDeeplink)], - from: self.previousController - ) - } - }.store(in: &cancellables) - - viewModel.requestCount - .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.requestsButton.updateNotification($0) } - .store(in: &cancellables) - } - - private func presentDrawer( - title: String, - subtitle: String, - actionTitle: String, - action: @escaping () -> Void - ) { - let actionButton = DrawerCapsuleButton(model: .init( - title: actionTitle, - style: .red - )) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 39 - ), - actionButton - ]) - - actionButton.action.receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - action() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: previousController) - } + screenView.xxdkVersionLabel.text = "XXDK \(viewModel.xxdk)" + screenView.buildLabel.text = Localized.Menu.build(viewModel.build) + screenView.versionLabel.text = Localized.Menu.version(viewModel.version) + setupBindings() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.lightContent) + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + statusBar.set(.darkContent) + } + + private func setupBindings() { + screenView + .headerView + .scanButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .scan else { return } + self.navigator.perform(PresentScan(on: self.navController!)) + } + }.store(in: &cancellables) + + screenView + .headerView + .nameButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .profile else { return } + self.navigator.perform(PresentProfile(on: self.navController!)) + } + }.store(in: &cancellables) + + screenView + .scanButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .scan else { return } + self.navigator.perform(PresentScan(on: self.navController!)) + } + }.store(in: &cancellables) + + screenView + .chatsButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .chats else { return } + self.navigator.perform(PresentChatList(on: self.navController!)) + } + }.store(in: &cancellables) + + screenView + .contactsButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .contacts else { return } + self.navigator.perform(PresentContactList(on: self.navController!)) + } + }.store(in: &cancellables) + + screenView + .settingsButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .settings else { return } + self.navigator.perform(PresentSettings(on: self.navController!)) + } + }.store(in: &cancellables) + + screenView + .dashboardButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .dashboard else { return } + self.presentDrawer( + title: Localized.ChatList.Dashboard.title, + subtitle: Localized.ChatList.Dashboard.subtitle, + actionTitle: Localized.ChatList.Dashboard.open) { + guard let url = URL(string: "https://dashboard.xx.network") else { return } + UIApplication.shared.open(url, options: [:]) + } + } + }.store(in: &cancellables) + + screenView + .requestsButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .requests else { return } + self.navigator.perform(PresentRequests(on: self.navController!)) + } + }.store(in: &cancellables) + + screenView + .joinButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .join else { return } + self.presentDrawer( + title: Localized.ChatList.Join.title, + subtitle: Localized.ChatList.Join.subtitle, + actionTitle: Localized.ChatList.Dashboard.open) { + guard let url = URL(string: "https://xx.network") else { return } + UIApplication.shared.open(url, options: [:]) + } + } + }.store(in: &cancellables) + + screenView + .shareButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self, self.currentItem != .share else { return } + self.navigator.perform(PresentActivitySheet(items: [ + Localized.Menu.shareContent(self.viewModel.referralDeeplink) + ], from: self.navController!.topViewController!)) + } + }.store(in: &cancellables) + + viewModel + .requestCount + .receive(on: DispatchQueue.main) + .sink { [weak screenView] in + screenView?.requestsButton.updateNotification($0) + }.store(in: &cancellables) + } + + private func presentDrawer( + title: String, + subtitle: String, + actionTitle: String, + action: @escaping () -> Void + ) { + let actionButton = DrawerCapsuleButton(model: .init( + title: actionTitle, + style: .red + )) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + action() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 39 + ), + actionButton + ], isDismissable: true, from: navController!.topViewController!)) + } } diff --git a/Sources/MenuFeature/Coordinator/MenuCoordinator.swift b/Sources/MenuFeature/Coordinator/MenuCoordinator.swift deleted file mode 100644 index 3e7d20cc85f6a4afa7e50e2f8133bdf041f5ace6..0000000000000000000000000000000000000000 --- a/Sources/MenuFeature/Coordinator/MenuCoordinator.swift +++ /dev/null @@ -1,73 +0,0 @@ -import UIKit -import Presentation - -public protocol MenuCoordinating { - func toFlow(_ item: MenuItem, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toActivityController(with: [Any], from: UIViewController) -} - -public struct MenuCoordinator: MenuCoordinating { - var modalPresenter: Presenting = ModalPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() - - var scanFactory: () -> UIViewController - var chatsFactory: () -> UIViewController - var profileFactory: () -> UIViewController - var settingsFactory: () -> UIViewController - var contactsFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var activityControllerFactory: ([Any]) -> UIViewController - = { UIActivityViewController(activityItems: $0, applicationActivities: nil) } - - public init( - scanFactory: @escaping () -> UIViewController, - chatsFactory: @escaping () -> UIViewController, - profileFactory: @escaping () -> UIViewController, - settingsFactory: @escaping () -> UIViewController, - contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController - ) { - self.scanFactory = scanFactory - self.chatsFactory = chatsFactory - self.profileFactory = profileFactory - self.settingsFactory = settingsFactory - self.contactsFactory = contactsFactory - self.requestsFactory = requestsFactory - } -} - -public extension MenuCoordinator { - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toFlow(_ item: MenuItem, from parent: UIViewController) { - let controller: UIViewController - - switch item { - case .scan: - controller = scanFactory() - case .chats: - controller = chatsFactory() - case .profile: - controller = profileFactory() - case .contacts: - controller = contactsFactory() - case .requests: - controller = requestsFactory() - case .settings: - controller = settingsFactory() - default: - fatalError() - } - - replacePresenter.present(controller, from: parent) - } - - func toActivityController(with items: [Any], from parent: UIViewController) { - let screen = activityControllerFactory(items) - modalPresenter.present(screen, from: parent) - } -} diff --git a/Sources/MenuFeature/ViewModels/MenuViewModel.swift b/Sources/MenuFeature/ViewModels/MenuViewModel.swift index 72fbe071d7b81db1f192b41d1ee5480a7a03d226..3005d8e2a7b22b375f44dfdac8635fd31f8c47d3 100644 --- a/Sources/MenuFeature/ViewModels/MenuViewModel.swift +++ b/Sources/MenuFeature/ViewModels/MenuViewModel.swift @@ -1,58 +1,61 @@ import Combine +import AppCore import XXModels +import XXClient import Defaults import Foundation -import Integration import ReportingFeature -import DependencyInjection +import ComposableArchitecture final class MenuViewModel { - @Dependency private var session: SessionType - @Dependency private var reportingStatus: ReportingStatus - - @KeyObject(.avatar, defaultValue: nil) var avatar: Data? - @KeyObject(.username, defaultValue: "") var username: String - - var requestCount: AnyPublisher<Int, Never> { - let groupQuery = Group.Query( - authStatus: [.pending], - isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, - isLeaderBanned: reportingStatus.isEnabled() ? false : nil - ) - - let contactsQuery = Contact.Query( - authStatus: [ - .verified, - .confirming, - .confirmationFailed, - .verificationFailed, - .verificationInProgress - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - return Publishers.CombineLatest( - session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure(), - session.dbManager.fetchGroupsPublisher(groupQuery).assertNoFailure() - ) - .map { $0.0.count + $0.1.count } - .eraseToAnyPublisher() - } - - var xxdk: String { - session.version - } - - var build: String { - Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" - } - - var version: String { - Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - } - - var referralDeeplink: String { - "https://elixxir.io/connect?username=\(username)" - } + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.reportingStatus) var reportingStatus: ReportingStatus + + @KeyObject(.avatar, defaultValue: nil) var avatar: Data? + @KeyObject(.username, defaultValue: "") var username: String + + var requestCount: AnyPublisher<Int, Never> { + let groupQuery = Group.Query( + authStatus: [.pending], + isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, + isLeaderBanned: reportingStatus.isEnabled() ? false : nil + ) + + let contactsQuery = Contact.Query( + authStatus: [ + .verified, + .confirming, + .confirmationFailed, + .verificationFailed, + .verificationInProgress + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return Publishers.CombineLatest( + try! dbManager.getDB().fetchContactsPublisher(contactsQuery) + .replaceError(with: []), + try! dbManager.getDB().fetchGroupsPublisher(groupQuery) + .replaceError(with: []) + ) + .map { $0.0.count + $0.1.count } + .eraseToAnyPublisher() + } + + var xxdk: String { + GetVersion.live() + } + + var build: String { + Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" + } + + var version: String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + } + + var referralDeeplink: String { + "https://elixxir.io/connect?username=\(username)" + } } diff --git a/Sources/MenuFeature/Views/MenuHeaderView.swift b/Sources/MenuFeature/Views/MenuHeaderView.swift index 2f925646b3b03c3aec498ccb67578a1b4f40fb89..eaea1321b5ea8f1eaaf7a7ebbda4c0190433979d 100644 --- a/Sources/MenuFeature/Views/MenuHeaderView.swift +++ b/Sources/MenuFeature/Views/MenuHeaderView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class MenuHeaderView: UIView { let nameButton = UIButton() diff --git a/Sources/MenuFeature/Views/MenuSectionButton.swift b/Sources/MenuFeature/Views/MenuSectionButton.swift index c5f6ea371a10f0ff3fc087b31f6dc744e11fd4a9..2432cdf6b8cb86833e407c9f5e22deeed2def9f5 100644 --- a/Sources/MenuFeature/Views/MenuSectionButton.swift +++ b/Sources/MenuFeature/Views/MenuSectionButton.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class MenuSectionButton: UIControl { let titleLabel = UILabel() diff --git a/Sources/MenuFeature/Views/MenuView.swift b/Sources/MenuFeature/Views/MenuView.swift index e256782eddaa5e686d2bfc7e3ef4af237c3eb51c..e62deae1b0ef07fdb6938845aec4048a824d2386 100644 --- a/Sources/MenuFeature/Views/MenuView.swift +++ b/Sources/MenuFeature/Views/MenuView.swift @@ -1,97 +1,81 @@ import UIKit import Shared +import AppResources final class MenuView: UIView { - let buildLabel = UILabel() - let versionLabel = UILabel() - let stackView = UIStackView() - let xxdkVersionLabel = UILabel() - let infoStackView = UIStackView() - let headerView = MenuHeaderView() - let joinButton = MenuSectionButton() - let scanButton = MenuSectionButton() - let shareButton = MenuSectionButton() - let chatsButton = MenuSectionButton() - let contactsButton = MenuSectionButton() - let requestsButton = MenuSectionButton() - let settingsButton = MenuSectionButton() - let dashboardButton = MenuSectionButton() + let buildLabel = UILabel() + let versionLabel = UILabel() + let stackView = UIStackView() + let xxdkVersionLabel = UILabel() + let infoStackView = UIStackView() + let headerView = MenuHeaderView() + let joinButton = MenuSectionButton() + let scanButton = MenuSectionButton() + let shareButton = MenuSectionButton() + let chatsButton = MenuSectionButton() + let contactsButton = MenuSectionButton() + let requestsButton = MenuSectionButton() + let settingsButton = MenuSectionButton() + let dashboardButton = MenuSectionButton() - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralDark.color + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralDark.color - scanButton.set(title: Localized.Menu.scan, image: Asset.menuScan.image) - shareButton.set(title: Localized.Menu.share, image: Asset.menuShare.image) - chatsButton.set(title: Localized.Menu.chats, image: Asset.menuChats.image) - joinButton.set(title: Localized.Menu.join, image: Asset.permissionLogo.image) - requestsButton.set(title: Localized.Menu.requests, image: Asset.menuRequests.image) - contactsButton.set(title: Localized.Menu.contacts, image: Asset.menuContacts.image) - settingsButton.set(title: Localized.Menu.settings, image: Asset.menuSettings.image) - dashboardButton.set(title: Localized.Menu.dashboard, image: Asset.menuDashboard.image) + scanButton.set(title: Localized.Menu.scan, image: Asset.menuScan.image) + shareButton.set(title: Localized.Menu.share, image: Asset.menuShare.image) + chatsButton.set(title: Localized.Menu.chats, image: Asset.menuChats.image) + joinButton.set(title: Localized.Menu.join, image: Asset.permissionLogo.image) + requestsButton.set(title: Localized.Menu.requests, image: Asset.menuRequests.image) + contactsButton.set(title: Localized.Menu.contacts, image: Asset.menuContacts.image) + settingsButton.set(title: Localized.Menu.settings, image: Asset.menuSettings.image) + dashboardButton.set(title: Localized.Menu.dashboard, image: Asset.menuDashboard.image) - stackView.addArrangedSubview(chatsButton) - stackView.addArrangedSubview(contactsButton) - stackView.addArrangedSubview(requestsButton) - stackView.addArrangedSubview(scanButton) - stackView.addArrangedSubview(settingsButton) - stackView.addArrangedSubview(dashboardButton) - stackView.addArrangedSubview(joinButton) - stackView.addArrangedSubview(shareButton) + stackView.addArrangedSubview(chatsButton) + stackView.addArrangedSubview(contactsButton) + stackView.addArrangedSubview(requestsButton) + stackView.addArrangedSubview(scanButton) + stackView.addArrangedSubview(settingsButton) + stackView.addArrangedSubview(dashboardButton) + stackView.addArrangedSubview(joinButton) + stackView.addArrangedSubview(shareButton) - infoStackView.spacing = 10 - infoStackView.axis = .vertical - [buildLabel, versionLabel, xxdkVersionLabel].forEach { - $0.textColor = Asset.neutralWeak.color - $0.font = Fonts.Mulish.regular.font(size: 12.0) - infoStackView.addArrangedSubview($0) - } + infoStackView.spacing = 10 + infoStackView.axis = .vertical + [buildLabel, versionLabel, xxdkVersionLabel].forEach { + $0.textColor = Asset.neutralWeak.color + $0.font = Fonts.Mulish.regular.font(size: 12.0) + infoStackView.addArrangedSubview($0) + } - stackView.spacing = 28 - stackView.axis = .vertical - stackView.distribution = .equalSpacing + stackView.spacing = 28 + stackView.axis = .vertical + stackView.distribution = .equalSpacing - addSubview(headerView) - addSubview(stackView) - addSubview(infoStackView) + addSubview(headerView) + addSubview(stackView) + addSubview(infoStackView) - setupConstraints() - } + setupConstraints() + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func select(item: MenuItem) { - switch item { - case .scan: - scanButton.set(color: Asset.brandPrimary.color) - case .chats: - chatsButton.set(color: Asset.brandPrimary.color) - case .contacts: - contactsButton.set(color: Asset.brandPrimary.color) - case .requests: - requestsButton.set(color: Asset.brandPrimary.color) - case .settings: - settingsButton.set(color: Asset.brandPrimary.color) - case .share, .join, .profile, .dashboard: - break - } + private func setupConstraints() { + headerView.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(20) + $0.left.equalToSuperview().offset(30) + $0.right.equalToSuperview().offset(-24) } - private func setupConstraints() { - headerView.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).offset(20) - $0.left.equalToSuperview().offset(30) - $0.right.equalToSuperview().offset(-24) - } - - stackView.snp.makeConstraints { - $0.left.equalToSuperview().offset(26) - $0.top.equalTo(headerView.snp.bottom).offset(75) - } + stackView.snp.makeConstraints { + $0.left.equalToSuperview().offset(26) + $0.top.equalTo(headerView.snp.bottom).offset(75) + } - infoStackView.snp.makeConstraints { - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) - $0.left.equalToSuperview().offset(20) - } + infoStackView.snp.makeConstraints { + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) + $0.left.equalToSuperview().offset(20) } + } } diff --git a/Sources/Models/AttributeConfirmation.swift b/Sources/Models/AttributeConfirmation.swift deleted file mode 100644 index 2f8b99ea4a332674224eeffb1559295d112f2099..0000000000000000000000000000000000000000 --- a/Sources/Models/AttributeConfirmation.swift +++ /dev/null @@ -1,15 +0,0 @@ -public struct AttributeConfirmation: Equatable { - public var content: String - public var isEmail: Bool = false - public var confirmationId: String? - - public init( - content: String, - isEmail: Bool = false, - confirmationId: String? = nil - ) { - self.content = content - self.isEmail = isEmail - self.confirmationId = confirmationId - } -} diff --git a/Sources/Models/Backup.swift b/Sources/Models/Backup.swift deleted file mode 100644 index a3518af44998b729a0c66f0c347d26d295634898..0000000000000000000000000000000000000000 --- a/Sources/Models/Backup.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -public struct Backup: Equatable, Codable { - public var id: String - public var date: Date - public var size: Float - - public init( - id: String, - date: Date, - size: Float - ) { - self.id = id - self.date = date - self.size = size - } -} diff --git a/Sources/Models/BackupSettings.swift b/Sources/Models/BackupSettings.swift deleted file mode 100644 index 1ec2883fc6119882f5008d26b86bb65ed54962c4..0000000000000000000000000000000000000000 --- a/Sources/Models/BackupSettings.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation - -public struct BackupSettings: Equatable, Codable { - public var wifiOnlyBackup: Bool - public var automaticBackups: Bool - public var enabledService: CloudService? - public var connectedServices: Set<CloudService> - public var backups: [CloudService: Backup] - - public init( - wifiOnlyBackup: Bool = false, - automaticBackups: Bool = false, - enabledService: CloudService? = nil, - connectedServices: Set<CloudService> = [], - backups: [CloudService: Backup] = [:] - ) { - self.wifiOnlyBackup = wifiOnlyBackup - self.automaticBackups = automaticBackups - self.enabledService = enabledService - self.connectedServices = connectedServices - self.backups = backups - } - - public func toData() -> Data { - (try? PropertyListEncoder().encode(self)) ?? Data() - } - - public init(fromData data: Data) { - let settings = try? PropertyListDecoder().decode(BackupSettings.self, from: data) - self.init( - wifiOnlyBackup: settings?.wifiOnlyBackup ?? false, - automaticBackups: settings?.automaticBackups ?? false, - enabledService: settings?.enabledService, - connectedServices: settings?.connectedServices ?? [], - backups: settings?.backups ?? [:] - ) - } -} - -public struct RestoreSettings { - public var backup: Backup? - public var cloudService: CloudService - - public init( - backup: Backup? = nil, - cloudService: CloudService - ) { - self.backup = backup - self.cloudService = cloudService - } -} diff --git a/Sources/Models/CloudService.swift b/Sources/Models/CloudService.swift deleted file mode 100644 index d217dca853e6ecf22f119520d9805567f7ead3a5..0000000000000000000000000000000000000000 --- a/Sources/Models/CloudService.swift +++ /dev/null @@ -1,6 +0,0 @@ -public enum CloudService: Equatable, Codable { - case drive - case icloud - case dropbox - case sftp -} diff --git a/Sources/Models/FactType.swift b/Sources/Models/FactType.swift deleted file mode 100644 index 5dce1f05c2a327db9a09852dfc62384e3636546a..0000000000000000000000000000000000000000 --- a/Sources/Models/FactType.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation - -public enum FactType: Int { - case username = 0 - case email - case phone - case nickname - - public var description: String { - switch self { - case .email: - return "Email" - case .nickname: - return "Nickname" - case .phone: - return "Phone" - case .username: - return "Username" - } - } - - public var prefix: String { - switch self { - case .email: - return "E" - case .nickname: - return "N" - case .phone: - return "P" - case .username: - return "U" - } - } -} diff --git a/Sources/Models/Payload.swift b/Sources/Models/Payload.swift deleted file mode 100644 index 8e6f519b5ca09531b97961d1616a63477325d002..0000000000000000000000000000000000000000 --- a/Sources/Models/Payload.swift +++ /dev/null @@ -1,37 +0,0 @@ -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 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() - } -} diff --git a/Sources/Models/Reply.swift b/Sources/Models/Reply.swift deleted file mode 100644 index 22fcf55aaa3c0b7f6c93618efe21531b1e4e36e6..0000000000000000000000000000000000000000 --- a/Sources/Models/Reply.swift +++ /dev/null @@ -1,19 +0,0 @@ -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 - } -} 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 ab805ca524e337bc8961c9dac719ac79d74f72f2..0000000000000000000000000000000000000000 --- a/Sources/NetworkMonitor/NetworkMonitor.swift +++ /dev/null @@ -1,90 +0,0 @@ -// https://www.reddit.com/r/swift/comments/ir8wn5/network_connectivity_is_always_unsatisfied_when/ - -import Network -import Combine -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 new file mode 100644 index 0000000000000000000000000000000000000000..92cdda4e7d229e51328343edce602841a14bb529 --- /dev/null +++ b/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift @@ -0,0 +1,184 @@ +import UIKit +import Shared +import Combine +import AppCore +import AppResources +import Dependencies +import AppNavigation +import DrawerFeature +import ScrollViewController + +public final class OnboardingCodeController: UIViewController { + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = OnboardingCodeView() + private lazy var scrollViewController = ScrollViewController() + + private let isEmail: Bool + private let content: String + private let viewModel: OnboardingCodeViewModel + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public init( + _ isEmail: Bool, + _ content: String, + _ confirmationId: String + ) { + self.viewModel = .init( + isEmail: isEmail, + content: content, + confirmationId: confirmationId + ) + self.isEmail = isEmail + self.content = content + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + + screenView.setupSubtitle( + isEmail ? + Localized.Onboarding.EmailConfirmation.subtitle(content) : + Localized.Onboarding.PhoneConfirmation.subtitle(content) + ) + + screenView.didTapInfo = { [weak self] in + guard let self else { return } + if self.isEmail { + self.presentInfo( + title: Localized.Onboarding.EmailConfirmation.Info.title, + subtitle: Localized.Onboarding.EmailConfirmation.Info.subtitle + ) + } else { + self.presentInfo( + title: Localized.Onboarding.PhoneConfirmation.Info.title, + subtitle: Localized.Onboarding.PhoneConfirmation.Info.subtitle + ) + } + } + } + + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } + + private func setupBindings() { + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.status) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.didConfirm) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard $0 == true else { return } + if isEmail { + navigator.perform(PresentOnboardingPhone(on: navigationController!)) + } else { + navigator.perform(PresentSearch( + fromOnboarding: true, + on: navigationController! + )) + } + }.store(in: &cancellables) + + screenView + .nextButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) + + screenView + .resendButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didTapResend() + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.resendDebouncer) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.resendButton.isEnabled = $0 == 0 + if $0 == 0 { + screenView.resendButton.setTitle(Localized.Profile.Code.resend(""), for: .normal) + } else { + screenView.resendButton.setTitle(Localized.Profile.Code.resend("(\($0))"), for: .disabled) + } + }.store(in: &cancellables) + } + + private func presentInfo(title: String, subtitle: String) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } +} diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift deleted file mode 100644 index 578a27dc5bdedfcd58ab9dbecfd1338c07bad324..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift +++ /dev/null @@ -1,152 +0,0 @@ -import HUD -import DrawerFeature -import Theme -import UIKit -import Shared -import Combine -import DependencyInjection -import ScrollViewController -import Models - -public final class OnboardingEmailConfirmationController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: OnboardingCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = OnboardingEmailConfirmationView() - lazy private var scrollViewController = ScrollViewController() - - private var cancellables = Set<AnyCancellable>() - private let completion: (UIViewController) -> Void - private var drawerCancellables = Set<AnyCancellable>() - private let viewModel: OnboardingEmailConfirmationViewModel - - public init( - _ confirmation: AttributeConfirmation, - _ completion: @escaping (UIViewController) -> Void - ) { - self.completion = completion - self.viewModel = OnboardingEmailConfirmationViewModel(confirmation) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - - screenView.setupSubtitle( - Localized.Onboarding.EmailConfirmation.subtitle(viewModel.confirmation.content) - ) - - screenView.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Onboarding.EmailConfirmation.Info.title, - subtitle: Localized.Onboarding.EmailConfirmation.Info.subtitle - ) - } - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color - } - - private func setupBindings() { - viewModel.hud.receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) - - viewModel.state - .map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) - - screenView.nextButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) - - viewModel.completionPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] _ in completion(self) } - .store(in: &cancellables) - - screenView.resendButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapResend() } - .store(in: &cancellables) - - viewModel.state - .map(\.resendDebouncer) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.resendButton.isEnabled = $0 == 0 - - if $0 == 0 { - screenView.resendButton.setTitle(Localized.Profile.Code.resend(""), for: .normal) - } else { - screenView.resendButton.setTitle(Localized.Profile.Code.resend("(\($0))"), for: .disabled) - } - }.store(in: &cancellables) - } - - private func presentInfo(title: String, subtitle: String) { - let actionButton = CapsuleButton() - actionButton.set(style: .seeThrough, title: Localized.Settings.InfoDrawer.action) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } -} diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift index 54fb8cf4efcf4cd29489729598ed22144b918f65..d15c6594393c5cb7942d78abd3173378dc4756b4 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift @@ -1,140 +1,147 @@ -import HUD -import DrawerFeature -import Theme import UIKit import Shared import Combine -import DependencyInjection +import AppCore +import AppResources +import Dependencies +import AppNavigation +import DrawerFeature import ScrollViewController public final class OnboardingEmailController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: OnboardingCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = OnboardingEmailView() - lazy private var scrollViewController = ScrollViewController() - - private var cancellables = Set<AnyCancellable>() - private let viewModel = OnboardingEmailViewModel() - private var drawerCancellables = Set<AnyCancellable>() - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(translucent: true) + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = OnboardingEmailView() + private lazy var scrollViewController = ScrollViewController() + + private var cancellables = Set<AnyCancellable>() + private let viewModel = OnboardingEmailViewModel() + private var drawerCancellables = Set<AnyCancellable>() + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = " " + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + screenView.didTapInfo = { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Onboarding.Email.Info.title, + subtitle: Localized.Onboarding.Email.Info.subtitle, + urlString: "https://links.xx.network/ud" + ) } + } - public override func viewDidLoad() { - super.viewDidLoad() - navigationItem.backButtonTitle = " " - - setupScrollView() - setupBindings() - - screenView.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Onboarding.Email.Info.title, - subtitle: Localized.Onboarding.Email.Info.subtitle, - urlString: "https://links.xx.network/ud" - ) - } - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() } - - private func setupBindings() { - viewModel.hud.receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) - - screenView.inputField.returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) - - viewModel.state - .map(\.confirmation) - .receive(on: DispatchQueue.main) - .compactMap { $0 } - .sink { [unowned self] in - viewModel.clearUp() - coordinator.toEmailConfirmation(with: $0, from: self) { controller in - let successModel = OnboardingSuccessModel( - title: Localized.Onboarding.Success.Email.title, - subtitle: nil, - nextController: coordinator.toPhone(from:) - ) - - coordinator.toSuccess(with: successModel, from: controller) - } - }.store(in: &cancellables) - - viewModel.state - .map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) - - screenView.nextButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) - - screenView.skipButton.publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toPhone(from: self) } - .store(in: &cancellables) - } - - private func presentInfo( - title: String, - subtitle: String, - urlString: String = "" - ) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } + + private func setupBindings() { + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) + + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) + + viewModel + .statePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let id = $0.confirmationId else { return } + viewModel.clearUp() + navigator.perform( + PresentOnboardingCode( + isEmail: true, + content: $0.input, + confirmationId: id, + on: navigationController! + ) ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.status) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) + + screenView + .nextButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) + + screenView + .skipButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentOnboardingPhone(on: navigationController!)) + }.store(in: &cancellables) + } + + private func presentInfo( + title: String, + subtitle: String, + urlString: String = "" + ) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift deleted file mode 100644 index 6207a5a7c9b19b1a5184557eb45cd07bd8aeb21b..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift +++ /dev/null @@ -1,152 +0,0 @@ -import HUD -import DrawerFeature -import Theme -import UIKit -import Shared -import Combine -import DependencyInjection -import ScrollViewController -import Models - -public final class OnboardingPhoneConfirmationController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: OnboardingCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = OnboardingPhoneConfirmationView() - lazy private var scrollViewController = ScrollViewController() - - private var cancellables = Set<AnyCancellable>() - private let completion: (UIViewController) -> Void - private var drawerCancellables = Set<AnyCancellable>() - private let viewModel: OnboardingPhoneConfirmationViewModel - - public init( - _ confirmation: AttributeConfirmation, - _ completion: @escaping (UIViewController) -> Void - ) { - self.completion = completion - self.viewModel = OnboardingPhoneConfirmationViewModel(confirmation) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - - screenView.setupSubtitle( - Localized.Onboarding.PhoneConfirmation.subtitle(viewModel.confirmation.content) - ) - - screenView.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Onboarding.PhoneConfirmation.Info.title, - subtitle: Localized.Onboarding.PhoneConfirmation.Info.subtitle - ) - } - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color - } - - private func setupBindings() { - viewModel.hud.receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) - - viewModel.state - .map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) - - screenView.nextButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) - - viewModel.completionPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] _ in completion(self) } - .store(in: &cancellables) - - screenView.resendButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapResend() } - .store(in: &cancellables) - - viewModel.state - .map(\.resendDebouncer) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.resendButton.isEnabled = $0 == 0 - - if $0 == 0 { - screenView.resendButton.setTitle(Localized.Profile.Code.resend(""), for: .normal) - } else { - screenView.resendButton.setTitle(Localized.Profile.Code.resend("(\($0))"), for: .disabled) - } - }.store(in: &cancellables) - } - - private func presentInfo(title: String, subtitle: String) { - let actionButton = CapsuleButton() - actionButton.set(style: .seeThrough, title: Localized.Settings.InfoDrawer.action) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } -} diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift index 8793508fe6ec8d94e3ee00ff5f3052031c70ee53..4eadd53e32ffa9fa633f2fd61824dab9e871c70b 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift @@ -1,153 +1,172 @@ -import HUD -import DrawerFeature -import Theme import UIKit import Shared import Combine -import DependencyInjection +import AppCore +import AppResources +import Dependencies +import AppNavigation +import DrawerFeature import ScrollViewController public final class OnboardingPhoneController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: OnboardingCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = OnboardingPhoneView() - lazy private var scrollViewController = ScrollViewController() - - private var cancellables = Set<AnyCancellable>() - private let viewModel = OnboardingPhoneViewModel() - private var drawerCancellables = Set<AnyCancellable>() - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLoad() { - super.viewDidLoad() - navigationItem.backButtonTitle = " " - - setupScrollView() - setupBindings() - - screenView.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Onboarding.Phone.Info.title, - subtitle: Localized.Onboarding.Phone.Info.subtitle, - urlString: "https://links.xx.network/ud" - ) - } - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = OnboardingPhoneView() + private lazy var scrollViewController = ScrollViewController() + + private var cancellables = Set<AnyCancellable>() + private let viewModel = OnboardingPhoneViewModel() + private var drawerCancellables = Set<AnyCancellable>() + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + screenView.didTapInfo = { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Onboarding.Phone.Info.title, + subtitle: Localized.Onboarding.Phone.Info.subtitle, + urlString: "https://links.xx.network/ud" + ) } + } - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) - - screenView.inputField.returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) - - viewModel.state.map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) - - screenView.nextButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) - - screenView.skipButton.publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toChats(from: self) } - .store(in: &cancellables) - - screenView.inputField.codePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toCountries(from: self) { viewModel.didChooseCountry($0) } - }.store(in: &cancellables) - - viewModel.state.map(\.confirmation) - .receive(on: DispatchQueue.main) - .compactMap { $0 } - .sink { [unowned self] in - viewModel.clearUp() - coordinator.toPhoneConfirmation(with: $0, from: self) { controller in - let successModel = OnboardingSuccessModel( - title: Localized.Onboarding.Success.Phone.title, - subtitle: nil, - nextController: coordinator.toChats(from:) - ) - - coordinator.toSuccess(with: successModel, from: controller) - } - }.store(in: &cancellables) - - viewModel.state.map(\.country) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.inputField.set(prefix: $0.prefixWithFlag) - screenView.inputField.update(placeholder: $0.example) - }.store(in: &cancellables) + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() } - - private func presentInfo( - title: String, - subtitle: String, - urlString: String = "" - ) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } + + private func setupBindings() { + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) + + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.status) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) + + screenView + .nextButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) + + screenView + .skipButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentSearch( + fromOnboarding: true, + on: navigationController! + )) + }.store(in: &cancellables) + + screenView + .inputField + .codePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentCountryList(completion: { [weak self] in + guard let self else { return } + self.navigator.perform(DismissModal(from: self)) + self.viewModel.didChooseCountry($0 as! Country) + }, from: self)) + }.store(in: &cancellables) + + viewModel + .statePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let id = $0.confirmationId, let content = $0.content else { return } + viewModel.clearUp() + navigator.perform( + PresentOnboardingCode( + isEmail: false, + content: content, + confirmationId: id, + on: navigationController! + ) ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.country) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.inputField.set(prefix: $0.prefixWithFlag) + screenView.inputField.update(placeholder: $0.example) + }.store(in: &cancellables) + } + + private func presentInfo( + title: String, + subtitle: String, + urlString: String = "" + ) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift index d9b970329040862abe2222493c0ecaf0ee2b34c4..0fd4158db96cda5d8f7cea46388de3b6890a7864 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift @@ -1,59 +1,51 @@ -import HUD import UIKit -import Theme -import Shared import Combine -import DependencyInjection +import AppNavigation +import ComposableArchitecture public final class OnboardingStartController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: OnboardingCoordinating - - lazy private var screenView = OnboardingStartView() - - private let ndf: String - private var cancellables = Set<AnyCancellable>() - - public override func loadView() { - view = screenView - } - - public init(_ ndf: String) { - self.ndf = ndf - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let gradient = CAGradientLayer() - gradient.colors = [ - UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, - UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, - UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, - UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor - ] - - gradient.startPoint = CGPoint(x: 0, y: 0) - gradient.endPoint = CGPoint(x: 1, y: 1) - - gradient.frame = screenView.bounds - screenView.layer.insertSublayer(gradient, at: 0) - } - - public override func viewDidLoad() { - super.viewDidLoad() - - screenView.startButton.publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toTerms(ndf: ndf, from: self) } - .store(in: &cancellables) - } + @Dependency(\.navigator) var navigator: Navigator + + private lazy var screenView = OnboardingStartView() + + private var cancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let gradient = CAGradientLayer() + gradient.colors = [ + UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, + UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, + UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, + UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor + ] + + gradient.startPoint = CGPoint(x: 0, y: 0) + gradient.endPoint = CGPoint(x: 1, y: 1) + + gradient.frame = screenView.bounds + screenView.layer.insertSublayer(gradient, at: 0) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + screenView + .startButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentTermsAndConditions(replacing: false, on: navigationController!)) + }.store(in: &cancellables) + } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingSuccessController.swift b/Sources/OnboardingFeature/Controllers/OnboardingSuccessController.swift deleted file mode 100644 index a260afa9a7c69369f0bf9a9510c851ebe2e20313..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/Controllers/OnboardingSuccessController.swift +++ /dev/null @@ -1,68 +0,0 @@ -import UIKit -import Theme -import Models -import Shared -import Combine -import Countries -import DependencyInjection - -public struct OnboardingSuccessModel { - var title: String - var subtitle: String? - var nextController: (UIViewController) -> Void -} - -public final class OnboardingSuccessController: UIViewController { - @Dependency private var coordinator: OnboardingCoordinating - - lazy private var screenView = OnboardingSuccessView() - private var cancellables = Set<AnyCancellable>() - - private var model: OnboardingSuccessModel - - public override func loadView() { - view = screenView - } - - public init(_ model: OnboardingSuccessModel) { - self.model = model - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let gradient = CAGradientLayer() - gradient.colors = [ - UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, - UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, - UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, - UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor - ] - - gradient.startPoint = CGPoint(x: 0, y: 0) - gradient.endPoint = CGPoint(x: 1, y: 1) - - gradient.frame = screenView.bounds - screenView.layer.insertSublayer(gradient, at: 0) - } - - public override func viewDidLoad() { - super.viewDidLoad() - - screenView.setTitle(model.title) - screenView.setSubtitle(model.subtitle) - - screenView.nextButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in model.nextController(self) } - .store(in: &cancellables) - } -} diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index d42e1f81f055d800cb2fe86afb5ac0870c6ca39a..619a983b7149929bf7ca1232aff937bcaf34baa8 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -1,147 +1,147 @@ -import HUD -import Theme import UIKit import Shared import Combine +import AppCore +import AppResources +import Dependencies +import AppNavigation import DrawerFeature -import DependencyInjection import ScrollViewController public final class OnboardingUsernameController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: OnboardingCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = OnboardingUsernameView() - lazy private var scrollViewController = ScrollViewController() - - private let ndf: String - private var cancellables = Set<AnyCancellable>() - private let viewModel: OnboardingUsernameViewModel! - private var drawerCancellables = Set<AnyCancellable>() - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(translucent: true) + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = OnboardingUsernameView() + private lazy var scrollViewController = ScrollViewController() + + private var cancellables = Set<AnyCancellable>() + private let viewModel = OnboardingUsernameViewModel() + private var drawerCancellables = Set<AnyCancellable>() + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + screenView.didTapInfo = { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Onboarding.Username.Info.title, + subtitle: Localized.Onboarding.Username.Info.subtitle, + urlString: "https://links.xx.network/ud" + ) } - - public init(_ ndf: String) { - self.ndf = ndf - self.viewModel = OnboardingUsernameViewModel(ndf: ndf) - super.init(nibName: nil, bundle: nil) + } + + private func setupScrollView() { + scrollViewController.scrollView.backgroundColor = .white + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() } - - required init?(coder: NSCoder) { nil } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - - screenView.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Onboarding.Username.Info.title, - subtitle: Localized.Onboarding.Username.Info.subtitle, - urlString: "https://links.xx.network/ud" - ) + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + } + + private func setupBindings() { + screenView + .inputField + .textPublisher + .removeDuplicates() + .compactMap { $0 } + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) + + screenView + .restoreView + .restoreButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentRestoreList(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + if screenView.nextButton.isEnabled { + viewModel.didTapRegister() + } else { + screenView.inputField.endEditing(true) } - } - - private func setupScrollView() { - scrollViewController.scrollView.backgroundColor = .white - - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - } - - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .removeDuplicates() - .compactMap { $0 } - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) - - screenView.restoreView.restoreButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toRestoreList(with: ndf, from: self) } - .store(in: &cancellables) - - screenView.inputField.returnPublisher - .sink { [unowned self] in - if screenView.nextButton.isEnabled { - viewModel.didTapRegister() - } else { - screenView.inputField.endEditing(true) - } - }.store(in: &cancellables) - - screenView.nextButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapRegister() } - .store(in: &cancellables) - - viewModel.greenPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toWelcome(from: self) } - .store(in: &cancellables) - - viewModel.state - .map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) - } - - private func presentInfo( - title: String, - subtitle: String, - urlString: String = "" - ) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &cancellables) + + screenView + .nextButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapRegister() + }.store(in: &cancellables) + + viewModel + .statePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard $0.didConfirm == true else { return } + navigator.perform(PresentOnboardingWelcome(on: navigationController!)) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.status) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) + } + + private func presentInfo( + title: String, + subtitle: String, + urlString: String = "" + ) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift index 33455d6627930f945bbadc2ddb19d53a047d5dc8..701e1359cea59cfd2912d099b7d6fe897aa2de64 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift @@ -1,95 +1,102 @@ -import DrawerFeature -import Theme import UIKit import Shared import Combine import Defaults -import DependencyInjection +import AppCore +import Dependencies +import AppResources +import AppNavigation +import DrawerFeature public final class OnboardingWelcomeController: UIViewController { - @KeyObject(.username, defaultValue: "") var username: String - @Dependency private var coordinator: OnboardingCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist - lazy private var screenView = OnboardingWelcomeView() + @KeyObject(.username, defaultValue: "") var username: String - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() + private lazy var screenView = OnboardingWelcomeView() - public override func loadView() { - view = screenView - } + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(translucent: true) - } + public override func loadView() { + view = screenView + } - public override func viewDidLoad() { - super.viewDidLoad() - setupBindings() + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(translucent: true) + } - screenView.setupTitle(Localized.Onboarding.Welcome.title(username)) + public override func viewDidLoad() { + super.viewDidLoad() + screenView.setupTitle( + Localized.Onboarding.Welcome.title(username) + ) + screenView + .continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentOnboardingEmail(on: navigationController!)) + }.store(in: &cancellables) - screenView.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Onboarding.Welcome.Info.title, - subtitle: Localized.Onboarding.Welcome.Info.subtitle, - urlString: "https://links.xx.network/ud" - ) - } - } + screenView + .skipButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentSearch( + fromOnboarding: true, + on: navigationController! + )) + }.store(in: &cancellables) - private func setupBindings() { - screenView.continueButton.publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toEmail(from: self) } - .store(in: &cancellables) - - screenView.skipButton.publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toChats(from: self) } - .store(in: &cancellables) + screenView.didTapInfo = { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Onboarding.Welcome.Info.title, + subtitle: Localized.Onboarding.Welcome.Info.subtitle, + urlString: "https://links.xx.network/ud" + ) } + } - private func presentInfo( - title: String, - subtitle: String, - urlString: String = "" - ) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) + private func presentInfo( + title: String, + subtitle: String, + urlString: String = "" + ) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - coordinator.toDrawer(drawer, from: self) - } + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } } diff --git a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift b/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift deleted file mode 100644 index 9ea57da2c51a02679f40c9132b88498fb139fa68..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift +++ /dev/null @@ -1,156 +0,0 @@ -import UIKit -import Shared -import Models -import Countries -import Presentation - -public typealias AttributeControllerClosure = (UIViewController) -> Void - -public protocol OnboardingCoordinating { - func toChats(from: UIViewController) - func toEmail(from: UIViewController) - func toPhone(from: UIViewController) - func toWelcome(from: UIViewController) - func toTerms(ndf: String, from: UIViewController) - func toUsername(with: String, from: UIViewController) - func toRestoreList(with: String, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toSuccess(with: OnboardingSuccessModel, from: UIViewController) - - func toEmailConfirmation( - with: AttributeConfirmation, - from: UIViewController, - completion: @escaping (UIViewController) -> Void - ) - - func toPhoneConfirmation( - with: AttributeConfirmation, - from: UIViewController, - completion: @escaping (UIViewController) -> Void - ) - - func toCountries( - from: UIViewController, - _ onChoose: @escaping (Country) -> Void - ) -} - -public struct OnboardingCoordinator: OnboardingCoordinating { - var pushPresenter: Presenting = PushPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() - - var emailFactory: () -> UIViewController - var phoneFactory: () -> UIViewController - var searchFactory: (String?) -> UIViewController - var welcomeFactory: () -> UIViewController - var chatListFactory: () -> UIViewController - var usernameFactory: (String) -> UIViewController - var restoreListFactory: (String) -> UIViewController - var termsFactory: (String?) -> UIViewController - var successFactory: (OnboardingSuccessModel) -> UIViewController - var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - var phoneConfirmationFactory: (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController - var emailConfirmationFactory: (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController - - public init( - emailFactory: @escaping () -> UIViewController, - phoneFactory: @escaping () -> UIViewController, - searchFactory: @escaping (String?) -> UIViewController, - welcomeFactory: @escaping () -> UIViewController, - chatListFactory: @escaping () -> UIViewController, - termsFactory: @escaping (String?) -> UIViewController, - usernameFactory: @escaping (String) -> UIViewController, - restoreListFactory: @escaping (String) -> UIViewController, - successFactory: @escaping (OnboardingSuccessModel) -> UIViewController, - countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController, - phoneConfirmationFactory: @escaping (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController, - emailConfirmationFactory: @escaping (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController - ) { - self.emailFactory = emailFactory - self.termsFactory = termsFactory - self.phoneFactory = phoneFactory - self.searchFactory = searchFactory - self.welcomeFactory = welcomeFactory - self.successFactory = successFactory - self.usernameFactory = usernameFactory - self.chatListFactory = chatListFactory - self.countriesFactory = countriesFactory - self.restoreListFactory = restoreListFactory - self.phoneConfirmationFactory = phoneConfirmationFactory - self.emailConfirmationFactory = emailConfirmationFactory - } -} - -public extension OnboardingCoordinator { - func toTerms( - ndf: String, - from parent: UIViewController - ) { - let screen = termsFactory(ndf) - pushPresenter.present(screen, from: parent) - } - - func toEmail(from parent: UIViewController) { - let screen = emailFactory() - replacePresenter.present(screen, from: parent) - } - - func toPhone(from parent: UIViewController) { - let screen = phoneFactory() - replacePresenter.present(screen, from: parent) - } - - func toWelcome(from parent: UIViewController) { - let screen = welcomeFactory() - replacePresenter.present(screen, from: parent) - } - - func toRestoreList(with ndf: String, from parent: UIViewController) { - let screen = restoreListFactory(ndf) - pushPresenter.present(screen, from: parent) - } - - func toSuccess(with model: OnboardingSuccessModel, from parent: UIViewController) { - let screen = successFactory(model) - replacePresenter.present(screen, from: parent) - } - - func toUsername(with ndf: String, from parent: UIViewController) { - let screen = usernameFactory(ndf) - replacePresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toChats(from parent: UIViewController) { - let searchScreen = searchFactory(nil) - let chatListScreen = chatListFactory() - replacePresenter.present(chatListScreen, searchScreen, from: parent) - } - - func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { - let screen = countriesFactory(onChoose) - pushPresenter.present(screen, from: parent) - } - - func toEmailConfirmation( - with confirmation: AttributeConfirmation, - from parent: UIViewController, - completion: @escaping (UIViewController) -> Void - ) { - let screen = emailConfirmationFactory(confirmation, completion) - pushPresenter.present(screen, from: parent) - } - - func toPhoneConfirmation( - with confirmation: AttributeConfirmation, - from parent: UIViewController, - completion: @escaping (UIViewController) -> Void - ) { - let screen = phoneConfirmationFactory(confirmation, completion) - pushPresenter.present(screen, from: parent) - } -} diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..994b83b211a75eeb4bafb4e95ec683e263325881 --- /dev/null +++ b/Sources/OnboardingFeature/ViewModels/OnboardingCodeViewModel.swift @@ -0,0 +1,98 @@ +import AppCore +import Shared +import Combine +import Defaults +import XXClient +import InputField +import Foundation +import CombineSchedulers +import XXMessengerClient +import ComposableArchitecture + +final class OnboardingCodeViewModel { + struct ViewState: Equatable { + var input: String = "" + var status: InputField.ValidationStatus = .unknown(nil) + var resendDebouncer: Int = 0 + var didConfirm: Bool = false + } + + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + @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? + + private var timer: Timer? + private let isEmail: Bool + private let content: String + private let confirmationId: String + private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) + + init( + isEmail: Bool, + content: String, + confirmationId: String + ) { + self.isEmail = isEmail + self.content = content + self.confirmationId = confirmationId + didTapResend() + } + + func didInput(_ string: String) { + stateSubject.value.input = string + validate() + } + + func didTapResend() { + guard stateSubject.value.resendDebouncer == 0 else { return } + stateSubject.value.resendDebouncer = 60 + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in + guard let self, self.stateSubject.value.resendDebouncer > 0 else { + $0.invalidate() + return + } + self.stateSubject.value.resendDebouncer -= 1 + } + } + + func didTapNext() { + hudManager.show() + bgQueue.schedule { [weak self] in + guard let self else { return } + do { + try self.messenger.ud.get()!.confirmFact( + confirmationId: self.confirmationId, + code: self.stateSubject.value.input + ) + if self.isEmail { + self.email = self.content + } else { + self.phone = self.content + } + self.timer?.invalidate() + self.hudManager.hide() + self.stateSubject.value.didConfirm = true + } catch { + self.hudManager.hide() + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.stateSubject.value.status = .invalid(xxError) + } + } + } + + private func validate() { + switch Validator.code.validate(stateSubject.value.input) { + case .success: + stateSubject.value.status = .valid(nil) + case .failure(let error): + stateSubject.value.status = .invalid(error) + } + } +} diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift deleted file mode 100644 index b3871d6234517f1d493ab61da1466bc754446fd7..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift +++ /dev/null @@ -1,89 +0,0 @@ -import HUD -import UIKit -import Models -import Shared -import Combine -import Defaults -import InputField -import Integration -import CombineSchedulers -import DependencyInjection - -struct OnboardingEmailConfirmationViewState: Equatable { - var input: String = "" - var status: InputField.ValidationStatus = .unknown(nil) - var resendDebouncer: Int = 0 -} - -final class OnboardingEmailConfirmationViewModel { - @Dependency private var session: SessionType - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } - private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() - - var timer: Timer? - let confirmation: AttributeConfirmation - - var state: AnyPublisher<OnboardingEmailConfirmationViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingEmailConfirmationViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init(_ confirmation: AttributeConfirmation) { - self.confirmation = confirmation - didTapResend() - } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func didTapResend() { - guard stateRelay.value.resendDebouncer == 0 else { return } - - stateRelay.value.resendDebouncer = 60 - - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in - guard let self = self, self.stateRelay.value.resendDebouncer > 0 else { - $0.invalidate() - return - } - - self.stateRelay.value.resendDebouncer -= 1 - } - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.confirm( - code: self.stateRelay.value.input, - confirmation: self.confirmation - ) - - self.timer?.invalidate() - self.hudRelay.send(.none) - self.completionRelay.send(self.confirmation) - } catch { - self.hudRelay.send(.error(.init(with: error))) - } - } - } - - private func validate() { - switch Validator.code.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } - } -} diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift index c3cbbb897840964e15f908b9fa83d5d6537f7b23..a175a674fa981f7d48cf2409c8d84041f60da5dc 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift @@ -1,69 +1,63 @@ -import HUD -import UIKit -import Models +import AppCore import Shared import Combine -import Defaults +import XXClient import InputField -import Integration +import Foundation import CombineSchedulers -import DependencyInjection +import XXMessengerClient +import ComposableArchitecture -struct OnboardingEmailViewState: Equatable { +final class OnboardingEmailViewModel { + struct ViewState: Equatable { var input: String = "" - var confirmation: AttributeConfirmation? = nil + var confirmationId: String? var status: InputField.ValidationStatus = .unknown(nil) -} - -final class OnboardingEmailViewModel { - @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications - - @Dependency private var session: SessionType - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<OnboardingEmailViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingEmailViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - func clearUp() { - stateRelay.value.confirmation = nil + } + + @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()) + + func clearUp() { + stateSubject.value.confirmationId = nil + } + + func didInput(_ string: String) { + stateSubject.value.input = string + validate() + } + + func didTapNext() { + 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.hudManager.hide() + self.stateSubject.value.confirmationId = confirmationId + } catch { + self.hudManager.hide() + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.stateSubject.value.status = .invalid(xxError) + } } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - self.session.register(.email, value: self.stateRelay.value.input) { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let confirmationId): - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = - .init(content: self.stateRelay.value.input, isEmail: true, confirmationId: confirmationId) - case .failure(let error): - self.hudRelay.send(.error(.init(with: error))) - } - } - } - } - - private func validate() { - switch Validator.email.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.email.validate(stateSubject.value.input) { + case .success: + stateSubject.value.status = .valid(nil) + case .failure(let error): + stateSubject.value.status = .invalid(error) } + } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift deleted file mode 100644 index 2bd5a7ae35fbca7bf871479f998abeef9e4ce625..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift +++ /dev/null @@ -1,89 +0,0 @@ -import HUD -import UIKit -import Models -import Shared -import Combine -import Defaults -import InputField -import Integration -import CombineSchedulers -import DependencyInjection - -struct OnboardingPhoneConfirmationViewState: Equatable { - var input: String = "" - var status: InputField.ValidationStatus = .unknown(nil) - var resendDebouncer: Int = 0 -} - -final class OnboardingPhoneConfirmationViewModel { - @Dependency private var session: SessionType - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } - private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() - - var timer: Timer? - let confirmation: AttributeConfirmation - - var state: AnyPublisher<OnboardingPhoneConfirmationViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingPhoneConfirmationViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init(_ confirmation: AttributeConfirmation) { - self.confirmation = confirmation - didTapResend() - } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func didTapResend() { - guard stateRelay.value.resendDebouncer == 0 else { return } - - stateRelay.value.resendDebouncer = 60 - - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in - guard let self = self, self.stateRelay.value.resendDebouncer > 0 else { - $0.invalidate() - return - } - - self.stateRelay.value.resendDebouncer -= 1 - } - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.confirm( - code: self.stateRelay.value.input, - confirmation: self.confirmation - ) - - self.timer?.invalidate() - self.hudRelay.send(.none) - self.completionRelay.send(self.confirmation) - } catch { - self.hudRelay.send(.error(.init(with: error))) - } - } - } - - private func validate() { - switch Validator.code.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } - } -} diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift index 0aff02f402420b959d03f166ba4eb456d1098bea..39d9f282be5757d19bc7ef47ff450dcfd2d1f48e 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift @@ -1,81 +1,73 @@ -import HUD +import AppCore import Shared -import Models import Combine -import Countries +import XXClient import InputField -import Integration +import Foundation import CombineSchedulers -import DependencyInjection +import XXMessengerClient +import CountryListFeature +import ComposableArchitecture -struct OnboardingPhoneViewState: Equatable { +final class OnboardingPhoneViewModel { + struct ViewState: Equatable { var input: String = "" - var confirmation: AttributeConfirmation? + var content: String? + var confirmationId: String? var status: InputField.ValidationStatus = .unknown(nil) var country: Country = .fromMyPhone() -} - -final class OnboardingPhoneViewModel { - @Dependency private var session: SessionType + } - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> - var state: AnyPublisher<OnboardingPhoneViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingPhoneViewState, Never>(.init()) + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() + } - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - // MARK: Public - - func clearUp() { - stateRelay.value.confirmation = nil - } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func didChooseCountry(_ country: Country) { - stateRelay.value.country = country - validate() - } - - func didGoForward() { - stateRelay.value.confirmation = nil - } + private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) - func didTapNext() { - hudRelay.send(.on) + func clearUp() { + stateSubject.value.confirmationId = nil + } - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + func didInput(_ string: String) { + stateSubject.value.input = string + validate() + } - let content = "\(self.stateRelay.value.input)\(self.stateRelay.value.country.code)" - self.session.register(.phone, value: content) { [weak self] in - guard let self = self else { return } + func didChooseCountry(_ country: Country) { + stateSubject.value.country = country + validate() + } - switch $0 { - case .success(let confirmationId): - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = .init( - content: content, - confirmationId: confirmationId - ) - case .failure(let error): - self.hudRelay.send(.error(.init(with: error))) - } - } - } + func didTapNext() { + 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.hudManager.hide() + self.stateSubject.value.content = content + self.stateSubject.value.confirmationId = confirmationId + } catch { + self.hudManager.hide() + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.stateSubject.value.status = .invalid(xxError) + } } + } - private func validate() { - switch Validator.phone.validate((stateRelay.value.country.regex, stateRelay.value.input)) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + private func validate() { + switch Validator.phone.validate((stateSubject.value.country.regex, stateSubject.value.input)) { + case .success: + stateSubject.value.status = .valid(nil) + case .failure(let error): + stateSubject.value.status = .invalid(error) } + } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift index ecb64035e73e259bf147ce4ee7f427723d40b3f3..917e9fea3bcd5f21227f5a285ceafe620b271e9f 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift @@ -1,78 +1,74 @@ -import HUD +import AppCore import Shared import Combine +import Defaults +import XXModels +import XXClient import InputField -import Integration -import CombineSchedulers -import DependencyInjection +import Foundation +import XXMessengerClient +import ComposableArchitecture -struct OnboardingUsernameViewState: Equatable { +final class OnboardingUsernameViewModel { + struct ViewState: Equatable { var input: String = "" var status: InputField.ValidationStatus = .unknown(nil) -} - -final class OnboardingUsernameViewModel { - - let ndf: String - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - var greenPublisher: AnyPublisher<Void, Never> { greenRelay.eraseToAnyPublisher() } - private let greenRelay = PassthroughSubject<Void, Never>() + var didConfirm: Bool = false + } - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) + @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> - var state: AnyPublisher<OnboardingUsernameViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingUsernameViewState, Never>(.init()) + @KeyObject(.username, defaultValue: "") var username: String - init(ndf: String) { - self.ndf = ndf - } + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() + } - func didInput(_ string: String) { - stateRelay.value.input = string.trimmingCharacters(in: .whitespacesAndNewlines) + private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) - switch Validator.username.validate(stateRelay.value.input) { - case .success(let text): - stateRelay.value.status = .valid(text) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + func didInput(_ string: String) { + stateSubject.value.input = string.trimmingCharacters(in: .whitespacesAndNewlines) + switch Validator.username.validate(stateSubject.value.input) { + case .success(let text): + stateSubject.value.status = .valid(text) + case .failure(let error): + stateSubject.value.status = .invalid(error) } - - func didTapRegister() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - var session: SessionType! - - if let injectedSession = try? DependencyInjection.Container.shared.resolve() as SessionType { - session = injectedSession - } else { - session = try Session(ndf: self.ndf) - DependencyInjection.Container.shared.register(session as SessionType) - } - - session.register(.username, value: self.stateRelay.value.input) { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(_): - self.hudRelay.send(.none) - self.greenRelay.send() - case .failure(let error): - self.hudRelay.send(.none) - self.stateRelay.value.status = .invalid(error.localizedDescription) - } - } - } catch { - self.hudRelay.send(.none) - self.stateRelay.value.status = .invalid(error.localizedDescription) - } - } + } + + func didTapRegister() { + hudManager.show() + bgQueue.schedule { [weak self] in + guard let self else { return } + do { + try self.messenger.register( + username: self.stateSubject.value.input + ) + 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, + email: nil, + phone: nil, + nickname: nil, + photo: nil, + authStatus: .friend, + isRecent: false, + isBlocked: false, + isBanned: false, + createdAt: Date() + )) + self.username = self.stateSubject.value.input + self.hudManager.hide() + self.stateSubject.value.didConfirm = true + } catch { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..63a33f1bdcfe95b33f9335f32f3358bdbe3d354c --- /dev/null +++ b/Sources/OnboardingFeature/Views/OnboardingCodeView.swift @@ -0,0 +1,118 @@ +import UIKit +import Shared +import InputField +import AppResources + +final class OnboardingCodeView: UIView { + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let inputField = InputField() + let nextButton = CapsuleButton() + let resendButton = UIButton() + let stackView = UIStackView() + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupTitle(Localized.Onboarding.EmailConfirmation.title) + + inputField.setup( + placeholder: Localized.Onboarding.EmailConfirmation.input, + subtitleColor: Asset.neutralWeak.color, + allowsEmptySpace: false, + keyboardType: .numberPad, + autocapitalization: .none, + contentType: .oneTimeCode + ) + + resendButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + resendButton.setTitleColor(Asset.neutralWeak.color, for: .disabled) + resendButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) + resendButton.setTitle(Localized.Onboarding.EmailConfirmation.resend(""), for: .normal) + + nextButton.set(style: .brandColored, title: Localized.Onboarding.EmailConfirmation.next) + nextButton.isEnabled = false + + stackView.spacing = 15 + stackView.axis = .vertical + stackView.addArrangedSubview(nextButton) + stackView.addArrangedSubview(resendButton) + + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(inputField) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(30) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + inputField.snp.makeConstraints { + $0.top.equalTo(subtitleView.snp.bottom).offset(24) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + } + stackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) + } + } + + required init?(coder: NSCoder) { nil } + + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + + switch status { + case .valid: + nextButton.isEnabled = true + case .invalid, .unknown: + nextButton.isEnabled = false + } + } + + private func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.0 + + attString.addAttribute(.paragraphStyle, value: paragraph) + 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 + } + + public func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + subtitleView.setup( + text: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ], + didTapInfo: { [weak self] in self?.didTapInfo?() } + ) + } +} diff --git a/Sources/OnboardingFeature/Views/OnboardingEmailConfirmationView.swift b/Sources/OnboardingFeature/Views/OnboardingEmailConfirmationView.swift deleted file mode 100644 index 8d41227472b0f0fdb7018f7f0dc02140f8900bdc..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/Views/OnboardingEmailConfirmationView.swift +++ /dev/null @@ -1,121 +0,0 @@ -import UIKit -import Shared -import InputField - -final class OnboardingEmailConfirmationView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let inputField = InputField() - let nextButton = CapsuleButton() - let resendButton = UIButton() - let stackView = UIStackView() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupTitle(Localized.Onboarding.EmailConfirmation.title) - - inputField.setup( - placeholder: Localized.Onboarding.EmailConfirmation.input, - subtitleColor: Asset.neutralWeak.color, - allowsEmptySpace: false, - keyboardType: .numberPad, - autocapitalization: .none, - contentType: .oneTimeCode - ) - - resendButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - resendButton.setTitleColor(Asset.neutralWeak.color, for: .disabled) - resendButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) - resendButton.setTitle(Localized.Onboarding.EmailConfirmation.resend(""), for: .normal) - - nextButton.set(style: .brandColored, title: Localized.Onboarding.EmailConfirmation.next) - nextButton.isEnabled = false - - stackView.spacing = 15 - stackView.axis = .vertical - stackView.addArrangedSubview(nextButton) - stackView.addArrangedSubview(resendButton) - - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(inputField) - addSubview(stackView) - - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(subtitleView.snp.bottom).offset(24) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) - } - - stackView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) - } - } - - required init?(coder: NSCoder) { nil } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - nextButton.isEnabled = true - case .invalid, .unknown: - nextButton.isEnabled = false - } - } - - private func setupTitle(_ title: String) { - let attString = NSMutableAttributedString(string: title) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.0 - - attString.addAttribute(.paragraphStyle, value: paragraph) - 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 - } - - public func setupSubtitle(_ subtitle: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - subtitleView.setup( - text: subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ], - didTapInfo: { [weak self] in self?.didTapInfo?() } - ) - } -} diff --git a/Sources/OnboardingFeature/Views/OnboardingEmailView.swift b/Sources/OnboardingFeature/Views/OnboardingEmailView.swift index 760996abb549a1658c573b9602de6777f11022dc..e98117828c9de253fe169b3cb4e9a8fff67e92d1 100644 --- a/Sources/OnboardingFeature/Views/OnboardingEmailView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingEmailView.swift @@ -1,120 +1,114 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingEmailView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let inputField = InputField() - let nextButton = CapsuleButton() - let skipButton = UIButton() - let stackView = UIStackView() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupTitle(Localized.Onboarding.Email.title) - setupSubtitle(Localized.Onboarding.Email.subtitle) - - inputField.setup( - placeholder: Localized.Onboarding.Email.input, - subtitleColor: Asset.neutralWeak.color, - allowsEmptySpace: false, - keyboardType: .emailAddress, - autocapitalization: .none, - contentType: .emailAddress - ) - - skipButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 14.0) - skipButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - skipButton.setTitle(Localized.Onboarding.Email.skip, for: .normal) - nextButton.set(style: .brandColored, title: Localized.Onboarding.Email.action) - nextButton.isEnabled = false - - stackView.spacing = 20 - stackView.axis = .vertical - stackView.addArrangedSubview(nextButton) - stackView.addArrangedSubview(skipButton) - - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(inputField) - addSubview(stackView) - - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(subtitleView.snp.bottom).offset(24) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) - } - - stackView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) - } + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let inputField = InputField() + let nextButton = CapsuleButton() + let skipButton = UIButton() + let stackView = UIStackView() + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupTitle(Localized.Onboarding.Email.title) + setupSubtitle(Localized.Onboarding.Email.subtitle) + + inputField.setup( + placeholder: Localized.Onboarding.Email.input, + subtitleColor: Asset.neutralWeak.color, + allowsEmptySpace: false, + keyboardType: .emailAddress, + autocapitalization: .none, + contentType: .emailAddress + ) + + skipButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 14.0) + skipButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + skipButton.setTitle(Localized.Onboarding.Email.skip, for: .normal) + nextButton.set(style: .brandColored, title: Localized.Onboarding.Email.action) + nextButton.isEnabled = false + + stackView.spacing = 20 + stackView.axis = .vertical + stackView.addArrangedSubview(nextButton) + stackView.addArrangedSubview(skipButton) + + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(inputField) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(30) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - required init?(coder: NSCoder) { nil } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - nextButton.isEnabled = true - case .invalid, .unknown: - nextButton.isEnabled = false - } + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - private func setupTitle(_ title: String) { - let attString = NSMutableAttributedString(string: title) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - attString.addAttribute(.paragraphStyle, value: paragraph) - 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 + inputField.snp.makeConstraints { + $0.top.equalTo(subtitleView.snp.bottom).offset(24) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + } + stackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } + } - private func setupSubtitle(_ subtitle: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 + required init?(coder: NSCoder) { nil } - subtitleView.setup( - text: subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ], - didTapInfo: { [weak self] in self?.didTapInfo?() } - ) + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + switch status { + case .valid: + nextButton.isEnabled = true + case .invalid, .unknown: + nextButton.isEnabled = false } + } + + private func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + attString.addAttribute(.paragraphStyle, value: paragraph) + 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 + } + + private func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + subtitleView.setup( + text: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ], + didTapInfo: { [weak self] in self?.didTapInfo?() } + ) + } } diff --git a/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift b/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift index 0509f5d52517dfe4f0b78614eca764250f4fb285..45584a61af46433c46833141b1cae00a00333275 100644 --- a/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingPhoneConfirmationView.swift @@ -1,121 +1,112 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingPhoneConfirmationView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let inputField = InputField() - let nextButton = CapsuleButton() - let resendButton = UIButton() - let stackView = UIStackView() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupTitle(Localized.Onboarding.PhoneConfirmation.title) - - inputField.setup( - placeholder: Localized.Onboarding.PhoneConfirmation.input, - subtitleColor: Asset.neutralWeak.color, - allowsEmptySpace: false, - keyboardType: .numberPad, - autocapitalization: .none, - contentType: .oneTimeCode - ) - - resendButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - resendButton.setTitleColor(Asset.neutralWeak.color, for: .disabled) - resendButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) - resendButton.setTitle(Localized.Onboarding.PhoneConfirmation.resend(""), for: .normal) - - nextButton.set(style: .brandColored, title: Localized.Onboarding.PhoneConfirmation.next) - nextButton.isEnabled = false - - stackView.spacing = 15 - stackView.axis = .vertical - stackView.addArrangedSubview(nextButton) - stackView.addArrangedSubview(resendButton) - - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(inputField) - addSubview(stackView) - - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(subtitleView.snp.bottom).offset(24) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) - } - - stackView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) - } + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let inputField = InputField() + let nextButton = CapsuleButton() + let resendButton = UIButton() + let stackView = UIStackView() + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + setupTitle(Localized.Onboarding.PhoneConfirmation.title) + + inputField.setup( + placeholder: Localized.Onboarding.PhoneConfirmation.input, + subtitleColor: Asset.neutralWeak.color, + allowsEmptySpace: false, + keyboardType: .numberPad, + autocapitalization: .none, + contentType: .oneTimeCode + ) + + resendButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + resendButton.setTitleColor(Asset.neutralWeak.color, for: .disabled) + resendButton.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0) + resendButton.setTitle(Localized.Onboarding.PhoneConfirmation.resend(""), for: .normal) + nextButton.set(style: .brandColored, title: Localized.Onboarding.PhoneConfirmation.next) + nextButton.isEnabled = false + + stackView.spacing = 15 + stackView.axis = .vertical + stackView.addArrangedSubview(nextButton) + stackView.addArrangedSubview(resendButton) + + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(inputField) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(30) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - required init?(coder: NSCoder) { nil } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - nextButton.isEnabled = true - case .invalid, .unknown: - nextButton.isEnabled = false - } + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - private func setupTitle(_ title: String) { - let attString = NSMutableAttributedString(string: title) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.0 - - attString.addAttribute(.paragraphStyle, value: paragraph) - 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 + inputField.snp.makeConstraints { + $0.top.equalTo(subtitleView.snp.bottom).offset(24) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + } + stackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } + } - public func setupSubtitle(_ subtitle: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 + required init?(coder: NSCoder) { nil } - subtitleView.setup( - text: subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ], - didTapInfo: { [weak self] in self?.didTapInfo?() } - ) + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + switch status { + case .valid: + nextButton.isEnabled = true + case .invalid, .unknown: + nextButton.isEnabled = false } + } + + private func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.0 + attString.addAttribute(.paragraphStyle, value: paragraph) + 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 + } + + public func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + subtitleView.setup( + text: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ], + didTapInfo: { [weak self] in self?.didTapInfo?() } + ) + } } diff --git a/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift b/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift index caec887d542c4d37f98486ef85c790d5bcda6ebb..f36c635ef4e0706297a6b40d328e9a387d3e556f 100644 --- a/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingPhoneView.swift @@ -1,120 +1,113 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingPhoneView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let inputField = InputField() - let nextButton = CapsuleButton() - let skipButton = UIButton() - let stackView = UIStackView() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupTitle(Localized.Onboarding.Phone.title) - setupSubtitle(Localized.Onboarding.Phone.subtitle) - - inputField.setup( - style: .phone, - placeholder: Localized.Onboarding.Phone.input, - subtitleColor: Asset.neutralWeak.color, - keyboardType: .phonePad, - contentType: .telephoneNumber, - codeAccessibility: Localized.Accessibility.Onboarding.Phone.code - ) - - skipButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 14.0) - skipButton.setTitleColor(Asset.brandPrimary.color, for: .normal) - skipButton.setTitle(Localized.Onboarding.Phone.skip, for: .normal) - nextButton.set(style: .brandColored, title: Localized.Onboarding.Phone.action) - nextButton.isEnabled = false - - stackView.spacing = 20 - stackView.axis = .vertical - stackView.addArrangedSubview(nextButton) - stackView.addArrangedSubview(skipButton) - - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(inputField) - addSubview(stackView) - - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(subtitleView.snp.bottom).offset(24) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) - } - - stackView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) - } + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let inputField = InputField() + let nextButton = CapsuleButton() + let skipButton = UIButton() + let stackView = UIStackView() + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupTitle(Localized.Onboarding.Phone.title) + setupSubtitle(Localized.Onboarding.Phone.subtitle) + + inputField.setup( + style: .phone, + placeholder: Localized.Onboarding.Phone.input, + subtitleColor: Asset.neutralWeak.color, + keyboardType: .phonePad, + contentType: .telephoneNumber, + codeAccessibility: Localized.Accessibility.Onboarding.Phone.code + ) + + skipButton.titleLabel?.font = Fonts.Mulish.bold.font(size: 14.0) + skipButton.setTitleColor(Asset.brandPrimary.color, for: .normal) + skipButton.setTitle(Localized.Onboarding.Phone.skip, for: .normal) + nextButton.set(style: .brandColored, title: Localized.Onboarding.Phone.action) + nextButton.isEnabled = false + + stackView.spacing = 20 + stackView.axis = .vertical + stackView.addArrangedSubview(nextButton) + stackView.addArrangedSubview(skipButton) + + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(inputField) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(30) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - required init?(coder: NSCoder) { nil } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - nextButton.isEnabled = true - case .invalid, .unknown: - nextButton.isEnabled = false - } + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - private func setupTitle(_ title: String) { - let attString = NSMutableAttributedString(string: title) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - attString.addAttribute(.paragraphStyle, value: paragraph) - 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 + inputField.snp.makeConstraints { + $0.top.equalTo(subtitleView.snp.bottom).offset(24) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + } + stackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } + } - private func setupSubtitle(_ subtitle: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 + required init?(coder: NSCoder) { nil } - subtitleView.setup( - text: subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ], - didTapInfo: { [weak self] in self?.didTapInfo?() } - ) + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + switch status { + case .valid: + nextButton.isEnabled = true + case .invalid, .unknown: + nextButton.isEnabled = false } + } + + private func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + attString.addAttribute(.paragraphStyle, value: paragraph) + 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 + } + + private func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + subtitleView.setup( + text: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ], + didTapInfo: { [weak self] in self?.didTapInfo?() } + ) + } } diff --git a/Sources/OnboardingFeature/Views/OnboardingStartView.swift b/Sources/OnboardingFeature/Views/OnboardingStartView.swift index a3e953aad2108ae43036f43c56530b8c63937ec9..b0864bb32bb8bdd4557b0a677df73068e5c747d5 100644 --- a/Sources/OnboardingFeature/Views/OnboardingStartView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingStartView.swift @@ -1,49 +1,49 @@ import UIKit import Shared +import AppResources final class OnboardingStartView: UIView { - let titleLabel = UILabel() - let stackView = UIStackView() - let logoImageView = UIImageView() - let startButton = CapsuleButton() - let bottomImageView = UIImageView() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - logoImageView.image = Asset.onboardingLogoStart.image - bottomImageView.image = Asset.onboardingBottomLogoStart.image - - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralWhite.color - titleLabel.font = Fonts.Mulish.bold.font(size: 18.0) - titleLabel.text = Localized.Onboarding.Start.title - startButton.set(style: .white, title: Localized.Onboarding.Start.action) - - logoImageView.contentMode = .center - bottomImageView.contentMode = .center - - stackView.spacing = 40 - stackView.axis = .vertical - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(startButton) - stackView.addArrangedSubview(bottomImageView) - stackView.setCustomSpacing(70, after: startButton) - - addSubview(logoImageView) - addSubview(stackView) - - logoImageView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(130) - make.centerX.equalToSuperview() - } - - stackView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-40) - } + let titleLabel = UILabel() + let stackView = UIStackView() + let logoImageView = UIImageView() + let startButton = CapsuleButton() + let bottomImageView = UIImageView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + logoImageView.image = Asset.onboardingLogoStart.image + bottomImageView.image = Asset.onboardingBottomLogoStart.image + + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralWhite.color + titleLabel.font = Fonts.Mulish.bold.font(size: 18.0) + titleLabel.text = Localized.Onboarding.Start.title + startButton.set(style: .white, title: Localized.Onboarding.Start.action) + + logoImageView.contentMode = .center + bottomImageView.contentMode = .center + + stackView.spacing = 40 + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(startButton) + stackView.addArrangedSubview(bottomImageView) + stackView.setCustomSpacing(70, after: startButton) + + addSubview(logoImageView) + addSubview(stackView) + + logoImageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(130) + $0.centerX.equalToSuperview() } + stackView.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-40) + } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/OnboardingFeature/Views/OnboardingSuccessView.swift b/Sources/OnboardingFeature/Views/OnboardingSuccessView.swift deleted file mode 100644 index 7e02167c2786cb11c4fcc04d6ad4930b9fab92e2..0000000000000000000000000000000000000000 --- a/Sources/OnboardingFeature/Views/OnboardingSuccessView.swift +++ /dev/null @@ -1,75 +0,0 @@ -import UIKit -import Shared - -final class OnboardingSuccessView: UIView { - let iconImageView = UIImageView() - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let nextButton = CapsuleButton() - - init() { - super.init(frame: .zero) - - iconImageView.contentMode = .center - iconImageView.image = Asset.onboardingSuccess.image - nextButton.set(style: .white, title: Localized.Onboarding.Success.action) - - subtitleLabel.numberOfLines = 0 - subtitleLabel.textColor = Asset.neutralWhite.color - subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) - - addSubview(iconImageView) - addSubview(titleLabel) - addSubview(subtitleLabel) - addSubview(nextButton) - - iconImageView.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide).offset(40) - make.left.equalToSuperview().offset(40) - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(iconImageView.snp.bottom).offset(40) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-90) - } - - subtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(30) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-90) - } - - nextButton.snp.makeConstraints { make in - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-60) - } - } - - required init?(coder: NSCoder) { nil } - - func setTitle(_ title: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.1 - - let attrString = NSMutableAttributedString(string: title) - - attrString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 39.0)) - attrString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color) - - attrString.addAttribute( - name: .foregroundColor, - value: Asset.neutralBody.color, - betweenCharacters: "#" - ) - - titleLabel.numberOfLines = 0 - titleLabel.attributedText = attrString - } - - func setSubtitle(_ subtitle: String?) { - subtitleLabel.text = subtitle - } -} diff --git a/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift b/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift index f03437558a9157fd6846ce76436f1ed1c4d8822b..7a418f71686b90456b03f24af3f4165d416bd7a8 100644 --- a/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingUsernameRestoreView.swift @@ -1,47 +1,46 @@ import UIKit import Shared +import AppResources final class OnboardingUsernameRestoreView: UIView { - let titleLabel = UILabel() - let restoreButton = CapsuleButton() - let separatorView = UIView() - - init() { - super.init(frame: .zero) - - titleLabel.text = Localized.Onboarding.Username.Restore.title - restoreButton.set(style: .seeThrough, title: Localized.Onboarding.Username.Restore.action) - - titleLabel.numberOfLines = 0 - titleLabel.textAlignment = .center - titleLabel.font = Fonts.Mulish.bold.font(size: 24) - - addSubview(titleLabel) - addSubview(restoreButton) - addSubview(separatorView) - - separatorView.backgroundColor = Asset.neutralLine.color - - separatorView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.height.equalTo(1) - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(separatorView.snp.bottom).offset(40) - make.left.equalToSuperview().offset(20) - make.right.equalToSuperview().offset(-20) - } - - restoreButton.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(34) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalToSuperview() - } + let titleLabel = UILabel() + let restoreButton = CapsuleButton() + let separatorView = UIView() + + init() { + super.init(frame: .zero) + + titleLabel.text = Localized.Onboarding.Username.Restore.title + restoreButton.set(style: .seeThrough, title: Localized.Onboarding.Username.Restore.action) + + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .center + titleLabel.font = Fonts.Mulish.bold.font(size: 24) + + addSubview(titleLabel) + addSubview(restoreButton) + addSubview(separatorView) + + separatorView.backgroundColor = Asset.neutralLine.color + + separatorView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + $0.height.equalTo(1) + } + titleLabel.snp.makeConstraints { + $0.top.equalTo(separatorView.snp.bottom).offset(40) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + } + restoreButton.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(34) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift b/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift index 32ec1e43e312aee7880f1003c9e3f4ec0f18e899..8f5bf92b6a34377960f449c2d0a77aab28b3225c 100644 --- a/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingUsernameView.swift @@ -1,116 +1,108 @@ import UIKit import Shared import InputField +import AppResources final class OnboardingUsernameView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let inputField = InputField() - let nextButton = CapsuleButton() - let restoreView = OnboardingUsernameRestoreView() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupTitle(Localized.Onboarding.Username.title) - setupSubtitle(Localized.Onboarding.Username.subtitle) - - inputField.setup( - placeholder: Localized.Onboarding.Username.input, - subtitleColor: Asset.neutralWeak.color, - allowsEmptySpace: false, - autocapitalization: .none - ) - - nextButton.set(style: .brandColored, title: Localized.Onboarding.Username.next) - nextButton.isEnabled = false - - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(inputField) - addSubview(nextButton) - addSubview(restoreView) - - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(subtitleView.snp.bottom).offset(24) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) - } - - nextButton.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - } - - restoreView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(nextButton.snp.bottom).offset(30) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) - } + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let inputField = InputField() + let nextButton = CapsuleButton() + let restoreView = OnboardingUsernameRestoreView() + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupTitle(Localized.Onboarding.Username.title) + setupSubtitle(Localized.Onboarding.Username.subtitle) + + inputField.setup( + placeholder: Localized.Onboarding.Username.input, + subtitleColor: Asset.neutralWeak.color, + allowsEmptySpace: false, + autocapitalization: .none + ) + + nextButton.set(style: .brandColored, title: Localized.Onboarding.Username.next) + nextButton.isEnabled = false + + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(inputField) + addSubview(nextButton) + addSubview(restoreView) + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(30) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - required init?(coder: NSCoder) { nil } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - nextButton.isEnabled = true - case .invalid, .unknown: - nextButton.isEnabled = false - } + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - private func setupTitle(_ title: String) { - let attString = NSMutableAttributedString(string: title) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - attString.addAttribute(.paragraphStyle, value: paragraph) - 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 + inputField.snp.makeConstraints { + $0.top.equalTo(subtitleView.snp.bottom).offset(24) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + } + nextButton.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + } + restoreView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(nextButton.snp.bottom).offset(30) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } + } - private func setupSubtitle(_ subtitle: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 + required init?(coder: NSCoder) { nil } - subtitleView.setup( - text: subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ], - didTapInfo: { [weak self] in self?.didTapInfo?() } - ) + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + switch status { + case .valid: + nextButton.isEnabled = true + case .invalid, .unknown: + nextButton.isEnabled = false } + } + + private func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + attString.addAttribute(.paragraphStyle, value: paragraph) + 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 + } + + private func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + subtitleView.setup( + text: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ], + didTapInfo: { [weak self] in self?.didTapInfo?() } + ) + } } diff --git a/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift b/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift index d3581a4d9f987e7e1e19f6f09831793cf57d5640..2daddf4b716a515abe8d17c61e9f6e2ee464aa6f 100644 --- a/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift +++ b/Sources/OnboardingFeature/Views/OnboardingWelcomeView.swift @@ -1,86 +1,81 @@ import UIKit import Shared +import AppResources final class OnboardingWelcomeView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let stackView = UIStackView() - let continueButton = CapsuleButton() - let skipButton = CapsuleButton() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupSubtitle(Localized.Onboarding.Welcome.subtitle) - - skipButton.set(style: .brandColored, title: Localized.Onboarding.Welcome.skip) - continueButton.set(style: .brandColored, title: Localized.Onboarding.Welcome.continue) - - stackView.spacing = 15 - stackView.axis = .vertical - stackView.addArrangedSubview(continueButton) - stackView.addArrangedSubview(skipButton) - - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(stackView) - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide).offset(30) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) - } - - stackView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) - } + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let stackView = UIStackView() + let continueButton = CapsuleButton() + let skipButton = CapsuleButton() + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupSubtitle(Localized.Onboarding.Welcome.subtitle) + + skipButton.set(style: .brandColored, title: Localized.Onboarding.Welcome.skip) + continueButton.set(style: .brandColored, title: Localized.Onboarding.Welcome.continue) + + stackView.spacing = 15 + stackView.axis = .vertical + stackView.addArrangedSubview(continueButton) + stackView.addArrangedSubview(skipButton) + + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(30) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - required init?(coder: NSCoder) { nil } - - func setupTitle(_ title: String) { - let attString = NSMutableAttributedString(string: title) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.1 - - attString.addAttribute(.paragraphStyle, value: paragraph) - 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 + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - - private func setupSubtitle(_ subtitle: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - subtitleView.setup( - text: subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ], - didTapInfo: { [weak self] in self?.didTapInfo?() } - ) + stackView.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } + } + + required init?(coder: NSCoder) { nil } + + func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.1 + attString.addAttribute(.paragraphStyle, value: paragraph) + 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 + } + + private func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + subtitleView.setup( + text: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ], + didTapInfo: { [weak self] in self?.didTapInfo?() } + ) + } } 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/RequestPermissionController.swift b/Sources/Permissions/RequestPermissionController.swift deleted file mode 100644 index d892ab60e8722633e8d7c2930eb7ec97b4049d8c..0000000000000000000000000000000000000000 --- a/Sources/Permissions/RequestPermissionController.swift +++ /dev/null @@ -1,100 +0,0 @@ -import UIKit -import Theme -import Shared -import Combine -import DependencyInjection - -public enum PermissionType { - case camera - case library - case microphone -} - -public final class RequestPermissionController: UIViewController { - @Dependency private var permissions: PermissionHandling - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = RequestPermissionView() - - private var type: PermissionType! - private var cancellables = Set<AnyCancellable>() - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupBindings() - } - - public func setup(type: PermissionType) { - self.type = type - - switch type { - case .camera: - screenView.setup( - title: Localized.Chat.Actions.Permission.Camera.title, - subtitle: Localized.Chat.Actions.Permission.Camera.subtitle, - image: Asset.permissionCamera.image - ) - case .library: - screenView.setup( - title: Localized.Chat.Actions.Permission.Library.title, - subtitle: Localized.Chat.Actions.Permission.Library.subtitle, - image: Asset.permissionLibrary.image - ) - case .microphone: - screenView.setup( - title: Localized.Chat.Actions.Permission.Microphone.title, - subtitle: Localized.Chat.Actions.Permission.Microphone.subtitle, - image: Asset.permissionMicrophone.image - ) - } - } - - private func setupBindings() { - screenView.notNowButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.navigationController?.popViewController(animated: true) - }.store(in: &cancellables) - - screenView.continueButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch type { - case .camera: - permissions.requestCamera { [weak self] _ in - DispatchQueue.main.async { - self?.navigationController?.popViewController(animated: true) - } - } - case .library: - permissions.requestPhotos { [weak self] _ in - DispatchQueue.main.async { - self?.navigationController?.popViewController(animated: true) - } - } - case .microphone: - permissions.requestMicrophone { [weak self] _ in - DispatchQueue.main.async { - self?.navigationController?.popViewController(animated: true) - } - } - case .none: - break - } - }.store(in: &cancellables) - } - -} 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/Dependency.swift b/Sources/PermissionsFeature/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..766f239ff492ca00b4ed0fc9a8afaa879818812b --- /dev/null +++ b/Sources/PermissionsFeature/Dependency.swift @@ -0,0 +1,13 @@ +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/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/PermissionPush.swift b/Sources/PermissionsFeature/PermissionPush.swift new file mode 100644 index 0000000000000000000000000000000000000000..a4be4209b7656f59615ca95b6a89ce187ccf5515 --- /dev/null +++ b/Sources/PermissionsFeature/PermissionPush.swift @@ -0,0 +1,66 @@ +import UserNotifications +import XCTestDynamicOverlay + +public struct PermissionPush { + public var status: PermissionPushStatus + public var request: PermissionPushRequest + + public static let live = PermissionPush( + status: .live, + request: .live + ) + public static let unimplemented = PermissionPush( + status: .unimplemented, + request: .unimplemented + ) +} + +public struct PermissionPushRequest { + public var run: (@escaping (Bool) -> Void) -> Void + + public func callAsFunction(_ completion: @escaping (Bool) -> Void) -> Void { + run(completion) + } +} + +extension PermissionPushRequest { + public static let live = PermissionPushRequest { completion in + let current = UNUserNotificationCenter.current() + current.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + if error != nil { + completion(false) + return + } + completion(granted) + } + } +} + +extension PermissionPushRequest { + public static let unimplemented = PermissionPushRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} + +public struct PermissionPushStatus { + public var run: (@escaping (Bool) -> Void) -> Void + + public func callAsFunction(_ completion: @escaping (Bool) -> Void) -> Void { + run(completion) + } +} + +extension PermissionPushStatus { + public static let live = PermissionPushStatus { completion in + let current = UNUserNotificationCenter.current() + current.getNotificationSettings { + completion($0.authorizationStatus == .authorized) + } + } +} + +extension PermissionPushStatus { + public static let unimplemented = PermissionPushStatus( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/PermissionsFeature/PermissionsManager.swift b/Sources/PermissionsFeature/PermissionsManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..33373e6e3fbf9faf738cbd80f89a24c2ee13f7eb --- /dev/null +++ b/Sources/PermissionsFeature/PermissionsManager.swift @@ -0,0 +1,22 @@ +public struct PermissionsManager { + public var push: PermissionPush + public var camera: PermissionCamera + public var library: PermissionLibrary + public var microphone: PermissionMicrophone + public var biometrics: PermissionBiometrics + + public static let live = PermissionsManager( + push: .live, + camera: .live, + library: .live, + microphone: .live, + biometrics: .live + ) + public static let unimplemented = PermissionsManager( + push: .unimplemented, + camera: .unimplemented, + library: .unimplemented, + microphone: .unimplemented, + biometrics: .unimplemented + ) +} 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 1baf98d11fe94e90217c41b39c081175cd00bcb9..0000000000000000000000000000000000000000 --- a/Sources/Presentation/Presenting.swift +++ /dev/null @@ -1,100 +0,0 @@ -import UIKit -import Theme - -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 = StatusBarViewController(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/ProcessBannedList/Dependency.swift b/Sources/ProcessBannedList/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..1f02756a5541c2907e9cb6f23fe45510c43afa16 --- /dev/null +++ b/Sources/ProcessBannedList/Dependency.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum ProcessBannedListDependencyKey: DependencyKey { + static let liveValue: ProcessBannedList = .live + static let testValue: ProcessBannedList = .unimplemented +} + +extension DependencyValues { + public var processBannedList: ProcessBannedList { + get { self[ProcessBannedListDependencyKey.self] } + set { self[ProcessBannedListDependencyKey.self] = newValue } + } +} diff --git a/Sources/ProcessBannedList/ProcessBannedList.swift b/Sources/ProcessBannedList/ProcessBannedList.swift new file mode 100644 index 0000000000000000000000000000000000000000..900738b323abf85587789fa040e891563cd9f85a --- /dev/null +++ b/Sources/ProcessBannedList/ProcessBannedList.swift @@ -0,0 +1,64 @@ +import Foundation +import SwiftCSV +import XCTestDynamicOverlay + +public struct ProcessBannedList { + public enum ElementError: Swift.Error { + case missingUserId + case invalidUserId(String) + } + + public enum Error: Swift.Error { + case invalidData + case csv(Swift.Error) + } + + public typealias ForEach = (Result<Data, ElementError>) -> Void + public typealias Completion = (Result<Void, Error>) -> Void + + public var run: (Data, ForEach, Completion) -> Void + + public func callAsFunction( + data: Data, + forEach: ForEach, + completion: Completion + ) { + run(data, forEach, completion) + } +} + +extension ProcessBannedList { + public static let live = ProcessBannedList { data, forEach, completion in + guard let csvString = String(data: data, encoding: .utf8) else { + completion(.failure(.invalidData)) + return + } + let csv: EnumeratedCSV + do { + csv = try EnumeratedCSV(string: csvString) + } + catch { + completion(.failure(.csv(error))) + return + } + csv.rows.forEach { row in + guard let userIdString = row.first else { + forEach(.failure(.missingUserId)) + return + } + guard let userId = Data(base64Encoded: userIdString) else { + forEach(.failure(.invalidUserId(userIdString))) + return + } + forEach(.success(userId)) + } + completion(.success(())) + } +} + +extension ProcessBannedList { + public static let unimplemented = ProcessBannedList { _, _, _ in + let run: () -> Void = XCTUnimplemented("\(Self.self)") + run() + } +} diff --git a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift index d612b9e9e79c4dbb362b5a0c7782eb1463dc4fe6..d5d640db0263cf29abda90a9b1ec962e89e9081c 100644 --- a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift @@ -1,123 +1,137 @@ -import HUD import UIKit -import Models import Shared import Combine -import Countries -import DependencyInjection +import AppCore +import AppResources +import Dependencies +import AppNavigation import ScrollViewController -public typealias ControllerClosure = (UIViewController, AttributeConfirmation) -> Void - public final class ProfileCodeController: UIViewController { - @Dependency private var hud: HUD - - lazy private var screenView = ProfileCodeView() - lazy private var scrollViewController = ScrollViewController() - - private let completion: ControllerClosure - private let confirmation: AttributeConfirmation - private var cancellables = Set<AnyCancellable>() - lazy private var viewModel = ProfileCodeViewModel(confirmation) - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public init( - _ confirmation: AttributeConfirmation, - _ completion: @escaping ControllerClosure - ) { - self.completion = completion - self.confirmation = confirmation - super.init(nibName: nil, bundle: nil) + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = ProfileCodeView() + private lazy var scrollViewController = ScrollViewController() + + private let isEmail: Bool + private let content: String + private let viewModel: ProfileCodeViewModel + private var cancellables = Set<AnyCancellable>() + + public init( + _ isEmail: Bool, + _ content: String, + _ confirmationId: String + ) { + self.viewModel = .init( + isEmail: isEmail, + content: content, + confirmationId: confirmationId + ) + self.isEmail = isEmail + self.content = content + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + + if isEmail { + screenView.set(content, isEmail: true) + } else { + let country = Country.findFrom(content) + screenView.set( + "\(country.prefix)\(content.dropLast(2))", + isEmail: false + ) } + } - required init?(coder: NSCoder) { nil } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - setupDetail() - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() } - - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) - - viewModel.state - .map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch $0 { - case .valid: - screenView.saveButton.isEnabled = true - case .invalid, .unknown: - screenView.saveButton.isEnabled = false - } - }.store(in: &cancellables) - - screenView.saveButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) - - viewModel.state - .map(\.resendDebouncer) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.resendButton.isEnabled = $0 == 0 - - if $0 == 0 { - screenView.resendButton.setTitle(Localized.Profile.Code.resend(""), for: .normal) - } else { - screenView.resendButton.setTitle(Localized.Profile.Code.resend("(\($0))"), for: .disabled) - } - }.store(in: &cancellables) - - screenView.resendButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapResend() } - .store(in: &cancellables) - - viewModel.completionPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in completion(self, $0) } - .store(in: &cancellables) - } - - private func setupDetail() { - var content: String! - - if confirmation.isEmail { - content = confirmation.content + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } + + private func setupBindings() { + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.status) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch $0 { + case .valid: + screenView.saveButton.isEnabled = true + case .invalid, .unknown: + screenView.saveButton.isEnabled = false + } + }.store(in: &cancellables) + + screenView + .saveButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.resendDebouncer) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.resendButton.isEnabled = $0 == 0 + if $0 == 0 { + screenView.resendButton.setTitle( + Localized.Profile.Code.resend(""), for: .normal + ) } else { - let country = Country.findFrom(confirmation.content) - content = "\(country.prefix)\(confirmation.content.dropLast(2))" + screenView.resendButton.setTitle( + Localized.Profile.Code.resend("(\($0))"), for: .disabled + ) } - - screenView.set(content, isEmail: confirmation.isEmail) - } + }.store(in: &cancellables) + + screenView + .resendButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didTapResend() + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.didConfirm) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let navigationController, $0 == true else { return } + navigator.perform(PopToRoot(on: navigationController)) + }.store(in: &cancellables) + } } diff --git a/Sources/ProfileFeature/Controllers/ProfileController.swift b/Sources/ProfileFeature/Controllers/ProfileController.swift index e3138f275dd967d73d58548649e4156409178e57..23dca61bf795b7ede333f78d8ed0c2d7fdfa2588 100644 --- a/Sources/ProfileFeature/Controllers/ProfileController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileController.swift @@ -1,225 +1,228 @@ -import HUD -import DrawerFeature import UIKit -import Theme import Shared import Combine -import DependencyInjection +import AppCore +import AppResources +import AppNavigation +import DrawerFeature +import ComposableArchitecture public final class ProfileController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: ProfileCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = ProfileView() - - private let viewModel = ProfileViewModel() - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.lightContent) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralBody.color) - viewModel.refresh() - } - - public override func viewDidLoad() { - super.viewDidLoad() - screenView.cardComponent.nameLabel.text = viewModel.username! - setupNavigationBar() - setupBindings() - } - - private func setupNavigationBar() { - navigationItem.backButtonTitle = "" - - let menuButton = UIButton() - menuButton.tintColor = Asset.neutralWhite.color - menuButton.setImage(Asset.chatListMenu.image, for: .normal) - menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) - menuButton.snp.makeConstraints { $0.width.equalTo(50) } - - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: menuButton) - } - - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.emailView.actionButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - if screenView.emailView.currentValue != nil { - presentDrawer( - title: Localized.Profile.Delete.title( - Localized.Profile.Email.title.capitalized - ), - subtitle: Localized.Profile.Delete.subtitle( - Localized.Profile.Email.title.lowercased(), Localized.Profile.Email.title.lowercased() - ), - actionTitle: Localized.Profile.Delete.action( - Localized.Profile.Email.title - )) { - viewModel.didTapDelete(isEmail: true) - } - } else { - coordinator.toEmail(from: self) - } - }.store(in: &cancellables) - - screenView.phoneView.actionButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - if screenView.phoneView.currentValue != nil { - presentDrawer( - title: Localized.Profile.Delete.title( - Localized.Profile.Phone.title.capitalized - ), - subtitle: Localized.Profile.Delete.subtitle( - Localized.Profile.Phone.title.lowercased(), Localized.Profile.Phone.title.lowercased() - ), - actionTitle: Localized.Profile.Delete.action( - Localized.Profile.Phone.title - )) { - viewModel.didTapDelete(isEmail: false) - } - } else { - coordinator.toPhone(from: self) - } - }.store(in: &cancellables) - - screenView.cardComponent.avatarView.editButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didRequestLibraryAccess() } - .store(in: &cancellables) - - viewModel.navigation - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [unowned self] in - switch $0 { - case .library: - presentDrawer( - title: Localized.Profile.Photo.title, - subtitle: Localized.Profile.Photo.subtitle, - actionTitle: Localized.Profile.Photo.continue) { - coordinator.toPhotos(from: self) - } - case .libraryPermission: - coordinator.toPermission(type: .library, from: self) - case .none: - break - } - - viewModel.didNavigateSomewhere() - }.store(in: &cancellables) - - viewModel.state - .map(\.email) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.emailView.set(value: $0) } - .store(in: &cancellables) - - viewModel.state - .map(\.phone) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.phoneView.set(value: $0) } - .store(in: &cancellables) - - viewModel.state - .map(\.photo) - .compactMap { $0 } - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.cardComponent.image = $0 } - .store(in: &cancellables) - } - - private func presentDrawer( - title: String, - subtitle: String, - actionTitle: String, - action: @escaping () -> Void - ) { - let actionButton = DrawerCapsuleButton(model: .init( - title: actionTitle, - style: .red - )) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = ProfileView() + + private let viewModel = ProfileViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.lightContent) + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralBody.color) + viewModel.refresh() + } + + public override func viewDidLoad() { + super.viewDidLoad() + screenView.cardComponent.nameLabel.text = viewModel.username! + setupNavigationBar() + setupBindings() + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let menuButton = UIButton() + menuButton.tintColor = Asset.neutralWhite.color + menuButton.setImage(Asset.chatListMenu.image, for: .normal) + menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) + menuButton.snp.makeConstraints { $0.width.equalTo(50) } + + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: menuButton) + } + + private func setupBindings() { + screenView.emailView.actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + if screenView.emailView.currentValue != nil { + presentDrawer( + title: Localized.Profile.Delete.title( + Localized.Profile.Email.title.capitalized ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 37 + subtitle: Localized.Profile.Delete.subtitle( + Localized.Profile.Email.title.lowercased(), Localized.Profile.Email.title.lowercased() ), - actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - - action() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } - - @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) - } + actionTitle: Localized.Profile.Delete.action( + Localized.Profile.Email.title + )) { + self.viewModel.didTapDelete(isEmail: true) + } + } else { + navigator.perform(PresentProfileEmail(on: navigationController!)) + } + }.store(in: &cancellables) + + screenView.phoneView.actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + if screenView.phoneView.currentValue != nil { + presentDrawer( + title: Localized.Profile.Delete.title( + Localized.Profile.Phone.title.capitalized + ), + subtitle: Localized.Profile.Delete.subtitle( + Localized.Profile.Phone.title.lowercased(), Localized.Profile.Phone.title.lowercased() + ), + actionTitle: Localized.Profile.Delete.action( + Localized.Profile.Phone.title + )) { + self.viewModel.didTapDelete(isEmail: false) + } + } else { + navigator.perform(PresentProfilePhone(on: navigationController!)) + } + }.store(in: &cancellables) + + screenView + .cardComponent + .avatarView + .editButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didRequestLibraryAccess() + }.store(in: &cancellables) + + viewModel + .navigation + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + switch $0 { + case .library: + presentDrawer( + title: Localized.Profile.Photo.title, + subtitle: Localized.Profile.Photo.subtitle, + actionTitle: Localized.Profile.Photo.continue) { + self.navigator.perform(PresentPhotoLibrary(from: self)) + } + case .libraryPermission: + self.navigator.perform(PresentPermissionRequest(type: .library, from: self)) + case .none: + break + } + viewModel.didNavigateSomewhere() + }.store(in: &cancellables) + + viewModel + .state + .map(\.email) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.emailView.set(value: $0) + }.store(in: &cancellables) + + viewModel + .state + .map(\.phone) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.phoneView.set(value: $0) + }.store(in: &cancellables) + + viewModel + .state + .map(\.photo) + .compactMap { $0 } + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.cardComponent.image = $0 + }.store(in: &cancellables) + } + + private func presentDrawer( + title: String, + subtitle: String, + actionTitle: String, + action: @escaping () -> Void + ) { + let actionButton = DrawerCapsuleButton(model: .init( + title: actionTitle, + style: .red + )) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + action() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 37 + ), + actionButton + ], isDismissable: true, from: self)) + } + + @objc private func didTapMenu() { + navigator.perform(PresentMenu(currentItem: .profile, from: self)) + } } extension ProfileController: UIImagePickerControllerDelegate { - public func imagePickerController( - _ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] - ) { - var image: UIImage? - - if let originalImage = info[.originalImage] as? UIImage { - image = originalImage - } - - if let croppedImage = info[.editedImage] as? UIImage { - image = croppedImage - } + public func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] + ) { + var image: UIImage? + + if let originalImage = info[.originalImage] as? UIImage { + image = originalImage + } - guard let image = image else { - picker.dismiss(animated: true) - return - } + if let croppedImage = info[.editedImage] as? UIImage { + image = croppedImage + } - picker.dismiss(animated: true) - viewModel.didChoosePhoto(image) + guard let image = image else { + picker.dismiss(animated: true) + return } + + picker.dismiss(animated: true) + viewModel.didChoosePhoto(image) + } } extension ProfileController: UINavigationControllerDelegate {} diff --git a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift index 3fb88d2b649ed6f9f20b8189467a021563188c13..ef9ef2f8f501ae6160d96cd72376d33222d9cdac 100644 --- a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift @@ -1,84 +1,91 @@ -import HUD import UIKit import Shared import Combine -import Theme -import DependencyInjection +import AppCore +import AppResources +import AppNavigation import ScrollViewController +import ComposableArchitecture public final class ProfileEmailController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: ProfileCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist - lazy private var screenView = ProfileEmailView() - lazy private var scrollViewController = ScrollViewController() + private lazy var screenView = ProfileEmailView() + private lazy var scrollViewController = ScrollViewController() - private let viewModel = ProfileEmailViewModel() - private var cancellables = Set<AnyCancellable>() + private let viewModel = ProfileEmailViewModel() + private var cancellables = Set<AnyCancellable>() - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color) + } - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - } + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + } - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) + private func setupBindings() { + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) - screenView.inputField.returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) + viewModel + .statePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let id = $0.confirmationId else { return } + viewModel.clearUp() + navigator.perform( + PresentProfileCode( + isEmail: true, + content: $0.input, + confirmationId: id, + on: navigationController! + ) + ) + }.store(in: &cancellables) - viewModel.state - .map(\.confirmation) - .receive(on: DispatchQueue.main) - .compactMap { $0 } - .sink { [unowned self] in - viewModel.clearUp() - coordinator.toCode(with: $0, from: self) { _, _ in - if let viewControllers = navigationController?.viewControllers { - navigationController?.popToViewController( - viewControllers[viewControllers.count - 3], - animated: true - ) - } - } - } - .store(in: &cancellables) + viewModel + .statePublisher + .map(\.status) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) - viewModel.state.map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) - - screenView.saveButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) - } + screenView + .saveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) + } } diff --git a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift index 01737802adc9611476892ca38675b5dd8ff6d743..99ac70b864328dd362fe88a2487fc3ba8fec2ce2 100644 --- a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift +++ b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift @@ -1,102 +1,113 @@ -import HUD import UIKit import Shared import Combine -import Theme -import DependencyInjection +import AppCore +import AppResources +import Dependencies +import AppNavigation import ScrollViewController -#warning("TODO: Merge ProfilePhoneController/ProfileEmailController") - public final class ProfilePhoneController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: ProfileCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist - lazy private var screenView = ProfilePhoneView() - lazy private var scrollViewController = ScrollViewController() + private lazy var screenView = ProfilePhoneView() + private lazy var scrollViewController = ScrollViewController() - private let viewModel = ProfilePhoneViewModel() - private var cancellables = Set<AnyCancellable>() + private let viewModel = ProfilePhoneViewModel() + private var cancellables = Set<AnyCancellable>() - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + statusBar.set(.darkContent) + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - } + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + } - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) + private func setupBindings() { + screenView + .inputField + .textPublisher + .sink { [unowned self] in + viewModel.didInput($0) + }.store(in: &cancellables) - screenView.inputField.textPublisher - .sink { [unowned self] in viewModel.didInput($0) } - .store(in: &cancellables) + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) - screenView.inputField.returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) + screenView + .inputField + .codePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentCountryList(completion: { [weak self] in + guard let self else { return } + self.viewModel.didChooseCountry($0 as! Country) + }, from: self)) + }.store(in: &cancellables) - screenView.inputField.codePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toCountries(from: self) { viewModel.didChooseCountry($0) } - }.store(in: &cancellables) + viewModel + .statePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard let id = $0.confirmationId, let content = $0.content else { return } + viewModel.clearUp() + navigator.perform( + PresentProfileCode( + isEmail: false, + content: content, + confirmationId: id, + on: navigationController! + ) + ) + }.store(in: &cancellables) - viewModel.state - .map(\.confirmation) - .receive(on: DispatchQueue.main) - .compactMap { $0 } - .sink { [unowned self] in - viewModel.clearUp() - coordinator.toCode(with: $0, from: self) { _, _ in - if let viewControllers = navigationController?.viewControllers { - navigationController?.popToViewController( - viewControllers[viewControllers.count - 3], - animated: true - ) - } - } - }.store(in: &cancellables) + viewModel + .statePublisher + .map(\.country) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.inputField.set(prefix: $0.prefixWithFlag) + screenView.inputField.update(placeholder: $0.example) + }.store(in: &cancellables) - viewModel.state - .map(\.country) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.inputField.set(prefix: $0.prefixWithFlag) - screenView.inputField.update(placeholder: $0.example) - } - .store(in: &cancellables) + viewModel + .statePublisher + .map(\.status) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) - viewModel.state - .map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) - - screenView.saveButton.publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapNext() } - .store(in: &cancellables) - } + screenView + .saveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapNext() + }.store(in: &cancellables) + } } diff --git a/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift b/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift deleted file mode 100644 index 947937b403a4f29cee289e590908589a66c57754..0000000000000000000000000000000000000000 --- a/Sources/ProfileFeature/Coordinator/ProfileCoordinator.swift +++ /dev/null @@ -1,108 +0,0 @@ -import UIKit -import Shared -import Models -import Countries -import Permissions -import MenuFeature -import Presentation - -public protocol ProfileCoordinating { - func toEmail(from: UIViewController) - func toPhone(from: UIViewController) - func toPhotos(from: UIViewController) - func toSideMenu(from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toPermission(type: PermissionType, from: UIViewController) - - func toCode( - with: AttributeConfirmation, - from: UIViewController, - _: @escaping ControllerClosure - ) - - func toCountries( - from: UIViewController, - _: @escaping (Country) -> Void - ) -} - -public struct ProfileCoordinator: ProfileCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var emailFactory: () -> UIViewController - var phoneFactory: () -> UIViewController - var imagePickerFactory: () -> UIImagePickerController - var permissionFactory: () -> RequestPermissionController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - var codeFactory: (AttributeConfirmation, @escaping ControllerClosure) -> UIViewController - - public init( - emailFactory: @escaping () -> UIViewController, - phoneFactory: @escaping () -> UIViewController, - imagePickerFactory: @escaping () -> UIImagePickerController, - permissionFactory: @escaping () -> RequestPermissionController, // âš ï¸ - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, - countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController, - codeFactory: @escaping (AttributeConfirmation, @escaping ControllerClosure) -> UIViewController - ) { - self.codeFactory = codeFactory - self.emailFactory = emailFactory - self.phoneFactory = phoneFactory - self.sideMenuFactory = sideMenuFactory - self.countriesFactory = countriesFactory - self.permissionFactory = permissionFactory - self.imagePickerFactory = imagePickerFactory - } -} - -public extension ProfileCoordinator { - func toEmail(from parent: UIViewController) { - let screen = emailFactory() - pushPresenter.present(screen, from: parent) - } - - func toPhone(from parent: UIViewController) { - let screen = phoneFactory() - pushPresenter.present(screen, from: parent) - } - - func toCode( - with confirmation: AttributeConfirmation, - from parent: UIViewController, - _ completion: @escaping ControllerClosure - ) { - let screen = codeFactory(confirmation, completion) - pushPresenter.present(screen, from: parent) - } - - func toPermission(type: PermissionType, from parent: UIViewController) { - let screen = permissionFactory() - screen.setup(type: type) - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { - let screen = countriesFactory(onChoose) - pushPresenter.present(screen, from: parent) - } - - func toPhotos(from parent: UIViewController) { - let screen = imagePickerFactory() - screen.delegate = (parent as? (UIImagePickerControllerDelegate & UINavigationControllerDelegate)) - screen.allowsEditing = true - modalPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.profile, parent) - sidePresenter.present(screen, from: parent) - } -} diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift index d9763e989e23e37c9736582a41fcf1b63aef03b2..a018da77f237fc1c4b604ea791aa0289452638d0 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift @@ -1,88 +1,96 @@ -import HUD import Shared -import Models import Combine +import AppCore +import Defaults +import XXClient import InputField -import Integration -import CombineSchedulers -import DependencyInjection +import Foundation +import XXMessengerClient +import ComposableArchitecture -struct ProfileCodeViewState: Equatable { +final class ProfileCodeViewModel { + struct ViewState: Equatable { var input: String = "" var status: InputField.ValidationStatus = .unknown(nil) var resendDebouncer: Int = 0 -} - -final class ProfileCodeViewModel { - @Dependency private var session: SessionType - - let confirmation: AttributeConfirmation - - var timer: Timer? - - var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } - private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<ProfileCodeViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ProfileCodeViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init(_ confirmation: AttributeConfirmation) { - self.confirmation = confirmation - didTapResend() - } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() + var didConfirm: Bool = false + } + + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + @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? + + private var timer: Timer? + private let isEmail: Bool + private let content: String + private let confirmationId: String + private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) + + init( + isEmail: Bool, + content: String, + confirmationId: String + ) { + self.isEmail = isEmail + self.content = content + self.confirmationId = confirmationId + didTapResend() + } + + func didInput(_ string: String) { + stateSubject.value.input = string + validate() + } + + func didTapResend() { + guard stateSubject.value.resendDebouncer == 0 else { return } + stateSubject.value.resendDebouncer = 60 + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in + guard let self, self.stateSubject.value.resendDebouncer > 0 else { + $0.invalidate() + return + } + self.stateSubject.value.resendDebouncer -= 1 } - - func didTapResend() { - guard stateRelay.value.resendDebouncer == 0 else { return } - - stateRelay.value.resendDebouncer = 60 - - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in - guard let self = self, self.stateRelay.value.resendDebouncer > 0 else { - $0.invalidate() - return - } - - self.stateRelay.value.resendDebouncer -= 1 - } - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.confirm( - code: self.stateRelay.value.input, - confirmation: self.confirmation - ) - - self.timer?.invalidate() - self.hudRelay.send(.none) - self.completionRelay.send(self.confirmation) - } catch { - self.hudRelay.send(.error(.init(with: error))) - } + } + + func didTapNext() { + hudManager.show() + bgQueue.schedule { [weak self] in + guard let self else { return } + do { + try self.messenger.ud.get()!.confirmFact( + confirmationId: self.confirmationId, + code: self.stateSubject.value.input + ) + if self.isEmail { + self.email = self.content + } else { + self.phone = self.content } + self.timer?.invalidate() + self.hudManager.hide() + self.stateSubject.value.didConfirm = true + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudManager.show(.init(content: xxError)) + } } - - private func validate() { - switch Validator.code.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.code.validate(stateSubject.value.input) { + case .success: + stateSubject.value.status = .valid(nil) + case .failure(let error): + stateSubject.value.status = .invalid(error) } + } } diff --git a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift index 6b57bc81fe4040faaccaeefff057a275a90deaac..96210b7dc00867639c7861d377d868df11bb8b62 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift @@ -1,76 +1,63 @@ -import HUD -import Models import Shared import Combine +import AppCore +import XXClient +import Foundation import InputField -import Integration import CombineSchedulers -import DependencyInjection +import XXMessengerClient +import ComposableArchitecture -struct ProfileEmailViewState: Equatable { +final class ProfileEmailViewModel { + struct ViewState: Equatable { var input: String = "" - var confirmation: AttributeConfirmation? = nil + var confirmationId: String? var status: InputField.ValidationStatus = .unknown(nil) -} - -final class ProfileEmailViewModel { - // MARK: Injected - - @Dependency private var session: SessionType - - // MARK: Properties - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<ProfileEmailViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ProfileEmailViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - // MARK: Public - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func clearUp() { - stateRelay.value.confirmation = nil - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - self.session.register(.email, value: self.stateRelay.value.input) { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let confirmationId): - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = .init( - content: self.stateRelay.value.input, - isEmail: true, - confirmationId: confirmationId - ) - case .failure(let error): - self.hudRelay.send(.error(.init(with: error))) - } - } - } + } + + @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()) + + func clearUp() { + stateSubject.value.confirmationId = nil + } + + func didInput(_ string: String) { + stateSubject.value.input = string + validate() + } + + func didTapNext() { + 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.hudManager.hide() + self.stateSubject.value.confirmationId = confirmationId + } catch { + self.hudManager.hide() + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.stateSubject.value.status = .invalid(xxError) + } } - - // MARK: Private - - private func validate() { - switch Validator.email.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.email.validate(stateSubject.value.input) { + case .success: + stateSubject.value.status = .valid(nil) + case .failure(let error): + stateSubject.value.status = .invalid(error) } + } } diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift index 1013725b419a118c4e437d3e8959c50748447d72..90387a69dde095491775b47cd4e138ae6786f483 100644 --- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift @@ -1,84 +1,73 @@ -import HUD import Shared -import Models import Combine -import Countries +import AppCore +import XXClient import InputField -import Integration +import Foundation +import Dependencies import CombineSchedulers -import DependencyInjection +import XXMessengerClient +import CountryListFeature -struct ProfilePhoneViewState: Equatable { +final class ProfilePhoneViewModel { + struct ViewState: Equatable { var input: String = "" - var confirmation: AttributeConfirmation? = nil + var content: String? + var confirmationId: String? var status: InputField.ValidationStatus = .unknown(nil) var country: Country = .fromMyPhone() -} - -final class ProfilePhoneViewModel { - // MARK: Injected - - @Dependency private var session: SessionType - - // MARK: Properties - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<ProfilePhoneViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ProfilePhoneViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - // MARK: Public - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() + } + + @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()) + + func didInput(_ string: String) { + stateSubject.value.input = string + validate() + } + + func clearUp() { + stateSubject.value.confirmationId = nil + } + + func didChooseCountry(_ country: Country) { + stateSubject.value.country = country + validate() + } + + func didTapNext() { + 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.hudManager.hide() + self.stateSubject.value.content = content + self.stateSubject.value.confirmationId = confirmationId + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudManager.show(.init(content: xxError)) + } } - - func clearUp() { - stateRelay.value.confirmation = nil - } - - func didChooseCountry(_ country: Country) { - stateRelay.value.country = country - validate() - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - let content = "\(self.stateRelay.value.input)\(self.stateRelay.value.country.code)" - - self.session.register(.phone, value: content) { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let confirmationId): - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = .init( - content: content, - confirmationId: confirmationId - ) - case .failure(let error): - self.hudRelay.send(.error(.init(with: error))) - } - } - } - } - - // MARK: Private - - private func validate() { - switch Validator.phone.validate((stateRelay.value.country.regex, stateRelay.value.input)) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.phone.validate((stateSubject.value.country.regex, stateSubject.value.input)) { + case .success: + stateSubject.value.status = .valid(nil) + case .failure(let error): + stateSubject.value.status = .invalid(error) } + } } diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index a7066eccfaada6e74960beb7a5b3b8b7e72548cb..07ab592ebd904e0a1dc91314e7b616a5cea7a646 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -1,101 +1,116 @@ -import HUD import UIKit import Shared +import AppCore import Combine import Defaults -import Countries -import Foundation -import Permissions -import Integration +import XXClient +import BackupFeature +import XXMessengerClient import CombineSchedulers -import DependencyInjection +import CountryListFeature +import PermissionsFeature +import ComposableArchitecture enum ProfileNavigationRoutes { - case none - case library - case libraryPermission + case none + case library + case libraryPermission } struct ProfileViewState: Equatable { - var email: String? - var phone: String? - var photo: UIImage? + var email: String? + var phone: String? + var photo: UIImage? } final class ProfileViewModel { - @KeyObject(.avatar, defaultValue: nil) var avatar: Data? - @KeyObject(.email, defaultValue: nil) var emailStored: String? - @KeyObject(.phone, defaultValue: nil) var phoneStored: String? - @KeyObject(.username, defaultValue: nil) var username: String? - @KeyObject(.sharingEmail, defaultValue: false) var isEmailSharing: Bool - @KeyObject(.sharingPhone, defaultValue: false) var isPhoneSharing: Bool - - @Dependency private var session: SessionType - @Dependency private var permissions: PermissionHandling - - var name: String { username! } - - var state: AnyPublisher<ProfileViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ProfileViewState, Never>(.init()) - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var navigation: AnyPublisher<ProfileNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } - private let navigationRoutes = PassthroughSubject<ProfileNavigationRoutes, Never>() - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - refresh() + @KeyObject(.avatar, defaultValue: nil) var avatar: Data? + @KeyObject(.email, defaultValue: nil) var emailStored: String? + @KeyObject(.phone, defaultValue: nil) var phoneStored: String? + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var isEmailSharing: Bool + @KeyObject(.sharingPhone, defaultValue: false) var isPhoneSharing: Bool + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.backupService) var backupService: BackupService + @Dependency(\.permissions) var permissions: PermissionsManager + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + var name: String { username! } + + var state: AnyPublisher<ProfileViewState, Never> { + stateRelay.eraseToAnyPublisher() + } + private let stateRelay = CurrentValueSubject<ProfileViewState, Never>(.init()) + + var navigation: AnyPublisher<ProfileNavigationRoutes, Never> { + navigationRoutes.eraseToAnyPublisher() + } + private let navigationRoutes = PassthroughSubject<ProfileNavigationRoutes, Never>() + + init() { + refresh() + } + + func refresh() { + var cleanPhone = phoneStored + + if let phone = cleanPhone { + let country = Country.findFrom(phone) + cleanPhone = "\(country.prefix)\(phone.dropLast(2))" } - func refresh() { - var cleanPhone = phoneStored - - if let phone = cleanPhone { - let country = Country.findFrom(phone) - cleanPhone = "\(country.prefix)\(phone.dropLast(2))" - } - - stateRelay.value = .init( - email: emailStored, - phone: cleanPhone, - photo: avatar != nil ? UIImage(data: avatar!) : nil - ) + stateRelay.value = .init( + email: emailStored, + phone: cleanPhone, + photo: avatar != nil ? UIImage(data: avatar!) : nil + ) + } + + func didRequestLibraryAccess() { + if permissions.library.status() { + navigationRoutes.send(.library) + } else { + navigationRoutes.send(.libraryPermission) } - - func didRequestLibraryAccess() { - if permissions.isPhotosAllowed { - navigationRoutes.send(.library) + } + + func didNavigateSomewhere() { + navigationRoutes.send(.none) + } + + func didChoosePhoto(_ photo: UIImage) { + stateRelay.value.photo = photo + avatar = photo.jpegData(compressionQuality: 0.0) + } + + func didTapDelete(isEmail: Bool) { + hudManager.show() + + bgQueue.schedule { [weak self] in + guard let self else { return } + do { + try self.messenger.ud.get()!.removeFact( + .init( + type: isEmail ? .email : .phone, + value: isEmail ? self.emailStored! : self.phoneStored! + ) + ) + if isEmail { + self.emailStored = nil + self.isEmailSharing = false } else { - navigationRoutes.send(.libraryPermission) - } - } - - func didNavigateSomewhere() { - navigationRoutes.send(.none) - } - - func didChoosePhoto(_ photo: UIImage) { - stateRelay.value.photo = photo - avatar = photo.jpegData(compressionQuality: 0.0) - } - - func didTapDelete(isEmail: Bool) { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.unregister(fact: isEmail ? .email : .phone) - - self.hudRelay.send(.none) - self.refresh() - } catch { - self.hudRelay.send(.error(.init(with: error))) - } + self.phoneStored = nil + self.isPhoneSharing = false } + self.backupService.didUpdateFacts() + self.hudManager.hide() + self.refresh() + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudManager.show(.init(content: xxError)) + } } + } } diff --git a/Sources/ProfileFeature/Views/ProfileCodeView.swift b/Sources/ProfileFeature/Views/ProfileCodeView.swift index f8d7199bb22c5b25af077cacd7315774b83ff5fe..dc4fb0310ff4bcbdf5736878b5c4e984a7472686 100644 --- a/Sources/ProfileFeature/Views/ProfileCodeView.swift +++ b/Sources/ProfileFeature/Views/ProfileCodeView.swift @@ -1,6 +1,7 @@ import UIKit import Shared import InputField +import AppResources final class ProfileCodeView: UIView { let titleLabel = UILabel() diff --git a/Sources/ProfileFeature/Views/ProfileEmailView.swift b/Sources/ProfileFeature/Views/ProfileEmailView.swift index 185c0ef797ec8574a31e722dc3478bac3d12fe5c..6c689f68169a7b55261c7444f6c7e3a4e5fe2026 100644 --- a/Sources/ProfileFeature/Views/ProfileEmailView.swift +++ b/Sources/ProfileFeature/Views/ProfileEmailView.swift @@ -1,74 +1,75 @@ import UIKit import Shared import InputField +import AppResources final class ProfileEmailView: UIView { - let titleLabel = UILabel() - let imageView = UIImageView() - let inputField = InputField() - let saveButton = CapsuleButton() + let titleLabel = UILabel() + let imageView = UIImageView() + let inputField = InputField() + let saveButton = CapsuleButton() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - titleLabel.text = Localized.Profile.EmailScreen.title - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) - imageView.contentMode = .center - imageView.image = Asset.profileEmail.image - saveButton.setStyle(.brandColored) - saveButton.setTitle(Localized.Profile.EmailScreen.action, for: .normal) + titleLabel.text = Localized.Profile.EmailScreen.title + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) + imageView.contentMode = .center + imageView.image = Asset.profileEmail.image + saveButton.setStyle(.brandColored) + saveButton.setTitle(Localized.Profile.EmailScreen.action, for: .normal) - inputField.setup( - title: Localized.Profile.EmailScreen.input, - placeholder: Localized.Profile.EmailScreen.input, - subtitleColor: Asset.neutralWeak.color, - allowsEmptySpace: false, - keyboardType: .emailAddress, - autocapitalization: .none, - contentType: .emailAddress - ) + inputField.setup( + title: Localized.Profile.EmailScreen.input, + placeholder: Localized.Profile.EmailScreen.input, + subtitleColor: Asset.neutralWeak.color, + allowsEmptySpace: false, + keyboardType: .emailAddress, + autocapitalization: .none, + contentType: .emailAddress + ) - addSubview(imageView) - addSubview(titleLabel) - addSubview(inputField) - addSubview(saveButton) + addSubview(imageView) + addSubview(titleLabel) + addSubview(inputField) + addSubview(saveButton) - imageView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(60) - make.centerX.equalToSuperview() - } + imageView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(60) + make.centerX.equalToSuperview() + } - titleLabel.snp.makeConstraints { make in - make.top.equalTo(imageView.snp.bottom).offset(39) - make.centerX.equalToSuperview() - } + titleLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom).offset(39) + make.centerX.equalToSuperview() + } - inputField.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(35) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } + inputField.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(35) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + } - saveButton.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(40) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-40) - } + saveButton.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(40) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.bottom.equalTo(safeAreaLayoutGuide).offset(-40) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) - switch status { - case .valid: - saveButton.isEnabled = true - case .invalid, .unknown: - saveButton.isEnabled = false - } + switch status { + case .valid: + saveButton.isEnabled = true + case .invalid, .unknown: + saveButton.isEnabled = false } + } } diff --git a/Sources/ProfileFeature/Views/ProfilePhoneView.swift b/Sources/ProfileFeature/Views/ProfilePhoneView.swift index 12f5062fd269d5092a036685943d260304b5b4a0..1dccecfa1276e2983520b01e792ff83d762d527c 100644 --- a/Sources/ProfileFeature/Views/ProfilePhoneView.swift +++ b/Sources/ProfileFeature/Views/ProfilePhoneView.swift @@ -1,73 +1,74 @@ import UIKit import Shared import InputField +import AppResources final class ProfilePhoneView: UIView { - let titleLabel = UILabel() - let imageView = UIImageView() - let inputField = InputField() - let saveButton = CapsuleButton() - - init() { - super.init(frame: .zero) - - titleLabel.text = "Add Phone" - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) - imageView.contentMode = .center - imageView.image = Asset.profilePhone.image - saveButton.setStyle(.brandColored) - saveButton.setTitle(Localized.Profile.PhoneScreen.action, for: .normal) - - inputField.setup( - style: .phone, - title: Localized.Profile.PhoneScreen.input, - placeholder: "10651613216", - subtitleColor: Asset.neutralWeak.color, - keyboardType: .phonePad, - contentType: .telephoneNumber - ) - - addSubview(imageView) - addSubview(titleLabel) - addSubview(inputField) - addSubview(saveButton) - - imageView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(60) - make.centerX.equalToSuperview() - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(imageView.snp.bottom).offset(39) - make.centerX.equalToSuperview() - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(35) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } - - saveButton.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(40) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-40) - } + let titleLabel = UILabel() + let imageView = UIImageView() + let inputField = InputField() + let saveButton = CapsuleButton() + + init() { + super.init(frame: .zero) + + titleLabel.text = "Add Phone" + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) + imageView.contentMode = .center + imageView.image = Asset.profilePhone.image + saveButton.setStyle(.brandColored) + saveButton.setTitle(Localized.Profile.PhoneScreen.action, for: .normal) + + inputField.setup( + style: .phone, + title: Localized.Profile.PhoneScreen.input, + placeholder: "10651613216", + subtitleColor: Asset.neutralWeak.color, + keyboardType: .phonePad, + contentType: .telephoneNumber + ) + + addSubview(imageView) + addSubview(titleLabel) + addSubview(inputField) + addSubview(saveButton) + + imageView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(60) + make.centerX.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom).offset(39) + make.centerX.equalToSuperview() } - - required init?(coder: NSCoder) { nil } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - saveButton.isEnabled = true - case .invalid, .unknown: - saveButton.isEnabled = false - } + + inputField.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(35) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + } + + saveButton.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(40) + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.bottom.equalTo(safeAreaLayoutGuide).offset(-40) + } + } + + required init?(coder: NSCoder) { nil } + + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + + switch status { + case .valid: + saveButton.isEnabled = true + case .invalid, .unknown: + saveButton.isEnabled = false } + } } diff --git a/Sources/ProfileFeature/Views/ProfileView.swift b/Sources/ProfileFeature/Views/ProfileView.swift index e95e3e63c08f97b705a4da31c9e3264d8c1ba1f4..55081d330c759c7bcc6fc798c9bc92a33969990d 100644 --- a/Sources/ProfileFeature/Views/ProfileView.swift +++ b/Sources/ProfileFeature/Views/ProfileView.swift @@ -1,43 +1,44 @@ import UIKit import Shared +import AppResources final class ProfileView: UIView { - let stackView = UIStackView() - let cardComponent = AvatarCardComponent() - let emailView = AttributeComponent() - let phoneView = AttributeComponent() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - let emailTitle = Localized.Profile.Email.title - let phoneTitle = Localized.Profile.Phone.title - - emailView.set(title: emailTitle, style: .interactive) - phoneView.set(title: phoneTitle, style: .interactive) - - stackView.spacing = 41 - stackView.axis = .vertical - stackView.addArrangedSubview(emailView) - stackView.addArrangedSubview(phoneView) - - addSubview(stackView) - addSubview(cardComponent) - - cardComponent.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - stackView.snp.makeConstraints { make in - make.top.equalTo(cardComponent.snp.bottom).offset(24) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-26) - make.bottom.lessThanOrEqualToSuperview() - } + let stackView = UIStackView() + let cardComponent = AvatarCardComponent() + let emailView = AttributeComponent() + let phoneView = AttributeComponent() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + let emailTitle = Localized.Profile.Email.title + let phoneTitle = Localized.Profile.Phone.title + + emailView.set(title: emailTitle, style: .interactive) + phoneView.set(title: phoneTitle, style: .interactive) + + stackView.spacing = 41 + stackView.axis = .vertical + stackView.addArrangedSubview(emailView) + stackView.addArrangedSubview(phoneView) + + addSubview(stackView) + addSubview(cardComponent) + + cardComponent.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + } + + stackView.snp.makeConstraints { + $0.top.equalTo(cardComponent.snp.bottom).offset(24) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-26) + $0.bottom.lessThanOrEqualToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/PushFeature/ContentsBuilder.swift b/Sources/PushFeature/ContentsBuilder.swift deleted file mode 100644 index 9aa75041564c08334bf2f8068d118128c3a8b52f..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/ContentsBuilder.swift +++ /dev/null @@ -1,23 +0,0 @@ -import UserNotifications - -public struct ContentsBuilder { - enum Constants { - static let threadIdentifier = "new_message_identifier" - } - - public var build: (String, Push) -> UNMutableNotificationContent -} - -public extension ContentsBuilder { - static let live = ContentsBuilder { title, push in - let content = UNMutableNotificationContent() - content.badge = 1 - content.body = title - content.title = title - content.sound = .default - content.userInfo["source"] = push.source - content.userInfo["type"] = push.type.rawValue - content.threadIdentifier = Constants.threadIdentifier - return content - } -} diff --git a/Sources/PushFeature/MockPushHandler.swift b/Sources/PushFeature/MockPushHandler.swift deleted file mode 100644 index 41aced17ade52d9c4b02ec092feec29276fc21fb..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/MockPushHandler.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit - -public struct MockPushHandler: PushHandling { - public init() {} - - public func registerToken(_ token: Data) { - // TODO - } - - public func requestAuthorization( - _ completion: @escaping (Result<Bool, Error>) -> Void - ) { - completion(.success(true)) - } - - public func handlePush( - _ notification: [AnyHashable : Any], - _ completion: @escaping (UIBackgroundFetchResult) -> Void - ) { - completion(.noData) - } - - public func handlePush( - _ request: UNNotificationRequest, - _ completion: @escaping (UNNotificationContent) -> Void - ) { - let content = UNMutableNotificationContent() - content.title = String(describing: Self.self) - completion(content) - } - - public func handleAction( - _ router: PushRouter, - _ userInfo: [AnyHashable : Any], - _ completion: @escaping () -> Void - ) { - completion() - } -} diff --git a/Sources/PushFeature/Push.swift b/Sources/PushFeature/Push.swift deleted file mode 100644 index 51c25bd52f513816ca850930e07a4c0a72204798..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/Push.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -public struct Push { - public let type: PushType - public let source: Data? - - public init?(type: String, source: Data?) { - guard let pushType = PushType(rawValue: type) else { - return nil - } - - self.type = pushType - self.source = source - } -} diff --git a/Sources/PushFeature/PushExtractor.swift b/Sources/PushFeature/PushExtractor.swift deleted file mode 100644 index 045f0e898276350914d9c0d335a58997d4fbdfa1..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/PushExtractor.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import Integration - -public struct PushExtractor { - enum Constants { - static let preImage = "preImage" - static let appGroup = "group.elixxir.messenger" - static let notificationData = "notificationData" - } - - public var extractFrom: ([AnyHashable: Any]) -> Result<[Push]?, Error> -} - -public extension PushExtractor { - static let live = PushExtractor { dictionary in - var error: NSError? - - guard let data = dictionary[Constants.notificationData] as? String, - let defaults = UserDefaults(suiteName: Constants.appGroup), - let preImage = defaults.value(forKey: Constants.preImage) as? String, - let reports = evaluateNotification(data, preImage, &error) else { - return .success(nil) - } - - if let error = error { - return .failure(error) - } - - let pushes = (0..<reports.len()) - .compactMap { try? reports.get(index: $0) } - .filter { $0.forMe() } - .filter { $0.type() != PushType.silent.rawValue } - .filter { $0.type() != PushType.default.rawValue } - .compactMap { Push(type: $0.type(), source: $0.source()) } - - return .success(pushes) - } -} diff --git a/Sources/PushFeature/PushHandler.swift b/Sources/PushFeature/PushHandler.swift deleted file mode 100644 index b090664f1f7bb55086cc4909afd8bf2639cf7e59..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/PushHandler.swift +++ /dev/null @@ -1,173 +0,0 @@ -import UIKit -import Models -import Defaults -import XXModels -import Integration -import ReportingFeature -import DependencyInjection - -public final class PushHandler: PushHandling { - private enum Constants { - static let appGroup = "group.elixxir.messenger" - static let usernamesSetting = "isShowingUsernames" - } - - @Dependency var reportingStatus: ReportingStatus - - @KeyObject(.pushNotifications, defaultValue: false) var isPushEnabled: Bool - - let requestAuth: RequestAuth - public static let defaultRequestAuth = UNUserNotificationCenter.current().requestAuthorization - public typealias RequestAuth = (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void - - public var pushExtractor: PushExtractor - public var contentsBuilder: ContentsBuilder - public var applicationState: () -> UIApplication.State - - public init( - requestAuth: @escaping RequestAuth = defaultRequestAuth, - pushExtractor: PushExtractor = .live, - contentsBuilder: ContentsBuilder = .live, - applicationState: @escaping () -> UIApplication.State = { UIApplication.shared.applicationState } - ) { - self.requestAuth = requestAuth - self.pushExtractor = pushExtractor - self.contentsBuilder = contentsBuilder - self.applicationState = applicationState - } - - public func registerToken(_ token: Data) { - do { - let session = try DependencyInjection.Container.shared.resolve() as SessionType - try session.registerNotifications(token) - } catch { - isPushEnabled = false - } - } - - public func requestAuthorization( - _ completion: @escaping (Result<Bool, Error>) -> Void - ) { - let options: UNAuthorizationOptions = [.alert, .sound, .badge] - - requestAuth(options) { granted, error in - guard let error = error else { - completion(.success(granted)) - return - } - - completion(.failure(error)) - } - } - - public func handlePush( - _ userInfo: [AnyHashable: Any], - _ completion: @escaping (UIBackgroundFetchResult) -> Void - ) { - do { - guard - let pushes = try pushExtractor.extractFrom(userInfo).get(), - applicationState() == .background, - pushes.isEmpty == false - else { - completion(.noData) - return - } - - let content = contentsBuilder.build("New Messages Available", pushes.first!) - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: Bundle.main.bundleIdentifier!, content: content, trigger: trigger) - - UNUserNotificationCenter.current().add(request) { error in - if error == nil { - completion(.newData) - } else { - completion(.failed) - } - } - } catch { - completion(.failed) - } - } - - public func handlePush( - _ request: UNNotificationRequest, - _ completion: @escaping (UNNotificationContent) -> Void - ) { - guard let pushes = try? pushExtractor.extractFrom(request.content.userInfo).get(), !pushes.isEmpty, - let defaults = UserDefaults(suiteName: Constants.appGroup) else { - return - } - - let dbPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("xxm_database") - .appendingPathExtension("sqlite").path - - let tuples: [(String, Push)] = pushes.compactMap { - guard let userId = $0.source, - let dbManager = try? Database.onDisk(path: dbPath), - let contact = try? dbManager.fetchContacts(.init(id: [userId])).first else { - return ($0.type.unknownSenderContent!, $0) - } - - if reportingStatus.isEnabled(), (contact.isBlocked || contact.isBanned) { - return nil - } - - if let showSender = defaults.value(forKey: Constants.usernamesSetting) as? Bool, showSender == true { - let name = (contact.nickname ?? contact.username) ?? "" - return ($0.type.knownSenderContent(name)!, $0) - } else { - return ($0.type.unknownSenderContent!, $0) - } - } - - tuples - .map(contentsBuilder.build) - .forEach { completion($0) } - } - - public func handleAction( - _ router: PushRouter, - _ userInfo: [AnyHashable : Any], - _ completion: @escaping () -> Void - ) { - guard let typeString = userInfo["type"] as? String, - let type = PushType(rawValue: typeString) else { - completion() - return - } - - let route: PushRouter.Route - - switch type { - case .e2e: - guard let source = userInfo["source"] as? Data else { - completion() - return - } - - route = .contactChat(id: source) - - case .group: - guard let source = userInfo["source"] as? Data else { - completion() - return - } - - route = .groupChat(id: source) - - case .request, .groupRq: - route = .requests - - case .silent, .`default`: - fatalError("Silent/Default push types should be filtered at this point") - - case .reset, .endFT, .confirm: - route = .requests - } - - router.navigateTo(route, completion) - } -} diff --git a/Sources/PushFeature/PushHandling.swift b/Sources/PushFeature/PushHandling.swift deleted file mode 100644 index c17c7e600d9c8f3ed222f3e7b8e1d6db035d85ce..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/PushHandling.swift +++ /dev/null @@ -1,70 +0,0 @@ -import UIKit - -public protocol PushHandling { - - /// Submits the APNS token to a 3rd-party service. - /// This should be called whenever the user accepts - /// receiving remote push notifications. - /// - /// - Parameters: - /// - token: The APNS provided token - /// - func registerToken( - _ token: Data - ) - - /// Prompts a system alert to the user requesting - /// permission for receiving remote push notifications - /// - /// - Parameters: - /// - completion: Async result closure containing the user reponse - /// - func requestAuthorization( - _ completion: @escaping (Result<Bool, Error>) -> Void - ) - - /// Evaluates if the notification should be displayed or not - /// and if yes, how should it look like. - /// - /// - Note: This function should be called by the main app target - /// - Warning: The notifications should only appear if the app is in background - /// - /// - Parameters: - /// - userInfo: Dictionary contaning the payload of the remote push - /// - completion: Async closure containing the operation chosed - /// - func handlePush( - _ userInfo: [AnyHashable: Any], - _ completion: @escaping (UIBackgroundFetchResult) -> Void - ) - - /// Evaluates if the notification should be displayed or not - /// and if yes, how it should look like and who is it from - /// - /// - Note: This function should be called by the `NotificationExtension` - /// - /// - Parameters: - /// - request: The notification request that arrived for the `NotificationExtension` - /// - completion: Async closure containing the operation chosed - /// - func handlePush( - _ request: UNNotificationRequest, - _ completion: @escaping (UNNotificationContent) -> Void - ) - - /// Deeplinks to any UI flow set within the notification. - /// It can get called either when the user starts the app - /// from a notification or when the user has the app in - /// background and resumes the app by tapping on a push - /// - /// - Parameters: - /// - router: Router instance that will decide the correct UI flow - /// - userInfo: Dictionary contaning the payload of the notification - /// - completion: Async empty closure - /// - func handleAction( - _ router: PushRouter, - _ userInfo: [AnyHashable: Any], - _ completion: @escaping () -> Void - ) -} diff --git a/Sources/PushFeature/PushRouter.swift b/Sources/PushFeature/PushRouter.swift deleted file mode 100644 index 05885d5196b6025f24d18bd47f5922f1164d1f43..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/PushRouter.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -public struct PushRouter { - public typealias NavigateTo = (Route, @escaping () -> Void) -> Void - - public enum Route { - case requests - case groupChat(id: Data) - case contactChat(id: Data) - case search(username: String) - } - - public var navigateTo: NavigateTo - - public init(navigateTo: @escaping NavigateTo) { - self.navigateTo = navigateTo - } -} - -public extension PushRouter { - static let noop = PushRouter { _, _ in } -} - diff --git a/Sources/PushFeature/PushType.swift b/Sources/PushFeature/PushType.swift deleted file mode 100644 index 7cd2fefefcbce0968a7d4478aa4e5cc01d771f98..0000000000000000000000000000000000000000 --- a/Sources/PushFeature/PushType.swift +++ /dev/null @@ -1,53 +0,0 @@ -public enum PushType: String { - case e2e - case reset - case endFT - case group - case silent - case groupRq - case confirm - case request - case `default` - - var unknownSenderContent: String? { - switch self { - case .silent, .`default`: - return nil - case .endFT: - return "New media received" - case .group: - return "New group message" - case .groupRq: - return "Group request received" - case .e2e: - return "New private message" - case .reset: - return "One of your contacts has restored their account" - case .request: - return "Request received" - case .confirm: - return "Request accepted" - } - } - - var knownSenderContent: (String) -> String? { - switch self { - case .silent, .`default`: - return { _ in nil } - case .e2e: - return { String(format: "%@ sent you a private message", $0) } - case .reset: - return { String(format: "%@ restored their account", $0) } - case .endFT: - return { String(format: "%@ sent you a file", $0) } - case .group: - return { String(format: "%@ sent you a group message", $0) } - case .groupRq: - return { String(format: "%@ sent you a group request", $0) } - case .confirm: - return { String(format: "%@ confirmed your contact request", $0) } - case .request: - return { String(format: "%@ sent you a contact request", $0) } - } - } -} diff --git a/Sources/ReportingFeature/Dependency.swift b/Sources/ReportingFeature/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..33e19ae1c98c65fba2b084c9dc0bbf5e10c03c33 --- /dev/null +++ b/Sources/ReportingFeature/Dependency.swift @@ -0,0 +1,25 @@ +import Dependencies + +private enum ReportingStatusDependencyKey: DependencyKey { + static let liveValue: ReportingStatus = .live() + static let testValue: ReportingStatus = .unimplemented +} + +extension DependencyValues { + public var reportingStatus: ReportingStatus { + get { self[ReportingStatusDependencyKey.self] } + set { self[ReportingStatusDependencyKey.self] = newValue } + } +} + +private enum SendReportDependencyKey: DependencyKey { + static let liveValue: SendReport = .live + static let testValue: SendReport = .unimplemented +} + +extension DependencyValues { + public var sendReport: SendReport { + get { self[SendReportDependencyKey.self] } + set { self[SendReportDependencyKey.self] = newValue } + } +} diff --git a/Sources/ReportingFeature/FetchBannedList.swift b/Sources/ReportingFeature/FetchBannedList.swift deleted file mode 100644 index 2620b15c84e5d31316f663a363ff6a4526d1880d..0000000000000000000000000000000000000000 --- a/Sources/ReportingFeature/FetchBannedList.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation -import XCTestDynamicOverlay - -public struct FetchBannedList { - public enum Error: Swift.Error, Equatable { - case network(URLError) - case invalidResponse - } - - public typealias Completion = (Result<Data, Error>) -> Void - - public var run: (@escaping Completion) -> Void - - public func callAsFunction(completion: @escaping Completion) { - run(completion) - } -} - -extension FetchBannedList { - public static let live = FetchBannedList { completion in - let url = URL(string: "https://elixxir-bins.s3.us-west-1.amazonaws.com/client/bannedUsers/banned.csv")! - let session = URLSession.shared - let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData) - let task = session.dataTask(with: request) { data, response, error in - if let error = error { - completion(.failure(.network(error as! URLError))) - return - } - guard let response = response as? HTTPURLResponse, - (200..<300).contains(response.statusCode), - let data = data - else { - completion(.failure(.invalidResponse)) - return - } - completion(.success(data)) - } - task.resume() - } -} - -extension FetchBannedList { - public static let unimplemented = FetchBannedList( - run: XCTUnimplemented("\(Self.self)") - ) -} diff --git a/Sources/ReportingFeature/MakeAppScreenshot.swift b/Sources/ReportingFeature/MakeAppScreenshot.swift index 7d44af879f35d792685f3e278cc6ce9c044d8a07..75a1fc31e6139e9a3fc2510eb811f00ffe37a628 100644 --- a/Sources/ReportingFeature/MakeAppScreenshot.swift +++ b/Sources/ReportingFeature/MakeAppScreenshot.swift @@ -3,51 +3,51 @@ import UIKit import XCTestDynamicOverlay public struct MakeAppScreenshot { - public enum Error: Swift.Error, Equatable { - case unableToGetForegroundWindowScene - case unableToGetKeyWindow - } - - public var run: () throws -> UIImage - - public func callAsFunction() throws -> UIImage { - try run() - } + public enum Error: Swift.Error, Equatable { + case unableToGetForegroundWindowScene + case unableToGetKeyWindow + } + + public var run: () throws -> UIImage + + public func callAsFunction() throws -> UIImage { + try run() + } } extension MakeAppScreenshot { - public static let live = MakeAppScreenshot { - let scene: UIWindowScene? = UIApplication.shared.connectedScenes - .filter { $0.activationState == .foregroundActive } - .compactMap { $0 as? UIWindowScene } - .first - - guard let scene = scene else { - throw Error.unableToGetForegroundWindowScene - } - - let window: UIWindow? = scene.windows.first(where: \.isKeyWindow) - - guard let keyWindow = window else { - throw Error.unableToGetKeyWindow - } - - let rendererFormat = UIGraphicsImageRendererFormat() - rendererFormat.scale = scene.screen.scale - - let renderer = UIGraphicsImageRenderer( - bounds: keyWindow.bounds, - format: rendererFormat - ) - - return renderer.image { ctx in - keyWindow.layer.render(in: ctx.cgContext) - } + public static let live = MakeAppScreenshot { + let scene: UIWindowScene? = UIApplication.shared.connectedScenes + .filter { $0.activationState == .foregroundActive } + .compactMap { $0 as? UIWindowScene } + .first + + guard let scene = scene else { + throw Error.unableToGetForegroundWindowScene + } + + let window: UIWindow? = scene.windows.first(where: \.isKeyWindow) + + guard let keyWindow = window else { + throw Error.unableToGetKeyWindow } + + let rendererFormat = UIGraphicsImageRendererFormat() + rendererFormat.scale = scene.screen.scale + + let renderer = UIGraphicsImageRenderer( + bounds: keyWindow.bounds, + format: rendererFormat + ) + + return renderer.image { ctx in + keyWindow.layer.render(in: ctx.cgContext) + } + } } extension MakeAppScreenshot { - public static let unimplemented = MakeAppScreenshot( - run: XCTUnimplemented("\(Self.self)") - ) + public static let unimplemented = MakeAppScreenshot( + run: XCTUnimplemented("\(Self.self)") + ) } diff --git a/Sources/ReportingFeature/MakeReportDrawer.swift b/Sources/ReportingFeature/MakeReportDrawer.swift index b8b4aa394481d3e292ef236711b1f31f82704d3e..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(with: [ - 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/ReportingFeature/ProcessBannedList.swift b/Sources/ReportingFeature/ProcessBannedList.swift deleted file mode 100644 index 3399a34ee3614bc41df393251d1e21a9309cdf52..0000000000000000000000000000000000000000 --- a/Sources/ReportingFeature/ProcessBannedList.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import SwiftCSV -import XCTestDynamicOverlay - -public struct ProcessBannedList { - public enum ElementError: Swift.Error { - case missingUserId - case invalidUserId(String) - } - - public enum Error: Swift.Error { - case invalidData - case csv(Swift.Error) - } - - public typealias ForEach = (Result<Data, ElementError>) -> Void - public typealias Completion = (Result<Void, Error>) -> Void - - public var run: (Data, ForEach, Completion) -> Void - - public func callAsFunction( - data: Data, - forEach: ForEach, - completion: Completion - ) { - run(data, forEach, completion) - } -} - -extension ProcessBannedList { - public static let live = ProcessBannedList { data, forEach, completion in - guard let csvString = String(data: data, encoding: .utf8) else { - completion(.failure(.invalidData)) - return - } - let csv: EnumeratedCSV - do { - csv = try EnumeratedCSV(string: csvString) - } - catch { - completion(.failure(.csv(error))) - return - } - csv.rows.forEach { row in - guard let userIdString = row.first else { - forEach(.failure(.missingUserId)) - return - } - guard let userId = Data(base64Encoded: userIdString) else { - forEach(.failure(.invalidUserId(userIdString))) - return - } - forEach(.success(userId)) - } - completion(.success(())) - } -} - -extension ProcessBannedList { - public static let unimplemented = ProcessBannedList { _, _, _ in - let run: () -> Void = XCTUnimplemented("\(Self.self)") - run() - } -} diff --git a/Sources/ReportingFeature/Report.swift b/Sources/ReportingFeature/Report.swift index c2032b6a6b87d096f8f6883085d4c6e887630600..56ff6192eeee64004e4cb5a3b5d978017d719998 100644 --- a/Sources/ReportingFeature/Report.swift +++ b/Sources/ReportingFeature/Report.swift @@ -1,52 +1,52 @@ import Foundation public struct Report: Encodable { - public init( - sender: ReportUser, - recipient: ReportUser, - type: ReportType, - screenshot: Data, - partyName: String? = nil, - partyBlob: String? = nil, - partyMembers: [ReportUser]? = nil - ) { - self.sender = sender - self.recipient = recipient - self.type = type - self.screenshot = screenshot - self.partyName = partyName - self.partyBlob = partyBlob - self.partyMembers = partyMembers - } - - public var sender: ReportUser - public var recipient: ReportUser - public var type: ReportType - public var screenshot: Data - public var partyName: String? - public var partyBlob: String? - public var partyMembers: [ReportUser]? + public init( + sender: ReportUser, + recipient: ReportUser, + type: ReportType, + screenshot: Data, + partyName: String? = nil, + partyBlob: String? = nil, + partyMembers: [ReportUser]? = nil + ) { + self.sender = sender + self.recipient = recipient + self.type = type + self.screenshot = screenshot + self.partyName = partyName + self.partyBlob = partyBlob + self.partyMembers = partyMembers + } + + public var sender: ReportUser + public var recipient: ReportUser + public var type: ReportType + public var screenshot: Data + public var partyName: String? + public var partyBlob: String? + public var partyMembers: [ReportUser]? } extension Report { - public struct ReportUser: Encodable { - public init( - userId: String, - username: String - ) { - self.userId = userId - self.username = username - } - - public var userId: String - public var username: String + public struct ReportUser: Encodable { + public init( + userId: String, + username: String + ) { + self.userId = userId + self.username = username } + + public var userId: String + public var username: String + } } extension Report { - public enum ReportType: String, Encodable { - case dm - case group - case channel - } + public enum ReportType: String, Encodable { + case dm + case group + case channel + } } diff --git a/Sources/ReportingFeature/ReportingStatus.swift b/Sources/ReportingFeature/ReportingStatus.swift index 1bb72983430de28a6d46c47cd7ec2a3a21612659..2da0592e647cc059be8d2152e72a4e71c81d5689 100644 --- a/Sources/ReportingFeature/ReportingStatus.swift +++ b/Sources/ReportingFeature/ReportingStatus.swift @@ -1,51 +1,58 @@ import Combine public struct ReportingStatus { - public var isOptional: () -> Bool - public var isEnabled: () -> Bool - public var isEnabledPublisher: () -> AnyPublisher<Bool, Never> - public var enable: (Bool) -> Void + public var isOptional: () -> Bool + public var isEnabled: () -> Bool + public var isEnabledPublisher: () -> AnyPublisher<Bool, Never> + public var enable: (Bool) -> Void } extension ReportingStatus { - public static func live( - isOptional: ReportingStatusIsOptional = .live(), - isEnabled: ReportingStatusIsEnabled = .live() - ) -> ReportingStatus { - ReportingStatus( - isOptional: { - isOptional.get() - }, - isEnabled: { - if isOptional.get() == false { - return true - } + public static func live( + isOptional: ReportingStatusIsOptional = .live(), + isEnabled: ReportingStatusIsEnabled = .live() + ) -> ReportingStatus { + ReportingStatus( + isOptional: { + isOptional.get() + }, + isEnabled: { + if isOptional.get() == false { + return true + } + + return isEnabled.get() + }, + isEnabledPublisher: { + if isOptional.get() == false { + return Just(true).eraseToAnyPublisher() + } + + return isEnabled.publisher() + }, + enable: { enabled in + isEnabled.set(enabled) + } + ) + } + + public static func mock( + isEnabled: Bool = false, + isOptional: Bool = true + ) -> ReportingStatus { + let isEnabledSubject = CurrentValueSubject<Bool, Never>(isEnabled) + return ReportingStatus( + isOptional: { isOptional }, + isEnabled: { isEnabledSubject.value }, + isEnabledPublisher: { isEnabledSubject.eraseToAnyPublisher() }, + enable: { isEnabledSubject.send($0) } + ) + } - return isEnabled.get() - }, - isEnabledPublisher: { - if isOptional.get() == false { - return Just(true).eraseToAnyPublisher() - } - - return isEnabled.publisher() - }, - enable: { enabled in - isEnabled.set(enabled) - } - ) - } - - public static func mock( - isEnabled: Bool = false, - isOptional: Bool = true - ) -> ReportingStatus { - let isEnabledSubject = CurrentValueSubject<Bool, Never>(isEnabled) - return ReportingStatus( - isOptional: { isOptional }, - isEnabled: { isEnabledSubject.value }, - isEnabledPublisher: { isEnabledSubject.eraseToAnyPublisher() }, - enable: { isEnabledSubject.send($0) } - ) - } + public static let unimplemented = ReportingStatus( + isOptional: { fatalError() }, + isEnabled: { fatalError() }, + isEnabledPublisher: { fatalError() }, + enable: { _ in } + ) } diff --git a/Sources/ReportingFeature/ReportingStatusIsEnabled.swift b/Sources/ReportingFeature/ReportingStatusIsEnabled.swift index 32f23fcb78a3b5e8fe7f6dc15a95a1b27e278bfb..ecf02aba50fff9f2334ad50dee4a6d2b8aa2dbb8 100644 --- a/Sources/ReportingFeature/ReportingStatusIsEnabled.swift +++ b/Sources/ReportingFeature/ReportingStatusIsEnabled.swift @@ -2,37 +2,37 @@ import Combine import Foundation public struct ReportingStatusIsEnabled { - public var get: () -> Bool - public var set: (Bool) -> Void - public var publisher: () -> AnyPublisher<Bool, Never> + public var get: () -> Bool + public var set: (Bool) -> Void + public var publisher: () -> AnyPublisher<Bool, Never> } extension ReportingStatusIsEnabled { - public static func live( - userDefaults: UserDefaults = .standard - ) -> ReportingStatusIsEnabled { - ReportingStatusIsEnabled( - get: { - userDefaults.isReportingEnabled - }, - set: { enabled in - userDefaults.isReportingEnabled = enabled - }, - publisher: { - userDefaults.publisher(for: \.isReportingEnabled).eraseToAnyPublisher() - } - ) - } + public static func live( + userDefaults: UserDefaults = .standard + ) -> ReportingStatusIsEnabled { + ReportingStatusIsEnabled( + get: { + userDefaults.isReportingEnabled + }, + set: { enabled in + userDefaults.isReportingEnabled = enabled + }, + publisher: { + userDefaults.publisher(for: \.isReportingEnabled).eraseToAnyPublisher() + } + ) + } } private extension UserDefaults { - static let isReportingEnabledKey = "isReportingEnabled" - - @objc var isReportingEnabled: Bool { - get { - bool(forKey: Self.isReportingEnabledKey) - } set { - set(newValue, forKey: Self.isReportingEnabledKey) - } + static let isReportingEnabledKey = "isReportingEnabled" + + @objc var isReportingEnabled: Bool { + get { + bool(forKey: Self.isReportingEnabledKey) + } set { + set(newValue, forKey: Self.isReportingEnabledKey) } + } } diff --git a/Sources/ReportingFeature/ReportingStatusIsOptional.swift b/Sources/ReportingFeature/ReportingStatusIsOptional.swift index e0cc6591bf1d13c68d022a2f8587cc495403f89d..ca5ad5654ba72678496581b7ac3aae56626bc577 100644 --- a/Sources/ReportingFeature/ReportingStatusIsOptional.swift +++ b/Sources/ReportingFeature/ReportingStatusIsOptional.swift @@ -1,24 +1,24 @@ import Foundation public struct ReportingStatusIsOptional { - public var get: () -> Bool + public var get: () -> Bool } extension ReportingStatusIsOptional { - public static func live( - plist url: URL = Bundle.main.url(forResource: "Info", withExtension: "plist")! - ) -> ReportingStatusIsOptional { - ReportingStatusIsOptional { - struct Plist: Decodable { - let isReportingOptional: Bool - } - - guard let data = try? Data(contentsOf: url), - let infoPlist = try? PropertyListDecoder().decode(Plist.self, from: data) else { - return true - } - - return infoPlist.isReportingOptional - } + public static func live( + plist url: URL = Bundle.main.url(forResource: "Info", withExtension: "plist")! + ) -> ReportingStatusIsOptional { + ReportingStatusIsOptional { + struct Plist: Decodable { + let isReportingOptional: Bool + } + + guard let data = try? Data(contentsOf: url), + let infoPlist = try? PropertyListDecoder().decode(Plist.self, from: data) else { + return true + } + + return infoPlist.isReportingOptional } + } } diff --git a/Sources/ReportingFeature/SendReport.swift b/Sources/ReportingFeature/SendReport.swift index 4793d998d49c43426012ac9760cd28e39e8eb8e6..678bfbc3d1ce2a8339975e74b56330591b6ff06d 100644 --- a/Sources/ReportingFeature/SendReport.swift +++ b/Sources/ReportingFeature/SendReport.swift @@ -2,93 +2,93 @@ import Foundation import XCTestDynamicOverlay public struct SendReport { - public typealias Completion = (Result<Void, Error>) -> Void + public typealias Completion = (Result<Void, Error>) -> Void - public var run: (Report, @escaping Completion) -> Void + public var run: (Report, @escaping Completion) -> Void - public func callAsFunction(_ report: Report, completion: @escaping Completion) { - run(report, completion) - } + public func callAsFunction(_ report: Report, completion: @escaping Completion) { + run(report, completion) + } } extension SendReport { - public static let live = SendReport { report, completion in - let url = URL(string: "https://3.74.237.181:11420/report")! - var request = URLRequest(url: url) - request.httpMethod = "POST" - do { - request.httpBody = try JSONEncoder().encode(report) - } catch { - completion(.failure(error)) - return - } - let session = URLSession( - configuration: .default, - delegate: SessionDelegate(), - delegateQueue: nil - ) - let task = session.dataTask(with: request) { _, _, error in - defer { session.invalidateAndCancel() } - if let error = error { - completion(.failure(error)) - return - } - completion(.success(())) - } - task.resume() + public static let live = SendReport { report, completion in + let url = URL(string: "https://3.74.237.181:11420/report")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + do { + request.httpBody = try JSONEncoder().encode(report) + } catch { + completion(.failure(error)) + return + } + let session = URLSession( + configuration: .default, + delegate: SessionDelegate(), + delegateQueue: nil + ) + let task = session.dataTask(with: request) { _, _, error in + defer { session.invalidateAndCancel() } + if let error = error { + completion(.failure(error)) + return + } + completion(.success(())) } + task.resume() + } - public static func mock( - result: Result<Void, Error> = .success(()) - ) -> SendReport { - SendReport { report, completion in - print("[SendReport.mock] Sending report: \(report)") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - print("[SendReport.mock] Sending report finished") - completion(result) - } - } + public static func mock( + result: Result<Void, Error> = .success(()) + ) -> SendReport { + SendReport { report, completion in + print("[SendReport.mock] Sending report: \(report)") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + print("[SendReport.mock] Sending report finished") + completion(result) + } } + } } extension SendReport { - public static let unimplemented = SendReport( - run: XCTUnimplemented("\(Self.self)") - ) + public static let unimplemented = SendReport( + run: XCTUnimplemented("\(Self.self)") + ) } private final class SessionDelegate: NSObject, URLSessionDelegate { - func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - let authMethod = challenge.protectionSpace.authenticationMethod - guard authMethod == NSURLAuthenticationMethodServerTrust else { - return completionHandler(.cancelAuthenticationChallenge, nil) - } - - guard let serverTrust = challenge.protectionSpace.serverTrust else { - return completionHandler(.cancelAuthenticationChallenge, nil) - } + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + let authMethod = challenge.protectionSpace.authenticationMethod + guard authMethod == NSURLAuthenticationMethodServerTrust else { + return completionHandler(.cancelAuthenticationChallenge, nil) + } - guard let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else { - return completionHandler(.cancelAuthenticationChallenge, nil) - } + guard let serverTrust = challenge.protectionSpace.serverTrust else { + return completionHandler(.cancelAuthenticationChallenge, nil) + } - let serverCertCFData = SecCertificateCopyData(serverCert) - let serverCertData = Data( - bytes: CFDataGetBytePtr(serverCertCFData), - count: CFDataGetLength(serverCertCFData) - ) + guard let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else { + return completionHandler(.cancelAuthenticationChallenge, nil) + } - let localCertURL = Bundle.module.url(forResource: "report_cert", withExtension: "der")! - let localCertData = try! Data(contentsOf: localCertURL) + let serverCertCFData = SecCertificateCopyData(serverCert) + let serverCertData = Data( + bytes: CFDataGetBytePtr(serverCertCFData), + count: CFDataGetLength(serverCertCFData) + ) - guard serverCertData == localCertData else { - return completionHandler(.cancelAuthenticationChallenge, nil) - } + let localCertURL = Bundle.module.url(forResource: "report_cert", withExtension: "der")! + let localCertData = try! Data(contentsOf: localCertURL) - completionHandler(.useCredential, URLCredential(trust: serverTrust)) + guard serverCertData == localCertData else { + return completionHandler(.cancelAuthenticationChallenge, nil) } + + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + } } diff --git a/Sources/RequestPermissionFeature/RequestPermissionController.swift b/Sources/RequestPermissionFeature/RequestPermissionController.swift new file mode 100644 index 0000000000000000000000000000000000000000..9c12752c92e20763c81647fe501e749d014eca3e --- /dev/null +++ b/Sources/RequestPermissionFeature/RequestPermissionController.swift @@ -0,0 +1,98 @@ +import UIKit +import Shared +import Combine +import AppCore +import Dependencies +import AppResources +import AppNavigation +import PermissionsFeature + +public final class RequestPermissionController: UIViewController { + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + @Dependency(\.permissions) var permissions: PermissionsManager + + private lazy var screenView = RequestPermissionView() + + private let permissionType: PermissionType + private var cancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + } + + public init(_ permissionType: PermissionType) { + self.permissionType = permissionType + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + switch permissionType { + case .camera: + screenView.setup( + title: Localized.Chat.Actions.Permission.Camera.title, + subtitle: Localized.Chat.Actions.Permission.Camera.subtitle, + image: Asset.permissionCamera.image + ) + case .library: + screenView.setup( + title: Localized.Chat.Actions.Permission.Library.title, + subtitle: Localized.Chat.Actions.Permission.Library.subtitle, + image: Asset.permissionLibrary.image + ) + case .microphone: + screenView.setup( + title: Localized.Chat.Actions.Permission.Microphone.title, + subtitle: Localized.Chat.Actions.Permission.Microphone.subtitle, + image: Asset.permissionMicrophone.image + ) + } + + screenView + .notNowButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dismiss(animated: true) + }.store(in: &cancellables) + + screenView + .continueButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch permissionType { + case .camera: + permissions.camera.request { [weak self] _ in + guard let self else { return } + self.shouldDismissModal() + } + case .library: + permissions.library.request { [weak self] _ in + guard let self else { return } + self.shouldDismissModal() + } + case .microphone: + permissions.microphone.request { [weak self] _ in + guard let self else { return } + self.shouldDismissModal() + } + } + }.store(in: &cancellables) + } + + private func shouldDismissModal() { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + 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 f3f7b3937059b0b30185e139b79b597f338061ca..49ab70bc1481be2c6797d9af64c2c3162c2026b3 100644 --- a/Sources/RequestsFeature/Controllers/RequestsContainerController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsContainerController.swift @@ -1,118 +1,121 @@ import UIKit -import Theme import Shared import Combine +import AppCore +import AppResources +import Dependencies +import AppNavigation import ContactFeature -import DependencyInjection public final class RequestsContainerController: UIViewController { - @Dependency private var coordinator: RequestsCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist - lazy private var screenView = RequestsContainerView() - private var cancellables = Set<AnyCancellable>() + private lazy var screenView = RequestsContainerView() + private var cancellables = Set<AnyCancellable>() - public override func loadView() { - view = screenView - screenView.scrollView.delegate = self + public override func loadView() { + view = screenView + screenView.scrollView.delegate = self - addChild(screenView.sentController) - addChild(screenView.failedController) - addChild(screenView.receivedController) + addChild(screenView.sentController) + addChild(screenView.failedController) + addChild(screenView.receivedController) - screenView.sentController.didMove(toParent: self) - screenView.failedController.didMove(toParent: self) - screenView.receivedController.didMove(toParent: self) + screenView.sentController.didMove(toParent: self) + screenView.failedController.didMove(toParent: self) + screenView.receivedController.didMove(toParent: self) - screenView.bringSubviewToFront(screenView.segmentedControl) - } + screenView.bringSubviewToFront(screenView.segmentedControl) + } - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupBindings() + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() - if let stack = navigationController?.viewControllers, stack.count > 1 { - if stack[stack.count - 2].isKind(of: ContactController.self) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } + if let stack = navigationController?.viewControllers, stack.count > 1 { + if stack[stack.count - 2].isKind(of: ContactController.self) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self else { return } - let point = CGPoint(x: self.screenView.frame.width, y: 0.0) - self.screenView.scrollView.setContentOffset(point, animated: true) - } - } + let point = CGPoint(x: self.screenView.frame.width, y: 0.0) + self.screenView.scrollView.setContentOffset(point, animated: true) } + } } - - private func setupNavigationBar() { - navigationItem.backButtonTitle = "" - - let titleLabel = UILabel() - titleLabel.text = Localized.Requests.title - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) - - let menuButton = UIButton() - menuButton.tintColor = Asset.neutralDark.color - menuButton.setImage(Asset.chatListMenu.image, for: .normal) - menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) - menuButton.snp.makeConstraints { $0.width.equalTo(50) } - - navigationItem.leftBarButtonItem = UIBarButtonItem( - customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) - ) - } - - private func setupBindings() { - screenView - .sentController - .connectionsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toSearch(from: self) } - .store(in: &cancellables) - - screenView - .segmentedControl - .receivedRequestsButton - .publisher(for: .touchUpInside) - .sink { [unowned self] _ in - screenView.scrollView.setContentOffset(.zero, animated: true) - }.store(in: &cancellables) - - screenView - .segmentedControl - .sentRequestsButton - .publisher(for: .touchUpInside) - .sink { [unowned self] _ in - let point = CGPoint(x: screenView.frame.width, y: 0.0) - screenView.scrollView.setContentOffset(point, animated: true) - }.store(in: &cancellables) - - screenView - .segmentedControl - .failedRequestsButton - .publisher(for: .touchUpInside) - .sink { [unowned self] _ in - let point = CGPoint(x: screenView.frame.width * 2.0, y: 0.0) - screenView.scrollView.setContentOffset(point, animated: true) - }.store(in: &cancellables) - } - - @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) - } + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let titleLabel = UILabel() + titleLabel.text = Localized.Requests.title + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) + + let menuButton = UIButton() + menuButton.tintColor = Asset.neutralDark.color + menuButton.setImage(Asset.chatListMenu.image, for: .normal) + menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) + menuButton.snp.makeConstraints { $0.width.equalTo(50) } + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) + ) + } + + private func setupBindings() { + screenView + .sentController + .connectionsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentSearch(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .segmentedControl + .receivedRequestsButton + .publisher(for: .touchUpInside) + .sink { [unowned self] _ in + screenView.scrollView.setContentOffset(.zero, animated: true) + }.store(in: &cancellables) + + screenView + .segmentedControl + .sentRequestsButton + .publisher(for: .touchUpInside) + .sink { [unowned self] _ in + let point = CGPoint(x: screenView.frame.width, y: 0.0) + screenView.scrollView.setContentOffset(point, animated: true) + }.store(in: &cancellables) + + screenView + .segmentedControl + .failedRequestsButton + .publisher(for: .touchUpInside) + .sink { [unowned self] _ in + let point = CGPoint(x: screenView.frame.width * 2.0, y: 0.0) + screenView.scrollView.setContentOffset(point, animated: true) + }.store(in: &cancellables) + } + + @objc private func didTapMenu() { + navigator.perform(PresentMenu(currentItem: .requests, from: self)) + } } extension RequestsContainerController: UIScrollViewDelegate { - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - screenView.segmentedControl.updateSwipePercentage(scrollView.contentOffset.x / view.frame.width) - } + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + screenView.segmentedControl.updateSwipePercentage(scrollView.contentOffset.x / view.frame.width) + } } diff --git a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift index c0af65d5f83a4bc1528065eef1551e597a52f6c9..a7e65e87d510deaac71d9a0b3a2024c8d58f99b3 100644 --- a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift @@ -1,13 +1,8 @@ -import HUD import UIKit -import Shared import Combine -import DependencyInjection final class RequestsFailedController: UIViewController { - @Dependency private var hud: HUD - - lazy private var screenView = RequestsFailedView() + private lazy var screenView = RequestsFailedView() private var cancellables = Set<AnyCancellable>() private let viewModel = RequestsFailedViewModel() private var dataSource: UICollectionViewDiffableDataSource<Section, Request>? @@ -27,7 +22,7 @@ final class RequestsFailedController: UIViewController { let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) cell.setupFor(requestFailed: request) cell.didTapStateButton = { [weak self] in - guard let self = self else { return } + guard let self else { return } self.viewModel.didTapStateButtonFor(request: request) } return cell @@ -39,10 +34,5 @@ final class RequestsFailedController: UIViewController { dataSource?.apply($0, animatingDifferences: false) screenView.collectionView.isHidden = $0.numberOfItems == 0 }.store(in: &cancellables) - - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) } } diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift index 0a30d05fb32b5ec4d3eb54b1fdeb9ced674a17ac..3ee84bb9e1b29b539448cb11e222fbcf3c452ce8 100644 --- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift @@ -1,572 +1,596 @@ -import HUD import UIKit -import Models import Shared import Combine import XXModels -import Countries -import ToastFeature +import AppCore +import AppResources +import Dependencies +import AppNavigation import DrawerFeature -import DependencyInjection +import CountryListFeature final class RequestsReceivedController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var toaster: ToastController - @Dependency private var coordinator: RequestsCoordinating - - lazy private var screenView = RequestsReceivedView() - private var cancellables = Set<AnyCancellable>() - private let viewModel = RequestsReceivedViewModel() - private var drawerCancellables = Set<AnyCancellable>() - private var dataSource: UICollectionViewDiffableDataSource<Section, RequestReceived>? - - override func loadView() { - view = screenView + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.toastManager) var toaster: ToastManager + + private lazy var screenView = RequestsReceivedView() + private var cancellables = Set<AnyCancellable>() + private let viewModel = RequestsReceivedViewModel() + private var drawerCancellables = Set<AnyCancellable>() + private var dataSource: UICollectionViewDiffableDataSource<Section, RequestReceived>? + + override func loadView() { + view = screenView + } + + override func viewDidLoad() { + super.viewDidLoad() + + screenView.collectionView.delegate = self + screenView.collectionView.register(RequestCell.self) + screenView.collectionView.register(RequestReceivedEmptyCell.self) + screenView.collectionView.registerSectionHeader(RequestsBlankSectionHeader.self) + screenView.collectionView.registerSectionHeader(RequestsHiddenSectionHeader.self) + + dataSource = UICollectionViewDiffableDataSource<Section, RequestReceived>( + collectionView: screenView.collectionView + ) { collectionView, indexPath, requestReceived in + guard let request = requestReceived.request else { + let cell: RequestReceivedEmptyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + return cell + } + + let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + cell.setupFor(requestReceived: requestReceived, isHidden: indexPath.section == 1) + cell.didTapStateButton = { [weak self] in + guard let self else { return } + self.viewModel.didTapStateButtonFor(request: request) + } + + return cell } - override func viewDidLoad() { - super.viewDidLoad() - - screenView.collectionView.delegate = self - screenView.collectionView.register(RequestCell.self) - screenView.collectionView.register(RequestReceivedEmptyCell.self) - screenView.collectionView.registerSectionHeader(RequestsBlankSectionHeader.self) - screenView.collectionView.registerSectionHeader(RequestsHiddenSectionHeader.self) - - dataSource = UICollectionViewDiffableDataSource<Section, RequestReceived>( - collectionView: screenView.collectionView - ) { collectionView, indexPath, requestReceived in - guard let request = requestReceived.request else { - let cell: RequestReceivedEmptyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - return cell - } - - let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - cell.setupFor(requestReceived: requestReceived, isHidden: indexPath.section == 1) - cell.didTapStateButton = { [weak self] in - guard let self = self else { return } - self.viewModel.didTapStateButtonFor(request: request) - } - - return cell - } - - dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in - let reuseIdentifier: String + dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in + let reuseIdentifier: String - if indexPath.section == Section.appearing.rawValue { - reuseIdentifier = String(describing: RequestsBlankSectionHeader.self) - } else { - reuseIdentifier = String(describing: RequestsHiddenSectionHeader.self) - } + if indexPath.section == Section.appearing.rawValue { + reuseIdentifier = String(describing: RequestsBlankSectionHeader.self) + } else { + reuseIdentifier = String(describing: RequestsHiddenSectionHeader.self) + } - let cell = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: reuseIdentifier, - for: indexPath - ) + let cell = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: reuseIdentifier, + for: indexPath + ) - if let cell = cell as? RequestsHiddenSectionHeader, let self = self { - cell.switcherView.setOn(self.viewModel.isShowingHiddenRequests, animated: true) + if let cell = cell as? RequestsHiddenSectionHeader, let self = self { + cell.switcherView.setOn(self.viewModel.isShowingHiddenRequests, animated: true) - cell.switcherView - .publisher(for: .valueChanged) - .sink { self.viewModel.didToggleHiddenRequestsSwitcher() } - .store(in: &cell.cancellables) - } + cell.switcherView + .publisher(for: .valueChanged) + .sink { self.viewModel.didToggleHiddenRequestsSwitcher() } + .store(in: &cell.cancellables) + } - return cell - } - - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - viewModel.verifyingPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentVerifyingDrawer() } - .store(in: &cancellables) - - viewModel.itemsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in dataSource?.apply($0, animatingDifferences: true) } - .store(in: &cancellables) - - viewModel.contactConfirmationPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentSingleRequestSuccessDrawer(forContact: $0) } - .store(in: &cancellables) - - viewModel.groupConfirmationPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentGroupRequestSuccessDrawer(forGroup: $0) } - .store(in: &cancellables) + return cell } + + viewModel + .verifyingPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentVerifyingDrawer() + }.store(in: &cancellables) + + viewModel + .itemsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dataSource?.apply($0, animatingDifferences: true) + }.store(in: &cancellables) + + viewModel + .contactConfirmationPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentSingleRequestSuccessDrawer(forContact: $0) + }.store(in: &cancellables) + + viewModel + .groupConfirmationPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentGroupRequestSuccessDrawer(forGroup: $0) + }.store(in: &cancellables) + } } extension RequestsReceivedController: UICollectionViewDelegate { - func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let request = dataSource?.itemIdentifier(for: indexPath)?.request else { return } - - switch request { - case .group(let group): - guard group.authStatus == .pending || group.authStatus == .hidden else { return } - presentGroupRequestDrawer(forGroup: group) - case .contact(let contact): - guard contact.authStatus == .verified || contact.authStatus == .hidden else { return } - presentSingleRequestDrawer(forContact: contact) - } + func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let request = dataSource?.itemIdentifier(for: indexPath)?.request else { return } + + switch request { + case .group(let group): + guard group.authStatus == .pending || group.authStatus == .hidden else { return } + presentGroupRequestDrawer(forGroup: group) + case .contact(let contact): + guard contact.authStatus == .verified || contact.authStatus == .hidden else { return } + presentSingleRequestDrawer(forContact: contact) } + } } // MARK: - Group Request Success Drawer extension RequestsReceivedController { - func presentGroupRequestSuccessDrawer(forGroup group: Group) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Group.Success.title, - color: Asset.accentSuccess.color, - spacingAfter: 20, - leftImage: Asset.requestAccepted.image - ) - - let drawerNickname = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: group.name, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Requests.Drawer.Group.Success.subtitle, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerNickname, - drawerSubtitle - ]) - - let drawerSendButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.Success.send, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerLaterButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.Success.later, - style: .simplestColoredBrand - ) - ) - - items.append(contentsOf: [ - drawerSendButton, - drawerLaterButton - ]) - - let drawer = DrawerController(with: items) - - drawerSendButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - let chatInfo = self.viewModel.groupChatWith(group: group) - self.coordinator.toGroupChat(with: chatInfo, from: self) - } - }.store(in: &drawerCancellables) - - drawerLaterButton.action - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + func presentGroupRequestSuccessDrawer(forGroup group: Group) { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Group.Success.title, + color: Asset.accentSuccess.color, + spacingAfter: 20, + leftImage: Asset.requestAccepted.image + ) + + let drawerNickname = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: group.name, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Requests.Drawer.Group.Success.subtitle, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerNickname, + drawerSubtitle + ]) + + let drawerSendButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.Success.send, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerLaterButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.Success.later, + style: .simplestColoredBrand + ) + ) + + items.append(contentsOf: [ + drawerSendButton, + drawerLaterButton + ]) + + drawerSendButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.navigator.perform(PresentGroupChat( + groupInfo: self.viewModel.groupChatWith(group: group), + on: self.navigationController! + )) + } + }.store(in: &drawerCancellables) + + drawerLaterButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) + } } // MARK: - Single Request Success Drawer extension RequestsReceivedController { - func presentSingleRequestSuccessDrawer(forContact contact: Contact) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.Success.title, - color: Asset.accentSuccess.color, - spacingAfter: 20, - leftImage: Asset.requestAccepted.image - ) - - let drawerNickname = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: (contact.nickname ?? contact.username) ?? "", - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Requests.Drawer.Single.Success.subtitle, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerNickname, - drawerSubtitle - ]) - - let drawerSendButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.Success.send, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerLaterButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.Success.later, - style: .simplestColoredBrand - ) - ) - - items.append(contentsOf: [ - drawerSendButton, - drawerLaterButton - ]) - - let drawer = DrawerController(with: items) - - drawerSendButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.coordinator.toSingleChat(with: contact, from: self) - } - }.store(in: &drawerCancellables) - - drawerLaterButton.action - .receive(on: DispatchQueue.main) - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + func presentSingleRequestSuccessDrawer(forContact contact: Contact) { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.Success.title, + color: Asset.accentSuccess.color, + spacingAfter: 20, + leftImage: Asset.requestAccepted.image + ) + + let drawerNickname = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: (contact.nickname ?? contact.username) ?? "", + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Requests.Drawer.Single.Success.subtitle, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerNickname, + drawerSubtitle + ]) + + let drawerSendButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.Success.send, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerLaterButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.Success.later, + style: .simplestColoredBrand + ) + ) + + items.append(contentsOf: [ + drawerSendButton, + drawerLaterButton + ]) + + drawerSendButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.navigator.perform(PresentChat(contact: contact, on: self.navigationController!)) + } + }.store(in: &drawerCancellables) + + drawerLaterButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) + } } // MARK: - Group Request Drawer extension RequestsReceivedController { - func presentGroupRequestDrawer(forGroup group: Group) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Group.title, - spacingAfter: 20 - ) - - let drawerGroupName = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: group.name, - color: Asset.neutralDark.color, - spacingAfter: 25 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerGroupName - ]) - - let drawerLoading = DrawerLoadingRetry() - drawerLoading.startSpinning() - - items.append(drawerLoading) + func presentGroupRequestDrawer(forGroup group: Group) { + drawerCancellables.removeAll() - let drawerTable = DrawerTable(spacingAfter: 23) + var items: [DrawerItem] = [] - drawerLoading.retryPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawerLoading.startSpinning() + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Group.title, + spacingAfter: 20 + ) - viewModel.fetchMembers(group) { [weak self] in - guard let _ = self else { return } + let drawerGroupName = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: group.name, + color: Asset.neutralDark.color, + spacingAfter: 25 + ) - switch $0 { - case .success(let models): - DispatchQueue.main.async { - drawerTable.update(models: models) - drawerLoading.stopSpinning(withRetry: false) - } - case .failure: - drawerLoading.stopSpinning(withRetry: true) - } - } - }.store(in: &drawerCancellables) + items.append(contentsOf: [ + drawerTitle, + drawerGroupName + ]) - viewModel.fetchMembers(group) { [weak self] in - guard let _ = self else { return } - - switch $0 { - case .success(let models): - DispatchQueue.main.async { - drawerTable.update(models: models) - drawerLoading.stopSpinning(withRetry: false) - } - case .failure: - drawerLoading.stopSpinning(withRetry: true) - } - } - - items.append(drawerTable) + let drawerLoading = DrawerLoadingRetry() + drawerLoading.startSpinning() - let drawerAcceptButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.accept, - style: .brandColored - ), spacingAfter: 5 - ) + items.append(drawerLoading) - let drawerHideButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Group.hide, - style: .simplestColoredBrand - ), spacingAfter: 5 - ) + let drawerTable = DrawerTable(spacingAfter: 23) - items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) + drawerLoading.retryPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + drawerLoading.startSpinning() - let drawer = DrawerController(with: items) + viewModel.fetchMembers(group) { [weak self] in + guard let _ = self else { return } - drawerAcceptButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didRequestAccept(group: group) - } - } - .store(in: &drawerCancellables) - - drawerHideButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didRequestHide(group: group) - } + switch $0 { + case .success(let models): + DispatchQueue.main.async { + drawerTable.update(models: models) + drawerLoading.stopSpinning(withRetry: false) } - .store(in: &drawerCancellables) - - coordinator.toDrawerBottom(drawer, from: self) - } -} + case .failure: + drawerLoading.stopSpinning(withRetry: true) + } + } + }.store(in: &drawerCancellables) -// MARK: - Single Request Drawer + viewModel.fetchMembers(group) { [weak self] in + guard let _ = self else { return } -extension RequestsReceivedController { - func presentSingleRequestDrawer(forContact contact: Contact) { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.title, - spacingAfter: 20 - ) - - let drawerUsername = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: contact.username ?? "", - color: Asset.neutralDark.color, - spacingAfter: 25 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerUsername - ]) - - let drawerEmailTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.email, - color: Asset.neutralWeak.color, - spacingAfter: 5 - ) - - if let email = contact.email { - let drawerEmailContent = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: email, - spacingAfter: 25 - ) - - items.append(contentsOf: [ - drawerEmailTitle, - drawerEmailContent - ]) + switch $0 { + case .success(let models): + DispatchQueue.main.async { + drawerTable.update(models: models) + drawerLoading.stopSpinning(withRetry: false) } + case .failure: + drawerLoading.stopSpinning(withRetry: true) + } + } - let drawerPhoneTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 12.0), - text: Localized.Requests.Drawer.Single.phone, - color: Asset.neutralWeak.color, - spacingAfter: 5 - ) - - if let phone = contact.phone { - let drawerPhoneContent = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", - spacingAfter: 30 - ) - - items.append(contentsOf: [ - drawerPhoneTitle, - drawerPhoneContent - ]) + items.append(drawerTable) + + let drawerAcceptButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.accept, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerHideButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Group.hide, + style: .simplestColoredBrand + ), spacingAfter: 5 + ) + + items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) + + drawerAcceptButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestAccept(group: group) } + }.store(in: &drawerCancellables) + + drawerHideButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestHide(group: group) + } + }.store(in: &drawerCancellables) - let drawerNicknameTitle = DrawerText( - font: Fonts.Mulish.bold.font(size: 16.0), - text: Localized.Requests.Drawer.Single.nickname, - color: Asset.neutralDark.color, - spacingAfter: 21 - ) - - items.append(drawerNicknameTitle) - - let drawerNicknameInput = DrawerInput( - placeholder: contact.username ?? "", - validator: .init( - wrongIcon: .image(Asset.sharedError.image), - correctIcon: .image(Asset.sharedSuccess.image), - shouldAcceptPlaceholder: true - ), - spacingAfter: 29 - ) - - items.append(drawerNicknameInput) - - let drawerAcceptButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.accept, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerHideButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Drawer.Single.hide, - style: .simplestColoredBrand - ), spacingAfter: 5 - ) - - items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) - - let drawer = DrawerController(with: items) - - var nickname: String? - var allowsSave = true - - drawerNicknameInput.validationPublisher - .receive(on: DispatchQueue.main) - .sink { allowsSave = $0 } - .store(in: &drawerCancellables) - - drawerNicknameInput.inputPublisher - .receive(on: DispatchQueue.main) - .sink { - guard !$0.isEmpty else { - nickname = contact.username - return - } - - nickname = $0 - } - .store(in: &drawerCancellables) - - drawerAcceptButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard allowsSave else { return } - - drawer.dismiss(animated: true) { - self.viewModel.didRequestAccept(contact: contact, nickname: nickname) - } - } - .store(in: &drawerCancellables) - - drawerHideButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didRequestHide(contact: contact) - } - } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) + } } -// MARK: - Verifying Drawer +// MARK: - Single Request Drawer extension RequestsReceivedController { - func presentVerifyingDrawer() { - drawerCancellables.removeAll() - - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: Localized.Requests.Received.Verifying.title, - spacingAfter: 20 - ) + func presentSingleRequestDrawer(forContact contact: Contact) { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.title, + spacingAfter: 20 + ) + + let drawerUsername = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: contact.username ?? "", + color: Asset.neutralDark.color, + spacingAfter: 25 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerUsername + ]) + + let drawerEmailTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.email, + color: Asset.neutralWeak.color, + spacingAfter: 5 + ) + + if let email = contact.email { + let drawerEmailContent = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: email, + spacingAfter: 25 + ) + + items.append(contentsOf: [ + drawerEmailTitle, + drawerEmailContent + ]) + } - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Requests.Received.Verifying.subtitle, - spacingAfter: 40 - ) + let drawerPhoneTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 12.0), + text: Localized.Requests.Drawer.Single.phone, + color: Asset.neutralWeak.color, + spacingAfter: 5 + ) + + if let phone = contact.phone { + let drawerPhoneContent = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", + spacingAfter: 30 + ) + + items.append(contentsOf: [ + drawerPhoneTitle, + drawerPhoneContent + ]) + } - items.append(contentsOf: [ - drawerTitle, - drawerSubtitle - ]) + let drawerNicknameTitle = DrawerText( + font: Fonts.Mulish.bold.font(size: 16.0), + text: Localized.Requests.Drawer.Single.nickname, + color: Asset.neutralDark.color, + spacingAfter: 21 + ) + + items.append(drawerNicknameTitle) + + let drawerNicknameInput = DrawerInput( + placeholder: contact.username ?? "", + validator: .init( + wrongIcon: .image(Asset.sharedError.image), + correctIcon: .image(Asset.sharedSuccess.image), + shouldAcceptPlaceholder: true + ), + spacingAfter: 29 + ) + + items.append(drawerNicknameInput) + + let drawerAcceptButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.accept, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerHideButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Drawer.Single.hide, + style: .simplestColoredBrand + ), spacingAfter: 5 + ) + + items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) + + var nickname: String? + var allowsSave = true + + drawerNicknameInput + .validationPublisher + .receive(on: DispatchQueue.main) + .sink { allowsSave = $0 } + .store(in: &drawerCancellables) + + drawerNicknameInput + .inputPublisher + .receive(on: DispatchQueue.main) + .sink { + guard !$0.isEmpty else { + nickname = contact.username + return + } - let drawerDoneButton = DrawerCapsuleButton( - model: .init( - title: Localized.Requests.Received.Verifying.action, - style: .brandColored - ), spacingAfter: 5 - ) + nickname = $0 + }.store(in: &drawerCancellables) + + drawerAcceptButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard allowsSave else { return } + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestAccept(contact: contact, nickname: nickname) + } + }.store(in: &drawerCancellables) + + drawerHideButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didRequestHide(contact: contact) + } + }.store(in: &drawerCancellables) - items.append(drawerDoneButton) + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) + } +} - let drawer = DrawerController(with: items) +// MARK: - Verifying Drawer - drawerDoneButton.action - .receive(on: DispatchQueue.main) - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) +extension RequestsReceivedController { + func presentVerifyingDrawer() { + drawerCancellables.removeAll() + + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: Localized.Requests.Received.Verifying.title, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Requests.Received.Verifying.subtitle, + spacingAfter: 40 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerSubtitle + ]) + + let drawerDoneButton = DrawerCapsuleButton( + model: .init( + title: Localized.Requests.Received.Verifying.action, + style: .brandColored + ), spacingAfter: 5 + ) + + items.append(drawerDoneButton) + + drawerDoneButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) - coordinator.toDrawer(drawer, from: self) - } + navigator.perform(PresentDrawer(items: items, isDismissable: true, from: self)) + } } diff --git a/Sources/RequestsFeature/Controllers/RequestsSentController.swift b/Sources/RequestsFeature/Controllers/RequestsSentController.swift index ceed2cc28989b51d72d0191e63e81ade2f930bdd..a2000f74cd532e7c90f404c79e1f4225ea919a7f 100644 --- a/Sources/RequestsFeature/Controllers/RequestsSentController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsSentController.swift @@ -1,17 +1,12 @@ -import HUD import UIKit -import Shared import Combine -import DependencyInjection final class RequestsSentController: UIViewController { - @Dependency private var hud: HUD - var connectionsPublisher: AnyPublisher<Void, Never> { connectionSubject.eraseToAnyPublisher() } - lazy private var screenView = RequestsSentView() + private lazy var screenView = RequestsSentView() private let viewModel = RequestsSentViewModel() private var cancellables = Set<AnyCancellable>() private let tapSubject = PassthroughSubject<Request, Never>() @@ -33,7 +28,7 @@ final class RequestsSentController: UIViewController { let cell: RequestCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) cell.setupFor(requestSent: requestSent) cell.didTapStateButton = { [weak self] in - guard let self = self else { return } + guard let self else { return } self.viewModel.didTapStateButtonFor(request: requestSent) } return cell @@ -46,11 +41,6 @@ final class RequestsSentController: UIViewController { screenView.collectionView.isHidden = $0.numberOfItems == 0 }.store(in: &cancellables) - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.connectionsButton .publisher(for: .touchUpInside) .sink { [unowned self] in connectionSubject.send() } diff --git a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift b/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift deleted file mode 100644 index 571bf625fc1a98b1cf2905154a660fd4cbebd92e..0000000000000000000000000000000000000000 --- a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift +++ /dev/null @@ -1,120 +0,0 @@ -import UIKit -import Shared -import Models -import XXModels -import MenuFeature -import Presentation -import ContactFeature -import ScrollViewController - -public protocol RequestsCoordinating { - func toSearch(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toSingleChat(with: Contact, from: UIViewController) - func toGroupChat(with: GroupInfo, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toDrawerBottom(_: UIViewController, from: UIViewController) - func toNickname(from: UIViewController, prefilled: String, _: @escaping StringClosure) -} - -public struct RequestsCoordinator: RequestsCoordinating { - var pushPresenter: Presenting = PushPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - - var searchFactory: (String?) -> UIViewController - var contactFactory: (Contact) -> UIViewController - var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupInfo) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var nicknameFactory: (String, @escaping StringClosure) -> UIViewController - - public init( - searchFactory: @escaping (String?) -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupInfo) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, - nicknameFactory: @escaping (String, @escaping StringClosure) -> UIViewController - ) { - self.searchFactory = searchFactory - self.contactFactory = contactFactory - self.nicknameFactory = nicknameFactory - self.sideMenuFactory = sideMenuFactory - self.groupChatFactory = groupChatFactory - self.singleChatFactory = singleChatFactory - } -} - -public extension RequestsCoordinator { - func toSingleChat( - with contact: Contact, - from parent: UIViewController - ) { - let screen = singleChatFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toGroupChat( - with info: GroupInfo, - from parent: UIViewController - ) { - let screen = groupChatFactory(info) - pushPresenter.present(screen, from: parent) - } - - func toDrawer( - _ drawer: UIViewController, - from parent: UIViewController - ) { - let target = ScrollViewController.embedding(drawer) - fullscreenPresenter.present(target, from: parent) - } - - func toDrawerBottom( - _ drawer: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(drawer, from: parent) - } - - func toSearch(from parent: UIViewController) { - let screen = searchFactory(nil) - pushPresenter.present(screen, from: parent) - } - - func toNickname( - from parent: UIViewController, - prefilled: String, - _ completion: @escaping StringClosure - ) { - let screen = nicknameFactory(prefilled, completion) - bottomPresenter.present(screen, from: parent) - } - - func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.requests, parent) - sidePresenter.present(screen, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/RequestsFeature/Models/Request.swift b/Sources/RequestsFeature/Models/Request.swift index 595410d43257294a139a0f7b77a717992ed6ffcf..aac0fd8c068a3e5c873e2d4d3edef334202c749b 100644 --- a/Sources/RequestsFeature/Models/Request.swift +++ b/Sources/RequestsFeature/Models/Request.swift @@ -1,4 +1,3 @@ -import Models import XXModels import Foundation diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift index b7d81db3a65b791e001a897506418295b56f8b3a..6481b67c4a5c25a148980b6a3ed2d454d61f7822 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift @@ -1,53 +1,93 @@ -import HUD import UIKit -import Models +import Shared +import AppCore import Combine -import Integration +import XXModels +import Defaults +import XXClient +import Dependencies import CombineSchedulers -import DependencyInjection +import XXMessengerClient final class RequestsFailedViewModel { - @Dependency private var session: SessionType + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() + var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, Request>, Never> { + itemsSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, Request>, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init() { + try! dbManager.getDB().fetchContactsPublisher(.init(authStatus: [.requestFailed, .confirmationFailed])) + .replaceError(with: []) + .map { data -> NSDiffableDataSourceSnapshot<Section, Request> in + var snapshot = NSDiffableDataSourceSnapshot<Section, Request>() + snapshot.appendSections([.appearing]) + snapshot.appendItems(data.map { Request.contact($0) }, toSection: .appearing) + return snapshot + }.sink { [unowned self] in itemsSubject.send($0) } + .store(in: &cancellables) + } + + func didTapStateButtonFor(request: Request) { + guard case var .contact(contact) = request, + request.status == .failedToRequest || + request.status == .failedToConfirm else { + return } - - var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, Request>, Never> { - itemsSubject.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, Request>, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - session.dbManager.fetchContactsPublisher(.init(authStatus: [.requestFailed])) - .assertNoFailure() - .map { data -> NSDiffableDataSourceSnapshot<Section, Request> in - var snapshot = NSDiffableDataSourceSnapshot<Section, Request>() - snapshot.appendSections([.appearing]) - snapshot.appendItems(data.map { Request.contact($0) }, toSection: .appearing) - return snapshot - }.sink { [unowned self] in itemsSubject.send($0) } - .store(in: &cancellables) - } - - func didTapStateButtonFor(request: Request) { - guard case let .contact(contact) = request, request.status == .failedToRequest else { return } - - hudSubject.send(.on) - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.retryRequest(contact) - self.hudSubject.send(.none) - } catch { - self.hudSubject.send(.error(.init(with: error))) - } + + hudManager.show() + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + if request.status == .failedToRequest { + + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() + + if let fact = myFacts.get(.username) { + includedFacts.append(fact) + } + + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) + } + + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) + } + + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(contact.marshaled!), + myFacts: includedFacts + ) + + contact.authStatus = .requested + } else { + let _ = try self.messenger.e2e.get()!.confirmReceivedRequest( + partner: XXClient.Contact.live(contact.marshaled!) + ) + + contact.authStatus = .friend } + + try self.dbManager.getDB().saveContact(contact) + self.hudManager.hide() + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudManager.show(.init(content: xxError)) + } } + } } diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index 3f4be9cbe3541dd0b2a2431967c1e4ec8df5ce93..46d4fe29bc99ebb71207264a27663a1f16de7731 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -1,242 +1,286 @@ -import HUD import UIKit -import Models import Shared +import AppCore import Combine import Defaults import XXModels -import Integration +import XXClient +import Dependencies import DrawerFeature import ReportingFeature import CombineSchedulers -import DependencyInjection +import XXMessengerClient + +import struct XXModels.Group struct RequestReceived: Hashable, Equatable { - var request: Request? - var isHidden: Bool - var leader: String? + var request: Request? + var isHidden: Bool + var leader: String? } final class RequestsReceivedViewModel { - @Dependency private var session: SessionType - @Dependency private var reportingStatus: ReportingStatus - - @KeyObject(.isShowingHiddenRequests, defaultValue: false) var isShowingHiddenRequests: Bool - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var verifyingPublisher: AnyPublisher<Void, Never> { - verifyingSubject.eraseToAnyPublisher() - } - - var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never> { - itemsSubject.eraseToAnyPublisher() - } - - var groupConfirmationPublisher: AnyPublisher<Group, Never> { - groupConfirmationSubject.eraseToAnyPublisher() - } - - var contactConfirmationPublisher: AnyPublisher<Contact, Never> { - contactConfirmationSubject.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let updateSubject = CurrentValueSubject<Void, Never>(()) - private let verifyingSubject = PassthroughSubject<Void, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let groupConfirmationSubject = PassthroughSubject<Group, Never>() - private let contactConfirmationSubject = PassthroughSubject<Contact, Never>() - private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - let groupsQuery = Group.Query( - authStatus: [ - .hidden, - .pending - ], - isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, - isLeaderBanned: reportingStatus.isEnabled() ? false : nil - ) - - let contactsQuery = Contact.Query( - authStatus: [ - .friend, - .hidden, - .verified, - .verificationFailed, - .verificationInProgress - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - let groupStream = session.dbManager.fetchGroupsPublisher(groupsQuery).assertNoFailure() - let contactsStream = session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure() - - Publishers.CombineLatest3( - groupStream, - contactsStream, - updateSubject.eraseToAnyPublisher() - ) - .subscribe(on: DispatchQueue.main) - .receive(on: DispatchQueue.global()) - .map { [unowned self] data -> NSDiffableDataSourceSnapshot<Section, RequestReceived> in - var snapshot = NSDiffableDataSourceSnapshot<Section, RequestReceived>() - snapshot.appendSections([.appearing, .hidden]) - - let contactsFilteringFriends = data.1.filter { $0.authStatus != .friend } - let requests = data.0.map(Request.group) + contactsFilteringFriends.map(Request.contact) - let receivedRequests = requests.map { request -> RequestReceived in - switch request { - case let .group(group): - func leaderName() -> String { - if let leader = data.1.first(where: { $0.id == group.leaderId }) { - return (leader.nickname ?? leader.username) ?? "Leader is not a friend" - } else { - return "[Error retrieving leader]" - } - } - - return RequestReceived( - request: request, - isHidden: group.authStatus == .hidden, - leader: leaderName() - ) - case let .contact(contact): - return RequestReceived( - request: request, - isHidden: contact.authStatus == .hidden, - leader: nil - ) - } - } - - if self.isShowingHiddenRequests { - snapshot.appendItems(receivedRequests.filter(\.isHidden), toSection: .hidden) - } - - guard receivedRequests.filter({ $0.isHidden == false }).count > 0 else { - snapshot.appendItems([RequestReceived(isHidden: false)], toSection: .appearing) - return snapshot - } - - snapshot.appendItems(receivedRequests.filter { $0.isHidden == false }, toSection: .appearing) - return snapshot - }.sink( - receiveCompletion: { _ in }, - receiveValue: { [unowned self] in itemsSubject.send($0) } - ).store(in: &cancellables) - } - - func didToggleHiddenRequestsSwitcher() { - isShowingHiddenRequests.toggle() - updateSubject.send() - } - - func didTapStateButtonFor(request: Request) { - guard case let .contact(contact) = request else { return } - - if request.status == .failedToVerify { - backgroundScheduler.schedule { [weak self] in - self?.session.verify(contact: contact) + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.hudManager) var hudManager + @Dependency(\.reportingStatus) var reportingStatus + + @KeyObject(.isShowingHiddenRequests, defaultValue: false) var isShowingHiddenRequests: Bool + + var verifyingPublisher: AnyPublisher<Void, Never> { + verifyingSubject.eraseToAnyPublisher() + } + + var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never> { + itemsSubject.eraseToAnyPublisher() + } + + var groupConfirmationPublisher: AnyPublisher<Group, Never> { + groupConfirmationSubject.eraseToAnyPublisher() + } + + var contactConfirmationPublisher: AnyPublisher<XXModels.Contact, Never> { + contactConfirmationSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let updateSubject = CurrentValueSubject<Void, Never>(()) + private let verifyingSubject = PassthroughSubject<Void, Never>() + private let groupConfirmationSubject = PassthroughSubject<Group, Never>() + private let contactConfirmationSubject = PassthroughSubject<XXModels.Contact, Never>() + private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init() { + let groupsQuery = Group.Query( + authStatus: [ + .hidden, + .pending + ], + isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, + isLeaderBanned: reportingStatus.isEnabled() ? false : nil + ) + + let contactsQuery = Contact.Query( + authStatus: [ + .friend, + .hidden, + .verified, + .verificationFailed, + .verificationInProgress + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + let groupStream = try! dbManager.getDB() + .fetchGroupsPublisher(groupsQuery) + .replaceError(with: []) + + let contactsStream = try! dbManager.getDB() + .fetchContactsPublisher(contactsQuery) + .replaceError(with: []) + + Publishers.CombineLatest3( + groupStream, + contactsStream, + updateSubject.eraseToAnyPublisher() + ) + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.global()) + .map { [unowned self] data -> NSDiffableDataSourceSnapshot<Section, RequestReceived> in + var snapshot = NSDiffableDataSourceSnapshot<Section, RequestReceived>() + snapshot.appendSections([.appearing, .hidden]) + + let contactsFilteringFriends = data.1.filter { $0.authStatus != .friend } + let requests = data.0.map(Request.group) + contactsFilteringFriends.map(Request.contact) + let receivedRequests = requests.map { request -> RequestReceived in + switch request { + case let .group(group): + func leaderName() -> String { + if let leader = data.1.first(where: { $0.id == group.leaderId }) { + return (leader.nickname ?? leader.username) ?? "Leader is not a friend" + } else { + return "[Error retrieving leader]" } - } else if request.status == .verifying { - verifyingSubject.send() + } + + return RequestReceived( + request: request, + isHidden: group.authStatus == .hidden, + leader: leaderName() + ) + case let .contact(contact): + return RequestReceived( + request: request, + isHidden: contact.authStatus == .hidden, + leader: nil + ) } - } - - func didRequestHide(group: Group) { - if var group = try? session.dbManager.fetchGroups(.init(id: [group.id])).first { - group.authStatus = .hidden - _ = try? session.dbManager.saveGroup(group) + } + + if self.isShowingHiddenRequests { + snapshot.appendItems(receivedRequests.filter(\.isHidden), toSection: .hidden) + } + + guard receivedRequests.filter({ $0.isHidden == false }).count > 0 else { + snapshot.appendItems([RequestReceived(isHidden: false)], toSection: .appearing) + return snapshot + } + + snapshot.appendItems(receivedRequests.filter { $0.isHidden == false }, toSection: .appearing) + return snapshot + }.sink( + receiveCompletion: { _ in }, + receiveValue: { [unowned self] in itemsSubject.send($0) } + ).store(in: &cancellables) + } + + func didToggleHiddenRequestsSwitcher() { + isShowingHiddenRequests.toggle() + updateSubject.send() + } + + func didTapStateButtonFor(request: Request) { + guard case var .contact(contact) = request else { return } + + if request.status == .failedToVerify { + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + contact.authStatus = .verificationInProgress + try self.dbManager.getDB().saveContact(contact) + + print(">>> [messenger.verifyContact] will start") + + if try self.messenger.verifyContact(XXClient.Contact.live(contact.marshaled!)) { + print(">>> [messenger.verifyContact] verified") + + contact.authStatus = .verified + contact = try self.dbManager.getDB().saveContact(contact) + } else { + print(">>> [messenger.verifyContact] is fake") + + try self.dbManager.getDB().deleteContact(contact) + } + } catch { + print(">>> [messenger.verifyContact] thrown an exception: \(error.localizedDescription)") + + contact.authStatus = .verificationFailed + _ = try? self.dbManager.getDB().saveContact(contact) } + } + } else if request.status == .verifying { + verifyingSubject.send() } - - func didRequestAccept(group: Group) { - hudSubject.send(.on) - - backgroundScheduler.schedule { [weak self] in - do { - try self?.session.join(group: group) - self?.hudSubject.send(.none) - self?.groupConfirmationSubject.send(group) - } catch { - self?.hudSubject.send(.error(.init(with: error))) - } - } + } + + func didRequestHide(group: Group) { + if var group = try? dbManager.getDB().fetchGroups(.init(id: [group.id])).first { + group.authStatus = .hidden + _ = try? dbManager.getDB().saveGroup(group) } - - func fetchMembers( - _ group: Group, - _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void - ) { - if let info = try? session.dbManager.fetchGroupInfos(.init(groupId: group.id)).first { - session.dbManager.fetchContactsPublisher(.init(id: Set(info.members.map(\.id)))) - .assertNoFailure() - .sink { members in - let withUsername = members - .filter { $0.username != nil } - .map { - DrawerTableCellModel( - id: $0.id, - title: $0.nickname ?? $0.username!, - image: $0.photo, - isCreator: $0.id == group.leaderId, - isConnection: $0.authStatus == .friend - ) - } - - let withoutUsername = members - .filter { $0.username == nil } - .map { - DrawerTableCellModel( - id: $0.id, - title: "Fetching username...", - image: $0.photo, - isCreator: $0.id == group.leaderId, - isConnection: $0.authStatus == .friend - ) - } - - completion(.success(withUsername + withoutUsername)) - }.store(in: &cancellables) - } + } + + func didRequestAccept(group: Group) { + hudManager.show() + + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + try self.messenger.groupChat()!.joinGroup(serializedGroupData: group.serialized) + + var group = group + group.authStatus = .participating + try self.dbManager.getDB().saveGroup(group) + + self.hudManager.hide() + self.groupConfirmationSubject.send(group) + } catch { + self.hudManager.show(.init(error: error)) + } } - - func didRequestHide(contact: Contact) { - if var contact = try? session.dbManager.fetchContacts(.init(id: [contact.id])).first { - contact.authStatus = .hidden - _ = try? session.dbManager.saveContact(contact) - } - } - - func didRequestAccept(contact: Contact, nickname: String? = nil) { - hudSubject.send(.on) - - var contact = contact - contact.nickname = nickname ?? contact.username - - backgroundScheduler.schedule { [weak self] in - do { - try self?.session.confirm(contact) - self?.hudSubject.send(.none) - self?.contactConfirmationSubject.send(contact) - } catch { - self?.hudSubject.send(.error(.init(with: error))) + } + + func fetchMembers( + _ group: Group, + _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void + ) { + if let info = try? dbManager.getDB().fetchGroupInfos(.init(groupId: group.id)).first { + try! dbManager.getDB().fetchContactsPublisher(.init(id: Set(info.members.map(\.id)))) + .replaceError(with: []) + .sink { members in + let withUsername = members + .filter { $0.username != nil } + .map { + DrawerTableCellModel( + id: $0.id, + title: $0.nickname ?? $0.username!, + image: $0.photo, + isCreator: $0.id == group.leaderId, + isConnection: $0.authStatus == .friend + ) } - } + + let withoutUsername = members + .filter { $0.username == nil } + .map { + DrawerTableCellModel( + id: $0.id, + title: "Fetching username...", + image: $0.photo, + isCreator: $0.id == group.leaderId, + isConnection: $0.authStatus == .friend + ) + } + + completion(.success(withUsername + withoutUsername)) + }.store(in: &cancellables) } - - func groupChatWith(group: Group) -> GroupInfo { - guard let info = try? session.dbManager.fetchGroupInfos(.init(groupId: group.id)).first else { - fatalError() - } - - return info + } + + func didRequestHide(contact: XXModels.Contact) { + if var contact = try? dbManager.getDB().fetchContacts(.init(id: [contact.id])).first { + contact.authStatus = .hidden + _ = try? dbManager.getDB().saveContact(contact) + } + } + + func didRequestAccept(contact: XXModels.Contact, nickname: String? = nil) { + hudManager.show() + + var contact = contact + contact.authStatus = .confirming + contact.nickname = nickname ?? contact.username + + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + try self.dbManager.getDB().saveContact(contact) + + let _ = try self.messenger.e2e.get()!.confirmReceivedRequest(partner: .live(contact.marshaled!)) + contact.authStatus = .friend + try self.dbManager.getDB().saveContact(contact) + + self.hudManager.hide() + self.contactConfirmationSubject.send(contact) + } catch { + contact.authStatus = .confirmationFailed + _ = try? self.dbManager.getDB().saveContact(contact) + self.hudManager.show(.init(error: error)) + } + } + } + + func groupChatWith(group: Group) -> GroupInfo { + guard let info = try? dbManager.getDB().fetchGroupInfos(.init(groupId: group.id)).first else { + fatalError() } + + return info + } } diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index 40964837a7a4254badcc48c5750389def82a3fda..abe6ba7b6c2e20c14115ce6f54535f841c693930 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -1,97 +1,129 @@ -import HUD import UIKit -import Models import Shared +import AppCore import Combine import Defaults import XXModels -import Integration -import ToastFeature +import Defaults +import XXClient +import AppResources +import Dependencies import ReportingFeature import CombineSchedulers -import DependencyInjection +import XXMessengerClient struct RequestSent: Hashable, Equatable { - var request: Request - var isResent: Bool = false + var request: Request + var isResent: Bool = false } final class RequestsSentViewModel { - @Dependency private var session: SessionType - @Dependency private var reportingStatus: ReportingStatus - @Dependency private var toastController: ToastController - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never> { - itemsSubject.eraseToAnyPublisher() + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @Dependency(\.app.toastManager) var toastManager: ToastManager + @Dependency(\.reportingStatus) var reportingStatus: ReportingStatus + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool + + var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never> { + itemsSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init() { + let query = Contact.Query( + authStatus: [ + .requested, + .requesting + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + try! dbManager.getDB().fetchContactsPublisher(query) + .replaceError(with: []) + .removeDuplicates() + .map { data -> NSDiffableDataSourceSnapshot<Section, RequestSent> in + var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() + snapshot.appendSections([.appearing]) + snapshot.appendItems(data.map { RequestSent(request: .contact($0)) }, toSection: .appearing) + return snapshot + }.sink { [unowned self] in itemsSubject.send($0) } + .store(in: &cancellables) + } + + func didTapStateButtonFor(request item: RequestSent) { + guard case let .contact(contact) = item.request, + item.request.status == .requested || + item.request.status == .requesting || + item.request.status == .failedToRequest else { + return } - - private var cancellables = Set<AnyCancellable>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - let query = Contact.Query( - authStatus: [ - .requested, - .requesting - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil + + let name = (contact.nickname ?? contact.username) ?? "" + + hudManager.show() + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() + + if let fact = myFacts.get(.username) { + includedFacts.append(fact) + } + + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) + } + + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) + } + + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(contact.marshaled!), + myFacts: includedFacts ) - - session.dbManager.fetchContactsPublisher(query) - .assertNoFailure() - .removeDuplicates() - .map { data -> NSDiffableDataSourceSnapshot<Section, RequestSent> in - var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() - snapshot.appendSections([.appearing]) - snapshot.appendItems(data.map { RequestSent(request: .contact($0)) }, toSection: .appearing) - return snapshot - }.sink { [unowned self] in itemsSubject.send($0) } - .store(in: &cancellables) - } - - func didTapStateButtonFor(request item: RequestSent) { - guard case let .contact(contact) = item.request, item.request.status == .requested else { return } - - hudSubject.send(.on) - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.retryRequest(contact) - self.hudSubject.send(.none) - - var item = item - var allRequests = self.itemsSubject.value.itemIdentifiers - - if let indexOfRequest = allRequests.firstIndex(of: item) { - allRequests.remove(at: indexOfRequest) - } - - item.isResent = true - allRequests.append(item) - - let name = (contact.nickname ?? contact.username) ?? "" - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resent(name), - leftImage: Asset.requestSentToaster.image - )) - - var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() - snapshot.appendSections([.appearing]) - snapshot.appendItems(allRequests, toSection: .appearing) - self.itemsSubject.send(snapshot) - } catch { - self.hudSubject.send(.error(.init(with: error))) - } + + self.hudManager.hide() + + var item = item + var allRequests = self.itemsSubject.value.itemIdentifiers + + if let indexOfRequest = allRequests.firstIndex(of: item) { + allRequests.remove(at: indexOfRequest) } + + item.isResent = true + allRequests.append(item) + + self.toastManager.enqueue(.init( + title: Localized.Requests.Sent.Toast.resent(name), + leftImage: Asset.requestSentToaster.image + )) + + var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() + snapshot.appendSections([.appearing]) + snapshot.appendItems(allRequests, toSection: .appearing) + self.itemsSubject.send(snapshot) + } catch { + self.toastManager.enqueue(.init( + title: Localized.Requests.Sent.Toast.resentFailed(name), + leftImage: Asset.requestFailedToaster.image + )) + + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudManager.show(.init(content: xxError)) + } } + } } diff --git a/Sources/RequestsFeature/Views/RequestCell.swift b/Sources/RequestsFeature/Views/RequestCell.swift index e0af9ec6017ef1241ed0831ee0dbad71af7dc3d4..217e969783cff7cfe659711861ebee6a5ef51d99 100644 --- a/Sources/RequestsFeature/Views/RequestCell.swift +++ b/Sources/RequestsFeature/Views/RequestCell.swift @@ -1,259 +1,260 @@ import UIKit import Shared import Combine -import Countries +import AppResources +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..005197063e8220863d70edec1cfc1c9a5e93bf5c 100644 --- a/Sources/RequestsFeature/Views/RequestCellButton.swift +++ b/Sources/RequestsFeature/Views/RequestCellButton.swift @@ -1,35 +1,36 @@ import UIKit import Shared +import AppResources 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..4239feca2ff52a7db5573e39b3dca1bff438bbe1 100644 --- a/Sources/RequestsFeature/Views/RequestReceivedEmptyCell.swift +++ b/Sources/RequestsFeature/Views/RequestReceivedEmptyCell.swift @@ -1,34 +1,35 @@ import UIKit import Shared +import AppResources 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..85f49138de97d52b9551ed2f8a659ddf562a9565 100644 --- a/Sources/RequestsFeature/Views/RequestSegmentedButton.swift +++ b/Sources/RequestsFeature/Views/RequestSegmentedButton.swift @@ -1,32 +1,33 @@ import UIKit import Shared +import AppResources 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..222a9eb74e6ac3075d63fe34d6917db8bb6cc4d3 100644 --- a/Sources/RequestsFeature/Views/RequestsContainerView.swift +++ b/Sources/RequestsFeature/Views/RequestsContainerView.swift @@ -1,58 +1,59 @@ import UIKit import Shared +import AppResources 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..b3ded76b653c0d8053e5476f42f0c1bfb62b9c08 100644 --- a/Sources/RequestsFeature/Views/RequestsFailedView.swift +++ b/Sources/RequestsFeature/Views/RequestsFailedView.swift @@ -1,40 +1,41 @@ import UIKit import Shared +import AppResources 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..9442a7a55097f411933b048522893da51afb0763 100644 --- a/Sources/RequestsFeature/Views/RequestsHiddenSectionHeader.swift +++ b/Sources/RequestsFeature/Views/RequestsHiddenSectionHeader.swift @@ -1,64 +1,65 @@ import UIKit import Shared import Combine +import AppResources 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..5e60e04ac94f513c3e96018e2dd3762fedb8ca56 100644 --- a/Sources/RequestsFeature/Views/RequestsReceivedView.swift +++ b/Sources/RequestsFeature/Views/RequestsReceivedView.swift @@ -1,52 +1,53 @@ import UIKit import Shared +import AppResources 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..6467704207b98661ee3158a471264bb073b1aa01 100644 --- a/Sources/RequestsFeature/Views/RequestsSegmentedControl.swift +++ b/Sources/RequestsFeature/Views/RequestsSegmentedControl.swift @@ -1,94 +1,95 @@ import UIKit import Shared import SnapKit +import AppResources 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..1035850ca0165d44893bef7fc3750c0679ee6bcf 100644 --- a/Sources/RequestsFeature/Views/RequestsSentView.swift +++ b/Sources/RequestsFeature/Views/RequestsSentView.swift @@ -1,53 +1,54 @@ import UIKit import Shared +import AppResources 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 f2081aaef8eca0773f552bf685976dbfe67d75c9..ac6f6ae1b3778411c35f750734fe2498371e3f90 100644 --- a/Sources/RestoreFeature/Controllers/RestoreController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreController.swift @@ -1,128 +1,138 @@ import UIKit -import Models import Shared -import DrawerFeature import Combine -import DependencyInjection +import AppResources +import AppNavigation +import DrawerFeature +import ComposableArchitecture public final class RestoreController: UIViewController { - @Dependency private var coordinator: RestoreCoordinating - - lazy private var screenView = RestoreView() - - private let viewModel: RestoreViewModel - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public init(_ ndf: String, _ settings: RestoreSettings) { - viewModel = .init(ndf: ndf, settings: settings) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - presentWarning() - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar.customize() - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupBindings() - } - - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.AccountRestore.header - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) - - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupBindings() { - viewModel.step - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [unowned self] in - screenView.updateFor(step: $0) - - if $0 == .wrongPass { - coordinator.toPassphrase(from: self) { pass in - self.viewModel.retryWith(passphrase: pass) - } - - return - } - - if $0 == .done { - coordinator.toSuccess(from: self) - } - }.store(in: &cancellables) - - screenView.backButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in didTapBack() } - .store(in: &cancellables) - - screenView.cancelButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in didTapBack() } - .store(in: &cancellables) - - screenView.restoreButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - coordinator.toPassphrase(from: self) { passphrase in - self.viewModel.didTapRestore(passphrase: passphrase) - } - }.store(in: &cancellables) - } - - @objc private func didTapBack() { - navigationController?.popViewController(animated: true) - } + @Dependency(\.navigator) var navigator: Navigator + + private lazy var screenView = RestoreView() + + private let viewModel: RestoreViewModel + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public init(_ details: RestorationDetails) { + viewModel = .init(details: details) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + presentWarning() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + } + + private func setupNavigationBar() { + let title = UILabel() + title.text = Localized.AccountRestore.header + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) + navigationItem.leftItemsSupplementBackButton = true + } + + private func setupBindings() { + viewModel + .stepPublisher + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + screenView.updateFor(step: $0) + if $0 == .wrongPass { + navigator.perform(PresentPassphrase(onCancel: { [weak self] in + guard let self else { return } + self.navigator.perform(DismissModal(from: self)) + }, onPassphrase: { [weak self] passphrase in + guard let self else { return } + self.viewModel.retryWith(passphrase: passphrase) + })) + return + } + if $0 == .done { +// coordinator.toSuccess(from: self) + } + }.store(in: &cancellables) + + screenView + .backButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + didTapBack() + }.store(in: &cancellables) + + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + didTapBack() + }.store(in: &cancellables) + + screenView + .restoreButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentPassphrase(onCancel: { [weak self] in + guard let self else { return } + self.navigator.perform(DismissModal(from: self)) + }, onPassphrase: { [weak self] passphrase in + guard let self else { return } + self.viewModel.didTapRestore(passphrase: passphrase) + })) + }.store(in: &cancellables) + } + + @objc private func didTapBack() { + navigationController?.popViewController(animated: true) + } } extension RestoreController { - private func presentWarning() { - let actionButton = DrawerCapsuleButton(model: .init( - title: Localized.AccountRestore.Warning.action, - style: .brandColored - )) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: Localized.AccountRestore.Warning.title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - text: Localized.AccountRestore.Warning.subtitle, - spacingAfter: 37 - ), - actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + private func presentWarning() { + let actionButton = DrawerCapsuleButton(model: .init( + title: Localized.AccountRestore.Warning.action, + style: .brandColored + )) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.AccountRestore.Warning.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + text: Localized.AccountRestore.Warning.subtitle, + spacingAfter: 37 + ), + actionButton + ], isDismissable: true, from: self)) + } } diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift index 33470e44212a67e1ee6833cfd70664aca840321f..cbce48e53d9b5943a84495906dfd64cc38bfd484 100644 --- a/Sources/RestoreFeature/Controllers/RestoreListController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -1,117 +1,122 @@ -import HUD import UIKit import Shared import Combine +import AppResources +import AppNavigation import DrawerFeature -import DependencyInjection +import ComposableArchitecture public final class RestoreListController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: RestoreCoordinating - - lazy private var screenView = RestoreListView() - - private let ndf: String - private let viewModel = RestoreListViewModel() - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public override func loadView() { - view = screenView - presentWarning() - } - - public init(_ ndf: String) { - self.ndf = ndf - super.init(nibName: nil, bundle: nil) - } - - public required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLoad() { - super.viewDidLoad() - - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - viewModel.backupPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toRestore(using: ndf, with: $0, from: self) - }.store(in: &cancellables) - - screenView.cancelButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in didTapBack() } - .store(in: &cancellables) - - screenView.driveButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - viewModel.didTapCloud(.drive, from: self) - }.store(in: &cancellables) - - screenView.icloudButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - viewModel.didTapCloud(.icloud, from: self) - }.store(in: &cancellables) - - screenView.dropboxButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - viewModel.didTapCloud(.dropbox, from: self) - }.store(in: &cancellables) - - screenView.sftpButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - viewModel.didTapCloud(.sftp, from: self) - }.store(in: &cancellables) - } - - @objc private func didTapBack() { - navigationController?.popViewController(animated: true) - } + @Dependency(\.navigator) var navigator: Navigator + + private lazy var screenView = RestoreListView() + + private let viewModel = RestoreListViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + presentWarning() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + viewModel + .sftpPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ 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 + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in +// coordinator.toRestore(with: $0, from: self) + }.store(in: &cancellables) + + screenView.cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in didTapBack() } + .store(in: &cancellables) + + screenView.driveButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.link(provider: .drive, from: self) { [weak self] in + guard let self else { return } + self.viewModel.fetch(provider: .drive) + } + }.store(in: &cancellables) + + screenView.icloudButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.link(provider: .icloud, from: self) { [weak self] in + guard let self else { return } + self.viewModel.fetch(provider: .icloud) + } + }.store(in: &cancellables) + + screenView.dropboxButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.link(provider: .dropbox, from: self) { [weak self] in + guard let self else { return } + self.viewModel.fetch(provider: .dropbox) + } + }.store(in: &cancellables) + + screenView.sftpButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.link(provider: .sftp, from: self) {} + }.store(in: &cancellables) + } + + @objc private func didTapBack() { + navigationController?.popViewController(animated: true) + } } extension RestoreListController { - private func presentWarning() { - let actionButton = DrawerCapsuleButton(model: .init( - title: Localized.AccountRestore.Warning.action, - style: .brandColored - )) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: Localized.AccountRestore.Warning.title, - spacingAfter: 19 - ), - DrawerText( - text: Localized.AccountRestore.Warning.subtitle, - spacingAfter: 37 - ), - actionButton - ]) - - actionButton.action - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + private func presentWarning() { + let actionButton = DrawerCapsuleButton(model: .init( + title: Localized.AccountRestore.Warning.action, + style: .brandColored + )) + + actionButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.AccountRestore.Warning.title, + spacingAfter: 19 + ), + DrawerText( + text: Localized.AccountRestore.Warning.subtitle, + spacingAfter: 37 + ), + actionButton + ], isDismissable: true, from: self)) + } } diff --git a/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift b/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift index 4d19e356729508ea5b688bb8435e09c602b5c8a6..660287c6ca004e36beb04bfeb60a4ad839f55e1f 100644 --- a/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift +++ b/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift @@ -2,92 +2,71 @@ import UIKit import Shared import Combine import InputField -import ScrollViewController public final class RestorePassphraseController: UIViewController { - lazy private var screenView = RestorePassphraseView() - - private var passphrase = "" { - didSet { - switch Validator.backupPassphrase.validate(passphrase) { - case .success: - screenView.continueButton.isEnabled = true - case .failure: - screenView.continueButton.isEnabled = false - } - } - } - - private let completion: StringClosure - private var cancellables = Set<AnyCancellable>() - private let keyboardListener = KeyboardFrameChangeListener(notificationCenter: .default) - - public init(_ completion: @escaping StringClosure) { - self.completion = completion - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - let view = UIView() - view.addSubview(screenView) - - screenView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(0) - } - - self.view = view - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupKeyboard() - setupBindings() - + private lazy var screenView = RestorePassphraseView() + + private var passphrase = "" { + didSet { + switch Validator.backupPassphrase.validate(passphrase) { + case .success: + screenView.continueButton.isEnabled = true + case .failure: screenView.continueButton.isEnabled = false + } } - - private func setupKeyboard() { - keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in - guard let self = self else { return } - - let inset = self.view.frame.height - self.view.convert(keyboard.frame, from: nil).minY - - self.screenView.snp.updateConstraints { - $0.bottom.equalToSuperview().offset(-inset) - } - - self.view.setNeedsLayout() - - UIView.animate(withDuration: keyboard.animationDuration) { - self.view.layoutIfNeeded() - } + } + + private let cancelClosure: () -> Void + private let stringClosure: (String) -> Void + private var cancellables = Set<AnyCancellable>() + + public init( + _ cancelClosure: @escaping () -> Void, + _ stringClosure: @escaping (String) -> Void + ) { + self.stringClosure = stringClosure + self.cancelClosure = cancelClosure + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + screenView + .inputField + .returnPublisher + .sink { [unowned self] in + screenView.inputField.endEditing(true) + }.store(in: &cancellables) + + screenView + .inputField + .textPublisher + .sink { [unowned self] in + passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) + }.store(in: &cancellables) + + screenView + .continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { + self.stringClosure(self.passphrase) } - } - - private func setupBindings() { - screenView.inputField.returnPublisher - .sink { [unowned self] in screenView.inputField.endEditing(true) } - .store(in: &cancellables) - - screenView.cancelButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in dismiss(animated: true) } - .store(in: &cancellables) - - screenView.inputField - .textPublisher - .sink { [unowned self] in passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .store(in: &cancellables) - - screenView.continueButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - dismiss(animated: true, completion: { self.completion(self.passphrase) }) - }.store(in: &cancellables) - } + }.store(in: &cancellables) + + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) + }.store(in: &cancellables) + } } diff --git a/Sources/RestoreFeature/Controllers/RestoreSFTPController.swift b/Sources/RestoreFeature/Controllers/RestoreSFTPController.swift new file mode 100644 index 0000000000000000000000000000000000000000..1cfe3d251c7826bcad639009778dc9b26837f4a1 --- /dev/null +++ b/Sources/RestoreFeature/Controllers/RestoreSFTPController.swift @@ -0,0 +1,80 @@ +import UIKit +import Combine +import ScrollViewController + +public final class RestoreSFTPController: UIViewController { + private lazy var screenView = RestoreSFTPView() + private lazy var scrollViewController = ScrollViewController() + + private let completion: (String, String, String) -> Void + private let viewModel = RestoreSFTPViewModel() + private var cancellables = Set<AnyCancellable>() + + public init(_ completion: @escaping (String, String, String) -> Void) { + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + } + + private func setupScrollView() { + scrollViewController.scrollView.backgroundColor = .white + + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + } + + private func setupBindings() { + viewModel.authPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] params in + dismiss(animated: true) { + self.completion(params.0, params.1, params.2) + } + }.store(in: &cancellables) + + screenView.hostField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterHost($0) } + .store(in: &cancellables) + + screenView.usernameField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterUsername($0) } + .store(in: &cancellables) + + screenView.passwordField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterPassword($0) } + .store(in: &cancellables) + + viewModel.statePublisher + .receive(on: DispatchQueue.main) + .map(\.isButtonEnabled) + .sink { [unowned self] in screenView.loginButton.isEnabled = $0 } + .store(in: &cancellables) + + screenView.loginButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapLogin() } + .store(in: &cancellables) + } +} diff --git a/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift index a0624385c1d0143c5826c358f7081ada3573f8b1..6fca825a4df49a270c800ca78b3f5d7757659a66 100644 --- a/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreSuccessController.swift @@ -1,44 +1,56 @@ import UIKit +import Shared import Combine -import DependencyInjection +import AppCore +import Dependencies +import AppNavigation public final class RestoreSuccessController: UIViewController { - @Dependency private var coordinator: RestoreCoordinating - - lazy private var screenView = RestoreSuccessView() - private var cancellables = Set<AnyCancellable>() - - public override func loadView() { - view = screenView - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupBindings() - } - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let gradient = CAGradientLayer() - gradient.colors = [ - UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, - UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, - UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, - UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor - ] - - gradient.startPoint = CGPoint(x: 0, y: 0) - gradient.endPoint = CGPoint(x: 1, y: 1) - - gradient.frame = screenView.bounds - screenView.layer.insertSublayer(gradient, at: 0) - } - - private func setupBindings() { - screenView.nextButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toChats(from: self) } - .store(in: &cancellables) - } + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = RestoreSuccessView() + private var cancellables = Set<AnyCancellable>() + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupBindings() + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let gradient = CAGradientLayer() + gradient.colors = [ + UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, + UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, + UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, + UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor + ] + + gradient.startPoint = CGPoint(x: 0, y: 0) + gradient.endPoint = CGPoint(x: 1, y: 1) + + gradient.frame = screenView.bounds + screenView.layer.insertSublayer(gradient, at: 0) + } + + private func setupBindings() { + screenView + .nextButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigator.perform(PresentChatList(on: navigationController!)) + }.store(in: &cancellables) + } } diff --git a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift deleted file mode 100644 index 2183035bca4a57ee9fd4435b2656fed7123de814..0000000000000000000000000000000000000000 --- a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift +++ /dev/null @@ -1,68 +0,0 @@ -import UIKit -import Models -import Shared -import Presentation - -public protocol RestoreCoordinating { - func toChats(from: UIViewController) - func toSuccess(from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toPassphrase(from: UIViewController, _: @escaping StringClosure) - func toRestore(using: String, with: RestoreSettings, from: UIViewController) -} - -public struct RestoreCoordinator: RestoreCoordinating { - var pushPresenter: Presenting = PushPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() - - var successFactory: () -> UIViewController - var chatListFactory: () -> UIViewController - var restoreFactory: (String, RestoreSettings) -> UIViewController - var passphraseFactory: (@escaping StringClosure) -> UIViewController - - public init( - successFactory: @escaping () -> UIViewController, - chatListFactory: @escaping () -> UIViewController, - restoreFactory: @escaping (String, RestoreSettings) -> UIViewController, - passphraseFactory: @escaping (@escaping StringClosure) -> UIViewController - ) { - self.successFactory = successFactory - self.restoreFactory = restoreFactory - self.chatListFactory = chatListFactory - self.passphraseFactory = passphraseFactory - } -} - -public extension RestoreCoordinator { - func toRestore( - using ndf: String, - with settings: RestoreSettings, - from parent: UIViewController - ) { - let screen = restoreFactory(ndf, settings) - pushPresenter.present(screen, from: parent) - } - - func toChats(from parent: UIViewController) { - let screen = chatListFactory() - replacePresenter.present(screen, from: parent) - } - - func toSuccess(from parent: UIViewController) { - let screen = successFactory() - replacePresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toPassphrase( - from parent: UIViewController, - _ completion: @escaping StringClosure - ) { - let screen = passphraseFactory(completion) - bottomPresenter.present(screen, from: parent) - } -} diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift index 438207bb1cc5df10104d3be19029ef01cc7d3c19..40428ff3d9c49c175afb5d48d5cf8c18af28320b 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift @@ -1,154 +1,84 @@ -import HUD import UIKit -import Models import Shared +import AppCore import Combine -import BackupFeature -import DependencyInjection +import CloudFiles +import CloudFilesSFTP +import ComposableArchitecture -import SFTPFeature -import iCloudFeature -import DropboxFeature -import GoogleDriveFeature +public struct RestorationDetails { + var provider: CloudService + var metadata: Fetch.Metadata? +} final class RestoreListViewModel { - @Dependency private var sftpService: SFTPService - @Dependency private var icloudService: iCloudInterface - @Dependency private var dropboxService: DropboxInterface - @Dependency private var googleDriveService: GoogleDriveInterface - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var backupPublisher: AnyPublisher<RestoreSettings, Never> { - backupSubject.eraseToAnyPublisher() - } - - private var dropboxAuthCancellable: AnyCancellable? - private let hudSubject = PassthroughSubject<HUDStatus, Never>() - private let backupSubject = PassthroughSubject<RestoreSettings, Never>() - - func didTapCloud(_ cloudService: CloudService, from parent: UIViewController) { - switch cloudService { - case .drive: - didRequestDriveAuthorization(from: parent) - case .icloud: - didRequestICloudAuthorization() - case .dropbox: - didRequestDropboxAuthorization(from: parent) - case .sftp: - didRequestSFTPAuthorization(from: parent) - } - } - - private func didRequestSFTPAuthorization(from controller: UIViewController) { - let params = SFTPAuthorizationParams(controller, { [weak self] in - guard let self = self else { return } - controller.navigationController?.popViewController(animated: true) - - self.hudSubject.send(.on) - - self.sftpService.fetchMetadata{ result in - switch result { - case .success(let settings): - self.hudSubject.send(.none) - - if let settings = settings { - self.backupSubject.send(settings) - } else { - self.backupSubject.send(.init(cloudService: .sftp)) - } - case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) - } - } - }) - - sftpService.authorizeFlow(params) + @Dependency(\.app.hudManager) var hudManager: HUDManager + + var sftpPublisher: AnyPublisher<Void, Never> { + sftpSubject.eraseToAnyPublisher() + } + + var detailsPublisher: AnyPublisher<RestorationDetails, Never> { + detailsSubject.eraseToAnyPublisher() + } + + private let sftpSubject = PassthroughSubject<Void, Never>() + private let detailsSubject = PassthroughSubject<RestorationDetails, Never>() + + func setupSFTP(host: String, username: String, password: String) { + CloudFilesManager.all[.sftp] = .sftp( + host: host, + username: username, + password: password, + fileName: "backup.xxm" + ) + fetch(provider: .sftp) + } + + func link( + provider: CloudService, + from controller: UIViewController, + onSuccess: @escaping () -> Void + ) { + if provider == .sftp { + sftpSubject.send(()) + return } - - private func didRequestDriveAuthorization(from controller: UIViewController) { - googleDriveService.authorize(presenting: controller) { authResult in - switch authResult { - case .success: - self.hudSubject.send(.on) - self.googleDriveService.downloadMetadata { downloadResult in - switch downloadResult { - case .success(let metadata): - var backup: Backup? - - if let metadata = metadata { - backup = .init(id: metadata.identifier, date: metadata.modifiedDate, size: metadata.size) - } - - self.hudSubject.send(.none) - self.backupSubject.send(RestoreSettings(backup: backup, cloudService: .drive)) - - case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) - } - } - case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) - } + do { + try CloudFilesManager.all[provider]!.link(controller) { [weak self] in + guard let self else {return } + + switch $0 { + case .success: + onSuccess() + case .failure(let error): + self.hudManager.show(.init(error: error)) } + } + } catch { + hudManager.show(.init(error: error)) } - - private func didRequestICloudAuthorization() { - if icloudService.isAuthorized() { - self.hudSubject.send(.on) - - icloudService.downloadMetadata { result in - switch result { - case .success(let metadata): - var backup: Backup? - - if let metadata = metadata { - backup = .init(id: metadata.path, date: metadata.modifiedDate, size: metadata.size) - } - - self.hudSubject.send(.none) - self.backupSubject.send(RestoreSettings(backup: backup, cloudService: .icloud)) - case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) - } - } - } else { - /// This could be an alert controller asking if user wants to enable/deeplink - /// - icloudService.openSettings() + } + + func fetch(provider: CloudService) { + hudManager.show() + do { + try CloudFilesManager.all[provider]!.fetch { [weak self] in + guard let self else { return } + + switch $0 { + case .success(let metadata): + self.hudManager.hide() + self.detailsSubject.send(.init( + provider: provider, + metadata: metadata + )) + case .failure(let error): + self.hudManager.show(.init(error: error)) } + } + } catch { + hudManager.show(.init(error: error)) } - - private func didRequestDropboxAuthorization(from controller: UIViewController) { - dropboxAuthCancellable = dropboxService.authorize(presenting: controller) - .receive(on: DispatchQueue.main) - .sink { [unowned self] authResult in - switch authResult { - case .success(let bool): - guard bool == true else { return } - - self.hudSubject.send(.on) - dropboxService.downloadMetadata { metadataResult in - switch metadataResult { - case .success(let metadata): - var backup: Backup? - - if let metadata = metadata { - backup = .init(id: metadata.path, date: metadata.modifiedDate, size: metadata.size) - } - - self.hudSubject.send(.none) - self.backupSubject.send(RestoreSettings(backup: backup, cloudService: .dropbox)) - - case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) - } - } - case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) - } - } - } + } } diff --git a/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..d18036fc37123a30f56b7126b4e595e4e9a1a89b --- /dev/null +++ b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift @@ -0,0 +1,85 @@ +import UIKit +import Shared +import Combine +import Foundation +import CloudFiles +import CloudFilesSFTP + +import AppCore +import ComposableArchitecture + +struct SFTPViewState { + var host: String = "" + var username: String = "" + var password: String = "" + var isButtonEnabled: Bool = false +} + +final class RestoreSFTPViewModel { + @Dependency(\.app.hudManager) var hudManager: HUDManager + + var statePublisher: AnyPublisher<SFTPViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + var authPublisher: AnyPublisher<(String, String, String), Never> { + authSubject.eraseToAnyPublisher() + } + + private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) + private let authSubject = PassthroughSubject<(String, String, String), Never>() + + func didEnterHost(_ string: String) { + stateSubject.value.host = string + validate() + } + + func didEnterUsername(_ string: String) { + stateSubject.value.username = string + validate() + } + + func didEnterPassword(_ string: String) { + stateSubject.value.password = string + validate() + } + + func didTapLogin() { + hudManager.show() + + let host = stateSubject.value.host + let username = stateSubject.value.username + let password = stateSubject.value.password + + let anyController = UIViewController() + + DispatchQueue.global().async { [weak self] in + guard let self else { return } + do { + try CloudFilesManager.sftp( + host: host, + username: username, + password: password, + fileName: "backup.xxm" + ).link(anyController) { + switch $0 { + case .success: + self.hudManager.hide() + self.authSubject.send((host, username, password)) + case .failure(let error): + self.hudManager.show(.init(error: error)) + } + } + } catch { + self.hudManager.show(.init(error: error)) + } + } + } + + private func validate() { + stateSubject.value.isButtonEnabled = + !stateSubject.value.host.isEmpty && + !stateSubject.value.username.isEmpty && + !stateSubject.value.password.isEmpty + } +} diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift index bea37e5113ad33a547e5680e478a0ed7717db741..1f7d88498e0f0e6ff1c04b654624a54b97bfadd3 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -1,172 +1,181 @@ import UIKit -import Models import Shared import Combine import Defaults -import Foundation -import Integration -import BackupFeature -import DependencyInjection - -import SFTPFeature -import iCloudFeature -import DropboxFeature -import GoogleDriveFeature - -enum RestorationStep { - case idle(CloudService, Backup?) - case downloading(Float, Float) - case failDownload(Error) - case wrongPass - case parsingData - case done +import CloudFiles + +import XXClient +import XXModels +import XXDatabase +import XXMessengerClient + +import AppCore +import ComposableArchitecture + +enum Step { + case done + case wrongPass + case parsingData + case failDownload(Error) + case downloading(Float, Float) + case idle(CloudService, CloudFiles.Fetch.Metadata?) } -extension RestorationStep: Equatable { - static func ==(lhs: RestorationStep, rhs: RestorationStep) -> Bool { - switch (lhs, rhs) { - case (.done, .done), (.wrongPass, .wrongPass): - return true - case let (.failDownload(a), .failDownload(b)): - return a.localizedDescription == b.localizedDescription - case let (.downloading(a, b), .downloading(c, d)): - return a == c && b == d - case (.idle, _), (.downloading, _), (.parsingData, _), - (.done, _), (.failDownload, _), (.wrongPass, _): - return false - } +extension Step: Equatable { + static func ==(lhs: Step, rhs: Step) -> Bool { + switch (lhs, rhs) { + case (.done, .done), (.wrongPass, .wrongPass): + return true + case let (.failDownload(a), .failDownload(b)): + return a.localizedDescription == b.localizedDescription + case let (.downloading(a, b), .downloading(c, d)): + return a == c && b == d + case (.idle, _), (.downloading, _), (.parsingData, _), + (.done, _), (.failDownload, _), (.wrongPass, _): + return false } + } } final class RestoreViewModel { - @Dependency private var sftpService: SFTPService - @Dependency private var iCloudService: iCloudInterface - @Dependency private var dropboxService: DropboxInterface - @Dependency private var googleService: GoogleDriveInterface - - @KeyObject(.username, defaultValue: nil) var username: String? - - var step: AnyPublisher<RestorationStep, Never> { - stepRelay.eraseToAnyPublisher() - } - - // TO REFACTOR: - // - private var pendingData: Data? - - private let ndf: String - private var passphrase: String! - private let settings: RestoreSettings - private let stepRelay: CurrentValueSubject<RestorationStep, Never> - - init(ndf: String, settings: RestoreSettings) { - self.ndf = ndf - self.settings = settings - self.stepRelay = .init(.idle(settings.cloudService, settings.backup)) + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + + @KeyObject(.phone, defaultValue: nil) var phone: String? + @KeyObject(.email, defaultValue: nil) var email: String? + @KeyObject(.username, defaultValue: nil) var username: String? + + var stepPublisher: AnyPublisher<Step, Never> { + stepSubject.eraseToAnyPublisher() + } + + private var pendingData: Data? + private var passphrase: String! + private let details: RestorationDetails + private let stepSubject: CurrentValueSubject<Step, Never> + + init(details: RestorationDetails) { + self.details = details + self.stepSubject = .init(.idle( + details.provider, + details.metadata + )) + } + + func retryWith(passphrase: String) { + self.passphrase = passphrase + continueRestoring(data: pendingData!) + } + + func didTapRestore(passphrase: String) { + self.passphrase = passphrase + + guard let metadata = details.metadata else { + fatalError() } - func retryWith(passphrase: String) { - self.passphrase = passphrase - continueRestoring(data: pendingData!) - } - - func didTapRestore(passphrase: String) { - self.passphrase = passphrase - - guard let backup = settings.backup else { fatalError() } + stepSubject.send(.downloading(0.0, metadata.size)) - stepRelay.send(.downloading(0.0, backup.size)) + do { + try CloudFilesManager.all[details.provider]!.download { [weak self] in + guard let self else { return } - switch settings.cloudService { - case .drive: - downloadBackupForDrive(backup) - case .dropbox: - downloadBackupForDropbox(backup) - case .icloud: - downloadBackupForiCloud(backup) - case .sftp: - downloadBackupForSFTP(backup) + switch $0 { + case .success(let data): + guard let data else { + fatalError("There was metadata, but not data.") + } + self.continueRestoring(data: data) + case .failure(let error): + self.stepSubject.send(.failDownload(error)) } + } + } catch { + stepSubject.send(.failDownload(error)) } - - private func downloadBackupForSFTP(_ backup: Backup) { - sftpService.downloadBackup(path: backup.id) { [weak self] in - guard let self = self else { return } - self.stepRelay.send(.downloading(backup.size, backup.size)) - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } + } + + private func continueRestoring(data: Data) { + stepSubject.send(.parsingData) + + DispatchQueue.global().async { [weak self] in + guard let self else { return } + + do { + print(">>> Calling messenger destroy") + try self.messenger.destroy() + + print(">>> Calling restore backup") + let result = try self.messenger.restoreBackup( + backupData: data, + backupPassphrase: self.passphrase + ) + + let facts = try self.messenger.ud.tryGet().getFacts() + self.username = facts.get(.username)!.value + self.email = facts.get(.email)?.value + self.phone = facts.get(.phone)?.value + + print(">>> Calling wait for network") + try self.messenger.waitForNetwork() + + print(">>> Calling waitForNodes") + try self.messenger.waitForNodes( + targetRatio: 0.5, + sleepInterval: 3, + retries: 15, + onProgress: { print(">>> \($0)") } + ) + + try self.dbManager.getDB().saveContact(.init( + id: self.messenger.e2e.get()!.getContact().getId(), + marshaled: self.messenger.e2e.get()!.getContact().data, + username: self.username!, + email: self.email, + phone: self.phone, + nickname: nil, + photo: nil, + authStatus: .friend, + isRecent: false, + isBlocked: false, + isBanned: false, + createdAt: Date() + )) + + print(">>> Calling multilookup") + let multilookup = try self.messenger.lookupContacts(ids: result.restoredContacts) + + multilookup.contacts.forEach { + try! self.dbManager.getDB().saveContact(.init( + id: try $0.getId(), + marshaled: $0.data, + username: try? $0.getFact(.username)?.value, + email: nil, + phone: nil, + nickname: try? $0.getFact(.username)?.value, + photo: nil, + authStatus: .friend, + isRecent: false, + isBlocked: false, + isBanned: false, + createdAt: Date() + )) + + let _ = try! self.messenger.e2e.get()!.resetAuthenticatedChannel(partner: $0) } - } - private func downloadBackupForDropbox(_ backup: Backup) { - dropboxService.downloadBackup(backup.id) { [weak self] in - guard let self = self else { return } - self.stepRelay.send(.downloading(backup.size, backup.size)) - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } - } - } + try self.messenger.start() - private func downloadBackupForiCloud(_ backup: Backup) { - iCloudService.downloadBackup(backup.id) { [weak self] in - guard let self = self else { return } - self.stepRelay.send(.downloading(backup.size, backup.size)) - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } + multilookup.errors.forEach { + print(">>> Error: \($0.localizedDescription)") } - } - private func downloadBackupForDrive(_ backup: Backup) { - googleService.downloadBackup(backup.id) { [weak self] in - if let stepRelay = self?.stepRelay { - stepRelay.send(.downloading($0, backup.size)) - } - } _: { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let data): - self.continueRestoring(data: data) - case .failure(let error): - self.stepRelay.send(.failDownload(error)) - } - } - } - - private func continueRestoring(data: Data) { - stepRelay.send(.parsingData) - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - do { - let session = try Session( - passphrase: self.passphrase, - backupFile: data, - ndf: self.ndf - ) - - DependencyInjection.Container.shared.register(session as SessionType) - self.stepRelay.send(.done) - } catch { - self.pendingData = data - self.stepRelay.send(.wrongPass) - } - } + self.stepSubject.send(.done) + } catch { + print(">>> Error on restoration: \(error.localizedDescription)") + self.pendingData = data + self.stepSubject.send(.wrongPass) + } } + } } diff --git a/Sources/RestoreFeature/Views/RestoreDetailsView.swift b/Sources/RestoreFeature/Views/RestoreDetailsView.swift index 55f18c4d2befcd5dcfd9fa296fe7f6c888fd1aee..b7fad9fbd8c71b50306c9efd66fbcc51b3904e6a 100644 --- a/Sources/RestoreFeature/Views/RestoreDetailsView.swift +++ b/Sources/RestoreFeature/Views/RestoreDetailsView.swift @@ -1,56 +1,57 @@ import UIKit import Shared +import AppResources final class RestoreDetailsView: UIView { - let separatorView = UIView() - let imageView = UIImageView() - let titleLabel = UILabel() - - let stackView = UIStackView() - let dateView = DetailRowButton() - let sizeView = DetailRowButton() - - init() { - super.init(frame: .zero) - separatorView.backgroundColor = Asset.neutralLine.color - - titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) - titleLabel.textColor = Asset.neutralActive.color - - stackView.axis = .vertical - stackView.spacing = 22 - stackView.addArrangedSubview(dateView) - stackView.addArrangedSubview(sizeView) - - addSubview(separatorView) - addSubview(imageView) - addSubview(titleLabel) - addSubview(stackView) - - separatorView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview().offset(25) - make.right.equalToSuperview().offset(-25) - make.height.equalTo(1) - } - - imageView.snp.makeConstraints { make in - make.top.equalTo(separatorView.snp.bottom).offset(40) - make.left.equalToSuperview().offset(24) - } - - titleLabel.snp.makeConstraints { make in - make.centerY.equalTo(imageView) - make.left.equalToSuperview().offset(92) - } - - stackView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(20) - make.left.equalTo(titleLabel) - make.right.equalToSuperview().offset(-40) - make.bottom.lessThanOrEqualToSuperview().offset(-20) - } + let separatorView = UIView() + let imageView = UIImageView() + let titleLabel = UILabel() + + let stackView = UIStackView() + let dateView = DetailRowButton() + let sizeView = DetailRowButton() + + init() { + super.init(frame: .zero) + separatorView.backgroundColor = Asset.neutralLine.color + + titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0) + titleLabel.textColor = Asset.neutralActive.color + + stackView.axis = .vertical + stackView.spacing = 22 + stackView.addArrangedSubview(dateView) + stackView.addArrangedSubview(sizeView) + + addSubview(separatorView) + addSubview(imageView) + addSubview(titleLabel) + addSubview(stackView) + + separatorView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview().offset(25) + make.right.equalToSuperview().offset(-25) + make.height.equalTo(1) } - required init?(coder: NSCoder) { nil } + imageView.snp.makeConstraints { make in + make.top.equalTo(separatorView.snp.bottom).offset(40) + make.left.equalToSuperview().offset(24) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalTo(imageView) + make.left.equalToSuperview().offset(92) + } + + stackView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(20) + make.left.equalTo(titleLabel) + make.right.equalToSuperview().offset(-40) + make.bottom.lessThanOrEqualToSuperview().offset(-20) + } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/RestoreFeature/Views/RestoreListView.swift b/Sources/RestoreFeature/Views/RestoreListView.swift index a955173a742b7c0b94f95601f90634353ada327f..bb8f4e5a0767c2c6a81002e36b3ddb491ce4569f 100644 --- a/Sources/RestoreFeature/Views/RestoreListView.swift +++ b/Sources/RestoreFeature/Views/RestoreListView.swift @@ -1,127 +1,128 @@ import UIKit import Shared +import AppResources final class RestoreListView: UIView { - let titleLabel = UILabel() - let stackView = UIStackView() - let firstSubtitleLabel = UILabel() - let secondSubtitleLabel = UILabel() - let sftpButton = RowButton() - let driveButton = RowButton() - let icloudButton = RowButton() - let dropboxButton = RowButton() - let cancelButton = CapsuleButton() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupTitle(Localized.AccountRestore.List.title) - setupSubtitle(Localized.AccountRestore.List.firstSubtitle) - - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - let attrString = NSMutableAttributedString( - string: Localized.AccountRestore.List.secondSubtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ] - ) - - secondSubtitleLabel.numberOfLines = 0 - secondSubtitleLabel.attributedText = attrString - - sftpButton.setup(title: Localized.Backup.sftp, icon: Asset.restoreSFTP.image) - icloudButton.setup(title: Localized.Backup.iCloud, icon: Asset.restoreIcloud.image) - dropboxButton.setup(title: Localized.Backup.dropbox, icon: Asset.restoreDropbox.image) - driveButton.setup(title: Localized.Backup.googleDrive, icon: Asset.restoreDrive.image) - - cancelButton.set(style: .seeThrough, title: Localized.AccountRestore.List.cancel) - - stackView.axis = .vertical - stackView.distribution = .fillEqually - stackView.addArrangedSubview(driveButton) - stackView.addArrangedSubview(icloudButton) - stackView.addArrangedSubview(dropboxButton) - stackView.addArrangedSubview(sftpButton) - - addSubview(titleLabel) - addSubview(firstSubtitleLabel) - addSubview(secondSubtitleLabel) - addSubview(stackView) - addSubview(cancelButton) - - titleLabel.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).offset(15) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-41) - } - - firstSubtitleLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(8) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-41) - } - - secondSubtitleLabel.snp.makeConstraints { - $0.top.equalTo(firstSubtitleLabel.snp.bottom).offset(8) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-41) - } - - stackView.snp.makeConstraints { - $0.top.equalTo(secondSubtitleLabel.snp.bottom).offset(28) - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - } - - cancelButton.snp.makeConstraints { - $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(20) - $0.left.equalToSuperview().offset(40) - $0.right.equalToSuperview().offset(-40) - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) - } + let titleLabel = UILabel() + let stackView = UIStackView() + let firstSubtitleLabel = UILabel() + let secondSubtitleLabel = UILabel() + let sftpButton = RowButton() + let driveButton = RowButton() + let icloudButton = RowButton() + let dropboxButton = RowButton() + let cancelButton = CapsuleButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupTitle(Localized.AccountRestore.List.title) + setupSubtitle(Localized.AccountRestore.List.firstSubtitle) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attrString = NSMutableAttributedString( + string: Localized.AccountRestore.List.secondSubtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ] + ) + + secondSubtitleLabel.numberOfLines = 0 + secondSubtitleLabel.attributedText = attrString + + sftpButton.setup(title: Localized.Backup.sftp, icon: Asset.restoreSFTP.image) + icloudButton.setup(title: Localized.Backup.iCloud, icon: Asset.restoreIcloud.image) + dropboxButton.setup(title: Localized.Backup.dropbox, icon: Asset.restoreDropbox.image) + driveButton.setup(title: Localized.Backup.googleDrive, icon: Asset.restoreDrive.image) + + cancelButton.set(style: .seeThrough, title: Localized.AccountRestore.List.cancel) + + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.addArrangedSubview(driveButton) + stackView.addArrangedSubview(icloudButton) + stackView.addArrangedSubview(dropboxButton) + stackView.addArrangedSubview(sftpButton) + + addSubview(titleLabel) + addSubview(firstSubtitleLabel) + addSubview(secondSubtitleLabel) + addSubview(stackView) + addSubview(cancelButton) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - required init?(coder: NSCoder) { nil } - - private func setupTitle(_ title: String) { - let attString = NSMutableAttributedString(string: title) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1 - - attString.addAttribute(.paragraphStyle, value: paragraph) - attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) - attString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 34.0) as Any) + firstSubtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } - attString.addAttributes(attributes: [ - .font: Fonts.Mulish.bold.font(size: 34.0) as Any, - .foregroundColor: Asset.brandPrimary.color - ], betweenCharacters: "#") + secondSubtitleLabel.snp.makeConstraints { + $0.top.equalTo(firstSubtitleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } - titleLabel.numberOfLines = 0 - titleLabel.attributedText = attString + stackView.snp.makeConstraints { + $0.top.equalTo(secondSubtitleLabel.snp.bottom).offset(28) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) } - private func setupSubtitle(_ subtitle: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - let attString = NSAttributedString( - string: subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ]) - - firstSubtitleLabel.numberOfLines = 0 - firstSubtitleLabel.attributedText = attString + cancelButton.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } + } + + required init?(coder: NSCoder) { nil } + + private func setupTitle(_ title: String) { + let attString = NSMutableAttributedString(string: title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1 + + attString.addAttribute(.paragraphStyle, value: paragraph) + 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 + } + + private func setupSubtitle(_ subtitle: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSAttributedString( + string: subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + firstSubtitleLabel.numberOfLines = 0 + firstSubtitleLabel.attributedText = attString + } } diff --git a/Sources/RestoreFeature/Views/RestorePassphraseView.swift b/Sources/RestoreFeature/Views/RestorePassphraseView.swift index d54fbd4a7577fe1f13d1813cf2047f3956eb8832..9eecc341c17383fd45c1a3eea48d810701e91222 100644 --- a/Sources/RestoreFeature/Views/RestorePassphraseView.swift +++ b/Sources/RestoreFeature/Views/RestorePassphraseView.swift @@ -1,67 +1,81 @@ import UIKit import Shared import InputField +import AppResources final class RestorePassphraseView: UIView { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let inputField = InputField() - let stackView = UIStackView() - let continueButton = CapsuleButton() - let cancelButton = CapsuleButton() + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let inputField = InputField() + let stackView = UIStackView() + let continueButton = CapsuleButton() + let cancelButton = CapsuleButton() - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } + init() { + super.init(frame: .zero) + layer.cornerRadius = 40 + backgroundColor = Asset.neutralWhite.color + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - private func setup() { - layer.cornerRadius = 40 - backgroundColor = Asset.neutralWhite.color - layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + setupInput() + setupLabels() + setupButtons() + setupStackView() + } - subtitleLabel.numberOfLines = 0 - titleLabel.textColor = Asset.neutralActive.color - subtitleLabel.textColor = Asset.neutralActive.color + required init?(coder: NSCoder) { nil } - inputField.setup( - style: .regular, - title: "Passphrase", - placeholder: "* * * * * *", - subtitleColor: Asset.neutralDisabled.color - ) + 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: .password + ) + } - titleLabel.text = "Backup password" - titleLabel.textAlignment = .left - titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + private func setupLabels() { + titleLabel.textAlignment = .left + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + titleLabel.text = Localized.Backup.Restore.Passphrase.title - subtitleLabel.text = "Please enter your backup password that you used when you did the backup setup" - subtitleLabel.textAlignment = .left - subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + subtitleLabel.numberOfLines = 0 + subtitleLabel.textAlignment = .left + subtitleLabel.textColor = Asset.neutralActive.color + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + subtitleLabel.text = Localized.Backup.Restore.Passphrase.subtitle + } - continueButton.setStyle(.brandColored) - continueButton.setTitle("Continue", for: .normal) + private func setupButtons() { + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Backup.Passphrase.cancel, for: .normal) - cancelButton.setStyle(.seeThrough) - cancelButton.setTitle("Cancel", for: .normal) + continueButton.isEnabled = false + continueButton.setStyle(.brandColored) + continueButton.setTitle(Localized.Backup.Passphrase.continue, for: .normal) + } - 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 { make in - make.top.equalToSuperview().offset(60) - make.left.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-50) - make.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/RestoreFeature/Views/RestoreProgressView.swift b/Sources/RestoreFeature/Views/RestoreProgressView.swift index 95a471f02bf53bf936ca1cd30d98217bc3afde7c..bde3b25657b8ff55cbdc6288e37f1bed7c0e695e 100644 --- a/Sources/RestoreFeature/Views/RestoreProgressView.swift +++ b/Sources/RestoreFeature/Views/RestoreProgressView.swift @@ -1,87 +1,88 @@ import UIKit import Shared +import AppResources final class RestoreProgressView: UIView { - let progressBarFull = UIView() - let progressBarFiller = UIView() - let progressLabel = UILabel() - let warningLabel = UILabel() - let descriptiveProgressLabel = UILabel() - - init() { - super.init(frame: .zero) - warningLabel.textColor = Asset.neutralDisabled.color - progressLabel.textColor = Asset.neutralDisabled.color - descriptiveProgressLabel.textColor = Asset.neutralDisabled.color - - warningLabel.font = Fonts.Mulish.regular.font(size: 14.0) - progressLabel.font = Fonts.Mulish.regular.font(size: 14.0) - descriptiveProgressLabel.font = Fonts.Mulish.regular.font(size: 14.0) - - descriptiveProgressLabel.textAlignment = .center - - progressBarFull.backgroundColor = Asset.neutralLine.color - progressBarFiller.backgroundColor = Asset.brandPrimary.color - progressBarFull.layer.masksToBounds = true - progressBarFull.layer.cornerRadius = 4 - - warningLabel.numberOfLines = 0 - descriptiveProgressLabel.numberOfLines = 0 - warningLabel.text = "This may take up to 5 mins, please don’t close the app and don’t put in background and don’t close your phone screen" - - addSubview(progressBarFull) - addSubview(progressLabel) - addSubview(warningLabel) - addSubview(descriptiveProgressLabel) - progressBarFull.addSubview(progressBarFiller) - - descriptiveProgressLabel.snp.makeConstraints { make in - make.top.greaterThanOrEqualToSuperview() - make.left.equalToSuperview().offset(42) - make.right.equalToSuperview().offset(-42) - make.bottom.equalTo(progressBarFull.snp.top).offset(-15) - } - - progressBarFull.snp.makeConstraints { make in - make.top.greaterThanOrEqualToSuperview() - make.left.equalToSuperview().offset(42) - make.right.equalToSuperview().offset(-42) - make.centerY.equalToSuperview() - make.height.equalTo(8) - } - - progressBarFiller.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.width.equalTo(0) - make.bottom.equalToSuperview() - } - - progressLabel.snp.makeConstraints { make in - make.top.equalTo(progressBarFull.snp.bottom).offset(15) - make.left.equalToSuperview().offset(42) - make.right.equalToSuperview().offset(-42) - } - - warningLabel.snp.makeConstraints { make in - make.top.equalTo(progressLabel.snp.bottom).offset(15) - make.left.equalToSuperview().offset(42) - make.right.equalToSuperview().offset(-42) - make.bottom.lessThanOrEqualToSuperview() - } + let progressBarFull = UIView() + let progressBarFiller = UIView() + let progressLabel = UILabel() + let warningLabel = UILabel() + let descriptiveProgressLabel = UILabel() + + init() { + super.init(frame: .zero) + warningLabel.textColor = Asset.neutralDisabled.color + progressLabel.textColor = Asset.neutralDisabled.color + descriptiveProgressLabel.textColor = Asset.neutralDisabled.color + + warningLabel.font = Fonts.Mulish.regular.font(size: 14.0) + progressLabel.font = Fonts.Mulish.regular.font(size: 14.0) + descriptiveProgressLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + descriptiveProgressLabel.textAlignment = .center + + progressBarFull.backgroundColor = Asset.neutralLine.color + progressBarFiller.backgroundColor = Asset.brandPrimary.color + progressBarFull.layer.masksToBounds = true + progressBarFull.layer.cornerRadius = 4 + + warningLabel.numberOfLines = 0 + descriptiveProgressLabel.numberOfLines = 0 + warningLabel.text = "This may take up to 5 mins, please don’t close the app and don’t put in background and don’t close your phone screen" + + addSubview(progressBarFull) + addSubview(progressLabel) + addSubview(warningLabel) + addSubview(descriptiveProgressLabel) + progressBarFull.addSubview(progressBarFiller) + + descriptiveProgressLabel.snp.makeConstraints { make in + make.top.greaterThanOrEqualToSuperview() + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + make.bottom.equalTo(progressBarFull.snp.top).offset(-15) } - required init?(coder: NSCoder) { nil } + progressBarFull.snp.makeConstraints { make in + make.top.greaterThanOrEqualToSuperview() + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + make.centerY.equalToSuperview() + make.height.equalTo(8) + } + + progressBarFiller.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.width.equalTo(0) + make.bottom.equalToSuperview() + } + + progressLabel.snp.makeConstraints { make in + make.top.equalTo(progressBarFull.snp.bottom).offset(15) + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + } + + warningLabel.snp.makeConstraints { make in + make.top.equalTo(progressLabel.snp.bottom).offset(15) + make.left.equalToSuperview().offset(42) + make.right.equalToSuperview().offset(-42) + make.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } - func update(downloaded: Float, total: Float) { - let totalkb = String(format: "%.1f kb", total/1000) - let downloadedKb = String(format: "%.1f kb", downloaded/1000) - let percent = String(format: "%.0f", downloaded/total * 100) + func update(downloaded: Float, total: Float) { + let totalkb = String(format: "%.1f kb", total/1000) + let downloadedKb = String(format: "%.1f kb", downloaded/1000) + let percent = String(format: "%.0f", downloaded/total * 100) - progressLabel.text = "Downloaded \(downloadedKb) of \(totalkb) (\(percent)%)" + progressLabel.text = "Downloaded \(downloadedKb) of \(totalkb) (\(percent)%)" - progressBarFiller.snp.updateConstraints { make in - make.width.equalTo(CGFloat(downloaded/total) * progressBarFull.frame.size.width) - } + progressBarFiller.snp.updateConstraints { make in + make.width.equalTo(CGFloat(downloaded/total) * progressBarFull.frame.size.width) } + } } diff --git a/Sources/RestoreFeature/Views/RestoreSFTPView.swift b/Sources/RestoreFeature/Views/RestoreSFTPView.swift new file mode 100644 index 0000000000000000000000000000000000000000..e6595e9f9cd31fe6dcb85447049eb32f3dafdb76 --- /dev/null +++ b/Sources/RestoreFeature/Views/RestoreSFTPView.swift @@ -0,0 +1,84 @@ +import UIKit +import Shared +import InputField +import AppResources + +final class RestoreSFTPView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let hostField = OutlinedInputField() + let usernameField = OutlinedInputField() + let passwordField = OutlinedInputField() + let loginButton = CapsuleButton() + let stackView = UIStackView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.neutralDark.color + titleLabel.text = Localized.AccountRestore.Sftp.title + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSMutableAttributedString( + string: Localized.AccountRestore.Sftp.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + attString.setAttributes( + attributes: [ + .foregroundColor: Asset.neutralDark.color, + .font: Fonts.Mulish.bold.font(size: 12.0) as Any, + .paragraphStyle: paragraph + ], betweenCharacters: "*") + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = attString + + hostField.setup(title: Localized.AccountRestore.Sftp.host) + usernameField.setup(title: Localized.AccountRestore.Sftp.username) + passwordField.setup(title: Localized.AccountRestore.Sftp.password, sensitive: true) + + loginButton.set(style: .brandColored, title: Localized.AccountRestore.Sftp.login) + + stackView.spacing = 30 + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.addArrangedSubview(hostField) + stackView.addArrangedSubview(usernameField) + stackView.addArrangedSubview(passwordField) + stackView.addArrangedSubview(loginButton) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).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().offset(38) + $0.right.equalToSuperview().offset(-38) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/RestoreFeature/Views/RestoreSuccessView.swift b/Sources/RestoreFeature/Views/RestoreSuccessView.swift index 35bdd4c04fc356e32b54a729b642514e504f8c38..ac6b3dea1ee72ec79ccc0d39202c6ab1c25bea1c 100644 --- a/Sources/RestoreFeature/Views/RestoreSuccessView.swift +++ b/Sources/RestoreFeature/Views/RestoreSuccessView.swift @@ -1,78 +1,79 @@ import UIKit import Shared +import AppResources final class RestoreSuccessView: UIView { - let iconImageView = UIImageView() - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let nextButton = CapsuleButton() - - init() { - super.init(frame: .zero) - - iconImageView.contentMode = .center - iconImageView.image = Asset.onboardingSuccess.image - nextButton.set(style: .white, title: Localized.Onboarding.Success.action) - - subtitleLabel.numberOfLines = 0 - subtitleLabel.textColor = Asset.neutralWhite.color - subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) - - addSubview(iconImageView) - addSubview(titleLabel) - addSubview(subtitleLabel) - addSubview(nextButton) - - iconImageView.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide).offset(40) - make.left.equalToSuperview().offset(40) - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(iconImageView.snp.bottom).offset(40) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-90) - } - - subtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(30) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-90) - } - - nextButton.snp.makeConstraints { make in - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-60) - } - - setTitle(Localized.AccountRestore.Success.title) - setSubtitle(Localized.AccountRestore.Success.subtitle) + let iconImageView = UIImageView() + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let nextButton = CapsuleButton() + + init() { + super.init(frame: .zero) + + iconImageView.contentMode = .center + iconImageView.image = Asset.onboardingSuccess.image + nextButton.set(style: .white, title: Localized.Onboarding.Success.action) + + subtitleLabel.numberOfLines = 0 + subtitleLabel.textColor = Asset.neutralWhite.color + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + addSubview(iconImageView) + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(nextButton) + + iconImageView.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide).offset(40) + make.left.equalToSuperview().offset(40) } - required init?(coder: NSCoder) { nil } + titleLabel.snp.makeConstraints { make in + make.top.equalTo(iconImageView.snp.bottom).offset(40) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-90) + } - private func setTitle(_ title: String) { - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.1 + subtitleLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(30) + make.left.equalToSuperview().offset(40) + make.right.equalToSuperview().offset(-90) + } - let attrString = NSMutableAttributedString(string: title) + nextButton.snp.makeConstraints { make in + make.left.equalToSuperview().offset(24) + make.right.equalToSuperview().offset(-24) + make.bottom.equalToSuperview().offset(-60) + } - attrString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 39.0)) - attrString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color) + setTitle(Localized.AccountRestore.Success.title) + setSubtitle(Localized.AccountRestore.Success.subtitle) + } - attrString.addAttribute( - name: .foregroundColor, - value: Asset.neutralBody.color, - betweenCharacters: "#" - ) + required init?(coder: NSCoder) { nil } - titleLabel.numberOfLines = 0 - titleLabel.attributedText = attrString - } + private func setTitle(_ title: String) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.1 - private func setSubtitle(_ subtitle: String?) { - subtitleLabel.text = subtitle - } + let attrString = NSMutableAttributedString(string: title) + + attrString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 39.0)) + attrString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color) + + attrString.addAttribute( + name: .foregroundColor, + value: Asset.neutralBody.color, + betweenCharacters: "#" + ) + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = attrString + } + + private func setSubtitle(_ subtitle: String?) { + subtitleLabel.text = subtitle + } } diff --git a/Sources/RestoreFeature/Views/RestoreView.swift b/Sources/RestoreFeature/Views/RestoreView.swift index ba2e643cafc4dcca652ac7daac8ad14d22f52d15..d4bd3aeb238f38a894755497e1ff7a72bcc49c46 100644 --- a/Sources/RestoreFeature/Views/RestoreView.swift +++ b/Sources/RestoreFeature/Views/RestoreView.swift @@ -1,171 +1,175 @@ import UIKit import Shared -import Models +import CloudFiles +import AppResources final class RestoreView: UIView { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let detailsView = RestoreDetailsView() - let progressView = RestoreProgressView() - - let bottomStackView = UIStackView() - let backButton = CapsuleButton() - let cancelButton = CapsuleButton() - let restoreButton = CapsuleButton() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - subtitleLabel.numberOfLines = 0 - titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) - subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) - titleLabel.textColor = Asset.neutralDark.color - subtitleLabel.textColor = Asset.neutralDark.color - - restoreButton.set(style: .brandColored, title: Localized.AccountRestore.Found.restore) - cancelButton.set(style: .simplestColoredBrand, title: Localized.AccountRestore.Found.cancel) - backButton.set(style: .seeThrough, title: Localized.AccountRestore.NotFound.back) - - bottomStackView.axis = .vertical - - addSubview(titleLabel) - addSubview(subtitleLabel) - addSubview(detailsView) - addSubview(progressView) - addSubview(bottomStackView) - - bottomStackView.addArrangedSubview(restoreButton) - bottomStackView.addArrangedSubview(cancelButton) - bottomStackView.addArrangedSubview(backButton) - - titleLabel.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).offset(20) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-38) - } - - subtitleLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(20) - $0.left.equalToSuperview().offset(38) - $0.right.equalToSuperview().offset(-38) - } - - detailsView.snp.makeConstraints { - $0.top.equalTo(subtitleLabel.snp.bottom).offset(40) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - } - - progressView.snp.makeConstraints { - $0.top.greaterThanOrEqualTo(detailsView.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.lessThanOrEqualTo(bottomStackView.snp.top) - } - - bottomStackView.snp.makeConstraints { - $0.top.greaterThanOrEqualTo(detailsView.snp.bottom).offset(10) - $0.left.equalToSuperview().offset(40) - $0.right.equalToSuperview().offset(-40) - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) - } + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let detailsView = RestoreDetailsView() + let progressView = RestoreProgressView() + + let bottomStackView = UIStackView() + let backButton = CapsuleButton() + let cancelButton = CapsuleButton() + let restoreButton = CapsuleButton() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + subtitleLabel.numberOfLines = 0 + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + titleLabel.textColor = Asset.neutralDark.color + subtitleLabel.textColor = Asset.neutralDark.color + + restoreButton.set(style: .brandColored, title: Localized.AccountRestore.Found.restore) + cancelButton.set(style: .simplestColoredBrand, title: Localized.AccountRestore.Found.cancel) + backButton.set(style: .seeThrough, title: Localized.AccountRestore.NotFound.back) + + bottomStackView.axis = .vertical + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(detailsView) + addSubview(progressView) + addSubview(bottomStackView) + + bottomStackView.addArrangedSubview(restoreButton) + bottomStackView.addArrangedSubview(cancelButton) + bottomStackView.addArrangedSubview(backButton) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(20) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) } - required init?(coder: NSCoder) { nil } + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + } + + detailsView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(40) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + } - func updateFor(step: RestorationStep) { - switch step { - case .idle(let cloudService, let backup): - guard let backup = backup else { - showNoBackupForCloud(named: cloudService.name()) - return - } + progressView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(detailsView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualTo(bottomStackView.snp.top) + } - showBackup(backup, fromCloud: cloudService) + bottomStackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(detailsView.snp.bottom).offset(10) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) + } + } - case .downloading(let downloaded, let total): - restoreButton.isHidden = true - cancelButton.isHidden = true - progressView.isHidden = false + required init?(coder: NSCoder) { nil } - progressView.update(downloaded: downloaded, total: total) - case .wrongPass: - progressView.descriptiveProgressLabel.text = "Incorrect password" + func updateFor(step: Step) { + switch step { + case .idle(let provider, let metadata): + guard let metadata = metadata else { + missingMetadataFor(provider) + return + } - case .failDownload(let error): - progressView.descriptiveProgressLabel.text = error.localizedDescription + displayDetailsFrom(provider, size: metadata.size, lastDate: metadata.lastModified) - case .parsingData: - progressView.descriptiveProgressLabel.text = "Parsing backup data" + case .downloading(let downloaded, let total): + restoreButton.isHidden = true + cancelButton.isHidden = true + progressView.isHidden = false - case .done: - progressView.descriptiveProgressLabel.text = "Done" - } - } + progressView.update(downloaded: downloaded, total: total) + case .wrongPass: + progressView.descriptiveProgressLabel.text = "Incorrect password" - private func showBackup(_ backup: Backup, fromCloud cloud: CloudService) { - titleLabel.text = Localized.AccountRestore.Found.title - subtitleLabel.text = Localized.AccountRestore.Found.subtitle - - detailsView.titleLabel.text = cloud.name() - detailsView.imageView.image = cloud.asset() - - detailsView.dateView.setup( - title: Localized.AccountRestore.Found.date, - value: backup.date.backupStyle(), - hasArrow: false - ) - - detailsView.sizeView.setup( - title: Localized.AccountRestore.Found.size, - value: String(format: "%.1f kb", backup.size/1000), - hasArrow: false - ) - - detailsView.isHidden = false - backButton.isHidden = true - restoreButton.isHidden = false - cancelButton.isHidden = false - progressView.isHidden = true - } + case .failDownload(let error): + progressView.descriptiveProgressLabel.text = error.localizedDescription - private func showNoBackupForCloud(named cloud: String) { - titleLabel.text = Localized.AccountRestore.NotFound.title - subtitleLabel.text = Localized.AccountRestore.NotFound.subtitle(cloud) + case .parsingData: + progressView.descriptiveProgressLabel.text = "Parsing backup data" - restoreButton.isHidden = true - cancelButton.isHidden = true - detailsView.isHidden = true - backButton.isHidden = false - progressView.isHidden = true + case .done: + progressView.descriptiveProgressLabel.text = "Done" } + } + + private func displayDetailsFrom( + _ provider: CloudService, + size: Float, + lastDate: Date + ) { + titleLabel.text = Localized.AccountRestore.Found.title + subtitleLabel.text = Localized.AccountRestore.Found.subtitle + detailsView.titleLabel.text = provider.name() + detailsView.imageView.image = provider.asset() + + detailsView.dateView.setup( + title: Localized.AccountRestore.Found.date, + value: lastDate.backupStyle(), + hasArrow: false + ) + + detailsView.sizeView.setup( + title: Localized.AccountRestore.Found.size, + value: String(format: "%.1f kb", size/1000), + hasArrow: false + ) + + detailsView.isHidden = false + backButton.isHidden = true + restoreButton.isHidden = false + cancelButton.isHidden = false + progressView.isHidden = true + } + + private func missingMetadataFor(_ provider: CloudService) { + titleLabel.text = Localized.AccountRestore.NotFound.title + subtitleLabel.text = Localized.AccountRestore.NotFound.subtitle(provider.name()) + + restoreButton.isHidden = true + cancelButton.isHidden = true + detailsView.isHidden = true + backButton.isHidden = false + progressView.isHidden = true + } } private extension CloudService { - func name() -> String { - switch self { - case .drive: - return Localized.Backup.googleDrive - case .icloud: - return Localized.Backup.iCloud - case .dropbox: - return Localized.Backup.dropbox - case .sftp: - return Localized.Backup.sftp - } + func name() -> String { + switch self { + case .drive: + return Localized.Backup.googleDrive + case .icloud: + return Localized.Backup.iCloud + case .dropbox: + return Localized.Backup.dropbox + case .sftp: + return Localized.Backup.sftp } - - func asset() -> UIImage { - switch self { - case .drive: - return Asset.restoreDrive.image - case .icloud: - return Asset.restoreIcloud.image - case .dropbox: - return Asset.restoreDropbox.image - case .sftp: - return Asset.restoreSFTP.image - } + } + + func asset() -> UIImage { + switch self { + case .drive: + return Asset.restoreDrive.image + case .icloud: + return Asset.restoreIcloud.image + case .dropbox: + return Asset.restoreDropbox.image + case .sftp: + return Asset.restoreSFTP.image } + } } diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift b/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift deleted file mode 100644 index 389cdd46fc0c0136247e09f3dbd900ec265a7858..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Shout -import Socket -import Keychain -import Foundation -import DependencyInjection - -public struct SFTPAuthenticator { - public var authenticate: (String, String, String) throws -> Void - - public func callAsFunction(host: String, username: String, password: String) throws { - try authenticate(host, username, password) - } -} - -extension SFTPAuthenticator { - static let mock = SFTPAuthenticator { host, username, password in - print("^^^ Requested authentication on sftp service.") - print("^^^ Host: \(host)") - print("^^^ Username: \(username)") - print("^^^ Password: \(password)") - } - - static let live = SFTPAuthenticator { host, username, password in - do { - try SSH.connect( - host: host, - port: 22, - username: username, - authMethod: SSHPassword(password)) { ssh in - _ = try ssh.openSftp() - - let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling - try keychain.store(key: .host, value: host) - try keychain.store(key: .pwd, value: password) - try keychain.store(key: .username, value: username) - } - } catch { - if let error = error as? SSHError { - print(error.kind) - print(error.message) - print(error.description) - } else if let error = error as? Socket.Error { - print(error.errorCode) - print(error.description) - print(error.errorReason) - print(error.localizedDescription) - } else { - print(error.localizedDescription) - } - - throw error - } - } -} diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift b/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift deleted file mode 100644 index 6a435df0051a2755f2fb73522f7be92a24c4dd77..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Shout -import Socket -import Keychain -import Foundation -import DependencyInjection - -public typealias SFTPDownloadResult = (Result<Data, Error>) -> Void - -public struct SFTPDownloader { - public var download: (String, @escaping SFTPDownloadResult) -> Void - - public func callAsFunction(path: String, completion: @escaping SFTPDownloadResult) { - download(path, completion) - } -} - -extension SFTPDownloader { - static let mock = SFTPDownloader { path, _ in - print("^^^ Requested backup download on sftp service.") - print("^^^ Path: \(path)") - } - - static let live = SFTPDownloader { path, completion in - DispatchQueue.global().async { - do { - let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling - let host = try keychain.get(key: .host) - let password = try keychain.get(key: .pwd) - let username = try keychain.get(key: .username) - - let ssh = try SSH(host: host!, port: 22) - try ssh.authenticate(username: username!, password: password!) - let sftp = try ssh.openSftp() - - let localURL = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("sftp") - - try sftp.download(remotePath: path, localURL: localURL) - - let data = try Data(contentsOf: localURL) - completion(.success(data)) - } catch { - completion(.failure(error)) - - if var error = error as? SSHError { - print(error.kind) - print(error.message) - print(error.description) - } else { - print(error.localizedDescription) - } - } - } - } -} diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift b/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift deleted file mode 100644 index a27df80ffe8e9be6f41f09185e9962351c44cff9..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Shout -import Socket -import Models -import Keychain -import Foundation -import DependencyInjection - -public typealias SFTPFetchResult = (Result<RestoreSettings?, Error>) -> Void - -public struct SFTPFetcher { - public var fetch: (@escaping SFTPFetchResult) -> Void - - public func callAsFunction(completion: @escaping SFTPFetchResult) { - fetch(completion) - } -} - -extension SFTPFetcher { - static let mock = SFTPFetcher { _ in - print("^^^ Requested backup metadata on sftp service.") - } - - static let live = SFTPFetcher { completion in - DispatchQueue.global().async { - do { - let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling - let host = try keychain.get(key: .host) - let password = try keychain.get(key: .pwd) - let username = try keychain.get(key: .username) - - let ssh = try SSH(host: host!, port: 22) - try ssh.authenticate(username: username!, password: password!) - let sftp = try ssh.openSftp() - - if let files = try? sftp.listFiles(in: "backup"), - let backup = files.filter({ file in file.0 == "backup.xxm" }).first { - completion(.success(.init( - backup: .init( - id: "backup/backup.xxm", - date: backup.value.lastModified, - size: Float(backup.value.size) - ), - cloudService: .sftp - ))) - - return - } - - completion(.success(nil)) - } catch { - if let error = error as? SSHError { - print(error.kind) - print(error.message) - print(error.description) - } else if let error = error as? Socket.Error { - print(error.errorCode) - print(error.description) - print(error.errorReason) - print(error.localizedDescription) - } else { - print(error.localizedDescription) - } - - completion(.failure(error)) - } - } - } -} diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift b/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift deleted file mode 100644 index fee691d1b7e1669226fecfad6ecc37c97e64f9c5..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Shout -import Socket -import Models -import Keychain -import Foundation -import DependencyInjection - -public typealias SFTPUploadResult = (Result<Backup, Error>) -> Void - -public struct SFTPUploader { - public var upload: (URL, @escaping SFTPUploadResult) -> Void - - public func callAsFunction(url: URL, completion: @escaping SFTPUploadResult) { - upload(url, completion) - } -} - -extension SFTPUploader { - static let mock = SFTPUploader( - upload: { url, _ in - print("^^^ Requested upload on sftp service") - print("^^^ URL path: \(url.path)") - } - ) - - static let live = SFTPUploader { url, completion in - DispatchQueue.global().async { - do { - let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling - let host = try keychain.get(key: .host) - let password = try keychain.get(key: .pwd) - let username = try keychain.get(key: .username) - - let ssh = try SSH(host: host!, port: 22) - try ssh.authenticate(username: username!, password: password!) - let sftp = try ssh.openSftp() - - let data = try Data(contentsOf: url) - - if (try? sftp.listFiles(in: "backup")) == nil { - try sftp.createDirectory("backup") - } - - try sftp.upload(data: data, remotePath: "backup/backup.xxm") - - completion(.success(.init( - id: "backup/backup.xxm", - date: Date(), - size: Float(data.count) - ))) - } catch { - if let error = error as? SSHError { - print(error.kind) - print(error.message) - print(error.description) - } else if let error = error as? Socket.Error { - print(error.errorCode) - print(error.description) - print(error.errorReason) - print(error.localizedDescription) - } else { - print(error.localizedDescription) - } - - completion(.failure(error)) - } - } - } -} diff --git a/Sources/SFTPFeature/SFTPController.swift b/Sources/SFTPFeature/SFTPController.swift deleted file mode 100644 index 21bf8ca1f224ad3b5439d619fbd27025a2a96295..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/SFTPController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import HUD -import UIKit -import Combine -import DependencyInjection -import ScrollViewController - -public final class SFTPController: UIViewController { - @Dependency private var hud: HUD - - lazy private var screenView = SFTPView() - lazy private var scrollViewController = ScrollViewController() - - private let completion: () -> Void - private let viewModel = SFTPViewModel() - private var cancellables = Set<AnyCancellable>() - - public init(_ completion: @escaping () -> Void) { - self.completion = completion - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar.customize(translucent: true) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - } - - private func setupScrollView() { - scrollViewController.scrollView.backgroundColor = .white - - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - } - - private func setupBindings() { - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - viewModel.authPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in completion() } - .store(in: &cancellables) - - screenView.hostField - .textPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didEnterHost($0) } - .store(in: &cancellables) - - screenView.usernameField - .textPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didEnterUsername($0) } - .store(in: &cancellables) - - screenView.passwordField - .textPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didEnterPassword($0) } - .store(in: &cancellables) - - viewModel.statePublisher - .receive(on: DispatchQueue.main) - .map(\.isButtonEnabled) - .sink { [unowned self] in screenView.loginButton.isEnabled = $0 } - .store(in: &cancellables) - - screenView.loginButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapLogin() } - .store(in: &cancellables) - } -} diff --git a/Sources/SFTPFeature/SFTPService.swift b/Sources/SFTPFeature/SFTPService.swift deleted file mode 100644 index f1a908564df810ed2aeaa1b4af358b367253f84b..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/SFTPService.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit -import Keychain -import Presentation -import DependencyInjection - -public typealias SFTPAuthorizationParams = (UIViewController, () -> Void) - -public struct SFTPService { - public var isAuthorized: () -> Bool - public var fetchMetadata: SFTPFetcher - public var uploadBackup: SFTPUploader - public var authorizeFlow: (SFTPAuthorizationParams) -> Void - public var authenticate: SFTPAuthenticator - public var downloadBackup: SFTPDownloader -} - -public extension SFTPService { - static var mock = SFTPService( - isAuthorized: { true }, - fetchMetadata: .mock, - uploadBackup: .mock, - authorizeFlow: { (_, completion) in completion() }, - authenticate: .mock, - downloadBackup: .mock - ) - - static var live = SFTPService( - isAuthorized: { - if let keychain = try? DependencyInjection.Container.shared.resolve() as KeychainHandling, - let pwd = try? keychain.get(key: .pwd), - let host = try? keychain.get(key: .host), - let username = try? keychain.get(key: .username) { - return true - } - - return false - }, - fetchMetadata: .live, - uploadBackup: .live , - authorizeFlow: { controller, completion in - var pushPresenter: Presenting = PushPresenter() - pushPresenter.present(SFTPController(completion), from: controller) - }, - authenticate: .live, - downloadBackup: .live - ) -} diff --git a/Sources/SFTPFeature/SFTPView.swift b/Sources/SFTPFeature/SFTPView.swift deleted file mode 100644 index 5653d85bacd6c112d737217dfdb9c8312c631de4..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/SFTPView.swift +++ /dev/null @@ -1,83 +0,0 @@ -import UIKit -import Shared -import InputField - -final class SFTPView: UIView { - let titleLabel = UILabel() - let subtitleLabel = UILabel() - let hostField = OutlinedInputField() - let usernameField = OutlinedInputField() - let passwordField = OutlinedInputField() - let loginButton = CapsuleButton() - let stackView = UIStackView() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - titleLabel.textColor = Asset.neutralDark.color - titleLabel.text = Localized.AccountRestore.Sftp.title - titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) - - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .left - paragraph.lineHeightMultiple = 1.15 - - let attString = NSMutableAttributedString( - string: Localized.AccountRestore.Sftp.subtitle, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ]) - - attString.setAttributes( - attributes: [ - .foregroundColor: Asset.neutralDark.color, - .font: Fonts.Mulish.bold.font(size: 12.0) as Any, - .paragraphStyle: paragraph - ], betweenCharacters: "*") - - subtitleLabel.numberOfLines = 0 - subtitleLabel.attributedText = attString - - hostField.setup(title: Localized.AccountRestore.Sftp.host) - usernameField.setup(title: Localized.AccountRestore.Sftp.username) - passwordField.setup(title: Localized.AccountRestore.Sftp.password, sensitive: true) - - loginButton.set(style: .brandColored, title: Localized.AccountRestore.Sftp.login) - - stackView.spacing = 30 - stackView.axis = .vertical - stackView.distribution = .fillEqually - stackView.addArrangedSubview(hostField) - stackView.addArrangedSubview(usernameField) - stackView.addArrangedSubview(passwordField) - stackView.addArrangedSubview(loginButton) - - addSubview(titleLabel) - addSubview(subtitleLabel) - addSubview(stackView) - - titleLabel.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).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().offset(38) - $0.right.equalToSuperview().offset(-38) - $0.bottom.lessThanOrEqualToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/SFTPFeature/SFTPViewModel.swift b/Sources/SFTPFeature/SFTPViewModel.swift deleted file mode 100644 index e64536bfd96277642d25ddb13f5c81570836a22b..0000000000000000000000000000000000000000 --- a/Sources/SFTPFeature/SFTPViewModel.swift +++ /dev/null @@ -1,77 +0,0 @@ -import HUD -import Combine -import Foundation -import DependencyInjection - -struct SFTPViewState { - var host: String = "" - var username: String = "" - var password: String = "" - var isButtonEnabled: Bool = false -} - -final class SFTPViewModel { - @Dependency private var service: SFTPService - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var statePublisher: AnyPublisher<SFTPViewState, Never> { - stateSubject.eraseToAnyPublisher() - } - - var authPublisher: AnyPublisher<Void, Never> { - authSubject.eraseToAnyPublisher() - } - - private let authSubject = PassthroughSubject<Void, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) - - func didEnterHost(_ string: String) { - stateSubject.value.host = string - validate() - } - - func didEnterUsername(_ string: String) { - stateSubject.value.username = string - validate() - } - - func didEnterPassword(_ string: String) { - stateSubject.value.password = string - validate() - } - - func didTapLogin() { - hudSubject.send(.on) - - let host = stateSubject.value.host - let username = stateSubject.value.username - let password = stateSubject.value.password - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - do { - try self.service.authenticate( - host: host, - username: username, - password: password - ) - - self.hudSubject.send(.none) - self.authSubject.send(()) - } catch { - self.hudSubject.send(.error(.init(with: error))) - } - } - } - - private func validate() { - stateSubject.value.isButtonEnabled = - !stateSubject.value.host.isEmpty && - !stateSubject.value.username.isEmpty && - !stateSubject.value.password.isEmpty - } -} diff --git a/Sources/ScanFeature/Controllers/ScanContainerController.swift b/Sources/ScanFeature/Controllers/ScanContainerController.swift index fb1f9d440421512de2f3034c1b46b5d1fd70b2a3..435710b65d50319e33f4ab0bf1de67f538692f36 100644 --- a/Sources/ScanFeature/Controllers/ScanContainerController.swift +++ b/Sources/ScanFeature/Controllers/ScanContainerController.swift @@ -1,179 +1,191 @@ import UIKit -import Theme import Shared import Combine +import AppCore +import Dependencies +import AppResources +import AppNavigation import DrawerFeature -import DependencyInjection public final class ScanContainerController: UIViewController { - @Dependency private var coordinator: ScanCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var screenView = ScanContainerView() - - private var previousPoint: CGPoint = .zero - private let scanController = ScanController() - private let displayController = ScanDisplayController() - - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public override func loadView() { - view = screenView - screenView.scrollView.delegate = self - - addChild(scanController) - addChild(displayController) - - screenView.scrollView.addSubview(scanController.view) - screenView.scrollView.addSubview(displayController.view) - - scanController.view.snp.makeConstraints { - $0.top.equalTo(screenView) - $0.width.equalTo(screenView) - $0.bottom.equalTo(screenView) - $0.left.equalToSuperview() - $0.right.equalTo(displayController.view.snp.left) - } - - displayController.view.snp.makeConstraints { - $0.top.equalTo(screenView.segmentedControl.snp.bottom) - $0.width.equalTo(scanController.view) - $0.bottom.equalTo(scanController.view) - } - - scanController.didMove(toParent: self) - displayController.didMove(toParent: self) - - screenView.bringSubviewToFront(screenView.segmentedControl) + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = ScanContainerView() + + private let scanController = ScanController() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + private let displayController = ScanDisplayController() + private let pageController = UIPageViewController( + transitionStyle: .scroll, + navigationOrientation: .horizontal + ) + + public override func loadView() { + view = screenView + + addChild(pageController) + screenView.addSubview(pageController.view) + pageController.view.snp.makeConstraints { + $0.top.equalTo(screenView.stackView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalTo(screenView) } - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - screenView.scrollView.contentOffset = previousPoint + pageController.delegate = self + pageController.dataSource = self + pageController.didMove(toParent: self) + pageController.setViewControllers([scanController], direction: .forward, animated: true) + screenView.bringSubviewToFront(screenView.stackView) + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.lightContent) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + + displayController.didTapInfo = { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Scan.Info.title, + subtitle: Localized.Scan.Info.subtitle + ) } - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - previousPoint = screenView.scrollView.contentOffset - screenView.scrollView.contentOffset = .zero + displayController.didTapAddEmail = { [weak self] in + guard let self else { return } + self.navigator.perform(PresentProfileEmail(on: self.navigationController!)) } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.lightContent) - navigationController?.navigationBar.customize(translucent: true) - screenView.scrollView.contentOffset = .zero + displayController.didTapAddPhone = { [weak self] in + guard let self else { return } + self.navigator.perform(PresentProfilePhone(on: self.navigationController!)) } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupBindings() - - displayController.didTapInfo = { [weak self] in - self?.presentInfo( - title: Localized.Scan.Info.title, - subtitle: Localized.Scan.Info.subtitle - ) + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let titleLabel = UILabel() + titleLabel.text = "QR Code" + titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) + titleLabel.textColor = Asset.neutralWhite.color + + let menuButton = UIButton() + menuButton.tintColor = Asset.neutralWhite.color + menuButton.setImage(Asset.chatListMenu.image, for: .normal) + menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) + menuButton.snp.makeConstraints { $0.width.equalTo(50) } + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) + ) + } + + private func setupBindings() { + screenView + .leftButton + .publisher(for: .touchUpInside) + .sink { [unowned self] _ in + screenView.leftButton.set(selected: true) + screenView.rightButton.set(selected: false) + pageController.setViewControllers([scanController], direction: .reverse, animated: true, completion: nil) + }.store(in: &cancellables) + + screenView + .rightButton + .publisher(for: .touchUpInside) + .sink { [unowned self] _ in + screenView.leftButton.set(selected: false) + screenView.rightButton.set(selected: true) + pageController.setViewControllers([displayController], direction: .forward, animated: true, completion: nil) + }.store(in: &cancellables) + } + + @objc private func didTapMenu() { + navigator.perform(PresentMenu(currentItem: .scan, from: self)) + } + + private func presentInfo(title: String, subtitle: String) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } +} - displayController.didTapAddEmail = { [weak self] in - guard let self = self else { return } - self.coordinator.toEmail(from: self) - } - - displayController.didTapAddPhone = { [weak self] in - guard let self = self else { return } - self.coordinator.toPhone(from: self) - } - } - - private func setupNavigationBar() { - navigationItem.backButtonTitle = "" - - let titleLabel = UILabel() - titleLabel.text = "QR Code" - titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) - titleLabel.textColor = Asset.neutralWhite.color - - let menuButton = UIButton() - menuButton.tintColor = Asset.neutralWhite.color - menuButton.setImage(Asset.chatListMenu.image, for: .normal) - menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) - menuButton.snp.makeConstraints { $0.width.equalTo(50) } - - navigationItem.leftBarButtonItem = UIBarButtonItem( - customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) - ) - } - - private func setupBindings() { - screenView.segmentedControl.leftButton - .publisher(for: .touchUpInside) - .sink { [unowned self] _ in screenView.scrollView.setContentOffset(.zero, animated: true) } - .store(in: &cancellables) - - screenView.segmentedControl.rightButton - .publisher(for: .touchUpInside) - .sink { [unowned self] _ in - let point = CGPoint(x: screenView.frame.width, y: 0.0) - screenView.scrollView.setContentOffset(point, animated: true) - }.store(in: &cancellables) - } - - @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) - } - - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - let percentage = scrollView.contentOffset.x / view.frame.width +extension ScanContainerController: UIPageViewControllerDataSource { + public func pageViewController( + _ pageViewController: UIPageViewController, + viewControllerAfter viewController: UIViewController + ) -> UIViewController? { + guard viewController != displayController else { return nil } + return displayController + } + + public func pageViewController( + _ pageViewController: UIPageViewController, + viewControllerBefore viewController: UIViewController + ) -> UIViewController? { + guard viewController != scanController else { return nil } + return scanController + } +} - scanController.view.alpha = 1 - percentage - displayController.view.alpha = percentage - screenView.segmentedControl.updateLeftConstraint(percentage) - } - private func presentInfo(title: String, subtitle: String) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) +extension ScanContainerController: UIPageViewControllerDelegate { + public func pageViewController( + _ pageViewController: UIPageViewController, + didFinishAnimating finished: Bool, + previousViewControllers: [UIViewController], + transitionCompleted completed: Bool + ) { + guard finished, completed else { return } + + if previousViewControllers.contains(scanController) { + screenView.leftButton.set(selected: false) + screenView.rightButton.set(selected: true) + } else { + screenView.leftButton.set(selected: true) + screenView.rightButton.set(selected: false) } + } } - -extension ScanContainerController: UIScrollViewDelegate {} diff --git a/Sources/ScanFeature/Controllers/ScanController.swift b/Sources/ScanFeature/Controllers/ScanController.swift index 2a1b99aa6da269007f9692b5ebb0b3b81e6ca377..e21b9d035bf0d664a06d7f2873244747614f2bf9 100644 --- a/Sources/ScanFeature/Controllers/ScanController.swift +++ b/Sources/ScanFeature/Controllers/ScanController.swift @@ -1,117 +1,123 @@ import UIKit import Shared import Combine -import Permissions +import AppCore +import AppNavigation import CombineSchedulers -import DependencyInjection +import PermissionsFeature +import ComposableArchitecture final class ScanController: UIViewController { - @Dependency private var coordinator: ScanCoordinating - @Dependency private var permissions: PermissionHandling - - lazy private var screenView = ScanView() - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - private var status: ScanStatus? - private let camera: CameraType - private let viewModel = ScanViewModel() - private var cancellables = Set<AnyCancellable>() - - init(camera: CameraType = Camera()) { - #if DEBUG - self.camera = MockCamera() - #else - self.camera = camera - #endif - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - override func loadView() { - view = screenView - } - - override func viewDidLoad() { - super.viewDidLoad() - screenView.layer.insertSublayer(camera.previewLayer, at: 0) - setupBindings() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - camera.previewLayer.frame = screenView.bounds + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.permissions) var permissions: PermissionsManager + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + private lazy var screenView = ScanView() + + private var status: ScanStatus? + private let camera: CameraType + private let viewModel = ScanViewModel() + private var cancellables = Set<AnyCancellable>() + + init(camera: CameraType = Camera()) { +#if DEBUG + self.camera = MockCamera() +#else + self.camera = camera +#endif + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func loadView() { + view = screenView + } + + override func viewDidLoad() { + super.viewDidLoad() + screenView.layer.insertSublayer(camera.previewLayer, at: 0) + setupBindings() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + camera.previewLayer.frame = screenView.bounds + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.resetScanner() + startCamera() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + bgQueue.schedule { [weak self] in + guard let self else { return } + self.camera.stop() } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - viewModel.resetScanner() - startCamera() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - self.camera.stop() + } + + private func startCamera() { + permissions.camera.request { [weak self] granted in + guard let self else { return } + + if granted { + self.bgQueue.schedule { + self.camera.start() } - } - - private func startCamera() { - permissions.requestCamera { [weak self] granted in - guard let self = self else { return } - - if granted { - self.backgroundScheduler.schedule { - self.camera.start() - } - } else { - DispatchQueue.main.async { - self.status = .failed(.cameraPermission) - self.screenView.update(with: .failed(.cameraPermission)) - } - } + } else { + DispatchQueue.main.async { + self.status = .failed(.cameraPermission) + self.screenView.update(with: .failed(.cameraPermission)) } + } } - - private func setupBindings() { - viewModel.contactPublisher - .receive(on: DispatchQueue.main) - .delay(for: 1, scheduler: DispatchQueue.main) - .sink { [unowned self] in coordinator.toContact($0, from: self) } - .store(in: &cancellables) - - viewModel.state - .map(\.status) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - status = $0 - screenView.update(with: $0) - }.store(in: &cancellables) - - screenView.actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch status { - case .failed(.cameraPermission): - guard let url = URL(string: UIApplication.openSettingsURLString) else { return } - UIApplication.shared.open(url, options: [:]) - case .failed(.requestOpened): - coordinator.toRequests(from: self) - case .failed(.alreadyFriends): - coordinator.toContacts(from: self) - default: - break - } - }.store(in: &cancellables) - - camera - .dataPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] data in viewModel.didScanData(data) } - .store(in: &cancellables) - } + } + + private func setupBindings() { + viewModel + .contactPublisher + .receive(on: DispatchQueue.main) + .delay(for: 1, scheduler: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentContact(contact: $0, on: navigationController!)) + }.store(in: &cancellables) + + viewModel + .statePublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + status = $0 + screenView.update(with: $0) + }.store(in: &cancellables) + + screenView + .actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch status { + case .failed(.cameraPermission): + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url, options: [:]) + case .failed(.requestOpened): + navigator.perform(PresentRequests(on: navigationController!)) + case .failed(.alreadyFriends): + navigator.perform(PresentContactList(on: navigationController!)) + default: + break + } + }.store(in: &cancellables) + + camera + .dataPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didScanData($0) + }.store(in: &cancellables) + } } diff --git a/Sources/ScanFeature/Controllers/ScanDisplayController.swift b/Sources/ScanFeature/Controllers/ScanDisplayController.swift index e373d4de40ca31bae004de3d80c85702d538b604..c439855ca678d594657c86a08896b7df2bbafa2d 100644 --- a/Sources/ScanFeature/Controllers/ScanDisplayController.swift +++ b/Sources/ScanFeature/Controllers/ScanDisplayController.swift @@ -2,7 +2,7 @@ import UIKit import Combine final class ScanDisplayController: UIViewController { - lazy private var screenView = ScanDisplayView() + private lazy var screenView = ScanDisplayView() private let viewModel = ScanDisplayViewModel() private var cancellables = Set<AnyCancellable>() diff --git a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift b/Sources/ScanFeature/Coordinator/ScanCoordinator.swift deleted file mode 100644 index 98e605775ccdf54b5ffda4b0a6ad0183a5c15fe1..0000000000000000000000000000000000000000 --- a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift +++ /dev/null @@ -1,88 +0,0 @@ -import UIKit -import Models -import XXModels -import MenuFeature -import Presentation -import ContactFeature - -public protocol ScanCoordinating { - func toEmail(from: UIViewController) - func toPhone(from: UIViewController) - func toContacts(from: UIViewController) - func toRequests(from: UIViewController) - func toSideMenu(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) -} - -public struct ScanCoordinator: ScanCoordinating { - var pushPresenter: Presenting = PushPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter(mode: .replaceLast) - - var emailFactory: () -> UIViewController - var phoneFactory: () -> UIViewController - var contactsFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - - public init( - emailFactory: @escaping () -> UIViewController, - phoneFactory: @escaping () -> UIViewController, - contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController - ) { - self.emailFactory = emailFactory - self.phoneFactory = phoneFactory - self.contactFactory = contactFactory - self.contactsFactory = contactsFactory - self.requestsFactory = requestsFactory - self.sideMenuFactory = sideMenuFactory - } -} - -public extension ScanCoordinator { - func toContact( - _ contact: Contact, - from parent: UIViewController - ) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - func toDrawer( - _ drawer: UIViewController, - from parent: UIViewController - ) { - bottomPresenter.present(drawer, from: parent) - } - - func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - replacePresenter.present(screen, from: parent) - } - - func toContacts(from parent: UIViewController) { - let screen = contactsFactory() - replacePresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.scan, parent) - sidePresenter.present(screen, from: parent) - } - - func toEmail(from parent: UIViewController) { - let screen = emailFactory() - pushPresenter.present(screen, from: parent) - } - - func toPhone(from parent: UIViewController) { - let screen = phoneFactory() - pushPresenter.present(screen, from: parent) - } -} diff --git a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift index df4560653bbf5bd8f342148a3847ddef9f383d97..75410119075162b70a45a95e8622b36083bb637e 100644 --- a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift +++ b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift @@ -1,9 +1,11 @@ import UIKit +import Shared import Combine import Defaults -import Countries -import Integration -import DependencyInjection +import XXClient +import AppCore +import Dependencies +import XXMessengerClient struct ScanDisplayViewState: Equatable { var image: CIImage? @@ -14,12 +16,13 @@ struct ScanDisplayViewState: Equatable { } final class ScanDisplayViewModel { - @Dependency private var session: SessionType + @Dependency(\.app.messenger) var messenger: Messenger - @KeyObject(.email, defaultValue: nil) private var email: String? - @KeyObject(.phone, defaultValue: nil) private var phone: String? - @KeyObject(.sharingEmail, defaultValue: false) private var sharingEmail: Bool - @KeyObject(.sharingPhone, defaultValue: false) private var sharingPhone: Bool + @KeyObject(.email, defaultValue: nil) var email: String? + @KeyObject(.phone, defaultValue: nil) var phone: String? + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool var statePublisher: AnyPublisher<ScanDisplayViewState, Never> { stateSubject.eraseToAnyPublisher() @@ -58,7 +61,21 @@ final class ScanDisplayViewModel { func generateQR() { guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return } - filter.setValue(session.myQR, forKey: "inputMessage") + var facts: [Fact] = [.init(type: .username, value: username!)] + + if sharingPhone { + facts.append(.init(type: .phone, value: phone!)) + } + + if sharingEmail { + facts.append(.init(type: .email, value: email!)) + } + + let e2e = messenger.e2e.get()! + var contact = e2e.getContact() + try! contact.setFacts(facts) + + filter.setValue(contact.data, forKey: "inputMessage") let transform = CGAffineTransform(scaleX: 5, y: 5) if let output = filter.outputImage?.transformed(by: transform) { diff --git a/Sources/ScanFeature/ViewModels/ScanViewModel.swift b/Sources/ScanFeature/ViewModels/ScanViewModel.swift index b940226d75390dc33a79118d44ca74ca4cbe3240..d69cbd692028a4485b276e75e7209d85301b087b 100644 --- a/Sources/ScanFeature/ViewModels/ScanViewModel.swift +++ b/Sources/ScanFeature/ViewModels/ScanViewModel.swift @@ -1,106 +1,98 @@ import Shared -import Models +import AppCore import Combine import XXModels +import XXClient import Foundation -import Integration -import CombineSchedulers -import DependencyInjection +import AppResources +import Dependencies +import ReportingFeature enum ScanStatus: Equatable { - case reading - case processing - case success - case failed(ScanError) + case reading + case processing + case success + case failed(ScanError) } enum ScanError: Equatable { - case requestOpened - case unknown(String) - case cameraPermission - case alreadyFriends(String) -} - -struct ScanViewState: Equatable { - var status: ScanStatus = .reading + case requestOpened + case unknown(String) + case cameraPermission + case alreadyFriends(String) } final class ScanViewModel { - @Dependency private var session: SessionType + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.reportingStatus) var reportingStatus: ReportingStatus - var backgroundScheduler: AnySchedulerOf<DispatchQueue> - = DispatchQueue.global().eraseToAnyScheduler() + var contactPublisher: AnyPublisher<XXModels.Contact, Never> { + contactSubject.eraseToAnyPublisher() + } - var contactPublisher: AnyPublisher<Contact, Never> { contactRelay.eraseToAnyPublisher() } - private let contactRelay = PassthroughSubject<Contact, Never>() + var statePublisher: AnyPublisher<ScanStatus, Never> { + stateSubject.eraseToAnyPublisher() + } - var state: AnyPublisher<ScanViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ScanViewState, Never>(.init()) + private let contactSubject = PassthroughSubject<XXModels.Contact, Never>() + private let stateSubject = CurrentValueSubject<ScanStatus, Never>(.reading) - func resetScanner() { - stateRelay.value.status = .reading - } + func resetScanner() { + stateSubject.send(.reading) + } - func didScanData(_ data: Data) { - guard stateRelay.value.status == .reading else { return } - stateRelay.value.status = .processing - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - guard let usernameAndId = try self.verifyScanned(data) else { - self.stateRelay.value.status = .failed(.unknown(Localized.Scan.Error.general)) - return - } - - - - if let previouslyAdded = try? self.session.dbManager.fetchContacts(.init(id: [usernameAndId.1])).first { - var error = ScanError.unknown(Localized.Scan.Error.general) - - switch previouslyAdded.authStatus { - case .friend: - error = .alreadyFriends(usernameAndId.0) - case .requested, .verified: - error = .requestOpened - default: - break - } - - self.stateRelay.value.status = .failed(error) - return - } - - let contact = Contact( - id: usernameAndId.1, - marshaled: data, - username: usernameAndId.0, - email: try? self.session.extract(fact: .email, from: data), - phone: try? self.session.extract(fact: .phone, from: data), - nickname: nil, - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - ) - - self.succeed(with: contact) - } catch { - self.stateRelay.value.status = .failed(.unknown(Localized.Scan.Error.invalid)) - } - } - } + func didScanData(_ data: Data) { + guard stateSubject.value == .reading else { return } + stateSubject.send(.processing) - private func verifyScanned(_ data: Data) throws -> (String, Data)? { - guard let username = try session.extract(fact: .username, from: data), - let id = session.getId(from: data) else { return nil } + let user = XXClient.Contact.live(data) - return (username, id) + guard let uid = try? user.getId(), + let facts = try? user.getFacts(), + let username = facts.first(where: { $0.type == .username })?.value else { + let errorTitle = Localized.Scan.Error.invalid + stateSubject.send(.failed(.unknown(errorTitle))) + return } - private func succeed(with contact: Contact) { - stateRelay.value.status = .success - contactRelay.send(contact) + let email = facts.first { $0.type == .email }?.value + let phone = facts.first { $0.type == .phone }?.value + + if let alreadyContact = try? dbManager.getDB().fetchContacts(.init(id: [uid])).first { + if alreadyContact.isBlocked, reportingStatus.isEnabled() { + stateSubject.send(.failed(.unknown("You previously blocked this user."))) + return + } + + if alreadyContact.isBanned, reportingStatus.isEnabled() { + stateSubject.send(.failed(.unknown("This user was banned."))) + return + } + + if alreadyContact.authStatus == .friend { + stateSubject.send(.failed(.alreadyFriends(username))) + } else if [.requested, .verified].contains(alreadyContact.authStatus) { + stateSubject.send(.failed(.requestOpened)) + } else { + let generalErrorTitle = Localized.Scan.Error.general + stateSubject.send(.failed(.unknown(generalErrorTitle))) + } + + return } + + stateSubject.send(.success) + contactSubject.send(.init( + id: uid, + marshaled: data, + username: username, + email: email, + phone: phone, + nickname: nil, + photo: nil, + authStatus: .stranger, + isRecent: false, + createdAt: Date() + )) + } } diff --git a/Sources/ScanFeature/Views/AttributeSwitcher.swift b/Sources/ScanFeature/Views/AttributeSwitcher.swift index 4bc957c6816dfc1c36b50ed5c47e46d6cf0180f5..464979a68754855cdd8b1c1f225acd2d66c4cb7e 100644 --- a/Sources/ScanFeature/Views/AttributeSwitcher.swift +++ b/Sources/ScanFeature/Views/AttributeSwitcher.swift @@ -1,102 +1,103 @@ import UIKit import Shared +import AppResources final class AttributeSwitcher: UIView { - struct State { - var content: String - var isVisible: Bool + struct State { + var content: String + var isVisible: Bool + } + + private let titleLabel = UILabel() + private let contentLabel = UILabel() + private let stackView = UIStackView() + private(set) var switcherView = UISwitch() + private let verticalStackView = UIStackView() + + private(set) var addButton: UIControl = { + let label = UILabel() + let icon = UIImageView() + let control = UIControl() + + icon.image = Asset.scanAdd.image + label.text = Localized.Scan.Display.Share.add + label.textColor = Asset.brandPrimary.color + + control.addSubview(icon) + control.addSubview(label) + + icon.snp.makeConstraints { + $0.left.equalToSuperview() + $0.top.equalToSuperview() + $0.bottom.equalToSuperview() + $0.width.equalTo(icon.snp.height) } - private let titleLabel = UILabel() - private let contentLabel = UILabel() - private let stackView = UIStackView() - private(set) var switcherView = UISwitch() - private let verticalStackView = UIStackView() - - private(set) var addButton: UIControl = { - let label = UILabel() - let icon = UIImageView() - let control = UIControl() - - icon.image = Asset.scanAdd.image - label.text = Localized.Scan.Display.Share.add - label.textColor = Asset.brandPrimary.color - - control.addSubview(icon) - control.addSubview(label) - - icon.snp.makeConstraints { - $0.left.equalToSuperview() - $0.top.equalToSuperview() - $0.bottom.equalToSuperview() - $0.width.equalTo(icon.snp.height) - } - - label.snp.makeConstraints { - $0.left.equalTo(icon.snp.right).offset(5) - $0.top.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + label.snp.makeConstraints { + $0.left.equalTo(icon.snp.right).offset(5) + $0.top.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } - return control - }() + return control + }() - public init() { - super.init(frame: .zero) + public init() { + super.init(frame: .zero) - contentLabel.textColor = Asset.neutralActive.color - titleLabel.textColor = Asset.neutralWeak.color - switcherView.onTintColor = Asset.brandPrimary.color + contentLabel.textColor = Asset.neutralActive.color + titleLabel.textColor = Asset.neutralWeak.color + switcherView.onTintColor = Asset.brandPrimary.color - contentLabel.numberOfLines = 0 - contentLabel.font = Fonts.Mulish.regular.font(size: 16.0) - titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) + contentLabel.numberOfLines = 0 + contentLabel.font = Fonts.Mulish.regular.font(size: 16.0) + titleLabel.font = Fonts.Mulish.bold.font(size: 12.0) - addSubview(stackView) + addSubview(stackView) - verticalStackView.spacing = 5 - verticalStackView.axis = .vertical - verticalStackView.addArrangedSubview(titleLabel) - verticalStackView.addArrangedSubview(contentLabel) + verticalStackView.spacing = 5 + verticalStackView.axis = .vertical + verticalStackView.addArrangedSubview(titleLabel) + verticalStackView.addArrangedSubview(contentLabel) - switcherView.setContentCompressionResistancePriority(.required, for: .vertical) - switcherView.setContentCompressionResistancePriority(.required, for: .horizontal) + switcherView.setContentCompressionResistancePriority(.required, for: .vertical) + switcherView.setContentCompressionResistancePriority(.required, for: .horizontal) - stackView.addArrangedSubview(verticalStackView) - stackView.addArrangedSubview(FlexibleSpace()) + stackView.addArrangedSubview(verticalStackView) + stackView.addArrangedSubview(FlexibleSpace()) - let otherHStack = UIStackView() - otherHStack.addArrangedSubview(addButton) - otherHStack.addArrangedSubview(switcherView) + let otherHStack = UIStackView() + otherHStack.addArrangedSubview(addButton) + otherHStack.addArrangedSubview(switcherView) - let otherVStack = UIStackView() - otherVStack.axis = .vertical - otherVStack.addArrangedSubview(otherHStack) - otherVStack.addArrangedSubview(FlexibleSpace()) + let otherVStack = UIStackView() + otherVStack.axis = .vertical + otherVStack.addArrangedSubview(otherHStack) + otherVStack.addArrangedSubview(FlexibleSpace()) - stackView.addArrangedSubview(otherVStack) + stackView.addArrangedSubview(otherVStack) - stackView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func setup(state: State?, title: String) { - titleLabel.text = title + func setup(state: State?, title: String) { + titleLabel.text = title - guard let state = state else { - addButton.isHidden = false - switcherView.isHidden = true - contentLabel.text = Localized.Scan.Display.Share.notAdded - return - } - - addButton.isHidden = true - switcherView.isHidden = false - switcherView.isOn = state.isVisible - contentLabel.text = state.isVisible ? state.content : Localized.Scan.Display.Share.hidden + guard let state = state else { + addButton.isHidden = false + switcherView.isHidden = true + contentLabel.text = Localized.Scan.Display.Share.notAdded + return } + + addButton.isHidden = true + switcherView.isHidden = false + switcherView.isOn = state.isVisible + contentLabel.text = state.isVisible ? state.content : Localized.Scan.Display.Share.hidden + } } diff --git a/Sources/ScanFeature/Views/ScanContainerView.swift b/Sources/ScanFeature/Views/ScanContainerView.swift index 7e5eeee37a1a32765a33735ec8e1c65a05473dd5..78432064b4d1b02a22629b81e462e4239a14b322 100644 --- a/Sources/ScanFeature/Views/ScanContainerView.swift +++ b/Sources/ScanFeature/Views/ScanContainerView.swift @@ -1,28 +1,37 @@ import UIKit import Shared +import AppResources final class ScanContainerView: UIView { - let scrollView = UIScrollView() - let segmentedControl = ScanSegmentedControl() - - init() { - super.init(frame: .zero) - - backgroundColor = Asset.neutralDark.color - 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) - } + let stackView = UIStackView() + let leftButton = ScanSegmentedControlButton() + let rightButton = ScanSegmentedControlButton() + + init() { + super.init(frame: .zero) + + backgroundColor = Asset.neutralDark.color + + leftButton.set(selected: true) + rightButton.set(selected: false) + leftButton.imageView.image = Asset.scanScan.image + rightButton.imageView.image = Asset.scanQr.image + leftButton.titleLabel.text = Localized.Scan.SegmentedControl.left + rightButton.titleLabel.text = Localized.Scan.SegmentedControl.right + + stackView.distribution = .fillEqually + stackView.addArrangedSubview(leftButton) + stackView.addArrangedSubview(rightButton) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(10) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + $0.height.equalTo(60) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/ScanFeature/Views/ScanDisplayShareView.swift b/Sources/ScanFeature/Views/ScanDisplayShareView.swift index acad98f7cab2ffd36c0b210737c17cd26dc21904..87beade8fdb98b3d6e07040d71ea072f27588234 100644 --- a/Sources/ScanFeature/Views/ScanDisplayShareView.swift +++ b/Sources/ScanFeature/Views/ScanDisplayShareView.swift @@ -2,197 +2,198 @@ import UIKit import Shared import SnapKit import Combine +import AppResources final class ScanDisplayShareView: UIView { - enum Action { - case info - case addEmail - case addPhone - case toggleEmail - case togglePhone + enum Action { + case info + case addEmail + case addPhone + case toggleEmail + case togglePhone + } + + private var isExpanded = false { + didSet { updateBottomConstraint() } + } + + private let upperView = UIView() + private let lowerView = UIView() + private var bottomConstraint: Constraint? + + private let imageView = UIImageView() + private let titleView = TextWithInfoView() + private let emailView = AttributeSwitcher() + private let phoneView = AttributeSwitcher() + private var cancellables = Set<AnyCancellable>() + + private var currentConstraintConstant: CGFloat = 0.0 { + didSet { bottomConstraint?.update(offset: currentConstraintConstant) } + } + + private var bottomConstraintExpanded: CGFloat { + -lowerView.frame.height + } + + private var bottomConstraintNotExpanded: CGFloat { + 0 + } + + var actionPublisher: AnyPublisher<Action, Never> { + actionSubject.eraseToAnyPublisher() + } + + private let actionSubject = PassthroughSubject<Action, Never>() + + init() { + super.init(frame: .zero) + + upperView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))) + lowerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))) + + layer.cornerRadius = 30 + imageView.image = Asset.scanDropdown.image + backgroundColor = Asset.neutralWhite.color + clipsToBounds = true + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + addSubview(upperView) + addSubview(lowerView) + + upperView.addSubview(imageView) + upperView.addSubview(titleView) + lowerView.addSubview(emailView) + lowerView.addSubview(phoneView) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineBreakMode = .byWordWrapping + + titleView.setup( + text: Localized.Scan.Display.Share.title, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraphStyle + ], + didTapInfo: { [weak self] in self?.actionSubject.send(.info) } + ) + + emailView.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in actionSubject.send(.toggleEmail) } + .store(in: &cancellables) + + phoneView.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in actionSubject.send(.togglePhone) } + .store(in: &cancellables) + + emailView.addButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in actionSubject.send(.addEmail) } + .store(in: &cancellables) + + phoneView.addButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in actionSubject.send(.addPhone) } + .store(in: &cancellables) + + emailView.setup(state: nil, title: Localized.Scan.Display.Share.email) + phoneView.setup(state: nil, title: Localized.Scan.Display.Share.phone) + emailView.alpha = 0.0 + phoneView.alpha = 0.0 + + imageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.centerX.equalToSuperview() } - - private var isExpanded = false { - didSet { updateBottomConstraint() } - } - - private let upperView = UIView() - private let lowerView = UIView() - private var bottomConstraint: Constraint? - - private let imageView = UIImageView() - private let titleView = TextWithInfoView() - private let emailView = AttributeSwitcher() - private let phoneView = AttributeSwitcher() - private var cancellables = Set<AnyCancellable>() - - private var currentConstraintConstant: CGFloat = 0.0 { - didSet { bottomConstraint?.update(offset: currentConstraintConstant) } + + titleView.snp.makeConstraints { + $0.top.equalTo(imageView.snp.bottom).offset(10) + $0.left.equalToSuperview().offset(40) + $0.right.lessThanOrEqualToSuperview().offset(-40) + $0.centerY.equalToSuperview() } - - private var bottomConstraintExpanded: CGFloat { - -lowerView.frame.height - } - - private var bottomConstraintNotExpanded: CGFloat { - 0 - } - - var actionPublisher: AnyPublisher<Action, Never> { - actionSubject.eraseToAnyPublisher() + + emailView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) } - - private let actionSubject = PassthroughSubject<Action, Never>() - - init() { - super.init(frame: .zero) - - upperView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))) - lowerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))) - - layer.cornerRadius = 30 - imageView.image = Asset.scanDropdown.image - backgroundColor = Asset.neutralWhite.color - clipsToBounds = true - layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - - addSubview(upperView) - addSubview(lowerView) - - upperView.addSubview(imageView) - upperView.addSubview(titleView) - lowerView.addSubview(emailView) - lowerView.addSubview(phoneView) - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineBreakMode = .byWordWrapping - - titleView.setup( - text: Localized.Scan.Display.Share.title, - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraphStyle - ], - didTapInfo: { [weak self] in self?.actionSubject.send(.info) } - ) - - emailView.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in actionSubject.send(.toggleEmail) } - .store(in: &cancellables) - - phoneView.switcherView - .publisher(for: .valueChanged) - .sink { [unowned self] in actionSubject.send(.togglePhone) } - .store(in: &cancellables) - - emailView.addButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in actionSubject.send(.addEmail) } - .store(in: &cancellables) - - phoneView.addButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in actionSubject.send(.addPhone) } - .store(in: &cancellables) - - emailView.setup(state: nil, title: Localized.Scan.Display.Share.email) - phoneView.setup(state: nil, title: Localized.Scan.Display.Share.phone) - emailView.alpha = 0.0 - phoneView.alpha = 0.0 - - imageView.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.centerX.equalToSuperview() - } - - titleView.snp.makeConstraints { - $0.top.equalTo(imageView.snp.bottom).offset(10) - $0.left.equalToSuperview().offset(40) - $0.right.lessThanOrEqualToSuperview().offset(-40) - $0.centerY.equalToSuperview() - } - - emailView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview().offset(40) - $0.right.equalToSuperview().offset(-40) - } - - phoneView.snp.makeConstraints { - $0.top.equalTo(emailView.snp.bottom).offset(25) - $0.left.equalToSuperview().offset(40) - $0.right.equalToSuperview().offset(-40) - $0.bottom.equalToSuperview().offset(-40) - } - - upperView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - bottomConstraint = $0.bottom - .equalTo(safeAreaLayoutGuide) - .constraint - } - - lowerView.snp.makeConstraints { - $0.top.equalTo(upperView.snp.bottom).offset(-30) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - } + + phoneView.snp.makeConstraints { + $0.top.equalTo(emailView.snp.bottom).offset(25) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalToSuperview().offset(-40) } - - required init?(coder: NSCoder) { nil } - - func setup(email state: AttributeSwitcher.State?) { - emailView.setup(state: state, title: Localized.Scan.Display.Share.email) + + upperView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + bottomConstraint = $0.bottom + .equalTo(safeAreaLayoutGuide) + .constraint } - - func setup(phone state: AttributeSwitcher.State?) { - phoneView.setup(state: state, title: Localized.Scan.Display.Share.phone) + + lowerView.snp.makeConstraints { + $0.top.equalTo(upperView.snp.bottom).offset(-30) + $0.left.equalToSuperview() + $0.right.equalToSuperview() } - - @objc private func didPan(_ sender: UIPanGestureRecognizer) { - switch sender.state { - case .began, .changed: - let isUpwards = sender.translation(in: self).y < 0 - let result = currentConstraintConstant + sender.translation(in: self).y - - if isUpwards { - currentConstraintConstant = max(bottomConstraintExpanded, result) - } else { - currentConstraintConstant = min(bottomConstraintNotExpanded, result) - } - - let currentMinusExpanded = currentConstraintConstant - bottomConstraintExpanded - let notExpandedMinusExpanded = bottomConstraintNotExpanded - bottomConstraintExpanded - let alpha = 1 - (currentMinusExpanded / abs(notExpandedMinusExpanded)) - emailView.alpha = alpha - phoneView.alpha = alpha - - case .cancelled, .ended, .failed: - let currentMinusExpanded = currentConstraintConstant - bottomConstraintExpanded - let notExpandedMinusExpanded = bottomConstraintNotExpanded - bottomConstraintExpanded - let percentage = currentMinusExpanded / abs(notExpandedMinusExpanded) - isExpanded = percentage < 0.5 - - case .possible: - break - @unknown default: - break - } + } + + required init?(coder: NSCoder) { nil } + + func setup(email state: AttributeSwitcher.State?) { + emailView.setup(state: state, title: Localized.Scan.Display.Share.email) + } + + func setup(phone state: AttributeSwitcher.State?) { + phoneView.setup(state: state, title: Localized.Scan.Display.Share.phone) + } + + @objc private func didPan(_ sender: UIPanGestureRecognizer) { + switch sender.state { + case .began, .changed: + let isUpwards = sender.translation(in: self).y < 0 + let result = currentConstraintConstant + sender.translation(in: self).y + + if isUpwards { + currentConstraintConstant = max(bottomConstraintExpanded, result) + } else { + currentConstraintConstant = min(bottomConstraintNotExpanded, result) + } + + let currentMinusExpanded = currentConstraintConstant - bottomConstraintExpanded + let notExpandedMinusExpanded = bottomConstraintNotExpanded - bottomConstraintExpanded + let alpha = 1 - (currentMinusExpanded / abs(notExpandedMinusExpanded)) + emailView.alpha = alpha + phoneView.alpha = alpha + + case .cancelled, .ended, .failed: + let currentMinusExpanded = currentConstraintConstant - bottomConstraintExpanded + let notExpandedMinusExpanded = bottomConstraintNotExpanded - bottomConstraintExpanded + let percentage = currentMinusExpanded / abs(notExpandedMinusExpanded) + isExpanded = percentage < 0.5 + + case .possible: + break + @unknown default: + break } - - private func updateBottomConstraint() { - if isExpanded { - emailView.alpha = 1.0 - phoneView.alpha = 1.0 - currentConstraintConstant = bottomConstraintExpanded - } else { - emailView.alpha = 0.0 - phoneView.alpha = 0.0 - currentConstraintConstant = bottomConstraintNotExpanded - } + } + + private func updateBottomConstraint() { + if isExpanded { + emailView.alpha = 1.0 + phoneView.alpha = 1.0 + currentConstraintConstant = bottomConstraintExpanded + } else { + emailView.alpha = 0.0 + phoneView.alpha = 0.0 + currentConstraintConstant = bottomConstraintNotExpanded } + } } diff --git a/Sources/ScanFeature/Views/ScanDisplayView.swift b/Sources/ScanFeature/Views/ScanDisplayView.swift index fa2b02438c0d735708b9a30ffc4a6c2228926890..5c3bf28c2f7ce73826007a228cd8f9c00e7027ad 100644 --- a/Sources/ScanFeature/Views/ScanDisplayView.swift +++ b/Sources/ScanFeature/Views/ScanDisplayView.swift @@ -1,101 +1,102 @@ import UIKit import Shared import Combine +import AppResources final class ScanDisplayView: UIView { - var actionPublisher: AnyPublisher<ScanDisplayShareView.Action, Never> { - shareSheetView.actionPublisher.eraseToAnyPublisher() + var actionPublisher: AnyPublisher<ScanDisplayShareView.Action, Never> { + shareSheetView.actionPublisher.eraseToAnyPublisher() + } + + private let copyLabel = UILabel() + private let codeButton = ScanQRButton() + private let copyImageView = UIImageView() + private let copyContainerButton = UIControl() + private var cancellables = Set<AnyCancellable>() + private let shareSheetView = ScanDisplayShareView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralDark.color + + copyImageView.image = Asset.scanCopy.image + copyLabel.text = Localized.Scan.Display.copy + copyLabel.textColor = Asset.neutralDisabled.color + copyLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + + codeButton.publisher(for: .touchUpInside) + .merge(with: copyContainerButton.publisher(for: .touchUpInside)) + .sink { [unowned self] in + UIGraphicsBeginImageContext(codeButton.frame.size) + codeButton.layer.render(in: UIGraphicsGetCurrentContext()!) + let output = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + UIImageWriteToSavedPhotosAlbum(output!, nil, nil, nil) + codeButton.blinkCopied() + }.store(in: &cancellables) + + addSubview(codeButton) + addSubview(copyContainerButton) + copyContainerButton.addSubview(copyLabel) + copyContainerButton.addSubview(copyImageView) + + addSubview(shareSheetView) + + codeButton.snp.makeConstraints { + $0.centerX.equalTo(safeAreaLayoutGuide) + $0.centerY.equalTo(safeAreaLayoutGuide).multipliedBy(0.6) + $0.width.equalTo(safeAreaLayoutGuide).multipliedBy(0.6) + $0.height.equalTo(codeButton.snp.width) } - private let copyLabel = UILabel() - private let codeButton = ScanQRButton() - private let copyImageView = UIImageView() - private let copyContainerButton = UIControl() - private var cancellables = Set<AnyCancellable>() - private let shareSheetView = ScanDisplayShareView() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralDark.color - - copyImageView.image = Asset.scanCopy.image - copyLabel.text = Localized.Scan.Display.copy - copyLabel.textColor = Asset.neutralDisabled.color - copyLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - - codeButton.publisher(for: .touchUpInside) - .merge(with: copyContainerButton.publisher(for: .touchUpInside)) - .sink { [unowned self] in - UIGraphicsBeginImageContext(codeButton.frame.size) - codeButton.layer.render(in: UIGraphicsGetCurrentContext()!) - let output = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - UIImageWriteToSavedPhotosAlbum(output!, nil, nil, nil) - codeButton.blinkCopied() - }.store(in: &cancellables) - - addSubview(codeButton) - addSubview(copyContainerButton) - copyContainerButton.addSubview(copyLabel) - copyContainerButton.addSubview(copyImageView) - - addSubview(shareSheetView) - - codeButton.snp.makeConstraints { - $0.centerX.equalTo(safeAreaLayoutGuide) - $0.centerY.equalTo(safeAreaLayoutGuide).multipliedBy(0.6) - $0.width.equalTo(safeAreaLayoutGuide).multipliedBy(0.6) - $0.height.equalTo(codeButton.snp.width) - } - - copyContainerButton.snp.makeConstraints { - $0.top.equalTo(codeButton.snp.bottom).offset(33) - $0.centerX.equalTo(codeButton) - } - - copyImageView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.bottom.equalToSuperview() - } - - copyLabel.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalTo(copyImageView.snp.right).offset(5) - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - - shareSheetView.snp.makeConstraints { - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + copyContainerButton.snp.makeConstraints { + $0.top.equalTo(codeButton.snp.bottom).offset(33) + $0.centerX.equalTo(codeButton) } - required init?(coder: NSCoder) { nil } + copyImageView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.bottom.equalToSuperview() + } + + copyLabel.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalTo(copyImageView.snp.right).offset(5) + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } - func setup(code image: CIImage) { - codeButton.setup(code: image) + shareSheetView.snp.makeConstraints { + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func setup(code image: CIImage) { + codeButton.setup(code: image) + } + + func setupAttributes( + email: String?, + phone: String?, + emailSharing: Bool, + phoneSharing: Bool + ) { + if let email = email { + shareSheetView.setup(email: .init(content: email, isVisible: emailSharing)) + } else { + shareSheetView.setup(email: nil) } - func setupAttributes( - email: String?, - phone: String?, - emailSharing: Bool, - phoneSharing: Bool - ) { - if let email = email { - shareSheetView.setup(email: .init(content: email, isVisible: emailSharing)) - } else { - shareSheetView.setup(email: nil) - } - - if let phone = phone { - shareSheetView.setup(phone: .init(content: phone, isVisible: phoneSharing)) - } else { - shareSheetView.setup(phone: nil) - } + if let phone = phone { + shareSheetView.setup(phone: .init(content: phone, isVisible: phoneSharing)) + } else { + shareSheetView.setup(phone: nil) } + } } diff --git a/Sources/ScanFeature/Views/ScanOverlayView.swift b/Sources/ScanFeature/Views/ScanOverlayView.swift index 14bc4c5dc736a388a41d9c04f21a51fa49467e99..060bfbdac4476f7c2a020ee654ee1b9ee7670718 100644 --- a/Sources/ScanFeature/Views/ScanOverlayView.swift +++ b/Sources/ScanFeature/Views/ScanOverlayView.swift @@ -1,181 +1,182 @@ import UIKit import Shared +import AppResources final class ScanOverlayView: UIView { - private let cropView = UIView() - private let scanViewLength = 266.0 - private let maskLayer = CAShapeLayer() - private let topLeftLayer = CAShapeLayer() - private let topRightLayer = CAShapeLayer() - private let bottomLeftLayer = CAShapeLayer() - private let bottomRightLayer = CAShapeLayer() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.5) - - addSubview(cropView) - - cropView.snp.makeConstraints { - $0.width.equalTo(scanViewLength) - $0.centerY.equalToSuperview().offset(-50) - $0.centerX.equalToSuperview() - $0.height.equalTo(scanViewLength) - } - - maskLayer.fillRule = .evenOdd - layer.mask = maskLayer - layer.masksToBounds = true - - [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { - $0.strokeColor = Asset.brandPrimary.color.cgColor - $0.fillColor = UIColor.clear.cgColor - $0.lineWidth = 3.0 - $0.lineCap = .round - layer.addSublayer($0) - } + private let cropView = UIView() + private let scanViewLength = 266.0 + private let maskLayer = CAShapeLayer() + private let topLeftLayer = CAShapeLayer() + private let topRightLayer = CAShapeLayer() + private let bottomLeftLayer = CAShapeLayer() + private let bottomRightLayer = CAShapeLayer() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.5) + + addSubview(cropView) + + cropView.snp.makeConstraints { + $0.width.equalTo(scanViewLength) + $0.centerY.equalToSuperview().offset(-50) + $0.centerX.equalToSuperview() + $0.height.equalTo(scanViewLength) } - required init?(coder: NSCoder) { nil } + maskLayer.fillRule = .evenOdd + layer.mask = maskLayer + layer.masksToBounds = true - override func layoutSubviews() { - super.layoutSubviews() - - maskLayer.frame = bounds - let path = UIBezierPath(rect: bounds) - path.append(UIBezierPath(roundedRect: cropView.frame, cornerRadius: 30.0)) - maskLayer.path = path.cgPath - - topLeftLayer.frame = bounds - topRightLayer.frame = bounds - bottomRightLayer.frame = bounds - bottomLeftLayer.frame = bounds - - topLeftLayer.path = topLeftPath() - topRightLayer.path = topRightPath() - bottomRightLayer.path = bottomRightPath() - bottomLeftLayer.path = bottomLeftPath() - } - - func updateCornerColor(_ color: UIColor) { - [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { - $0.strokeColor = color.cgColor - } - } - - func topLeftPath() -> CGPath { - let path = UIBezierPath() - - let vert0X = cropView.frame.minX - 15 - let vert0Y = cropView.frame.minY + 45 - let vert0 = CGPoint(x: vert0X, y: vert0Y) - path.move(to: vert0) - - let vertNX = cropView.frame.minX - 15 - let vertNY = cropView.frame.minY + 15 - let vertN = CGPoint(x: vertNX, y: vertNY) - path.addLine(to: vertN) - - let arcCenterX = cropView.frame.minX + 15 - let arcCenterY = cropView.frame.minY + 15 - let arcCenter = CGPoint(x: arcCenterX , y: arcCenterY) - path.addArc(center: arcCenter, startAngle: .pi) - - let horizX = cropView.frame.minX + 45 - let horizY = cropView.frame.minY - 15 - let horiz = CGPoint(x: horizX, y: horizY) - path.addLine(to: horiz) - - return path.cgPath + [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { + $0.strokeColor = Asset.brandPrimary.color.cgColor + $0.fillColor = UIColor.clear.cgColor + $0.lineWidth = 3.0 + $0.lineCap = .round + layer.addSublayer($0) } + } - func topRightPath() -> CGPath { - let path = UIBezierPath() + required init?(coder: NSCoder) { nil } - let horiz0X = cropView.frame.maxX - 45 - let horiz0Y = cropView.frame.minY - 15 - let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) - path.move(to: horiz0) + override func layoutSubviews() { + super.layoutSubviews() - let horizNX = cropView.frame.maxX - 15 - let horizNY = cropView.frame.minY - 15 - let horizN = CGPoint(x: horizNX, y: horizNY) - path.addLine(to: horizN) + maskLayer.frame = bounds + let path = UIBezierPath(rect: bounds) + path.append(UIBezierPath(roundedRect: cropView.frame, cornerRadius: 30.0)) + maskLayer.path = path.cgPath - let arcCenterX = cropView.frame.maxX - 15 - let arcCenterY = cropView.frame.minY + 15 - let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) - path.addArc(center: arcCenter, startAngle: 3 * .pi/2) + topLeftLayer.frame = bounds + topRightLayer.frame = bounds + bottomRightLayer.frame = bounds + bottomLeftLayer.frame = bounds - let vertX = cropView.frame.maxX + 15 - let vertY = cropView.frame.minY + 45 - let vert = CGPoint(x: vertX, y: vertY) - path.addLine(to: vert) + topLeftLayer.path = topLeftPath() + topRightLayer.path = topRightPath() + bottomRightLayer.path = bottomRightPath() + bottomLeftLayer.path = bottomLeftPath() + } - return path.cgPath - } - - func bottomRightPath() -> CGPath { - let path = UIBezierPath() - - let vert0X = cropView.frame.maxX + 15 - let vert0Y = cropView.frame.maxY - 45 - let vert0 = CGPoint(x: vert0X, y: vert0Y) - path.move(to: vert0) - - let vertNX = cropView.frame.maxX + 15 - let vertNY = cropView.frame.maxY - 15 - let vertN = CGPoint(x: vertNX, y: vertNY) - path.addLine(to: vertN) - - let arcCenterX = cropView.frame.maxX - 15 - let arcCenterY = cropView.frame.maxY - 15 - let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) - path.addArc(center: arcCenter, startAngle: 0) - - let horizX = cropView.frame.maxX - 45 - let horizY = cropView.frame.maxY + 15 - let horiz = CGPoint(x: horizX, y: horizY) - path.addLine(to: horiz) - - return path.cgPath - } - - func bottomLeftPath() -> CGPath { - let path = UIBezierPath() - - let horiz0X = cropView.frame.minX + 45 - let horiz0Y = cropView.frame.maxY + 15 - let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) - path.move(to: horiz0) - - let horizNX = cropView.frame.minX + 15 - let horizNY = cropView.frame.maxY + 15 - let horizN = CGPoint(x: horizNX, y: horizNY) - path.addLine(to: horizN) - - let arcCenterX = cropView.frame.minX + 15 - let arcCenterY = cropView.frame.maxY - 15 - let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) - path.addArc(center: arcCenter, startAngle: .pi/2) - - let vertX = cropView.frame.minX - 15 - let vertY = cropView.frame.maxY - 45 - let vert = CGPoint(x: vertX, y: vertY) - path.addLine(to: vert) - - return path.cgPath + func updateCornerColor(_ color: UIColor) { + [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { + $0.strokeColor = color.cgColor } + } + + func topLeftPath() -> CGPath { + let path = UIBezierPath() + + let vert0X = cropView.frame.minX - 15 + let vert0Y = cropView.frame.minY + 45 + let vert0 = CGPoint(x: vert0X, y: vert0Y) + path.move(to: vert0) + + let vertNX = cropView.frame.minX - 15 + let vertNY = cropView.frame.minY + 15 + let vertN = CGPoint(x: vertNX, y: vertNY) + path.addLine(to: vertN) + + let arcCenterX = cropView.frame.minX + 15 + let arcCenterY = cropView.frame.minY + 15 + let arcCenter = CGPoint(x: arcCenterX , y: arcCenterY) + path.addArc(center: arcCenter, startAngle: .pi) + + let horizX = cropView.frame.minX + 45 + let horizY = cropView.frame.minY - 15 + let horiz = CGPoint(x: horizX, y: horizY) + path.addLine(to: horiz) + + return path.cgPath + } + + func topRightPath() -> CGPath { + let path = UIBezierPath() + + let horiz0X = cropView.frame.maxX - 45 + let horiz0Y = cropView.frame.minY - 15 + let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) + path.move(to: horiz0) + + let horizNX = cropView.frame.maxX - 15 + let horizNY = cropView.frame.minY - 15 + let horizN = CGPoint(x: horizNX, y: horizNY) + path.addLine(to: horizN) + + let arcCenterX = cropView.frame.maxX - 15 + let arcCenterY = cropView.frame.minY + 15 + let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) + path.addArc(center: arcCenter, startAngle: 3 * .pi/2) + + let vertX = cropView.frame.maxX + 15 + let vertY = cropView.frame.minY + 45 + let vert = CGPoint(x: vertX, y: vertY) + path.addLine(to: vert) + + return path.cgPath + } + + func bottomRightPath() -> CGPath { + let path = UIBezierPath() + + let vert0X = cropView.frame.maxX + 15 + let vert0Y = cropView.frame.maxY - 45 + let vert0 = CGPoint(x: vert0X, y: vert0Y) + path.move(to: vert0) + + let vertNX = cropView.frame.maxX + 15 + let vertNY = cropView.frame.maxY - 15 + let vertN = CGPoint(x: vertNX, y: vertNY) + path.addLine(to: vertN) + + let arcCenterX = cropView.frame.maxX - 15 + let arcCenterY = cropView.frame.maxY - 15 + let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) + path.addArc(center: arcCenter, startAngle: 0) + + let horizX = cropView.frame.maxX - 45 + let horizY = cropView.frame.maxY + 15 + let horiz = CGPoint(x: horizX, y: horizY) + path.addLine(to: horiz) + + return path.cgPath + } + + func bottomLeftPath() -> CGPath { + let path = UIBezierPath() + + let horiz0X = cropView.frame.minX + 45 + let horiz0Y = cropView.frame.maxY + 15 + let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) + path.move(to: horiz0) + + let horizNX = cropView.frame.minX + 15 + let horizNY = cropView.frame.maxY + 15 + let horizN = CGPoint(x: horizNX, y: horizNY) + path.addLine(to: horizN) + + let arcCenterX = cropView.frame.minX + 15 + let arcCenterY = cropView.frame.maxY - 15 + let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) + path.addArc(center: arcCenter, startAngle: .pi/2) + + let vertX = cropView.frame.minX - 15 + let vertY = cropView.frame.maxY - 45 + let vert = CGPoint(x: vertX, y: vertY) + path.addLine(to: vert) + + return path.cgPath + } } private extension UIBezierPath { - func addArc(center: CGPoint, startAngle: CGFloat) { - addArc( - withCenter: center, - radius: 30, - startAngle: startAngle, - endAngle: startAngle + .pi/2, - clockwise: true - ) - } + func addArc(center: CGPoint, startAngle: CGFloat) { + addArc( + withCenter: center, + radius: 30, + startAngle: startAngle, + endAngle: startAngle + .pi/2, + clockwise: true + ) + } } diff --git a/Sources/ScanFeature/Views/ScanQRButton.swift b/Sources/ScanFeature/Views/ScanQRButton.swift index 66d911c602a67c4ad01a02002fc4f9de2b2563ac..35c045df11c1b3eb54da0360d653695d85a79e2b 100644 --- a/Sources/ScanFeature/Views/ScanQRButton.swift +++ b/Sources/ScanFeature/Views/ScanQRButton.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class ScanQRButton: UIControl { private let overlayView = UIView() diff --git a/Sources/ScanFeature/Views/ScanSegmentedControl.swift b/Sources/ScanFeature/Views/ScanSegmentedControl.swift deleted file mode 100644 index f3fd72acf3326b05b8057e5a9fe0948aa113688c..0000000000000000000000000000000000000000 --- a/Sources/ScanFeature/Views/ScanSegmentedControl.swift +++ /dev/null @@ -1,80 +0,0 @@ -import UIKit -import Shared -import SnapKit - -final class ScanSegmentedControl: UIView { - private let trackHeight = 2.0 - private let numberOfTabs = 2.0 - private let trackView = UIView() - private let stackView = UIStackView() - private var leftConstraint: Constraint? - private let trackIndicatorView = UIView() - private(set) var leftButton = ScanSegmentedControlButton() - private(set) var rightButton = ScanSegmentedControlButton() - - init() { - super.init(frame: .zero) - - rightButton.setup( - title: Localized.Scan.SegmentedControl.right, - icon: Asset.scanQr.image - ) - - leftButton.setup( - title: Localized.Scan.SegmentedControl.left, - icon: Asset.scanScan.image - ) - - trackView.backgroundColor = Asset.neutralLine.color - trackIndicatorView.backgroundColor = Asset.brandPrimary.color - - stackView.distribution = .fillEqually - stackView.addArrangedSubview(leftButton) - stackView.addArrangedSubview(rightButton) - - addSubview(stackView) - addSubview(trackView) - trackView.addSubview(trackIndicatorView) - - stackView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalTo(trackView.snp.top) - } - - trackView.snp.makeConstraints { - $0.height.equalTo(trackHeight) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - - trackIndicatorView.snp.makeConstraints { - $0.top.equalToSuperview() - leftConstraint = $0.left.equalToSuperview().constraint - $0.width.equalToSuperview().dividedBy(numberOfTabs) - $0.bottom.equalToSuperview() - } - } - - required init?(coder: NSCoder) { nil } - - func updateLeftConstraint(_ percentageScrolled: CGFloat) { - let tabWidth = bounds.width / numberOfTabs - let leftOffset = percentageScrolled * tabWidth - leftConstraint?.update(offset: leftOffset) - - leftButton.update(color: .fade( - from: Asset.brandPrimary.color, - to: Asset.neutralLine.color, - pcent: percentageScrolled - )) - - rightButton.update(color: .fade( - from: Asset.brandPrimary.color, - to: Asset.neutralLine.color, - pcent: 1 - percentageScrolled - )) - } -} diff --git a/Sources/ScanFeature/Views/ScanSegmentedControlButton.swift b/Sources/ScanFeature/Views/ScanSegmentedControlButton.swift index b9e711c614b239dfb55158c530adbe121f0ca691..010997ef1d52245501f61ec9a5fb1a8cb8381980 100644 --- a/Sources/ScanFeature/Views/ScanSegmentedControlButton.swift +++ b/Sources/ScanFeature/Views/ScanSegmentedControlButton.swift @@ -1,41 +1,74 @@ import UIKit import Shared +import AppResources final class ScanSegmentedControlButton: UIControl { - private let titleLabel = UILabel() - private let imageView = UIImageView() + let titleLabel = UILabel() + let separatorView = UIView() + let imageView = UIImageView() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralWhite.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + separatorView.alpha = 0.0 + titleLabel.textAlignment = .center + imageView.tintColor = Asset.neutralWeak.color + titleLabel.textColor = Asset.neutralWeak.color + separatorView.backgroundColor = Asset.neutralWhite.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 13) + imageView.transform = imageView.transform.scaledBy(x: 0.9, y: 0.9) + titleLabel.transform = titleLabel.transform.scaledBy(x: 0.9, y: 0.9) - addSubview(titleLabel) - addSubview(imageView) + addSubview(titleLabel) + addSubview(imageView) + addSubview(separatorView) - imageView.snp.makeConstraints { - $0.top.equalToSuperview().offset(7.5) - $0.centerX.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(imageView.snp.bottom).offset(2) - $0.centerX.equalToSuperview() - $0.bottom.equalToSuperview().offset(-7.5) - } + imageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(7.5) + $0.centerX.equalToSuperview() } - 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) + } - func setup(title: String, icon: UIImage) { - titleLabel.text = title - imageView.image = icon + separatorView.snp.makeConstraints { + $0.height.equalTo(2) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + $0.bottom.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } + + func set(selected: Bool) { + switch (isSelected, selected) { + case (true, false): + UIView.animate(withDuration: 0.25) { + self.imageView.transform = self.imageView.transform.scaledBy(x: 0.9, y: 0.9) + self.titleLabel.transform = self.titleLabel.transform.scaledBy(x: 0.9, y: 0.9) + self.imageView.tintColor = Asset.neutralWeak.color + self.titleLabel.textColor = Asset.neutralWeak.color + self.separatorView.alpha = 0.0 + } completion: { _ in + self.isSelected = false + } - func update(color: UIColor) { - imageView.tintColor = color - titleLabel.textColor = color + case (false, true): + UIView.animate(withDuration: 0.25) { + self.imageView.transform = .identity + self.titleLabel.transform = .identity + self.imageView.tintColor = Asset.neutralWhite.color + self.titleLabel.textColor = Asset.neutralWhite.color + self.separatorView.alpha = 1.0 + } completion: { _ in + self.isSelected = true + } + case (true, true), (false, false): + break } + } } diff --git a/Sources/ScanFeature/Views/ScanView.swift b/Sources/ScanFeature/Views/ScanView.swift index 540f68f524bac7122bb0db21111c62e1aaddb454..8ae5933bf9a82d430ac0cd7cff31ec00faf5bee8 100644 --- a/Sources/ScanFeature/Views/ScanView.swift +++ b/Sources/ScanFeature/Views/ScanView.swift @@ -1,112 +1,113 @@ import UIKit import Shared +import AppResources final class ScanView: UIView { - private let statusLabel = UILabel() - private let imageView = UIImageView() - private let stackView = UIStackView() - private let animationView = DotAnimation() - private let overlayView = ScanOverlayView() - private(set) var actionButton = CapsuleButton() - - init() { - super.init(frame: .zero) - imageView.contentMode = .center - actionButton.setStyle(.brandColored) - - statusLabel.numberOfLines = 0 - statusLabel.textAlignment = .center - statusLabel.textColor = Asset.neutralWhite.color - statusLabel.font = Fonts.Mulish.regular.font(size: 14.0) - - stackView.spacing = 15 - stackView.axis = .vertical - stackView.addArrangedSubview(animationView) - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(statusLabel) - stackView.addArrangedSubview(actionButton) - - imageView.isHidden = true - actionButton.isHidden = true - animationView.isHidden = false - - addSubview(overlayView) - addSubview(stackView) - - overlayView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - - stackView.snp.makeConstraints { - $0.left.equalToSuperview().offset(57) - $0.right.equalToSuperview().offset(-57) - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-100) - } + private let statusLabel = UILabel() + private let imageView = UIImageView() + private let stackView = UIStackView() + private let animationView = DotAnimation() + private let overlayView = ScanOverlayView() + private(set) var actionButton = CapsuleButton() + + init() { + super.init(frame: .zero) + imageView.contentMode = .center + actionButton.setStyle(.brandColored) + + statusLabel.numberOfLines = 0 + statusLabel.textAlignment = .center + statusLabel.textColor = Asset.neutralWhite.color + statusLabel.font = Fonts.Mulish.regular.font(size: 14.0) + + stackView.spacing = 15 + stackView.axis = .vertical + stackView.addArrangedSubview(animationView) + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(statusLabel) + stackView.addArrangedSubview(actionButton) + + imageView.isHidden = true + actionButton.isHidden = true + animationView.isHidden = false + + addSubview(overlayView) + addSubview(stackView) + + overlayView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } - required init?(coder: NSCoder) { nil } - - func update(with state: ScanStatus) { - var text: String - - switch state { - case .reading, .processing: - imageView.isHidden = true - actionButton.isHidden = true - text = Localized.Scan.Status.reading - overlayView.updateCornerColor(Asset.brandPrimary.color) - - case .success: - animationView.isHidden = true - actionButton.isHidden = true - imageView.isHidden = false - imageView.image = Asset.sharedSuccess.image - text = Localized.Scan.Status.success - overlayView.updateCornerColor(Asset.accentSuccess.color) - - case .failed(let error): - animationView.isHidden = true - imageView.image = Asset.scanError.image - imageView.isHidden = false - overlayView.updateCornerColor(Asset.accentDanger.color) - - switch error { - case .requestOpened: - text = Localized.Scan.Error.requested - actionButton.setTitle(Localized.Scan.requests, for: .normal) - actionButton.isHidden = false - - case .alreadyFriends(let name): - text = Localized.Scan.Error.alreadyFriends(name) - actionButton.setTitle(Localized.Scan.contact, for: .normal) - actionButton.isHidden = false - - case .cameraPermission: - text = Localized.Scan.Error.cameraPermissionNeeded - actionButton.setTitle(Localized.Scan.settings, for: .normal) - actionButton.isHidden = false - - case .unknown(let content): - text = content - } - } - - let attString = NSMutableAttributedString(string: text) - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .center - paragraph.lineHeightMultiple = 1.35 - - attString.addAttribute(.paragraphStyle, value: paragraph) - attString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color) - attString.addAttribute(.font, value: Fonts.Mulish.regular.font(size: 14.0) as Any) - - if text.contains("#") { - attString.addAttribute(name: .foregroundColor, value: Asset.brandPrimary.color, betweenCharacters: "#") - } - - statusLabel.attributedText = attString + stackView.snp.makeConstraints { + $0.left.equalToSuperview().offset(57) + $0.right.equalToSuperview().offset(-57) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-100) } + } + + required init?(coder: NSCoder) { nil } + + func update(with state: ScanStatus) { + var text: String + + switch state { + case .reading, .processing: + imageView.isHidden = true + actionButton.isHidden = true + text = Localized.Scan.Status.reading + overlayView.updateCornerColor(Asset.brandPrimary.color) + + case .success: + animationView.isHidden = true + actionButton.isHidden = true + imageView.isHidden = false + imageView.image = Asset.sharedSuccess.image + text = Localized.Scan.Status.success + overlayView.updateCornerColor(Asset.accentSuccess.color) + + case .failed(let error): + animationView.isHidden = true + imageView.image = Asset.scanError.image + imageView.isHidden = false + overlayView.updateCornerColor(Asset.accentDanger.color) + + switch error { + case .requestOpened: + text = Localized.Scan.Error.requested + actionButton.setTitle(Localized.Scan.requests, for: .normal) + actionButton.isHidden = false + + case .alreadyFriends(let name): + text = Localized.Scan.Error.alreadyFriends(name) + actionButton.setTitle(Localized.Scan.contact, for: .normal) + actionButton.isHidden = false + + case .cameraPermission: + text = Localized.Scan.Error.cameraPermissionNeeded + actionButton.setTitle(Localized.Scan.settings, for: .normal) + actionButton.isHidden = false + + case .unknown(let content): + text = content + } + } + + let attString = NSMutableAttributedString(string: text) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .center + paragraph.lineHeightMultiple = 1.35 + + attString.addAttribute(.paragraphStyle, value: paragraph) + attString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color) + attString.addAttribute(.font, value: Fonts.Mulish.regular.font(size: 14.0) as Any) + + if text.contains("#") { + attString.addAttribute(name: .foregroundColor, value: Asset.brandPrimary.color, betweenCharacters: "#") + } + + statusLabel.attributedText = attString + } } diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index 3a1bf1e45af9077b7bc70315f5584be93b116242..ec9bcd59eb990643390ba4d458c184d488b4d193 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -1,183 +1,187 @@ import UIKit -import Theme import Shared import Combine +import AppCore import XXModels +import Dependencies +import AppResources +import AppNavigation import DrawerFeature -import DependencyInjection public final class SearchContainerController: UIViewController { - @Dependency var coordinator: SearchCoordinating - @Dependency var statusBarController: StatusBarStyleControlling - - lazy private var screenView = SearchContainerView() - - private var contentOffset: CGPoint? - private var cancellables = Set<AnyCancellable>() - private let leftController: SearchLeftController - private let viewModel = SearchContainerViewModel() - private let rightController = SearchRightController() - private var drawerCancellables = Set<AnyCancellable>() - - public init(_ invitation: String? = nil) { - self.leftController = .init(invitation) - super.init(nibName: nil, bundle: nil) + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var screenView = SearchContainerView() + + private var contentOffset: CGPoint? + private var cancellables = Set<AnyCancellable>() + private let leftController: SearchLeftController + private let viewModel = SearchContainerViewModel() + private let rightController = SearchRightController() + private var drawerCancellables = Set<AnyCancellable>() + + public init(_ invitation: String? = nil) { + self.leftController = .init(invitation) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + embedControllers() + } + + public func startSearchingFor(_ string: String) { + leftController.viewModel.invitation = string + leftController.viewModel.viewDidAppear() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + statusBar.set(.darkContent) + navigationController?.navigationBar.customize( + backgroundColor: Asset.neutralWhite.color + ) + + if let contentOffset = self.contentOffset { + screenView.scrollView.setContentOffset(contentOffset, animated: true) } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - embedControllers() - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - statusBarController.style.send(.darkContent) - navigationController?.navigationBar.customize( - backgroundColor: Asset.neutralWhite.color - ) - - if let contentOffset = self.contentOffset { - screenView.scrollView.setContentOffset(contentOffset, animated: true) + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + contentOffset = screenView.scrollView.contentOffset + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.didAppear() + rightController.viewModel.viewWillAppear() + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + } + + private func setupNavigationBar() { + let title = UILabel() + title.text = Localized.Ud.title + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) + navigationItem.leftItemsSupplementBackButton = true + } + + private func setupBindings() { + screenView.segmentedControl + .actionPublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + if $0 == .qr { + let point = CGPoint(x: screenView.frame.width, y: 0.0) + screenView.scrollView.setContentOffset(point, animated: true) + leftController.endEditing() + } else { + screenView.scrollView.setContentOffset(.zero, animated: true) + leftController.viewModel.didSelectItem($0) } + }.store(in: &cancellables) + + viewModel.coverTrafficPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in presentCoverTrafficDrawer() } + .store(in: &cancellables) + } + + private func embedControllers() { + addChild(leftController) + addChild(rightController) + + screenView.scrollView.addSubview(leftController.view) + screenView.scrollView.addSubview(rightController.view) + + leftController.view.snp.makeConstraints { + $0.top.equalTo(screenView.segmentedControl.snp.bottom) + $0.width.equalTo(screenView) + $0.bottom.equalTo(screenView) + $0.left.equalToSuperview() + $0.right.equalTo(rightController.view.snp.left) } - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - contentOffset = screenView.scrollView.contentOffset - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - viewModel.didAppear() - rightController.viewModel.viewWillAppear() + rightController.view.snp.makeConstraints { + $0.top.equalTo(screenView.segmentedControl.snp.bottom) + $0.width.equalTo(screenView) + $0.bottom.equalTo(screenView) } - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupBindings() - } - - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.Ud.title - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) - - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupBindings() { - screenView.segmentedControl - .actionPublisher - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - if $0 == .qr { - let point = CGPoint(x: screenView.frame.width, y: 0.0) - screenView.scrollView.setContentOffset(point, animated: true) - leftController.endEditing() - } else { - screenView.scrollView.setContentOffset(.zero, animated: true) - leftController.viewModel.didSelectItem($0) - } - }.store(in: &cancellables) - - viewModel.coverTrafficPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentCoverTrafficDrawer() } - .store(in: &cancellables) - } - - private func embedControllers() { - addChild(leftController) - addChild(rightController) - - screenView.scrollView.addSubview(leftController.view) - screenView.scrollView.addSubview(rightController.view) - - leftController.view.snp.makeConstraints { - $0.top.equalTo(screenView.segmentedControl.snp.bottom) - $0.width.equalTo(screenView) - $0.bottom.equalTo(screenView) - $0.left.equalToSuperview() - $0.right.equalTo(rightController.view.snp.left) - } - - rightController.view.snp.makeConstraints { - $0.top.equalTo(screenView.segmentedControl.snp.bottom) - $0.width.equalTo(screenView) - $0.bottom.equalTo(screenView) - } - - leftController.didMove(toParent: self) - rightController.didMove(toParent: self) - } + leftController.didMove(toParent: self) + rightController.didMove(toParent: self) + } } extension SearchContainerController { - private func presentCoverTrafficDrawer() { - let enableButton = CapsuleButton() - enableButton.set( - style: .brandColored, - title: Localized.ChatList.Traffic.positive - ) - - let dismissButton = CapsuleButton() - dismissButton.set( - style: .seeThrough, - title: Localized.ChatList.Traffic.negative - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: Localized.ChatList.Traffic.title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.ChatList.Traffic.subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 39 - ), - DrawerStack( - axis: .horizontal, - spacing: 20, - distribution: .fillEqually, - views: [enableButton, dismissButton] - ) - ]) - - enableButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - self.viewModel.didEnableCoverTraffic() - } - }.store(in: &drawerCancellables) - - dismissButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + private func presentCoverTrafficDrawer() { + let enableButton = CapsuleButton() + enableButton.set( + style: .brandColored, + title: Localized.ChatList.Traffic.positive + ) + let dismissButton = CapsuleButton() + dismissButton.set( + style: .seeThrough, + title: Localized.ChatList.Traffic.negative + ) + + enableButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didEnableCoverTraffic() + } + }.store(in: &drawerCancellables) + + dismissButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.ChatList.Traffic.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.ChatList.Traffic.subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 39 + ), + DrawerStack( + axis: .horizontal, + spacing: 20, + distribution: .fillEqually, + views: [enableButton, dismissButton] + ) + ], isDismissable: true, from: self)) + } } diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index cc46ad60cf83f8baa03aa3ac7bb34562fbccaea8..4485fa22e286efcc668220c973c8aa4ab9ea0b3a 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -1,451 +1,459 @@ -import HUD import UIKit import Shared import Combine import XXModels import Defaults -import Countries +import AppResources +import Dependencies +import AppNavigation import DrawerFeature -import DependencyInjection +import CountryListFeature final class SearchLeftController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: SearchCoordinating - - @KeyObject(.email, defaultValue: nil) var email: String? - @KeyObject(.phone, defaultValue: nil) var phone: String? - @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool - @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool - - lazy private var screenView = SearchLeftView() - - let viewModel: SearchLeftViewModel - private var dataSource: SearchDiffableDataSource! - private var drawerCancellables = Set<AnyCancellable>() - private let adrpURLString = "https://links.xx.network/adrp" - - private var cancellables = Set<AnyCancellable>() - private var hudCancellables = Set<AnyCancellable>() - - init(_ invitation: String? = nil) { - self.viewModel = .init(invitation) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } + @Dependency(\.navigator) var navigator: Navigator + @KeyObject(.email, defaultValue: nil) var email: String? + @KeyObject(.phone, defaultValue: nil) var phone: String? + @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool + + private lazy var screenView = SearchLeftView() + + private(set) var viewModel: SearchLeftViewModel + private var dataSource: SearchDiffableDataSource! + private var drawerCancellables = Set<AnyCancellable>() + private let adrpURLString = "https://links.xx.network/adrp" + + private var cancellables = Set<AnyCancellable>() + private var hudCancellables = Set<AnyCancellable>() + + init(_ invitation: String? = nil) { + self.viewModel = .init(invitation) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + override func loadView() { + view = screenView + } + + override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + setupBindings() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.viewDidAppear() + } + + func endEditing() { + screenView.inputField.endEditing(true) + } + + private func setupTableView() { + screenView.tableView.separatorStyle = .none + screenView.tableView.tableFooterView = UIView() + screenView.tableView.register(AvatarCell.self) + screenView.tableView.dataSource = dataSource + screenView.tableView.delegate = self + + dataSource = SearchDiffableDataSource( + tableView: screenView.tableView + ) { tableView, indexPath, item in + let contact: Contact + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) + + let h1Text: String + var h2Text: String? + + switch item { + case .stranger(let stranger): + contact = stranger + h1Text = stranger.username ?? "" + + if stranger.authStatus == .requested { + h2Text = "Request pending" + } else if stranger.authStatus == .requestFailed { + h2Text = "Request failed" + } - override func loadView() { - view = screenView - } + case .connection(let connection): + contact = connection + h1Text = (connection.nickname ?? contact.username) ?? "" - override func viewDidLoad() { - super.viewDidLoad() - setupTableView() - setupBindings() + if connection.nickname != nil { + h2Text = contact.username ?? "" + } + } + + cell.setup( + title: h1Text, + image: contact.photo, + firstSubtitle: h2Text, + secondSubtitle: contact.email, + thirdSubtitle: contact.phone, + showSeparator: false, + sent: contact.authStatus == .requested + ) + + cell.didTapStateButton = { [weak self] in + guard let self else { return } + self.viewModel.didTapResend(contact: contact) + cell.updateToResent() + } + + return cell } + } + + private func setupBindings() { + viewModel + .statePublisher + .map(\.item) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.updateUIForItem(item: $0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.country) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.countryButton.setFlag($0.flag, prefix: $0.prefix) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.input) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.inputField.update(content: $0) + }.store(in: &cancellables) + + viewModel + .statePublisher + .compactMap(\.snapshot) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.placeholderView.isHidden = true + screenView.emptyView.isHidden = $0.numberOfItems != 0 + dataSource.apply($0, animatingDifferences: false) + }.store(in: &cancellables) + + screenView + .placeholderView + .infoPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentSearchDisclaimer() + }.store(in: &cancellables) + + screenView + .countryButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentCountryList(completion: { [weak self] in + guard let self else { return } + self.viewModel.didPick(country: $0 as! Country) + }, from: self)) + }.store(in: &cancellables) + + screenView + .inputField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didEnterInput($0) + }.store(in: &cancellables) + + screenView + .inputField + .returnPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in + viewModel.didStartSearching() + }.store(in: &cancellables) + + screenView + .inputField + .isEditingPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] isEditing in + UIView.animate(withDuration: 0.25) { + self.screenView.placeholderView.titleLabel.alpha = isEditing ? 0.1 : 1.0 + self.screenView.placeholderView.subtitleWithInfo.alpha = isEditing ? 0.1 : 1.0 + } + }.store(in: &cancellables) + + viewModel + .successPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentSucessDrawerFor(contact: $0) + }.store(in: &cancellables) + } + + private func presentSearchDisclaimer() { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Ud.Placeholder.Drawer.action + ) + + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: Localized.Ud.Placeholder.Drawer.title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: Localized.Ud.Placeholder.Drawer.subtitle, + urlString: adrpURLString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } + + private func presentSucessDrawerFor(contact: Contact) { + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: Localized.Ud.NicknameDrawer.title, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: Localized.Ud.NicknameDrawer.subtitle, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + items.append(contentsOf: [ + drawerTitle, + drawerSubtitle + ]) + + let drawerNicknameInput = DrawerInput( + placeholder: contact.username!, + validator: .init( + wrongIcon: .image(Asset.sharedError.image), + correctIcon: .image(Asset.sharedSuccess.image), + shouldAcceptPlaceholder: true + ), + spacingAfter: 29 + ) + + items.append(drawerNicknameInput) + + let drawerSaveButton = DrawerCapsuleButton( + model: .init( + title: Localized.Ud.NicknameDrawer.save, + style: .brandColored + ), spacingAfter: 5 + ) + + items.append(drawerSaveButton) + + let drawer = DrawerController(items) + var nickname: String? + var allowsSave = true + + drawerNicknameInput + .validationPublisher + .receive(on: DispatchQueue.main) + .sink { allowsSave = $0 } + .store(in: &drawerCancellables) + + drawerNicknameInput + .inputPublisher + .receive(on: DispatchQueue.main) + .sink { + guard !$0.isEmpty else { + nickname = contact.username + return + } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - viewModel.viewDidAppear() - } + nickname = $0 + } + .store(in: &drawerCancellables) - func endEditing() { - screenView.inputField.endEditing(true) - } + drawerSaveButton.action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard allowsSave else { return } - private func setupTableView() { - screenView.tableView.separatorStyle = .none - screenView.tableView.tableFooterView = UIView() - screenView.tableView.register(AvatarCell.self) - screenView.tableView.dataSource = dataSource - screenView.tableView.delegate = self - - dataSource = SearchDiffableDataSource( - tableView: screenView.tableView - ) { tableView, indexPath, item in - let contact: Contact - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) - - let h1Text: String - var h2Text: String? - - switch item { - case .stranger(let stranger): - contact = stranger - h1Text = stranger.username ?? "" - - if stranger.authStatus == .requested { - h2Text = "Request pending" - } else if stranger.authStatus == .requestFailed { - h2Text = "Request failed" - } - - case .connection(let connection): - contact = connection - h1Text = (connection.nickname ?? contact.username) ?? "" - - if connection.nickname != nil { - h2Text = contact.username ?? "" - } - } - - cell.setup( - title: h1Text, - image: contact.photo, - firstSubtitle: h2Text, - secondSubtitle: contact.email, - thirdSubtitle: contact.phone, - showSeparator: false, - sent: contact.authStatus == .requested - ) - - cell.didTapStateButton = { [weak self] in - guard let self = self else { return } - self.viewModel.didTapResend(contact: contact) - cell.updateToResent() - } - - return cell + drawer.dismiss(animated: true) { + self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact) } + } + .store(in: &drawerCancellables) + + //coordinator.toNicknameDrawer(drawer, from: self) + } + + private func presentRequestDrawer(forContact contact: Contact) { + var items: [DrawerItem] = [] + + let drawerTitle = DrawerText( + font: Fonts.Mulish.extraBold.font(size: 26.0), + text: Localized.Ud.RequestDrawer.title, + color: Asset.neutralDark.color, + spacingAfter: 20 + ) + + var subtitleFragment = "Share your information with #\(contact.username ?? "")" + + if let email = contact.email { + subtitleFragment.append(contentsOf: " (\(email))#") + } else if let phone = contact.phone { + subtitleFragment.append(contentsOf: " (\(Country.findFrom(phone).prefix) \(phone.dropLast(2)))#") + } else { + subtitleFragment.append(contentsOf: "#") } - private func setupBindings() { - viewModel.hudPublisher - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - hud.update(with: $0) - - if case .onAction = $0, let hudBtn = hud.actionButton { - hudBtn.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapCancelSearch() } - .store(in: &self.hudCancellables) - } else { - hudCancellables.forEach { $0.cancel() } - hudCancellables.removeAll() - } - } - .store(in: &cancellables) - - - viewModel.statePublisher - .map(\.item) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.updateUIForItem(item: $0) } - .store(in: &cancellables) - - viewModel.statePublisher - .map(\.country) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.countryButton.setFlag($0.flag, prefix: $0.prefix) } - .store(in: &cancellables) - - viewModel.statePublisher - .map(\.input) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in screenView.inputField.update(content: $0) } - .store(in: &cancellables) - - viewModel.statePublisher - .compactMap(\.snapshot) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - screenView.placeholderView.isHidden = true - screenView.emptyView.isHidden = $0.numberOfItems != 0 - - dataSource.apply($0, animatingDifferences: false) - }.store(in: &cancellables) - - screenView.placeholderView - .infoPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentSearchDisclaimer() } - .store(in: &cancellables) - - screenView.countryButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - coordinator.toCountries(from: self) { [weak self] country in - guard let self = self else { return } - self.viewModel.didPick(country: country) - } - }.store(in: &cancellables) - - screenView.inputField - .textPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didEnterInput($0) } - .store(in: &cancellables) - - screenView.inputField - .returnPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] _ in viewModel.didStartSearching() } - .store(in: &cancellables) - - screenView.inputField - .isEditingPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] isEditing in - UIView.animate(withDuration: 0.25) { - self.screenView.placeholderView.titleLabel.alpha = isEditing ? 0.1 : 1.0 - self.screenView.placeholderView.subtitleWithInfo.alpha = isEditing ? 0.1 : 1.0 - } - }.store(in: &cancellables) - - viewModel.successPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in presentSucessDrawerFor(contact: $0) } - .store(in: &cancellables) - } - - private func presentSearchDisclaimer() { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Ud.Placeholder.Drawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: Localized.Ud.Placeholder.Drawer.title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: Localized.Ud.Placeholder.Drawer.subtitle, - urlString: adrpURLString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &self.drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } - - private func presentSucessDrawerFor(contact: Contact) { - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: Localized.Ud.NicknameDrawer.title, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: Localized.Ud.NicknameDrawer.subtitle, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) - - items.append(contentsOf: [ - drawerTitle, - drawerSubtitle - ]) - - let drawerNicknameInput = DrawerInput( - placeholder: contact.username!, - validator: .init( - wrongIcon: .image(Asset.sharedError.image), - correctIcon: .image(Asset.sharedSuccess.image), - shouldAcceptPlaceholder: true - ), - spacingAfter: 29 - ) - - items.append(drawerNicknameInput) - - let drawerSaveButton = DrawerCapsuleButton( - model: .init( - title: Localized.Ud.NicknameDrawer.save, - style: .brandColored - ), spacingAfter: 5 - ) - - items.append(drawerSaveButton) - - let drawer = DrawerController(with: items) - var nickname: String? - var allowsSave = true - - drawerNicknameInput.validationPublisher - .receive(on: DispatchQueue.main) - .sink { allowsSave = $0 } - .store(in: &drawerCancellables) - - drawerNicknameInput.inputPublisher - .receive(on: DispatchQueue.main) - .sink { - guard !$0.isEmpty else { - nickname = contact.username - return - } - - nickname = $0 - } - .store(in: &drawerCancellables) - - drawerSaveButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard allowsSave else { return } - - drawer.dismiss(animated: true) { - self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact) - } - } - .store(in: &drawerCancellables) - - coordinator.toNicknameDrawer(drawer, from: self) + subtitleFragment.append(contentsOf: " so they know its you.") + + let drawerSubtitle = DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitleFragment, + color: Asset.neutralDark.color, + spacingAfter: 31.5, + customAttributes: [ + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .foregroundColor: Asset.brandPrimary.color + ] + ) + + items.append(contentsOf: [ + drawerTitle, + drawerSubtitle + ]) + + if let email = email { + let drawerEmail = DrawerSwitch( + title: Localized.Ud.RequestDrawer.email, + content: email, + spacingAfter: phone != nil ? 23 : 31, + isInitiallyOn: isSharingEmail + ) + + items.append(drawerEmail) + + drawerEmail.isOnPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.isSharingEmail = $0 } + .store(in: &drawerCancellables) } - private func presentRequestDrawer(forContact contact: Contact) { - var items: [DrawerItem] = [] - - let drawerTitle = DrawerText( - font: Fonts.Mulish.extraBold.font(size: 26.0), - text: Localized.Ud.RequestDrawer.title, - color: Asset.neutralDark.color, - spacingAfter: 20 - ) + if let phone = phone { + let drawerPhone = DrawerSwitch( + title: Localized.Ud.RequestDrawer.phone, + content: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", + spacingAfter: 31, + isInitiallyOn: isSharingPhone + ) - var subtitleFragment = "Share your information with #\(contact.username ?? "")" + items.append(drawerPhone) - if let email = contact.email { - subtitleFragment.append(contentsOf: " (\(email))#") - } else if let phone = contact.phone { - subtitleFragment.append(contentsOf: " (\(Country.findFrom(phone).prefix) \(phone.dropLast(2)))#") - } else { - subtitleFragment.append(contentsOf: "#") - } + drawerPhone.isOnPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.isSharingPhone = $0 } + .store(in: &drawerCancellables) + } - subtitleFragment.append(contentsOf: " so they know its you.") - - let drawerSubtitle = DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitleFragment, - color: Asset.neutralDark.color, - spacingAfter: 31.5, - customAttributes: [ - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .foregroundColor: Asset.brandPrimary.color - ] - ) - - items.append(contentsOf: [ - drawerTitle, - drawerSubtitle - ]) - - if let email = email { - let drawerEmail = DrawerSwitch( - title: Localized.Ud.RequestDrawer.email, - content: email, - spacingAfter: phone != nil ? 23 : 31, - isInitiallyOn: isSharingEmail - ) - - items.append(drawerEmail) - - drawerEmail.isOnPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.isSharingEmail = $0 } - .store(in: &drawerCancellables) + let drawerSendButton = DrawerCapsuleButton( + model: .init( + title: Localized.Ud.RequestDrawer.send, + style: .brandColored + ), spacingAfter: 5 + ) + + let drawerCancelButton = DrawerCapsuleButton( + model: .init( + title: Localized.Ud.RequestDrawer.cancel, + style: .simplestColoredBrand + ), spacingAfter: 5 + ) + + items.append(contentsOf: [drawerSendButton, drawerCancelButton]) + + drawerSendButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + self.viewModel.didTapRequest(contact: contact) } - - if let phone = phone { - let drawerPhone = DrawerSwitch( - title: Localized.Ud.RequestDrawer.phone, - content: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))", - spacingAfter: 31, - isInitiallyOn: isSharingPhone - ) - - items.append(drawerPhone) - - drawerPhone.isOnPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.isSharingPhone = $0 } - .store(in: &drawerCancellables) + }.store(in: &drawerCancellables) + + drawerCancelButton + .action + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() } - - let drawerSendButton = DrawerCapsuleButton( - model: .init( - title: Localized.Ud.RequestDrawer.send, - style: .brandColored - ), spacingAfter: 5 - ) - - let drawerCancelButton = DrawerCapsuleButton( - model: .init( - title: Localized.Ud.RequestDrawer.cancel, - style: .simplestColoredBrand - ), spacingAfter: 5 - ) - - items.append(contentsOf: [drawerSendButton, drawerCancelButton]) - let drawer = DrawerController(with: items) - - drawerSendButton.action - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - drawer.dismiss(animated: true) { - self.viewModel.didTapRequest(contact: contact) - } - }.store(in: &drawerCancellables) - - drawerCancelButton.action - .receive(on: DispatchQueue.main) - .sink { drawer.dismiss(animated: true) } - .store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer( + items: items, + isDismissable: true, + from: self + )) + } } extension SearchLeftController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let item = dataSource.itemIdentifier(for: indexPath) { - switch item { - case .stranger(let contact): - didTap(contact: contact) - case .connection(let contact): - didTap(contact: contact) - } - } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let item = dataSource.itemIdentifier(for: indexPath) { + switch item { + case .stranger(let contact): + didTap(contact: contact) + case .connection(let contact): + didTap(contact: contact) + } } + } - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { - (view as! UITableViewHeaderFooterView).textLabel?.textColor = Asset.neutralWeak.color - } - - private func didTap(contact: Contact) { - guard contact.authStatus == .stranger else { - coordinator.toContact(contact, from: self) - return - } + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + (view as! UITableViewHeaderFooterView).textLabel?.textColor = Asset.neutralWeak.color + } - presentRequestDrawer(forContact: contact) + private func didTap(contact: Contact) { + guard contact.authStatus == .stranger else { + navigator.perform(PresentContact(contact: contact, on: navigationController!)) + return } + + presentRequestDrawer(forContact: contact) + } } diff --git a/Sources/SearchFeature/Controllers/SearchRightController.swift b/Sources/SearchFeature/Controllers/SearchRightController.swift index 35240054497992ff2b5a277618321dceffeb652a..3445f2266bc8a43dd479eb7cfe2bb9287e49e34b 100644 --- a/Sources/SearchFeature/Controllers/SearchRightController.swift +++ b/Sources/SearchFeature/Controllers/SearchRightController.swift @@ -1,81 +1,88 @@ import UIKit import Combine -import DependencyInjection +import AppNavigation +import ComposableArchitecture final class SearchRightController: UIViewController { - @Dependency var coordinator: SearchCoordinating + @Dependency(\.navigator) var navigator: Navigator - lazy private var screenView = SearchRightView() + private lazy var screenView = SearchRightView() - private var cancellables = Set<AnyCancellable>() - private let cameraController = CameraController() - private(set) var viewModel = SearchRightViewModel() + private var cancellables = Set<AnyCancellable>() + private let cameraController = CameraController() + private(set) var viewModel = SearchRightViewModel() - override func loadView() { - view = screenView - } + override func loadView() { + view = screenView + } - override func viewDidLoad() { - super.viewDidLoad() - screenView.layer.insertSublayer(cameraController.previewLayer, at: 0) - setupBindings() - } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + cameraController.previewLayer.frame = screenView.bounds + } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - cameraController.previewLayer.frame = screenView.bounds - } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.viewWillDisappear() + } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - viewModel.viewWillDisappear() - } + override func viewDidLoad() { + super.viewDidLoad() + screenView.layer.insertSublayer( + cameraController.previewLayer, at: 0 + ) - private func setupBindings() { - cameraController - .dataPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didScan(data: $0) } - .store(in: &cancellables) + cameraController + .dataPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + viewModel.didScan(data: $0) + }.store(in: &cancellables) - viewModel.cameraSemaphorePublisher - .removeDuplicates() - .receive(on: DispatchQueue.global()) - .sink { [unowned self] setOn in - if setOn { - cameraController.start() - } else { - cameraController.stop() - } - }.store(in: &cancellables) + viewModel + .cameraSemaphorePublisher + .removeDuplicates() + .receive(on: DispatchQueue.global()) + .sink { [unowned self] in + if $0 { + cameraController.start() + } else { + cameraController.stop() + } + }.store(in: &cancellables) - viewModel.foundPublisher - .receive(on: DispatchQueue.main) - .delay(for: 1, scheduler: DispatchQueue.main) - .sink { [unowned self] in coordinator.toContact($0, from: self) } - .store(in: &cancellables) + viewModel + .foundPublisher + .receive(on: DispatchQueue.main) + .delay(for: 1, scheduler: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentContact(contact: $0, on: navigationController!)) + }.store(in: &cancellables) - viewModel.statusPublisher - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [unowned self] in screenView.update(status: $0) } - .store(in: &cancellables) + viewModel + .statusPublisher + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [unowned self] in + screenView.update(status: $0) + }.store(in: &cancellables) - screenView.actionButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - switch viewModel.statusSubject.value { - case .failed(.cameraPermission): - guard let url = URL(string: UIApplication.openSettingsURLString) else { return } - UIApplication.shared.open(url, options: [:]) - case .failed(.requestOpened): - coordinator.toRequests(from: self) - case .failed(.alreadyFriends): - coordinator.toContacts(from: self) - default: - break - } - }.store(in: &cancellables) - } + screenView + .actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + switch viewModel.statusSubject.value { + case .failed(.cameraPermission): + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url, options: [:]) + case .failed(.requestOpened): + navigator.perform(PresentRequests(on: navigationController!)) + case .failed(.alreadyFriends): + navigator.perform(PresentContactList(on: navigationController!)) + default: + break + } + }.store(in: &cancellables) + } } diff --git a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift deleted file mode 100644 index 21a9d7b3d5eb9b1cef283a410135157fb7571ef1..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift +++ /dev/null @@ -1,84 +0,0 @@ -import UIKit -import Models -import XXModels -import Countries -import Presentation -import ScrollViewController - -public protocol SearchCoordinating { - func toRequests(from: UIViewController) - func toContacts(from: UIViewController) - func toContact(_: Contact, from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toNicknameDrawer(_: UIViewController, from: UIViewController) - func toCountries(from: UIViewController, _: @escaping (Country) -> Void) -} - -public struct SearchCoordinator { - var pushPresenter: Presenting = PushPresenter() - var bottomPresenter: Presenting = BottomPresenter() - var replacePresenter: Presenting = ReplacePresenter() - var fullscreenPresenter: Presenting = FullscreenPresenter() - - var contactsFactory: () -> UIViewController - var requestsFactory: () -> UIViewController - var contactFactory: (Contact) -> UIViewController - var countriesFactory: (@escaping (Country) -> Void) -> UIViewController - - public init( - contactsFactory: @escaping () -> UIViewController, - requestsFactory: @escaping () -> UIViewController, - contactFactory: @escaping (Contact) -> UIViewController, - countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController - ) { - self.contactFactory = contactFactory - self.contactsFactory = contactsFactory - self.requestsFactory = requestsFactory - self.countriesFactory = countriesFactory - } -} - -extension SearchCoordinator: SearchCoordinating { - public func toRequests(from parent: UIViewController) { - let screen = requestsFactory() - replacePresenter.present(screen, from: parent) - } - - public func toContacts(from parent: UIViewController) { - let screen = contactsFactory() - replacePresenter.present(screen, from: parent) - } - - public func toContact(_ contact: Contact, from parent: UIViewController) { - let screen = contactFactory(contact) - pushPresenter.present(screen, from: parent) - } - - public func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - public func toCountries(from parent: UIViewController, _ onChoose: @escaping (Country) -> Void) { - let screen = countriesFactory(onChoose) - pushPresenter.present(screen, from: parent) - } - - public func toNicknameDrawer(_ target: UIViewController, from parent: UIViewController) { - let screen = ScrollViewController.embedding(target) - fullscreenPresenter.present(screen, from: parent) - } -} - -extension ScrollViewController { - static func embedding(_ viewController: UIViewController) -> ScrollViewController { - let scrollViewController = ScrollViewController() - scrollViewController.addChild(viewController) - scrollViewController.contentView = viewController.view - scrollViewController.wrapperView.handlesTouchesOutsideContent = false - scrollViewController.wrapperView.alignContentToBottom = true - scrollViewController.scrollView.bounces = false - - viewController.didMove(toParent: scrollViewController) - return scrollViewController - } -} diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift index e308f6a5b21134224ec17155b9585a9718cf1204..1571afdae49af23f77a166b50518bffe21f92582 100644 --- a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift @@ -1,58 +1,51 @@ import UIKit import Combine import Defaults -import Integration -import PushFeature -import DependencyInjection +import XXClient +import PermissionsFeature +import ComposableArchitecture final class SearchContainerViewModel { - @Dependency var session: SessionType - @Dependency var pushHandler: PushHandling - - @KeyObject(.dummyTrafficOn, defaultValue: false) var isCoverTrafficEnabled - @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications - @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic - - var coverTrafficPublisher: AnyPublisher<Void, Never> { - coverTrafficSubject.eraseToAnyPublisher() - } - - private let coverTrafficSubject = PassthroughSubject<Void, Never>() - - func didAppear() { - verifyCoverTraffic() - verifyNotifications() - } - - func didEnableCoverTraffic() { - isCoverTrafficEnabled = true - session.setDummyTraffic(status: true) - } - - private func verifyCoverTraffic() { - guard offeredCoverTraffic == false else { return } - offeredCoverTraffic = true - coverTrafficSubject.send() - } - - private func verifyNotifications() { - guard pushNotifications == false else { return } - - pushHandler.requestAuthorization { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let granted): - if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - - self.pushNotifications = granted - case .failure: - self.pushNotifications = false - } + @Dependency(\.permissions) var permissions: PermissionsManager + //@Dependency(\.app.dummyTraffic) var dummyTraffic: DummyTraffic + + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn + @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications + @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic + + var coverTrafficPublisher: AnyPublisher<Void, Never> { + coverTrafficSubject.eraseToAnyPublisher() + } + + private let coverTrafficSubject = PassthroughSubject<Void, Never>() + + func didAppear() { + verifyCoverTraffic() + verifyNotifications() + } + + func didEnableCoverTraffic() { +// try! dummyTraffic.setStatus(true) + dummyTrafficOn = true + } + + private func verifyCoverTraffic() { + guard offeredCoverTraffic == false else { return } + offeredCoverTraffic = true + coverTrafficSubject.send() + } + + private func verifyNotifications() { + guard pushNotifications == false else { return } + + permissions.push.request { [weak self] granted in + guard let self else { return } + if granted == true { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() } + } + self.pushNotifications = granted } + } } diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index 6f708401cc71d12838efcc64ca594818ecf2ae42..9a9aacb84b1b125233beb46f38601296a4f7df6d 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -1,188 +1,377 @@ -import HUD +import Retry import UIKit import Shared import Combine import XXModels +import XXClient import Defaults -import Countries -import Integration -import NetworkMonitor +import CustomDump +import AppResources import ReportingFeature -import DependencyInjection +import CombineSchedulers +import XXMessengerClient +import CountryListFeature +import Dependencies +import AppCore typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> struct SearchLeftViewState { - var input = "" - var snapshot: SearchSnapshot? - var country: Country = .fromMyPhone() - var item: SearchSegmentedControl.Item = .username + var input = "" + var snapshot: SearchSnapshot? + var country: Country = .fromMyPhone() + var item: SearchSegmentedControl.Item = .username } final class SearchLeftViewModel { - @Dependency var session: SessionType - @Dependency var reportingStatus: ReportingStatus - @Dependency var networkMonitor: NetworkMonitoring - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() + @Dependency(\.app.dbManager) var dbManager + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.hudManager) var hudManager + @Dependency(\.app.toastManager) var toastManager + @Dependency(\.reportingStatus) var reportingStatus + @Dependency(\.app.networkMonitor) var networkMonitor + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var successPublisher: AnyPublisher<XXModels.Contact, Never> { + successSubject.eraseToAnyPublisher() + } + + var statePublisher: AnyPublisher<SearchLeftViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + var invitation: String? + private var searchCancellables = Set<AnyCancellable>() + private let successSubject = PassthroughSubject<XXModels.Contact, Never>() + private let stateSubject = CurrentValueSubject<SearchLeftViewState, Never>(.init()) + private var networkCancellable = Set<AnyCancellable>() + + init(_ invitation: String? = nil) { + self.invitation = invitation + } + + func viewDidAppear() { + if let pendingInvitation = invitation { + invitation = nil + stateSubject.value.input = pendingInvitation + hudManager.show(.init( + actionTitle: Localized.Ud.Search.cancel, + hasDotAnimation: true, + onTapClosure: { [weak self] in + guard let self else { return } + self.didTapCancelSearch() + } + )) + + networkCancellable.removeAll() + +// networkMonitor +// .statusPublisher +// .first { $0 == .available } +// .eraseToAnyPublisher() +// .flatMap { _ in +// self.waitForNodes(timeout: 5) +// }.sink(receiveCompletion: { +// if case .failure(let error) = $0 { +// self.hudManager.show(.init(error: error)) +// } +// }, receiveValue: { +// self.didStartSearching() +// }).store(in: &networkCancellable) + } + } + + func didEnterInput(_ string: String) { + stateSubject.value.input = string + } + + func didPick(country: Country) { + stateSubject.value.country = country + } + + func didSelectItem(_ item: SearchSegmentedControl.Item) { + stateSubject.value.item = item + } + + func didTapCancelSearch() { + searchCancellables.forEach { $0.cancel() } + searchCancellables.removeAll() + hudManager.hide() + } + + func didStartSearching() { + guard stateSubject.value.input.isEmpty == false else { return } + + hudManager.show(.init( + actionTitle: Localized.Ud.Search.cancel, + hasDotAnimation: true, + onTapClosure: { [weak self] in + guard let self else { return } + self.didTapCancelSearch() + } + )) + + var content = stateSubject.value.input + + if stateSubject.value.item == .phone { + content += stateSubject.value.country.code } - var successPublisher: AnyPublisher<Contact, Never> { - successSubject.eraseToAnyPublisher() + enum NodeRegistrationError: Error { + case unhealthyNet + case belowMinimum } - var statePublisher: AnyPublisher<SearchLeftViewState, Never> { - stateSubject.eraseToAnyPublisher() + retry(max: 5, retryStrategy: .delay(seconds: 2)) { [weak self] in + guard let self else { return } + + do { + let nrr = try self.messenger.cMix.get()!.getNodeRegistrationStatus() + if nrr.ratio < 0.8 { throw NodeRegistrationError.belowMinimum } + } catch { + throw NodeRegistrationError.unhealthyNet + } + }.finalCatch { [weak self] in + guard let self else { return } + + if case .unhealthyNet = $0 as? NodeRegistrationError { + self.hudManager.show(.init(content: "Network is not healthy yet, try again within the next minute or so.")) + } else if case .belowMinimum = $0 as? NodeRegistrationError { + self.hudManager.show(.init(content:"Node registration ratio is still below 80%, try again within the next minute or so.")) + } else { + self.hudManager.show(.init(error: $0)) + } + + return } - private var invitation: String? - private var searchCancellables = Set<AnyCancellable>() - private let successSubject = PassthroughSubject<Contact, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let stateSubject = CurrentValueSubject<SearchLeftViewState, Never>(.init()) - private var networkCancellable = Set<AnyCancellable>() + var factType: FactType = .username - init(_ invitation: String? = nil) { - self.invitation = invitation + if stateSubject.value.item == .phone { + factType = .phone + } else if stateSubject.value.item == .email { + factType = .email } - func viewDidAppear() { - if let pendingInvitation = invitation { - invitation = nil - stateSubject.value.input = pendingInvitation - hudSubject.send(.onAction(Localized.Ud.Search.cancel)) - - networkCancellable.removeAll() - - networkMonitor.statusPublisher - .first { $0 == .available } - .eraseToAnyPublisher() - .flatMap { _ in self.session.waitForNodes(timeout: 5) } - .sink { - if case .failure(let error) = $0 { - self.hudSubject.send(.error(.init(with: error))) - } - } receiveValue: { - self.didStartSearching() - }.store(in: &networkCancellable) - } - } + backgroundScheduler.schedule { [weak self] in + guard let self else { return } + + do { + let report = try SearchUD.live( + params: .init( + e2eId: self.messenger.e2e.get()!.getId(), + udContact: self.messenger.ud.get()!.getContact(), + facts: [.init(type: factType, value: content)] + ), + callback: .init(handle: { + switch $0 { + case .success(let results): + self.hudManager.hide() + self.appendToLocalSearch( + XXModels.Contact( + id: try! results.first!.getId(), + marshaled: results.first!.data, + username: try! results.first?.getFacts().first(where: { $0.type == .username })?.value, + email: try? results.first?.getFacts().first(where: { $0.type == .email })?.value, + phone: try? results.first?.getFacts().first(where: { $0.type == .phone })?.value, + nickname: nil, + photo: nil, + authStatus: .stranger, + isRecent: true, + isBlocked: false, + isBanned: false, + createdAt: Date() + ) + ) + case .failure(let error): + print(">>> SearchUD error: \(error.localizedDescription)") + + self.appendToLocalSearch(nil) + self.hudManager.show(.init(error: error)) + } + }) + ) - func didEnterInput(_ string: String) { - stateSubject.value.input = string + print(">>> UDSearch.Report: \(report))") + } catch { + print(">>> UDSearch.Exception: \(error.localizedDescription)") + } } + } - func didPick(country: Country) { - stateSubject.value.country = country - } + func didTapResend(contact: XXModels.Contact) { + hudManager.show() - func didSelectItem(_ item: SearchSegmentedControl.Item) { - stateSubject.value.item = item - } + var contact = contact + contact.authStatus = .requesting - func didTapCancelSearch() { - searchCancellables.forEach { $0.cancel() } - searchCancellables.removeAll() - hudSubject.send(.none) - } + backgroundScheduler.schedule { [weak self] in + guard let self else { return } - func didStartSearching() { - guard stateSubject.value.input.isEmpty == false else { return } + do { + try self.dbManager.getDB().saveContact(contact) - hudSubject.send(.onAction(Localized.Ud.Search.cancel)) + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() - var content = stateSubject.value.input - let prefix = stateSubject.value.item.written.first!.uppercased() + if let fact = myFacts.get(.username) { + includedFacts.append(fact) + } - if stateSubject.value.item == .phone { - content += stateSubject.value.country.code + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) } - session.search(fact: "\(prefix)\(content)") - .sink { [unowned self] in - if case .failure(let error) = $0 { - self.appendToLocalSearch(nil) - self.hudSubject.send(.error(.init(with: error))) - } - } receiveValue: { contact in - self.hudSubject.send(.none) - self.appendToLocalSearch(contact) - }.store(in: &searchCancellables) - } + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) + } - func didTapResend(contact: Contact) { - hudSubject.send(.on) + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(contact.marshaled!), + myFacts: includedFacts + ) - do { - try self.session.retryRequest(contact) - hudSubject.send(.none) - } catch { - hudSubject.send(.error(.init(with: error))) - } - } + contact.authStatus = .requested + contact = try self.dbManager.getDB().saveContact(contact) - func didTapRequest(contact: Contact) { - hudSubject.send(.on) - var contact = contact - contact.nickname = contact.username - - do { - try self.session.add(contact) - hudSubject.send(.none) - successSubject.send(contact) - } catch { - hudSubject.send(.error(.init(with: error))) - } + self.hudManager.hide() + self.presentSuccessToast(for: contact, resent: true) + } catch { + contact.authStatus = .requestFailed + _ = try? self.dbManager.getDB().saveContact(contact) + self.hudManager.show(.init(error: error)) + } } + } - func didSet(nickname: String, for contact: Contact) { - if var contact = try? session.dbManager.fetchContacts(.init(id: [contact.id])).first { - contact.nickname = nickname - _ = try? session.dbManager.saveContact(contact) - } - } + func didTapRequest(contact: XXModels.Contact) { + hudManager.show() - private func appendToLocalSearch(_ user: Contact?) { - var snapshot = SearchSnapshot() + var contact = contact + contact.nickname = contact.username + contact.authStatus = .requesting - if var user = user { - if let contact = try! session.dbManager.fetchContacts(.init(id: [user.id])).first { - user.isBanned = contact.isBanned - user.isBlocked = contact.isBlocked - user.authStatus = contact.authStatus - } + backgroundScheduler.schedule { [weak self] in + guard let self else { return } - if user.authStatus != .friend, !reportingStatus.isEnabled() { - snapshot.appendSections([.stranger]) - snapshot.appendItems([.stranger(user)], toSection: .stranger) - } else if user.authStatus != .friend, reportingStatus.isEnabled(), !user.isBanned, !user.isBlocked { - snapshot.appendSections([.stranger]) - snapshot.appendItems([.stranger(user)], toSection: .stranger) - } + do { + try self.dbManager.getDB().saveContact(contact) + + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() + + if let fact = myFacts.get(.username) { + includedFacts.append(fact) } - let localsQuery = Contact.Query( - text: stateSubject.value.input, - authStatus: [.friend], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) + } - if let locals = try? session.dbManager.fetchContacts(localsQuery), - let localsWithoutMe = removeMyself(from: locals), - localsWithoutMe.isEmpty == false { - snapshot.appendSections([.connections]) - snapshot.appendItems( - localsWithoutMe.map(SearchItem.connection), - toSection: .connections - ) + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) } - stateSubject.value.snapshot = snapshot + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(contact.marshaled!), + myFacts: includedFacts + ) + + contact.authStatus = .requested + contact = try self.dbManager.getDB().saveContact(contact) + + self.hudManager.hide() + self.successSubject.send(contact) + self.presentSuccessToast(for: contact, resent: false) + } catch { + contact.authStatus = .requestFailed + _ = try? self.dbManager.getDB().saveContact(contact) + self.hudManager.show(.init(error: error)) + } + } + } + + func didSet(nickname: String, for contact: XXModels.Contact) { + if var contact = try? dbManager.getDB().fetchContacts(.init(id: [contact.id])).first { + contact.nickname = nickname + _ = try? dbManager.getDB().saveContact(contact) + } + } + + private func appendToLocalSearch(_ user: XXModels.Contact?) { + var snapshot = SearchSnapshot() + + if var user = user { + if let contact = try? dbManager.getDB().fetchContacts(.init(id: [user.id])).first { + user.isBanned = contact.isBanned + user.isBlocked = contact.isBlocked + user.authStatus = contact.authStatus + } + + if user.authStatus != .friend, !reportingStatus.isEnabled() { + snapshot.appendSections([.stranger]) + snapshot.appendItems([.stranger(user)], toSection: .stranger) + } else if user.authStatus != .friend, reportingStatus.isEnabled(), !user.isBanned, !user.isBlocked { + snapshot.appendSections([.stranger]) + snapshot.appendItems([.stranger(user)], toSection: .stranger) + } } - private func removeMyself(from collection: [Contact]) -> [Contact]? { - collection.filter { $0.id != session.myId } + let localsQuery = Contact.Query( + text: stateSubject.value.input, + authStatus: [.friend], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + if let locals = try? dbManager.getDB().fetchContacts(localsQuery), + let localsWithoutMe = removeMyself(from: locals), + localsWithoutMe.isEmpty == false { + snapshot.appendSections([.connections]) + snapshot.appendItems( + localsWithoutMe.map(SearchItem.connection), + toSection: .connections + ) } + + stateSubject.value.snapshot = snapshot + } + + private func removeMyself(from collection: [XXModels.Contact]) -> [XXModels.Contact]? { + collection.filter { $0.id != myId } + } + + private func presentSuccessToast(for contact: XXModels.Contact, resent: Bool) { + let name = contact.nickname ?? contact.username + let sentTitle = Localized.Requests.Sent.Toast.sent(name ?? "") + let resentTitle = Localized.Requests.Sent.Toast.resent(name ?? "") + + toastManager.enqueue(.init( + title: resent ? resentTitle : sentTitle, + leftImage: Asset.sharedSuccess.image + )) + } + + private func waitForNodes(timeout: Int) -> AnyPublisher<Void, Error> { + Deferred { + Future { promise in + retry(max: timeout, retryStrategy: .delay(seconds: 1)) { [weak self] in + guard let self else { return } + _ = try self.messenger.cMix.get()!.getNodeRegistrationStatus() + promise(.success(())) + }.finalCatch { + promise(.failure($0)) + } + } + }.eraseToAnyPublisher() + } } diff --git a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift index db977528e9a65491ada83947cdad167897532982..12dab510ef00415aaa67d419f2f3c3e75000540e 100644 --- a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift @@ -1,124 +1,135 @@ import Shared import Combine +import AppCore import XXModels import Defaults +import XXClient import Foundation -import Permissions -import Integration +import AppResources import ReportingFeature -import DependencyInjection +import XXMessengerClient +import PermissionsFeature +import ComposableArchitecture enum ScanningStatus: Equatable { - case reading - case processing - case success - case failed(ScanningError) + case reading + case processing + case success + case failed(ScanningError) } enum ScanningError: Equatable { - case requestOpened - case unknown(String) - case cameraPermission - case alreadyFriends(String) + case requestOpened + case unknown(String) + case cameraPermission + case alreadyFriends(String) } final class SearchRightViewModel { - @Dependency var session: SessionType - @Dependency var permissions: PermissionHandling - @Dependency var reportingStatus: ReportingStatus - - var foundPublisher: AnyPublisher<Contact, Never> { - foundSubject.eraseToAnyPublisher() - } - - var cameraSemaphorePublisher: AnyPublisher<Bool, Never> { - cameraSemaphoreSubject.eraseToAnyPublisher() + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.permissions) var permissions: PermissionsManager + @Dependency(\.reportingStatus) var reportingStatus: ReportingStatus + + var foundPublisher: AnyPublisher<XXModels.Contact, Never> { + foundSubject.eraseToAnyPublisher() + } + + var cameraSemaphorePublisher: AnyPublisher<Bool, Never> { + cameraSemaphoreSubject.eraseToAnyPublisher() + } + + var statusPublisher: AnyPublisher<ScanningStatus, Never> { + statusSubject.eraseToAnyPublisher() + } + + private let foundSubject = PassthroughSubject<XXModels.Contact, Never>() + private let cameraSemaphoreSubject = PassthroughSubject<Bool, Never>() + private(set) var statusSubject = CurrentValueSubject<ScanningStatus, Never>(.reading) + + func viewWillAppear() { + permissions.camera.request { [weak self] granted in + guard let self else { return } + + if granted { + self.statusSubject.value = .reading + self.cameraSemaphoreSubject.send(true) + } else { + self.statusSubject.send(.failed(.cameraPermission)) + } } - - var statusPublisher: AnyPublisher<ScanningStatus, Never> { - statusSubject.eraseToAnyPublisher() + } + + func viewWillDisappear() { + cameraSemaphoreSubject.send(false) + } + + func didScan(data: Data) { + /// We need to be accepting new readings in order + /// to process what just got scanned. + /// + guard statusSubject.value == .reading else { return } + statusSubject.send(.processing) + + /// Whatever got scanned, needs to have id and username + /// otherwise is just noise or an unknown qr code + /// + let user = XXClient.Contact.live(data) + + guard + let uid = try? user.getId(), + let facts = try? user.getFacts(), + let username = facts.first(where: { $0.type == .username })?.value + else { + let errorTitle = Localized.Scan.Error.invalid + statusSubject.send(.failed(.unknown(errorTitle))) + return } - private let foundSubject = PassthroughSubject<Contact, Never>() - private let cameraSemaphoreSubject = PassthroughSubject<Bool, Never>() - private(set) var statusSubject = CurrentValueSubject<ScanningStatus, Never>(.reading) - - func viewWillAppear() { - permissions.requestCamera { [weak self] granted in - guard let self = self else { return } - - if granted { - self.statusSubject.value = .reading - self.cameraSemaphoreSubject.send(true) - } else { - self.statusSubject.send(.failed(.cameraPermission)) - } - } + let email = facts.first { $0.type == .email }?.value + let phone = facts.first { $0.type == .phone }?.value + + /// Make sure we are not processing a contact + /// that we already have + /// + if let alreadyContact = try? dbManager.getDB().fetchContacts(.init(id: [uid])).first { + if alreadyContact.isBlocked, reportingStatus.isEnabled() { + statusSubject.send(.failed(.unknown("You previously blocked this user."))) + return + } + + if alreadyContact.isBanned, reportingStatus.isEnabled() { + statusSubject.send(.failed(.unknown("This user was banned."))) + return + } + + /// Show error accordingly to the auth status + /// + if alreadyContact.authStatus == .friend { + statusSubject.send(.failed(.alreadyFriends(username))) + } else if [.requested, .verified].contains(alreadyContact.authStatus) { + statusSubject.send(.failed(.requestOpened)) + } else { + let generalErrorTitle = Localized.Scan.Error.general + statusSubject.send(.failed(.unknown(generalErrorTitle))) + } + + return } - func viewWillDisappear() { - cameraSemaphoreSubject.send(false) - } - - func didScan(data: Data) { - /// We need to be accepting new readings in order - /// to process what just got scanned. - /// - guard statusSubject.value == .reading else { return } - statusSubject.send(.processing) - - /// Whatever got scanned, needs to have id and username - /// otherwise is just noise or an unknown qr code - /// - guard let userId = session.getId(from: data), - let username = try? session.extract(fact: .username, from: data) else { - let errorTitle = Localized.Scan.Error.invalid - statusSubject.send(.failed(.unknown(errorTitle))) - return - } - - /// Make sure we are not processing a contact - /// that we already have - /// - if let alreadyContact = try? session.dbManager.fetchContacts(.init(id: [userId])).first { - if alreadyContact.isBlocked, reportingStatus.isEnabled() { - statusSubject.send(.failed(.unknown("You previously blocked this user."))) - return - } - - if alreadyContact.isBanned, reportingStatus.isEnabled() { - statusSubject.send(.failed(.unknown("This user was banned."))) - return - } - - /// Show error accordingly to the auth status - /// - if alreadyContact.authStatus == .friend { - statusSubject.send(.failed(.alreadyFriends(username))) - } else if [.requested, .verified].contains(alreadyContact.authStatus) { - statusSubject.send(.failed(.requestOpened)) - } else { - let generalErrorTitle = Localized.Scan.Error.general - statusSubject.send(.failed(.unknown(generalErrorTitle))) - } - - return - } - - statusSubject.send(.success) - cameraSemaphoreSubject.send(false) - - foundSubject.send(.init( - id: userId, - marshaled: data, - username: username, - email: try? session.extract(fact: .email, from: data), - phone: try? session.extract(fact: .phone, from: data), - nickname: nil, - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - )) - } + statusSubject.send(.success) + cameraSemaphoreSubject.send(false) + + foundSubject.send(.init( + id: uid, + marshaled: data, + username: username, + email: email, + phone: phone, + nickname: nil, + photo: nil, + authStatus: .stranger, + isRecent: false, + createdAt: Date() + )) + } } diff --git a/Sources/SearchFeature/Views/OverlayView.swift b/Sources/SearchFeature/Views/OverlayView.swift index 8242857716936fe49cf21a59bad280195c726165..4e85a3ea143885bd0ed21e1cb236c126a6ba9843 100644 --- a/Sources/SearchFeature/Views/OverlayView.swift +++ b/Sources/SearchFeature/Views/OverlayView.swift @@ -1,181 +1,182 @@ import UIKit import Shared +import AppResources final class OverlayView: UIView { - private let cropView = UIView() - private let scanViewLength = 266.0 - private let maskLayer = CAShapeLayer() - private let topLeftLayer = CAShapeLayer() - private let topRightLayer = CAShapeLayer() - private let bottomLeftLayer = CAShapeLayer() - private let bottomRightLayer = CAShapeLayer() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.5) - - addSubview(cropView) - - cropView.snp.makeConstraints { - $0.width.equalTo(scanViewLength) - $0.centerY.equalToSuperview().offset(-50) - $0.centerX.equalToSuperview() - $0.height.equalTo(scanViewLength) - } - - maskLayer.fillRule = .evenOdd - layer.mask = maskLayer - layer.masksToBounds = true - - [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { - $0.strokeColor = Asset.brandPrimary.color.cgColor - $0.fillColor = UIColor.clear.cgColor - $0.lineWidth = 3.0 - $0.lineCap = .round - layer.addSublayer($0) - } + private let cropView = UIView() + private let scanViewLength = 266.0 + private let maskLayer = CAShapeLayer() + private let topLeftLayer = CAShapeLayer() + private let topRightLayer = CAShapeLayer() + private let bottomLeftLayer = CAShapeLayer() + private let bottomRightLayer = CAShapeLayer() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.5) + + addSubview(cropView) + + cropView.snp.makeConstraints { + $0.width.equalTo(scanViewLength) + $0.centerY.equalToSuperview().offset(-50) + $0.centerX.equalToSuperview() + $0.height.equalTo(scanViewLength) } - required init?(coder: NSCoder) { nil } + maskLayer.fillRule = .evenOdd + layer.mask = maskLayer + layer.masksToBounds = true - override func layoutSubviews() { - super.layoutSubviews() - - maskLayer.frame = bounds - let path = UIBezierPath(rect: bounds) - path.append(UIBezierPath(roundedRect: cropView.frame, cornerRadius: 30.0)) - maskLayer.path = path.cgPath - - topLeftLayer.frame = bounds - topRightLayer.frame = bounds - bottomRightLayer.frame = bounds - bottomLeftLayer.frame = bounds - - topLeftLayer.path = topLeftPath() - topRightLayer.path = topRightPath() - bottomRightLayer.path = bottomRightPath() - bottomLeftLayer.path = bottomLeftPath() - } - - func updateCornerColor(_ color: UIColor) { - [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { - $0.strokeColor = color.cgColor - } - } - - func topLeftPath() -> CGPath { - let path = UIBezierPath() - - let vert0X = cropView.frame.minX - 15 - let vert0Y = cropView.frame.minY + 45 - let vert0 = CGPoint(x: vert0X, y: vert0Y) - path.move(to: vert0) - - let vertNX = cropView.frame.minX - 15 - let vertNY = cropView.frame.minY + 15 - let vertN = CGPoint(x: vertNX, y: vertNY) - path.addLine(to: vertN) - - let arcCenterX = cropView.frame.minX + 15 - let arcCenterY = cropView.frame.minY + 15 - let arcCenter = CGPoint(x: arcCenterX , y: arcCenterY) - path.addArc(center: arcCenter, startAngle: .pi) - - let horizX = cropView.frame.minX + 45 - let horizY = cropView.frame.minY - 15 - let horiz = CGPoint(x: horizX, y: horizY) - path.addLine(to: horiz) - - return path.cgPath + [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { + $0.strokeColor = Asset.brandPrimary.color.cgColor + $0.fillColor = UIColor.clear.cgColor + $0.lineWidth = 3.0 + $0.lineCap = .round + layer.addSublayer($0) } + } - func topRightPath() -> CGPath { - let path = UIBezierPath() + required init?(coder: NSCoder) { nil } - let horiz0X = cropView.frame.maxX - 45 - let horiz0Y = cropView.frame.minY - 15 - let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) - path.move(to: horiz0) + override func layoutSubviews() { + super.layoutSubviews() - let horizNX = cropView.frame.maxX - 15 - let horizNY = cropView.frame.minY - 15 - let horizN = CGPoint(x: horizNX, y: horizNY) - path.addLine(to: horizN) + maskLayer.frame = bounds + let path = UIBezierPath(rect: bounds) + path.append(UIBezierPath(roundedRect: cropView.frame, cornerRadius: 30.0)) + maskLayer.path = path.cgPath - let arcCenterX = cropView.frame.maxX - 15 - let arcCenterY = cropView.frame.minY + 15 - let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) - path.addArc(center: arcCenter, startAngle: 3 * .pi/2) + topLeftLayer.frame = bounds + topRightLayer.frame = bounds + bottomRightLayer.frame = bounds + bottomLeftLayer.frame = bounds - let vertX = cropView.frame.maxX + 15 - let vertY = cropView.frame.minY + 45 - let vert = CGPoint(x: vertX, y: vertY) - path.addLine(to: vert) + topLeftLayer.path = topLeftPath() + topRightLayer.path = topRightPath() + bottomRightLayer.path = bottomRightPath() + bottomLeftLayer.path = bottomLeftPath() + } - return path.cgPath - } - - func bottomRightPath() -> CGPath { - let path = UIBezierPath() - - let vert0X = cropView.frame.maxX + 15 - let vert0Y = cropView.frame.maxY - 45 - let vert0 = CGPoint(x: vert0X, y: vert0Y) - path.move(to: vert0) - - let vertNX = cropView.frame.maxX + 15 - let vertNY = cropView.frame.maxY - 15 - let vertN = CGPoint(x: vertNX, y: vertNY) - path.addLine(to: vertN) - - let arcCenterX = cropView.frame.maxX - 15 - let arcCenterY = cropView.frame.maxY - 15 - let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) - path.addArc(center: arcCenter, startAngle: 0) - - let horizX = cropView.frame.maxX - 45 - let horizY = cropView.frame.maxY + 15 - let horiz = CGPoint(x: horizX, y: horizY) - path.addLine(to: horiz) - - return path.cgPath - } - - func bottomLeftPath() -> CGPath { - let path = UIBezierPath() - - let horiz0X = cropView.frame.minX + 45 - let horiz0Y = cropView.frame.maxY + 15 - let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) - path.move(to: horiz0) - - let horizNX = cropView.frame.minX + 15 - let horizNY = cropView.frame.maxY + 15 - let horizN = CGPoint(x: horizNX, y: horizNY) - path.addLine(to: horizN) - - let arcCenterX = cropView.frame.minX + 15 - let arcCenterY = cropView.frame.maxY - 15 - let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) - path.addArc(center: arcCenter, startAngle: .pi/2) - - let vertX = cropView.frame.minX - 15 - let vertY = cropView.frame.maxY - 45 - let vert = CGPoint(x: vertX, y: vertY) - path.addLine(to: vert) - - return path.cgPath + func updateCornerColor(_ color: UIColor) { + [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach { + $0.strokeColor = color.cgColor } + } + + func topLeftPath() -> CGPath { + let path = UIBezierPath() + + let vert0X = cropView.frame.minX - 15 + let vert0Y = cropView.frame.minY + 45 + let vert0 = CGPoint(x: vert0X, y: vert0Y) + path.move(to: vert0) + + let vertNX = cropView.frame.minX - 15 + let vertNY = cropView.frame.minY + 15 + let vertN = CGPoint(x: vertNX, y: vertNY) + path.addLine(to: vertN) + + let arcCenterX = cropView.frame.minX + 15 + let arcCenterY = cropView.frame.minY + 15 + let arcCenter = CGPoint(x: arcCenterX , y: arcCenterY) + path.addArc(center: arcCenter, startAngle: .pi) + + let horizX = cropView.frame.minX + 45 + let horizY = cropView.frame.minY - 15 + let horiz = CGPoint(x: horizX, y: horizY) + path.addLine(to: horiz) + + return path.cgPath + } + + func topRightPath() -> CGPath { + let path = UIBezierPath() + + let horiz0X = cropView.frame.maxX - 45 + let horiz0Y = cropView.frame.minY - 15 + let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) + path.move(to: horiz0) + + let horizNX = cropView.frame.maxX - 15 + let horizNY = cropView.frame.minY - 15 + let horizN = CGPoint(x: horizNX, y: horizNY) + path.addLine(to: horizN) + + let arcCenterX = cropView.frame.maxX - 15 + let arcCenterY = cropView.frame.minY + 15 + let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) + path.addArc(center: arcCenter, startAngle: 3 * .pi/2) + + let vertX = cropView.frame.maxX + 15 + let vertY = cropView.frame.minY + 45 + let vert = CGPoint(x: vertX, y: vertY) + path.addLine(to: vert) + + return path.cgPath + } + + func bottomRightPath() -> CGPath { + let path = UIBezierPath() + + let vert0X = cropView.frame.maxX + 15 + let vert0Y = cropView.frame.maxY - 45 + let vert0 = CGPoint(x: vert0X, y: vert0Y) + path.move(to: vert0) + + let vertNX = cropView.frame.maxX + 15 + let vertNY = cropView.frame.maxY - 15 + let vertN = CGPoint(x: vertNX, y: vertNY) + path.addLine(to: vertN) + + let arcCenterX = cropView.frame.maxX - 15 + let arcCenterY = cropView.frame.maxY - 15 + let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) + path.addArc(center: arcCenter, startAngle: 0) + + let horizX = cropView.frame.maxX - 45 + let horizY = cropView.frame.maxY + 15 + let horiz = CGPoint(x: horizX, y: horizY) + path.addLine(to: horiz) + + return path.cgPath + } + + func bottomLeftPath() -> CGPath { + let path = UIBezierPath() + + let horiz0X = cropView.frame.minX + 45 + let horiz0Y = cropView.frame.maxY + 15 + let horiz0 = CGPoint(x: horiz0X, y: horiz0Y) + path.move(to: horiz0) + + let horizNX = cropView.frame.minX + 15 + let horizNY = cropView.frame.maxY + 15 + let horizN = CGPoint(x: horizNX, y: horizNY) + path.addLine(to: horizN) + + let arcCenterX = cropView.frame.minX + 15 + let arcCenterY = cropView.frame.maxY - 15 + let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY) + path.addArc(center: arcCenter, startAngle: .pi/2) + + let vertX = cropView.frame.minX - 15 + let vertY = cropView.frame.maxY - 45 + let vert = CGPoint(x: vertX, y: vertY) + path.addLine(to: vert) + + return path.cgPath + } } private extension UIBezierPath { - func addArc(center: CGPoint, startAngle: CGFloat) { - addArc( - withCenter: center, - radius: 30, - startAngle: startAngle, - endAngle: startAngle + .pi/2, - clockwise: true - ) - } + func addArc(center: CGPoint, startAngle: CGFloat) { + addArc( + withCenter: center, + radius: 30, + startAngle: startAngle, + endAngle: startAngle + .pi/2, + clockwise: true + ) + } } diff --git a/Sources/SearchFeature/Views/SearchContainerView.swift b/Sources/SearchFeature/Views/SearchContainerView.swift index 3a270bc5a52891fc915951bf68f738f2e5cdcd6f..878d438a464f7c22006518517f5f534f92c17a77 100644 --- a/Sources/SearchFeature/Views/SearchContainerView.swift +++ b/Sources/SearchFeature/Views/SearchContainerView.swift @@ -1,35 +1,32 @@ import UIKit import Shared +import AppResources final class SearchContainerView: UIView { - let scrollView = UIScrollView() - let segmentedControl = SearchSegmentedControl() + let scrollView = UIScrollView() + let segmentedControl = SearchSegmentedControl() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - addSubview(segmentedControl) - addSubview(scrollView) + backgroundColor = Asset.neutralWhite.color + addSubview(segmentedControl) + addSubview(scrollView) - setupConstraints() + segmentedControl.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(10) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.height.equalTo(60) } - required init?(coder: NSCoder) { nil } - - private func setupConstraints() { - segmentedControl.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).offset(10) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.height.equalTo(60) - } - - scrollView.snp.makeConstraints { - $0.top.equalTo(segmentedControl.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + scrollView.snp.makeConstraints { + $0.top.equalTo(segmentedControl.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/SearchFeature/Views/SearchLeftEmptyView.swift b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift index 84c64c87a6096a16b2bbf793a9eadc1c95495324..4f33b053f0a104cd83a8d9c59e5ebbefdb40fc7b 100644 --- a/Sources/SearchFeature/Views/SearchLeftEmptyView.swift +++ b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift @@ -1,26 +1,27 @@ import UIKit import Shared +import AppResources final class SearchLeftEmptyView: UIView { - let titleLabel = UILabel() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - titleLabel.numberOfLines = 0 - titleLabel.textAlignment = .center - titleLabel.font = Fonts.Mulish.regular.font(size: 15.0) - titleLabel.textColor = Asset.neutralSecondaryAlternative.color - - addSubview(titleLabel) - - titleLabel.snp.makeConstraints { - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - } + let titleLabel = UILabel() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .center + titleLabel.font = Fonts.Mulish.regular.font(size: 15.0) + titleLabel.textColor = Asset.neutralSecondaryAlternative.color + + addSubview(titleLabel) + + titleLabel.snp.makeConstraints { + $0.center.equalToSuperview() + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) } - - required init?(coder: NSCoder) { nil } + } + + required init?(coder: NSCoder) { nil } } diff --git a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift index 7742ff1d64c56151a88ae9b5ae47e82ebb59dc1b..fa7f5c4fd37a6ade7e3105bf2f17fd3f45ab6938 100644 --- a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift +++ b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift @@ -1,74 +1,75 @@ import UIKit import Shared import Combine +import AppResources final class SearchLeftPlaceholderView: UIView { - let titleLabel = UILabel() - let subtitleWithInfo = TextWithInfoView() - - var infoPublisher: AnyPublisher<Void, Never> { - infoSubject.eraseToAnyPublisher() - } - - private let infoSubject = PassthroughSubject<Void, Never>() - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - let attrString = NSMutableAttributedString( - string: Localized.Ud.Search.Placeholder.title, - attributes: [ - .foregroundColor: Asset.neutralDark.color, - .font: Fonts.Mulish.bold.font(size: 32.0) - ] - ) - - attrString.addAttribute( - name: .foregroundColor, - value: Asset.brandPrimary.color, - betweenCharacters: "#" - ) - - titleLabel.numberOfLines = 0 - titleLabel.attributedText = attrString - - let paragraph = NSMutableParagraphStyle() - paragraph.lineHeightMultiple = 1.3 - - subtitleWithInfo.setup( - text: Localized.Ud.Search.Placeholder.subtitle, - attributes: [ - .paragraphStyle: paragraph, - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) - ], - didTapInfo: { [weak self] in - guard let self = self else { return } - self.infoSubject.send(()) - } - ) - - addSubview(titleLabel) - addSubview(subtitleWithInfo) - - setupConstraints() + let titleLabel = UILabel() + let subtitleWithInfo = TextWithInfoView() + + var infoPublisher: AnyPublisher<Void, Never> { + infoSubject.eraseToAnyPublisher() + } + + private let infoSubject = PassthroughSubject<Void, Never>() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + let attrString = NSMutableAttributedString( + string: Localized.Ud.Search.Placeholder.title, + attributes: [ + .foregroundColor: Asset.neutralDark.color, + .font: Fonts.Mulish.bold.font(size: 32.0) + ] + ) + + attrString.addAttribute( + name: .foregroundColor, + value: Asset.brandPrimary.color, + betweenCharacters: "#" + ) + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = attrString + + let paragraph = NSMutableParagraphStyle() + paragraph.lineHeightMultiple = 1.3 + + subtitleWithInfo.setup( + text: Localized.Ud.Search.Placeholder.subtitle, + attributes: [ + .paragraphStyle: paragraph, + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) + ], + didTapInfo: { [weak self] in + guard let self else { return } + self.infoSubject.send(()) + } + ) + + addSubview(titleLabel) + addSubview(subtitleWithInfo) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + private func setupConstraints() { + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(50) + $0.left.equalToSuperview().offset(32.5) + $0.right.equalToSuperview().offset(-32.5) } - - required init?(coder: NSCoder) { nil } - - private func setupConstraints() { - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(50) - $0.left.equalToSuperview().offset(32.5) - $0.right.equalToSuperview().offset(-32.5) - } - - subtitleWithInfo.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(30) - $0.left.equalToSuperview().offset(32.5) - $0.right.equalToSuperview().offset(-32.5) - $0.bottom.equalToSuperview() - } + + subtitleWithInfo.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(30) + $0.left.equalToSuperview().offset(32.5) + $0.right.equalToSuperview().offset(-32.5) + $0.bottom.equalToSuperview() } + } } diff --git a/Sources/SearchFeature/Views/SearchLeftView.swift b/Sources/SearchFeature/Views/SearchLeftView.swift index bbdda3f926818439e4c28d5beb020239a623048a..4eefca9a834265e687fee288e19a9a88689529d7 100644 --- a/Sources/SearchFeature/Views/SearchLeftView.swift +++ b/Sources/SearchFeature/Views/SearchLeftView.swift @@ -1,71 +1,72 @@ import UIKit import Shared +import AppResources final class SearchLeftView: UIView { - let tableView = UITableView() - let inputStackView = UIStackView() - let inputField = SearchComponent() - let emptyView = SearchLeftEmptyView() - let countryButton = SearchCountryComponent() - let placeholderView = SearchLeftPlaceholderView() + let tableView = UITableView() + let inputStackView = UIStackView() + let inputField = SearchComponent() + let emptyView = SearchLeftEmptyView() + let countryButton = SearchCountryComponent() + let placeholderView = SearchLeftPlaceholderView() - init() { - super.init(frame: .zero) + init() { + super.init(frame: .zero) - emptyView.isHidden = true - backgroundColor = Asset.neutralWhite.color - tableView.backgroundColor = Asset.neutralWhite.color + emptyView.isHidden = true + backgroundColor = Asset.neutralWhite.color + tableView.backgroundColor = Asset.neutralWhite.color - inputStackView.spacing = 5 - inputStackView.addArrangedSubview(countryButton) - inputStackView.addArrangedSubview(inputField) + inputStackView.spacing = 5 + inputStackView.addArrangedSubview(countryButton) + inputStackView.addArrangedSubview(inputField) - addSubview(inputStackView) - addSubview(tableView) - addSubview(emptyView) - addSubview(placeholderView) + addSubview(inputStackView) + addSubview(tableView) + addSubview(emptyView) + addSubview(placeholderView) - setupConstraints() - } + setupConstraints() + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } - func updateUIForItem(item: SearchSegmentedControl.Item) { - countryButton.isHidden = item != .phone + func updateUIForItem(item: SearchSegmentedControl.Item) { + countryButton.isHidden = item != .phone - let emptyTitle = Localized.Ud.Search.empty(item.written) - emptyView.titleLabel.text = emptyTitle + let emptyTitle = Localized.Ud.Search.empty(item.written) + emptyView.titleLabel.text = emptyTitle - let inputFieldTitle = Localized.Ud.Search.input(item.written) - inputField.set(placeholder: inputFieldTitle, imageAtRight: nil) - } + let inputFieldTitle = Localized.Ud.Search.input(item.written) + inputField.set(placeholder: inputFieldTitle, imageAtRight: nil) + } - private func setupConstraints() { - inputStackView.snp.makeConstraints { - $0.top.equalToSuperview().offset(20) - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - } + private func setupConstraints() { + inputStackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + } - tableView.snp.makeConstraints { - $0.top.equalTo(inputField.snp.bottom).offset(20) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + tableView.snp.makeConstraints { + $0.top.equalTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } - emptyView.snp.makeConstraints { - $0.top.equalTo(inputField.snp.bottom).offset(20) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + emptyView.snp.makeConstraints { + $0.top.equalTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } - placeholderView.snp.makeConstraints { - $0.top.equalTo(inputField.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } + placeholderView.snp.makeConstraints { + $0.top.equalTo(inputField.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } + } } diff --git a/Sources/SearchFeature/Views/SearchRightView.swift b/Sources/SearchFeature/Views/SearchRightView.swift index c2fd74760c79e8799c31bb91b0f73a85d4d5cd98..a374439fb729112f781ce56848daa2ebf78cf60d 100644 --- a/Sources/SearchFeature/Views/SearchRightView.swift +++ b/Sources/SearchFeature/Views/SearchRightView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import AppResources final class SearchRightView: UIView { let statusLabel = UILabel() diff --git a/Sources/SearchFeature/Views/SearchSegmentedButton.swift b/Sources/SearchFeature/Views/SearchSegmentedButton.swift index 3b8e65fb1b778703a748626155d6f2b0b18ec94b..ead382a4d69a9caab5e0e48fce4e7f89a1b737e7 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedButton.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedButton.swift @@ -1,49 +1,50 @@ import UIKit import Shared +import AppResources final class SearchSegmentedButton: UIControl { - private let titleLabel = UILabel() - private let imageView = UIImageView() - private let highlightColor = Asset.brandPrimary.color - private let discreteColor = Asset.neutralDisabled.color - - init() { - super.init(frame: .zero) - - imageView.contentMode = .center - titleLabel.textAlignment = .center - titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) - - addSubview(titleLabel) - addSubview(imageView) - - setupConstraints() - } - - required init?(coder: NSCoder) { nil } - - func setup(title: String, icon: UIImage) { - imageView.image = icon - titleLabel.text = title - imageView.tintColor = discreteColor - titleLabel.textColor = discreteColor - } - - func setSelected(_ bool: Bool) { - imageView.tintColor = bool ? highlightColor : discreteColor - titleLabel.textColor = bool ? highlightColor : discreteColor + private let titleLabel = UILabel() + private let imageView = UIImageView() + private let highlightColor = Asset.brandPrimary.color + private let discreteColor = Asset.neutralDisabled.color + + init() { + super.init(frame: .zero) + + imageView.contentMode = .center + titleLabel.textAlignment = .center + titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) + + addSubview(titleLabel) + addSubview(imageView) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + func setup(title: String, icon: UIImage) { + imageView.image = icon + titleLabel.text = title + imageView.tintColor = discreteColor + titleLabel.textColor = discreteColor + } + + func setSelected(_ bool: Bool) { + imageView.tintColor = bool ? highlightColor : discreteColor + titleLabel.textColor = bool ? highlightColor : discreteColor + } + + private func setupConstraints() { + imageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(7.5) + $0.centerX.equalToSuperview() } - private func setupConstraints() { - imageView.snp.makeConstraints { - $0.top.equalToSuperview().offset(7.5) - $0.centerX.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(imageView.snp.bottom).offset(2) - $0.centerX.equalToSuperview() - $0.bottom.equalToSuperview().offset(-7.5) - } + titleLabel.snp.makeConstraints { + $0.top.equalTo(imageView.snp.bottom).offset(2) + $0.centerX.equalToSuperview() + $0.bottom.equalToSuperview().offset(-7.5) } + } } diff --git a/Sources/SearchFeature/Views/SearchSegmentedControl.swift b/Sources/SearchFeature/Views/SearchSegmentedControl.swift index 6141360378cd659e89dbdf67c4180f4a0e9b4ce6..94833095934ff15522cbf2f01255bdaaf652fb91 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedControl.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedControl.swift @@ -2,6 +2,7 @@ import UIKit import Shared import SnapKit import Combine +import AppResources final class SearchSegmentedControl: UIView { enum Item: Int { diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift deleted file mode 100644 index ee5be65a72df17338ab3daca3ddcf58531057338..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ /dev/null @@ -1,120 +0,0 @@ -import HUD -import UIKit -import DrawerFeature -import Shared -import Combine -import Defaults -import ScrollViewController -import DependencyInjection - -public final class AccountDeleteController: UIViewController { - @KeyObject(.username, defaultValue: "") var username: String - - @Dependency private var hud: HUD - @Dependency private var coordinator: SettingsCoordinating - - lazy private var screenView = AccountDeleteView() - lazy private var scrollViewController = ScrollViewController() - - private let viewModel = AccountDeleteViewModel() - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupScrollView() - setupBindings() - - screenView.update(username: username) - - screenView.setInfoClosure { [weak self] in - self?.presentInfo( - title: Localized.Settings.Delete.Info.title, - subtitle: Localized.Settings.Delete.Info.subtitle - ) - } - } - - private func setupScrollView() { - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color - } - - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in dismiss(animated: true) } - .store(in: &cancellables) - - screenView.inputField.textPublisher - .sink { [unowned self] in screenView.update(status: $0 == username ? .valid("") : .invalid("")) } - .store(in: &cancellables) - - screenView.confirmButton.publisher(for: .touchUpInside) - .sink { [unowned self] in - DispatchQueue.global().async { [weak self] in - self?.viewModel.didTapDelete() - } - }.store(in: &cancellables) - - screenView.cancelButton.publisher(for: .touchUpInside) - .sink { [unowned self] in navigationController?.popViewController(animated: true) } - .store(in: &cancellables) - } - - private func presentInfo(title: String, subtitle: String) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } -} diff --git a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift index efe25a0030b832db3507172bf4a966d26e0ed54f..96225a7dbebff485ce4289417abe0646150fc6f1 100644 --- a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift @@ -1,90 +1,111 @@ import UIKit -import Shared import Combine -import DependencyInjection +import Dependencies +import AppResources +import AppNavigation public final class SettingsAdvancedController: UIViewController { - @Dependency private var coordinator: SettingsCoordinating - - lazy private var screenView = SettingsAdvancedView() - - private var cancellables = Set<AnyCancellable>() - private let viewModel = SettingsAdvancedViewModel() - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupNavigationBar() - setupBindings() - - viewModel.loadCachedSettings() - } - - private func setupNavigationBar() { - let title = UILabel() - title.text = Localized.Settings.Advanced.title - title.textColor = Asset.neutralActive.color - title.font = Fonts.Mulish.semiBold.font(size: 18.0) - - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) - navigationItem.leftItemsSupplementBackButton = true - } - - private func setupBindings() { - screenView.downloadLogsButton - .publisher(for: .touchUpInside) - .sink { [weak viewModel] in viewModel?.didTapDownloadLogs() } - .store(in: &cancellables) - - screenView.logRecordingSwitcher.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleRecordLogs() } - .store(in: &cancellables) - - screenView.showUsernamesSwitcher.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleShowUsernames() } - .store(in: &cancellables) - - screenView.crashReportingSwitcher.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleCrashReporting() } - .store(in: &cancellables) - - screenView.reportingSwitcher.switcherView - .publisher(for: .valueChanged) - .compactMap { [weak screenView] _ in screenView?.reportingSwitcher.switcherView.isOn } - .sink { [weak viewModel] isOn in viewModel?.didSetReporting(enabled: isOn) } - .store(in: &cancellables) - - viewModel.sharePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toActivityController(with: [$0], from: self) } - .store(in: &cancellables) - - viewModel.state - .removeDuplicates() - .map(\.isReportingOptional) - .sink { [unowned self] in screenView.reportingSwitcher.isHidden = !$0 } - .store(in: &cancellables) - - viewModel.state - .removeDuplicates() - .sink { [unowned self] state in - screenView.logRecordingSwitcher.switcherView.setOn(state.isRecordingLogs, animated: true) - screenView.crashReportingSwitcher.switcherView.setOn(state.isCrashReporting, animated: true) - screenView.showUsernamesSwitcher.switcherView.setOn(state.isShowingUsernames, animated: true) - screenView.reportingSwitcher.switcherView.setOn(state.isReportingEnabled, animated: true) - }.store(in: &cancellables) - } + @Dependency(\.navigator) var navigator: Navigator + + private lazy var screenView = SettingsAdvancedView() + + private var cancellables = Set<AnyCancellable>() + private let viewModel = SettingsAdvancedViewModel() + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupBindings() + + viewModel.loadCachedSettings() + } + + private func setupNavigationBar() { + let title = UILabel() + title.text = Localized.Settings.Advanced.title + title.textColor = Asset.neutralActive.color + title.font = Fonts.Mulish.semiBold.font(size: 18.0) + + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: title) + navigationItem.leftItemsSupplementBackButton = true + } + + private func setupBindings() { + screenView + .downloadLogsButton + .publisher(for: .touchUpInside) + .sink { [weak viewModel] in + viewModel?.didTapDownloadLogs() + }.store(in: &cancellables) + + screenView + .logRecordingSwitcher + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleRecordLogs() + }.store(in: &cancellables) + + screenView + .showUsernamesSwitcher + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleShowUsernames() + }.store(in: &cancellables) + + screenView + .crashReportingSwitcher + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleCrashReporting() + }.store(in: &cancellables) + + screenView + .reportingSwitcher + .switcherView + .publisher(for: .valueChanged) + .compactMap { [weak screenView] _ in + screenView?.reportingSwitcher.switcherView.isOn + }.sink { [weak viewModel] isOn in + viewModel?.didSetReporting(enabled: isOn) + }.store(in: &cancellables) + + viewModel + .sharePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentActivitySheet(items: [$0], from: self)) + }.store(in: &cancellables) + + viewModel + .state + .removeDuplicates() + .map(\.isReportingOptional) + .sink { [unowned self] in + screenView.reportingSwitcher.isHidden = !$0 + }.store(in: &cancellables) + + viewModel + .state + .removeDuplicates() + .sink { [unowned self] state in + screenView.logRecordingSwitcher.switcherView.setOn(state.isRecordingLogs, animated: true) + screenView.crashReportingSwitcher.switcherView.setOn(state.isCrashReporting, animated: true) + screenView.showUsernamesSwitcher.switcherView.setOn(state.isShowingUsernames, animated: true) + screenView.reportingSwitcher.switcherView.setOn(state.isReportingEnabled, animated: true) + }.store(in: &cancellables) + } } diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift deleted file mode 100644 index e58a347096e53ddefc3014517c4f11c946a06429..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/Controllers/SettingsController.swift +++ /dev/null @@ -1,300 +0,0 @@ -import HUD -import DrawerFeature -import UIKit -import Theme -import Shared -import Combine -import DependencyInjection -import ScrollViewController - -public final class SettingsController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: SettingsCoordinating - @Dependency private var statusBarController: StatusBarStyleControlling - - lazy private var scrollViewController = ScrollViewController() - lazy private var screenView = SettingsView { - switch $0 { - case .icognitoKeyboard: - self.presentInfo( - title: Localized.Settings.InfoDrawer.Icognito.title, - subtitle: Localized.Settings.InfoDrawer.Icognito.subtitle - ) - case .biometrics: - self.presentInfo( - title: Localized.Settings.InfoDrawer.Biometrics.title, - subtitle: Localized.Settings.InfoDrawer.Biometrics.subtitle - ) - case .notifications: - self.presentInfo( - title: Localized.Settings.InfoDrawer.Notifications.title, - subtitle: Localized.Settings.InfoDrawer.Notifications.subtitle, - urlString: "https://links.xx.network/denseids" - ) - - case .dummyTraffic: - self.presentInfo( - title: Localized.Settings.InfoDrawer.Traffic.title, - subtitle: Localized.Settings.InfoDrawer.Traffic.subtitle, - urlString: "https://links.xx.network/covertraffic" - ) - } - } - - private let viewModel = SettingsViewModel() - private var cancellables = Set<AnyCancellable>() - private var drawerCancellables = Set<AnyCancellable>() - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - statusBarController.style.send(.darkContent) - navigationController?.navigationBar - .customize(backgroundColor: Asset.neutralWhite.color) - } - - public override func viewDidLoad() { - super.viewDidLoad() - - setupNavigationBar() - setupScrollView() - setupBindings() - - viewModel.loadCachedSettings() - } - - private func setupNavigationBar() { - navigationItem.backButtonTitle = "" - - let titleLabel = UILabel() - titleLabel.text = Localized.Settings.title - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) - - let menuButton = UIButton() - menuButton.tintColor = Asset.neutralDark.color - menuButton.setImage(Asset.chatListMenu.image, for: .normal) - menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) - menuButton.snp.makeConstraints { $0.width.equalTo(50) } - - navigationItem.leftBarButtonItem = UIBarButtonItem( - customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) - ) - } - - private func setupScrollView() { - scrollViewController.view.backgroundColor = Asset.neutralWhite.color - - addChild(scrollViewController) - view.addSubview(scrollViewController.view) - - scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } - scrollViewController.didMove(toParent: self) - scrollViewController.contentView = screenView - } - - private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - - screenView.inAppNotifications.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleInAppNotifications() } - .store(in: &cancellables) - - screenView.dummyTraffic.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleDummyTraffic() } - .store(in: &cancellables) - - screenView.remoteNotifications.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didTogglePushNotifications() } - .store(in: &cancellables) - - screenView.hideActiveApp.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleHideActiveApps() } - .store(in: &cancellables) - - screenView.icognitoKeyboard.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleIcognitoKeyboard() } - .store(in: &cancellables) - - screenView.biometrics.switcherView - .publisher(for: .valueChanged) - .sink { [weak viewModel] in viewModel?.didToggleBiometrics() } - .store(in: &cancellables) - - screenView.privacyPolicyButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - presentDrawer( - title: Localized.Settings.Drawer.title(Localized.Settings.privacyPolicy), - subtitle: Localized.Settings.Drawer.subtitle(Localized.Settings.privacyPolicy), - actionTitle: Localized.ChatList.Dashboard.open) { - guard let url = URL(string: "https://elixxir.io/privategrity-corporation-privacy-policy/") else { return } - UIApplication.shared.open(url, options: [:]) - } - }.store(in: &cancellables) - - screenView.disclosuresButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - presentDrawer( - title: Localized.Settings.Drawer.title(Localized.Settings.disclosures), - subtitle: Localized.Settings.Drawer.subtitle(Localized.Settings.disclosures), - actionTitle: Localized.ChatList.Dashboard.open) { - guard let url = URL(string: "https://elixxir.io/privategrity-corporation-terms-of-use/") else { return } - UIApplication.shared.open(url, options: [:]) - } - }.store(in: &cancellables) - - screenView.deleteButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toDelete(from: self) } - .store(in: &cancellables) - - screenView.accountBackupButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toBackup(from: self) } - .store(in: &cancellables) - - screenView.advancedButton - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toAdvanced(from: self) } - .store(in: &cancellables) - - viewModel.state - .map(\.isBiometricsPossible) - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak screenView] in screenView?.biometrics.switcherView.isEnabled = $0 } - .store(in: &cancellables) - - viewModel.state - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] state in - screenView.biometrics.switcherView.setOn(state.isBiometricsEnabled, animated: true) - screenView.hideActiveApp.switcherView.setOn(state.isHideActiveApps, animated: true) - screenView.icognitoKeyboard.switcherView.setOn(state.isIcognitoKeyboard, animated: true) - screenView.inAppNotifications.switcherView.setOn(state.isInAppNotification, animated: true) - screenView.remoteNotifications.switcherView.setOn(state.isPushNotification, animated: true) - screenView.dummyTraffic.switcherView.setOn(state.isDummyTrafficOn, animated: true) - }.store(in: &cancellables) - } - - private func presentDrawer( - title: String, - subtitle: String, - actionTitle: String, - action: @escaping () -> Void - ) { - let actionButton = CapsuleButton() - actionButton.setStyle(.red) - actionButton.setTitle(actionTitle, for: .normal) - - let cancelButton = CapsuleButton() - cancelButton.setStyle(.seeThrough) - cancelButton.setTitle(Localized.ChatList.Dashboard.cancel, for: .normal) - - let drawer = DrawerController(with: [ - DrawerImage( - image: Asset.drawerNegative.image - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 18.0), - text: title, - color: Asset.neutralActive.color - ), - DrawerText( - font: Fonts.Mulish.semiBold.font(size: 14.0), - text: subtitle, - color: Asset.neutralWeak.color, - lineHeightMultiple: 1.35, - spacingAfter: 25 - ), - DrawerStack( - spacing: 20.0, - views: [actionButton, cancelButton] - ) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - - action() - } - }.store(in: &drawerCancellables) - - cancelButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - self?.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } - - @objc private func didTapMenu() { - coordinator.toSideMenu(from: self) - } -} - -extension SettingsController { - private func presentInfo( - title: String, - subtitle: String, - urlString: String = "" - ) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } -} diff --git a/Sources/SettingsFeature/Controllers/SettingsDeleteController.swift b/Sources/SettingsFeature/Controllers/SettingsDeleteController.swift new file mode 100644 index 0000000000000000000000000000000000000000..0c64d28d92144c28ed4718a281b352f8ab749669 --- /dev/null +++ b/Sources/SettingsFeature/Controllers/SettingsDeleteController.swift @@ -0,0 +1,128 @@ +import UIKit +import Shared +import Combine +import Defaults +import Dependencies +import AppResources +import AppNavigation +import DrawerFeature +import ScrollViewController + +public final class SettingsDeleteController: UIViewController { + @Dependency(\.navigator) var navigator: Navigator + + @KeyObject(.username, defaultValue: "") var username: String + + private lazy var screenView = SettingsDeleteView() + private lazy var scrollViewController = ScrollViewController() + + private let viewModel = SettingsDeleteViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + + screenView.update(username: username) + + screenView.setInfoClosure { [weak self] in + guard let self else { return } + self.presentInfo( + title: Localized.Settings.Delete.Info.title, + subtitle: Localized.Settings.Delete.Info.subtitle + ) + } + } + + private func setupScrollView() { + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() + } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + scrollViewController.scrollView.backgroundColor = Asset.neutralWhite.color + } + + private func setupBindings() { + screenView + .cancelButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + dismiss(animated: true) + }.store(in: &cancellables) + + screenView + .inputField + .textPublisher + .sink { [unowned self] in + screenView.update( + status: $0 == username ? + .valid("") : .invalid("") + ) + }.store(in: &cancellables) + + screenView + .confirmButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapDelete() + }.store(in: &cancellables) + + screenView + .cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + } + + private func presentInfo(title: String, subtitle: String) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } +} diff --git a/Sources/SettingsFeature/Controllers/SettingsMainController.swift b/Sources/SettingsFeature/Controllers/SettingsMainController.swift new file mode 100644 index 0000000000000000000000000000000000000000..574c0dca110aa011dd7643eae8e60a2b028827e5 --- /dev/null +++ b/Sources/SettingsFeature/Controllers/SettingsMainController.swift @@ -0,0 +1,321 @@ +import UIKit +import Shared +import Combine +import AppCore +import Dependencies +import AppResources +import AppNavigation +import DrawerFeature +import ScrollViewController + +public final class SettingsMainController: UIViewController { + @Dependency(\.navigator) var navigator: Navigator + @Dependency(\.app.statusBar) var statusBar: StatusBarStylist + + private lazy var scrollViewController = ScrollViewController() + private lazy var screenView = SettingsMainView { + switch $0 { + case .icognitoKeyboard: + self.presentInfo( + title: Localized.Settings.InfoDrawer.Icognito.title, + subtitle: Localized.Settings.InfoDrawer.Icognito.subtitle + ) + case .biometrics: + self.presentInfo( + title: Localized.Settings.InfoDrawer.Biometrics.title, + subtitle: Localized.Settings.InfoDrawer.Biometrics.subtitle + ) + case .notifications: + self.presentInfo( + title: Localized.Settings.InfoDrawer.Notifications.title, + subtitle: Localized.Settings.InfoDrawer.Notifications.subtitle, + urlString: "https://links.xx.network/denseids" + ) + + case .dummyTraffic: + self.presentInfo( + title: Localized.Settings.InfoDrawer.Traffic.title, + subtitle: Localized.Settings.InfoDrawer.Traffic.subtitle, + urlString: "https://links.xx.network/covertraffic" + ) + } + } + + private let viewModel = SettingsMainViewModel() + private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBar.set(.darkContent) + navigationController?.navigationBar + .customize(backgroundColor: Asset.neutralWhite.color) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + setupNavigationBar() + setupScrollView() + setupBindings() + + viewModel.loadCachedSettings() + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let titleLabel = UILabel() + titleLabel.text = Localized.Settings.title + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0) + + let menuButton = UIButton() + menuButton.tintColor = Asset.neutralDark.color + menuButton.setImage(Asset.chatListMenu.image, for: .normal) + menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) + menuButton.snp.makeConstraints { $0.width.equalTo(50) } + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [menuButton, titleLabel]) + ) + } + + private func setupScrollView() { + scrollViewController.view.backgroundColor = Asset.neutralWhite.color + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { + $0.edges.equalToSuperview() + } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + } + + private func setupBindings() { + screenView + .inAppNotifications + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleInAppNotifications() + }.store(in: &cancellables) + + screenView + .dummyTraffic + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleDummyTraffic() + }.store(in: &cancellables) + + screenView + .remoteNotifications + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didTogglePushNotifications() + }.store(in: &cancellables) + + screenView + .hideActiveApp + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleHideActiveApps() + }.store(in: &cancellables) + + screenView + .icognitoKeyboard + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleIcognitoKeyboard() + }.store(in: &cancellables) + + screenView + .biometrics + .switcherView + .publisher(for: .valueChanged) + .sink { [weak viewModel] in + viewModel?.didToggleBiometrics() + }.store(in: &cancellables) + + screenView + .privacyPolicyButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentDrawer( + title: Localized.Settings.Drawer.title(Localized.Settings.privacyPolicy), + subtitle: Localized.Settings.Drawer.subtitle(Localized.Settings.privacyPolicy), + actionTitle: Localized.ChatList.Dashboard.open) { + guard let url = URL(string: "https://elixxir.io/privategrity-corporation-privacy-policy/") else { return } + UIApplication.shared.open(url, options: [:]) + } + }.store(in: &cancellables) + + screenView + .disclosuresButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentDrawer( + title: Localized.Settings.Drawer.title(Localized.Settings.disclosures), + subtitle: Localized.Settings.Drawer.subtitle(Localized.Settings.disclosures), + actionTitle: Localized.ChatList.Dashboard.open) { + guard let url = URL(string: "https://elixxir.io/privategrity-corporation-terms-of-use/") else { return } + UIApplication.shared.open(url, options: [:]) + } + }.store(in: &cancellables) + + screenView + .deleteButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentSettingsAccountDelete(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .accountBackupButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentSettingsBackup(on: navigationController!)) + }.store(in: &cancellables) + + screenView + .advancedButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(PresentSettingsAdvanced(on: navigationController!)) + }.store(in: &cancellables) + + viewModel + .statePublisher + .map(\.isBiometricsPossible) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak screenView] in + screenView?.biometrics.switcherView.isEnabled = $0 + }.store(in: &cancellables) + + viewModel + .statePublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] state in + screenView.biometrics.switcherView.setOn(state.isBiometricsEnabled, animated: true) + screenView.hideActiveApp.switcherView.setOn(state.isHideActiveApps, animated: true) + screenView.icognitoKeyboard.switcherView.setOn(state.isIcognitoKeyboard, animated: true) + screenView.inAppNotifications.switcherView.setOn(state.isInAppNotification, animated: true) + screenView.remoteNotifications.switcherView.setOn(state.isPushNotification, animated: true) + screenView.dummyTraffic.switcherView.setOn(state.isDummyTrafficOn, animated: true) + }.store(in: &cancellables) + } + + private func presentDrawer( + title: String, + subtitle: String, + actionTitle: String, + action: @escaping () -> Void + ) { + let actionButton = CapsuleButton() + actionButton.setStyle(.red) + actionButton.setTitle(actionTitle, for: .normal) + + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.ChatList.Dashboard.cancel, for: .normal) + + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + action() + } + }.store(in: &drawerCancellables) + + cancelButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + spacing: 20.0, + views: [actionButton, cancelButton] + ) + ], isDismissable: true, from: self)) + } + + @objc private func didTapMenu() { + navigator.perform(PresentMenu(currentItem: .settings, from: self)) + } +} + +extension SettingsMainController { + private func presentInfo( + title: String, + subtitle: String, + urlString: String = "" + ) { + let actionButton = CapsuleButton() + actionButton.set( + style: .seeThrough, + title: Localized.Settings.InfoDrawer.action + ) + actionButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { [weak self] in + guard let self else { return } + self.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerLinkText( + text: subtitle, + urlString: urlString, + spacingAfter: 37 + ), + DrawerStack(views: [ + actionButton, + FlexibleSpace() + ]) + ], isDismissable: true, from: self)) + } +} diff --git a/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift b/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift deleted file mode 100644 index 73de549152915e86ff93f911c5e28626dff74d00..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/Coordinator/SettingsCoordinator.swift +++ /dev/null @@ -1,70 +0,0 @@ -import UIKit -import Shared -import MenuFeature -import Presentation - -public protocol SettingsCoordinating { - func toBackup(from: UIViewController) - func toDelete(from: UIViewController) - func toAdvanced(from: UIViewController) - func toSideMenu(from: UIViewController) - func toDrawer(_: UIViewController, from: UIViewController) - func toActivityController(with: [Any], from: UIViewController) -} - -public struct SettingsCoordinator: SettingsCoordinating { - var pushPresenter: Presenting = PushPresenter() - var modalPresenter: Presenting = ModalPresenter() - var sidePresenter: Presenting = SideMenuPresenter() - var bottomPresenter: Presenting = BottomPresenter() - - var backupFactory: () -> UIViewController - var advancedFactory: () -> UIViewController - var accountDeleteFactory: () -> UIViewController - var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController - var activityControllerFactory: ([Any]) -> UIViewController - = { UIActivityViewController(activityItems: $0, applicationActivities: nil) } - - public init( - backupFactory: @escaping () -> UIViewController, - advancedFactory: @escaping () -> UIViewController, - accountDeleteFactory: @escaping () -> UIViewController, - sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController - ) { - self.backupFactory = backupFactory - self.advancedFactory = advancedFactory - self.sideMenuFactory = sideMenuFactory - self.accountDeleteFactory = accountDeleteFactory - } -} - -public extension SettingsCoordinator { - func toAdvanced(from parent: UIViewController) { - let screen = advancedFactory() - pushPresenter.present(screen, from: parent) - } - - func toDelete(from parent: UIViewController) { - let screen = accountDeleteFactory() - pushPresenter.present(screen, from: parent) - } - - func toBackup(from parent: UIViewController) { - let screen = backupFactory() - pushPresenter.present(screen, from: parent) - } - - func toDrawer(_ drawer: UIViewController, from parent: UIViewController) { - bottomPresenter.present(drawer, from: parent) - } - - func toActivityController(with items: [Any], from parent: UIViewController) { - let screen = activityControllerFactory(items) - modalPresenter.present(screen, from: parent) - } - - func toSideMenu(from parent: UIViewController) { - let screen = sideMenuFactory(.settings, parent) - sidePresenter.present(screen, from: parent) - } -} diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift deleted file mode 100644 index db7e64016a317b8fe74fa496102437c771f500db..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift +++ /dev/null @@ -1,41 +0,0 @@ -import HUD -import Combine -import Integration -import Foundation -import DependencyInjection - -final class AccountDeleteViewModel { - @Dependency private var session: SessionType - - var deleting = false - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - func didTapDelete() { - guard deleting == false else { return } - deleting = true - - DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.on) - } - - do { - try session.deleteMyself() - DependencyInjection.Container.shared.unregister(SessionType.self) - - DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.error(.init( - content: "Now kill the app and re-open", - title: "Account deleted", - dismissable: false - ))) - } - } catch { - - DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.error(.init(with: error))) - } - } - } -} diff --git a/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift index 3ec9be365abd621e7c1d9d421aff530d0560acc7..585880a067a7664e55035300cf9deeb2e15c0c70 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift @@ -1,93 +1,98 @@ import Combine -import XXLogger +import AppCore import Defaults import Foundation -import CrashReporting +import CrashReport import ReportingFeature -import DependencyInjection +import ComposableArchitecture struct AdvancedViewState: Equatable { - var isRecordingLogs = false - var isCrashReporting = false - var isShowingUsernames = false - var isReportingEnabled = false - var isReportingOptional = false + var isRecordingLogs = false + var isCrashReporting = false + var isShowingUsernames = false + var isReportingEnabled = false + var isReportingOptional = false } final class SettingsAdvancedViewModel { - @KeyObject(.recordingLogs, defaultValue: true) var isRecordingLogs: Bool - @KeyObject(.crashReporting, defaultValue: true) var isCrashReporting: Bool + @KeyObject(.recordingLogs, defaultValue: true) var isRecordingLogs: Bool + @KeyObject(.crashReporting, defaultValue: true) var isCrashReporting: Bool - private var cancellables = Set<AnyCancellable>() - private let isShowingUsernamesKey = "isShowingUsernames" + private var cancellables = Set<AnyCancellable>() + private let isShowingUsernamesKey = "isShowingUsernames" - @Dependency private var logger: XXLogger - @Dependency private var crashReporter: CrashReporter - @Dependency private var reportingStatus: ReportingStatus + @Dependency(\.app.log) var logger: Logger + @Dependency(\.crashReport) var crashReport: CrashReport + @Dependency(\.reportingStatus) var reportingStatus: ReportingStatus - var sharePublisher: AnyPublisher<URL, Never> { shareRelay.eraseToAnyPublisher() } - private let shareRelay = PassthroughSubject<URL, Never>() + var sharePublisher: AnyPublisher<URL, Never> { + shareRelay.eraseToAnyPublisher() + } - var state: AnyPublisher<AdvancedViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<AdvancedViewState, Never>(.init()) + private let shareRelay = PassthroughSubject<URL, Never>() - func loadCachedSettings() { - stateRelay.value.isRecordingLogs = isRecordingLogs - stateRelay.value.isCrashReporting = isCrashReporting - stateRelay.value.isReportingOptional = reportingStatus.isOptional() + var state: AnyPublisher<AdvancedViewState, Never> { + stateRelay.eraseToAnyPublisher() + } + private let stateRelay = CurrentValueSubject<AdvancedViewState, Never>(.init()) - reportingStatus - .isEnabledPublisher() - .sink { [weak stateRelay] in stateRelay?.value.isReportingEnabled = $0 } - .store(in: &cancellables) + func loadCachedSettings() { + stateRelay.value.isRecordingLogs = isRecordingLogs + stateRelay.value.isCrashReporting = isCrashReporting + stateRelay.value.isReportingOptional = reportingStatus.isOptional() - guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { - print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") - return - } + reportingStatus + .isEnabledPublisher() + .sink { [weak stateRelay] in stateRelay?.value.isReportingEnabled = $0 } + .store(in: &cancellables) - guard let isShowingUsernames = defaults.value(forKey: isShowingUsernamesKey) as? Bool else { - defaults.set(false, forKey: isShowingUsernamesKey) - return - } + guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { + print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") + return + } - stateRelay.value.isShowingUsernames = isShowingUsernames + guard let isShowingUsernames = defaults.value(forKey: isShowingUsernamesKey) as? Bool else { + defaults.set(false, forKey: isShowingUsernamesKey) + return } - func didToggleShowUsernames() { - stateRelay.value.isShowingUsernames.toggle() + stateRelay.value.isShowingUsernames = isShowingUsernames + } - guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { - print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") - return - } + func didToggleShowUsernames() { + stateRelay.value.isShowingUsernames.toggle() - defaults.set(stateRelay.value.isShowingUsernames, forKey: isShowingUsernamesKey) + guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { + print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)") + return } - func didToggleRecordLogs() { - if isRecordingLogs == true { - XXLogger.stop() - } else { - XXLogger.start() - } + defaults.set(stateRelay.value.isShowingUsernames, forKey: isShowingUsernamesKey) + } - isRecordingLogs.toggle() - stateRelay.value.isRecordingLogs.toggle() + func didToggleRecordLogs() { + if isRecordingLogs == true { +// XXLogger.stop() + } else { +// XXLogger.start() } - func didTapDownloadLogs() { - let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - shareRelay.send(url.appendingPathComponent("swiftybeaver.log")) - } + isRecordingLogs.toggle() + stateRelay.value.isRecordingLogs.toggle() + } - func didToggleCrashReporting() { - isCrashReporting.toggle() - stateRelay.value.isCrashReporting.toggle() - crashReporter.setEnabled(isCrashReporting) - } + func didTapDownloadLogs() { + let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + shareRelay.send(url.appendingPathComponent("swiftybeaver.log")) + } - func didSetReporting(enabled: Bool) { - reportingStatus.enable(enabled) - } + func didToggleCrashReporting() { + isCrashReporting.toggle() + stateRelay.value.isCrashReporting.toggle() + crashReport.setEnabled(isCrashReporting) + } + + func didSetReporting(enabled: Bool) { + reportingStatus.enable(enabled) + } } diff --git a/Sources/SettingsFeature/ViewModels/SettingsDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsDeleteViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..87dadb95c894fa10d06b82a53b094ccc04d00a67 --- /dev/null +++ b/Sources/SettingsFeature/ViewModels/SettingsDeleteViewModel.swift @@ -0,0 +1,60 @@ +import AppCore +import Defaults +import Keychain +import Foundation +import Dependencies +import XXMessengerClient + +final class SettingsDeleteViewModel { + @Dependency(\.keychain) var keychain: KeychainManager + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.hudManager) var hudManager: HUDManager + @KeyObject(.username, defaultValue: nil) var username: String? + + private var isCurrentlyDeleting = false + + func didTapDelete() { + guard isCurrentlyDeleting == false else { return } + isCurrentlyDeleting = true + + hudManager.show() + + do { + try cleanUD() + try messenger.destroy() + try keychain.destroy() + try dbManager.removeDB() + try deleteDatabase() + + UserDefaults.resetStandardUserDefaults() + UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) + UserDefaults.standard.synchronize() + + hudManager.show(.init( + title: "Account deleted", + content: "Now kill the app and re-open" + )) + } catch { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.hudManager.show(.init(error: error)) + } + } + } + + private func cleanUD() throws { + try messenger.ud.get()!.permanentDeleteAccount( + username: .init(type: .username, value: username!) + ) + } + + private func deleteDatabase() throws { + let dbPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("xxm_database") + .appendingPathExtension("sqlite").path + + try FileManager.default.removeItem(atPath: dbPath) + } +} diff --git a/Sources/SettingsFeature/ViewModels/SettingsMainViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsMainViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..4fd4323dd0086207e7be26d2acd82bc117779a8b --- /dev/null +++ b/Sources/SettingsFeature/ViewModels/SettingsMainViewModel.swift @@ -0,0 +1,128 @@ +import UIKit +import AppCore +import Combine +import XXClient +import Defaults +import XXMessengerClient +import PermissionsFeature +import ComposableArchitecture + +final class SettingsMainViewModel { + struct ViewState: Equatable { + var isHideActiveApps: Bool = false + var isPushNotification: Bool = false + var isIcognitoKeyboard: Bool = false + var isInAppNotification: Bool = false + var isBiometricsEnabled: Bool = false + var isBiometricsPossible: Bool = false + var isDummyTrafficOn = false + } + + @Dependency(\.app.bgQueue) var bgQueue + @Dependency(\.permissions) var permissions + @Dependency(\.app.messenger) var messenger + @Dependency(\.dummyTraffic) var dummyTraffic + @Dependency(\.app.hudManager) var hudManager + + @KeyObject(.biometrics, defaultValue: false) var biometrics + @KeyObject(.hideAppList, defaultValue: false) var hideAppList + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn + @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard + @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications + @KeyObject(.inappnotifications, defaultValue: true) var inAppNotifications + + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + private let stateSubject = CurrentValueSubject<ViewState, Never>(.init()) + + func loadCachedSettings() { + stateSubject.value.isHideActiveApps = hideAppList + stateSubject.value.isBiometricsEnabled = biometrics + stateSubject.value.isIcognitoKeyboard = icognitoKeyboard + stateSubject.value.isPushNotification = pushNotifications + stateSubject.value.isInAppNotification = inAppNotifications + stateSubject.value.isBiometricsPossible = permissions.biometrics.status() + stateSubject.value.isDummyTrafficOn = dummyTraffic.get()!.getStatus() + } + + func didToggleBiometrics() { + biometricAuthentication(enable: !biometrics) + } + + func didToggleInAppNotifications() { + inAppNotifications.toggle() + stateSubject.value.isInAppNotification.toggle() + } + + func didTogglePushNotifications() { + pushNotifications(enable: !pushNotifications) + } + + func didToggleDummyTraffic() { + let currently = dummyTraffic.get()!.getStatus() + try! dummyTraffic.get()!.setStatus(!currently) + stateSubject.value.isDummyTrafficOn = !currently + dummyTrafficOn = stateSubject.value.isDummyTrafficOn + } + + func didToggleHideActiveApps() { + hideAppList.toggle() + stateSubject.value.isHideActiveApps.toggle() + } + + func didToggleIcognitoKeyboard() { + icognitoKeyboard.toggle() + stateSubject.value.isIcognitoKeyboard.toggle() + } + + private func biometricAuthentication(enable: Bool) { + stateSubject.value.isBiometricsEnabled = enable + + guard enable == true else { + biometrics = false + stateSubject.value.isBiometricsEnabled = false + return + } + + permissions.biometrics.request { [weak self] in + guard let self else { return } + self.biometrics = $0 + self.stateSubject.value.isBiometricsEnabled = $0 + } + } + + private func pushNotifications(enable: Bool) { + hudManager.show() + + if enable == true { + permissions.push.request { [weak self] granted in + guard let self else { return } + self.pushNotifications = granted + self.stateSubject.value.isPushNotification = granted + if granted { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } + self.hudManager.hide() + } + } else { + bgQueue.schedule { [weak self] in + guard let self else { return } + do { + try UnregisterForNotifications.live( + e2eId: self.messenger.e2e.get()!.getId() + ) + self.hudManager.hide() + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudManager.show(.init(content: xxError)) + } + self.pushNotifications = false + self.stateSubject.value.isPushNotification = false + } + } + } +} diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift deleted file mode 100644 index 9b985922329599ac17010bdd2d16c43dec81a454..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ /dev/null @@ -1,148 +0,0 @@ -import HUD -import UIKit -import Shared -import Combine -import Defaults -import Permissions -import Integration -import PushFeature -import UserNotifications -import CombineSchedulers -import DependencyInjection - -struct SettingsViewState: Equatable { - var isHideActiveApps: Bool = false - var isPushNotification: Bool = false - var isIcognitoKeyboard: Bool = false - var isInAppNotification: Bool = false - var isBiometricsEnabled: Bool = false - var isBiometricsPossible: Bool = false - var isDummyTrafficOn = false -} - -final class SettingsViewModel { - @Dependency private var session: SessionType - @Dependency private var pushHandler: PushHandling - @Dependency private var permissions: PermissionHandling - - @KeyObject(.dummyTrafficOn, defaultValue: false) var isDummyTrafficOn: Bool - @KeyObject(.biometrics, defaultValue: false) private var biometrics - @KeyObject(.hideAppList, defaultValue: false) private var hideAppList - @KeyObject(.icognitoKeyboard, defaultValue: false) private var icognitoKeyboard - @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications - @KeyObject(.inappnotifications, defaultValue: true) private var inAppNotifications - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<SettingsViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<SettingsViewState, Never>(.init()) - - func loadCachedSettings() { - stateRelay.value.isHideActiveApps = hideAppList - stateRelay.value.isBiometricsEnabled = biometrics - stateRelay.value.isIcognitoKeyboard = icognitoKeyboard - stateRelay.value.isPushNotification = pushNotifications - stateRelay.value.isInAppNotification = inAppNotifications - stateRelay.value.isBiometricsPossible = permissions.isBiometricsAvailable - stateRelay.value.isDummyTrafficOn = isDummyTrafficOn - } - - func didToggleBiometrics() { - biometricAuthentication(enable: !biometrics) - } - - func didToggleInAppNotifications() { - inAppNotifications.toggle() - stateRelay.value.isInAppNotification.toggle() - } - - func didTogglePushNotifications() { - pushNotifications(enable: !pushNotifications) - } - - func didToggleDummyTraffic() { - isDummyTrafficOn.toggle() - stateRelay.value.isDummyTrafficOn = isDummyTrafficOn - session.setDummyTraffic(status: isDummyTrafficOn) - } - - func didToggleHideActiveApps() { - hideAppList.toggle() - stateRelay.value.isHideActiveApps.toggle() - } - - func didToggleIcognitoKeyboard() { - icognitoKeyboard.toggle() - stateRelay.value.isIcognitoKeyboard.toggle() - } - - // MARK: Private - - private func biometricAuthentication(enable: Bool) { - stateRelay.value.isBiometricsEnabled = enable - - guard enable == true else { - biometrics = false - stateRelay.value.isBiometricsEnabled = false - return - } - - permissions.requestBiometrics { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let granted): - if granted { - self.biometrics = true - self.stateRelay.value.isBiometricsEnabled = true - } else { - self.biometrics = false - self.stateRelay.value.isBiometricsEnabled = false - } - case .failure: - self.biometrics = false - self.stateRelay.value.isBiometricsEnabled = false - } - } - } - - private func pushNotifications(enable: Bool) { - hudRelay.send(.on) - - if enable == true { - pushHandler.requestAuthorization { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let granted): - self.pushNotifications = granted - self.stateRelay.value.isPushNotification = granted - if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }} - self.hudRelay.send(.none) - - case .failure(let error): - self.hudRelay.send(.error(.init(with: error))) - self.pushNotifications = false - self.stateRelay.value.isPushNotification = false - } - } - } else { - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.unregisterNotifications() - self.hudRelay.send(.none) - } catch { - self.hudRelay.send(.error(.init(with: error))) - } - - self.pushNotifications = false - self.stateRelay.value.isPushNotification = false - } - } - } -} diff --git a/Sources/SettingsFeature/Views/AccountDeleteView.swift b/Sources/SettingsFeature/Views/AccountDeleteView.swift deleted file mode 100644 index e4aef7f5642929216a84e9e7b889ec0d3fa095ff..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/Views/AccountDeleteView.swift +++ /dev/null @@ -1,120 +0,0 @@ -import UIKit -import Shared -import InputField - -final class AccountDeleteView: UIView { - let titleLabel = UILabel() - let subtitleView = TextWithInfoView() - let iconImageView = UIImageView() - let inputField = InputField() - - let stackView = UIStackView() - let confirmButton = CapsuleButton() - let cancelButton = CapsuleButton() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - iconImageView.image = Asset.settingsDeleteLarge.image - - iconImageView.contentMode = .center - - inputField.setup( - style: .regular, - title: Localized.Settings.Delete.input, - placeholder: "", - leftView: .image(Asset.personGray.image), - subtitleColor: Asset.neutralDisabled.color, - allowsEmptySpace: false, - autocapitalization: .none - ) - - titleLabel.text = Localized.Settings.Delete.title - titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) - - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = .center - paragraph.lineHeightMultiple = 1.1 - - subtitleView.setup( - text: Localized.Settings.Delete.subtitle, - attributes: [ - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.regular.font(size: 16.0), - .paragraphStyle: paragraph - ], - didTapInfo: { self.didTapInfo?() } - ) - - confirmButton.setStyle(.red) - confirmButton.isEnabled = false - confirmButton.setTitle(Localized.Settings.Delete.delete, for: .normal) - cancelButton.setStyle(.simplestColoredRed) - cancelButton.setTitle(Localized.Settings.Delete.cancel, for: .normal) - - stackView.spacing = 12 - stackView.axis = .vertical - stackView.addArrangedSubview(confirmButton) - stackView.addArrangedSubview(cancelButton) - - addSubview(iconImageView) - addSubview(inputField) - addSubview(titleLabel) - addSubview(subtitleView) - addSubview(stackView) - - iconImageView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(30) - make.centerX.equalToSuperview() - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(iconImageView.snp.bottom).offset(34) - make.centerX.equalToSuperview() - } - - subtitleView.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(20) - make.left.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-50) - } - - inputField.snp.makeConstraints { make in - make.top.equalTo(subtitleView.snp.bottom).offset(50) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) - } - - stackView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) - make.left.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-50) - make.bottom.equalToSuperview().offset(-44) - } - } - - required init?(coder: NSCoder) { nil } - - func setInfoClosure(_ closure: @escaping () -> Void) { - didTapInfo = closure - } - - func update(username: String) { - inputField.update(placeholder: username) - } - - func update(status: InputField.ValidationStatus) { - inputField.update(status: status) - - switch status { - case .valid: - confirmButton.isEnabled = true - case .invalid, .unknown: - confirmButton.isEnabled = false - } - } -} diff --git a/Sources/SettingsFeature/Views/SettingsAdvancedView.swift b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift index 0e5e5d89159fbb554f78dd781f5a9d3d4cdfe9da..3d976f2505a4e8c23090724abdbbcd1a00fe70f5 100644 --- a/Sources/SettingsFeature/Views/SettingsAdvancedView.swift +++ b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift @@ -1,64 +1,62 @@ import UIKit import Shared +import AppResources final class SettingsAdvancedView: UIView { - let stackView = UIStackView() - let downloadLogsButton = UIButton() - let logRecordingSwitcher = SettingsSwitcher() - let crashReportingSwitcher = SettingsSwitcher() - let showUsernamesSwitcher = SettingsSwitcher() - let reportingSwitcher = SettingsSwitcher() - - init() { - super.init(frame: .zero) - - backgroundColor = Asset.neutralWhite.color - downloadLogsButton.setImage(Asset.settingsDownload.image, for: .normal) - - showUsernamesSwitcher.set( - title: Localized.Settings.Advanced.ShowUsername.title, - text: Localized.Settings.Advanced.ShowUsername.description, - icon: Asset.settingsHide.image - ) - - logRecordingSwitcher.set( - title: Localized.Settings.Advanced.Logs.title, - text: Localized.Settings.Advanced.Logs.description, - icon: Asset.settingsLogs.image, - extraAction: downloadLogsButton - ) - - crashReportingSwitcher.set( - title: Localized.Settings.Advanced.Crashes.title, - text: Localized.Settings.Advanced.Crashes.description, - icon: Asset.settingsCrash.image - ) - - reportingSwitcher.set( - title: Localized.Settings.Advanced.Reporting.title, - text: Localized.Settings.Advanced.Reporting.description, - icon: Asset.settingsCrash.image - ) - - stackView.axis = .vertical - stackView.addArrangedSubview(logRecordingSwitcher) - stackView.addArrangedSubview(crashReportingSwitcher) - stackView.addArrangedSubview(showUsernamesSwitcher) - stackView.addArrangedSubview(reportingSwitcher) - - stackView.setCustomSpacing(20, after: logRecordingSwitcher) - stackView.setCustomSpacing(10, after: crashReportingSwitcher) - stackView.setCustomSpacing(10, after: showUsernamesSwitcher) - stackView.setCustomSpacing(10, after: reportingSwitcher) - - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.top.equalToSuperview().offset(24) - $0.left.equalToSuperview().offset(16) - $0.right.equalToSuperview().offset(-16) - } + let stackView = UIStackView() + let downloadLogsButton = UIButton() + let logRecordingSwitcher = SettingsSwitcher() + let crashReportingSwitcher = SettingsSwitcher() + let showUsernamesSwitcher = SettingsSwitcher() + let reportingSwitcher = SettingsSwitcher() + + init() { + super.init(frame: .zero) + + backgroundColor = Asset.neutralWhite.color + downloadLogsButton.setImage(Asset.settingsDownload.image, for: .normal) + + showUsernamesSwitcher.set( + title: Localized.Settings.Advanced.ShowUsername.title, + text: Localized.Settings.Advanced.ShowUsername.description, + icon: Asset.settingsHide.image + ) + logRecordingSwitcher.set( + title: Localized.Settings.Advanced.Logs.title, + text: Localized.Settings.Advanced.Logs.description, + icon: Asset.settingsLogs.image, + extraAction: downloadLogsButton + ) + crashReportingSwitcher.set( + title: Localized.Settings.Advanced.Crashes.title, + text: Localized.Settings.Advanced.Crashes.description, + icon: Asset.settingsCrash.image + ) + reportingSwitcher.set( + title: Localized.Settings.Advanced.Reporting.title, + text: Localized.Settings.Advanced.Reporting.description, + icon: Asset.settingsCrash.image + ) + + stackView.axis = .vertical + stackView.addArrangedSubview(logRecordingSwitcher) + stackView.addArrangedSubview(crashReportingSwitcher) + stackView.addArrangedSubview(showUsernamesSwitcher) + stackView.addArrangedSubview(reportingSwitcher) + + stackView.setCustomSpacing(20, after: logRecordingSwitcher) + stackView.setCustomSpacing(10, after: crashReportingSwitcher) + stackView.setCustomSpacing(10, after: showUsernamesSwitcher) + stackView.setCustomSpacing(10, after: reportingSwitcher) + + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) } + } - required init?(coder: NSCoder) { nil } + required init?(coder: NSCoder) { nil } } diff --git a/Sources/SettingsFeature/Views/SettingsDeleteView.swift b/Sources/SettingsFeature/Views/SettingsDeleteView.swift new file mode 100644 index 0000000000000000000000000000000000000000..7ac68bccbbd95c0f299936ba726744fc3e26023b --- /dev/null +++ b/Sources/SettingsFeature/Views/SettingsDeleteView.swift @@ -0,0 +1,117 @@ +import UIKit +import Shared +import InputField +import AppResources + +final class SettingsDeleteView: UIView { + let titleLabel = UILabel() + let subtitleView = TextWithInfoView() + let iconImageView = UIImageView() + let inputField = InputField() + + let stackView = UIStackView() + let confirmButton = CapsuleButton() + let cancelButton = CapsuleButton() + + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + iconImageView.image = Asset.settingsDeleteLarge.image + + iconImageView.contentMode = .center + + inputField.setup( + style: .regular, + title: Localized.Settings.Delete.input, + placeholder: "", + leftView: .image(Asset.personGray.image), + subtitleColor: Asset.neutralDisabled.color, + allowsEmptySpace: false, + autocapitalization: .none + ) + + titleLabel.text = Localized.Settings.Delete.title + titleLabel.textAlignment = .center + titleLabel.textColor = Asset.neutralActive.color + titleLabel.font = Fonts.Mulish.bold.font(size: 32.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .center + paragraph.lineHeightMultiple = 1.1 + + subtitleView.setup( + text: Localized.Settings.Delete.subtitle, + attributes: [ + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.regular.font(size: 16.0), + .paragraphStyle: paragraph + ], + didTapInfo: { self.didTapInfo?() } + ) + + confirmButton.setStyle(.red) + confirmButton.isEnabled = false + confirmButton.setTitle(Localized.Settings.Delete.delete, for: .normal) + cancelButton.setStyle(.simplestColoredRed) + cancelButton.setTitle(Localized.Settings.Delete.cancel, for: .normal) + + stackView.spacing = 12 + stackView.axis = .vertical + stackView.addArrangedSubview(confirmButton) + stackView.addArrangedSubview(cancelButton) + + addSubview(iconImageView) + addSubview(inputField) + addSubview(titleLabel) + addSubview(subtitleView) + addSubview(stackView) + + iconImageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(30) + $0.centerX.equalToSuperview() + } + titleLabel.snp.makeConstraints { + $0.top.equalTo(iconImageView.snp.bottom).offset(34) + $0.centerX.equalToSuperview() + } + subtitleView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + } + inputField.snp.makeConstraints { + $0.top.equalTo(subtitleView.snp.bottom).offset(50) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) + } + stackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(inputField.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(50) + $0.right.equalToSuperview().offset(-50) + $0.bottom.equalToSuperview().offset(-44) + } + } + + required init?(coder: NSCoder) { nil } + + func setInfoClosure(_ closure: @escaping () -> Void) { + didTapInfo = closure + } + + func update(username: String) { + inputField.update(placeholder: username) + } + + func update(status: InputField.ValidationStatus) { + inputField.update(status: status) + + switch status { + case .valid: + confirmButton.isEnabled = true + case .invalid, .unknown: + confirmButton.isEnabled = false + } + } +} diff --git a/Sources/SettingsFeature/Views/SettingsMainView.swift b/Sources/SettingsFeature/Views/SettingsMainView.swift new file mode 100644 index 0000000000000000000000000000000000000000..f1f84e5438f9a25b9fd823e4a53e3f37806e508d --- /dev/null +++ b/Sources/SettingsFeature/Views/SettingsMainView.swift @@ -0,0 +1,178 @@ +import UIKit +import Shared +import AppResources + +final class SettingsMainView: UIView { + enum InfoTapped { + case dummyTraffic + case biometrics + case notifications + case icognitoKeyboard + } + + let generalStack = UIStackView() + let generalTitle = UILabel() + let biometrics = SettingsInfoSwitcher() + let remoteNotifications = SettingsInfoSwitcher() + let dummyTraffic = SettingsInfoSwitcher() + let inAppNotifications = SettingsSwitcher() + + let chatStack = UIStackView() + let chatTitle = UILabel() + let hideActiveApp = SettingsSwitcher() + let icognitoKeyboard = SettingsInfoSwitcher() + + let otherStackView = UIStackView() + let privacyPolicyButton = RowButton() + let disclosuresButton = RowButton() + let advancedButton = RowButton() + let accountBackupButton = RowButton() + let deleteButton = RowButton() + + let didTap: (InfoTapped) -> Void + + init(didTap: @escaping (InfoTapped) -> Void) { + self.didTap = didTap + + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + setupGeneralStack() + setupChatStack() + setupOtherStack() + } + + required init?(coder: NSCoder) { nil } + + private func setupGeneralStack() { + generalTitle.text = Localized.Settings.general + generalTitle.textColor = Asset.neutralActive.color + generalTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) + + remoteNotifications.set( + title: Localized.Settings.RemoteNotifications.title, + text: Localized.Settings.RemoteNotifications.description, + icon: Asset.settingsNotifications.image + ) { self.didTap(.notifications) } + + inAppNotifications.set( + title: Localized.Settings.InAppNotifications.title, + text: Localized.Settings.InAppNotifications.description, + icon: Asset.settingsNotifications.image + ) + + dummyTraffic.set( + title: Localized.Settings.Traffic.title, + text: Localized.Settings.Traffic.subtitle, + icon: Asset.settingsBiometrics.image, + separator: false, + didTapInfo: { self.didTap(.dummyTraffic) } + ) + + biometrics.set( + title: Localized.Settings.Biometrics.title, + text: Localized.Settings.Biometrics.description, + icon: Asset.settingsBiometrics.image, + separator: false + ) { self.didTap(.biometrics) } + + generalStack.axis = .vertical + generalStack.addArrangedSubview(remoteNotifications) + generalStack.addArrangedSubview(dummyTraffic) + generalStack.addArrangedSubview(inAppNotifications) + generalStack.addArrangedSubview(biometrics) + + addSubview(generalTitle) + addSubview(generalStack) + + generalTitle.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.left.equalToSuperview().offset(21) + } + + generalStack.snp.makeConstraints { + $0.top.equalTo(generalTitle.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) + } + } + + private func setupChatStack() { + chatTitle.text = Localized.Settings.chat + chatTitle.textColor = Asset.neutralActive.color + chatTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) + + hideActiveApp.set( + title: Localized.Settings.HideActiveApps.title, + text: Localized.Settings.HideActiveApps.description, + icon: Asset.settingsHide.image + ) + + icognitoKeyboard.set( + title: Localized.Settings.IcognitoKeyboard.title, + text: Localized.Settings.IcognitoKeyboard.description, + icon: Asset.settingsKeyboard.image + ) { self.didTap(.icognitoKeyboard) } + + chatStack.axis = .vertical + chatStack.addArrangedSubview(hideActiveApp) + chatStack.addArrangedSubview(icognitoKeyboard) + + addSubview(chatTitle) + addSubview(chatStack) + + chatTitle.snp.makeConstraints { + $0.top.equalTo(generalStack.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(21) + } + chatStack.snp.makeConstraints { + $0.top.equalTo(chatTitle.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) + } + } + + private func setupOtherStack() { + privacyPolicyButton.setup( + title: Localized.Settings.privacyPolicy, + icon: Asset.settingsPrivacy.image, + separator: false + ) + disclosuresButton.setup( + title: Localized.Settings.disclosures, + icon: Asset.settingsFolder.image + ) + advancedButton.setup( + title: Localized.Settings.advanced, + icon: Asset.settingsAdvanced.image + ) + accountBackupButton.setup( + title: Localized.Settings.Advanced.AccountBackup.title, + icon: Asset.settingsAdvanced.image, + style: .clean, + separator: false + ) + deleteButton.setup( + title: Localized.Settings.delete, + icon: Asset.settingsDelete.image, + style: .delete, + separator: false + ) + + otherStackView.axis = .vertical + otherStackView.addArrangedSubview(privacyPolicyButton) + otherStackView.addArrangedSubview(disclosuresButton) + otherStackView.addArrangedSubview(accountBackupButton) + otherStackView.addArrangedSubview(advancedButton) + otherStackView.addArrangedSubview(deleteButton) + + addSubview(otherStackView) + + otherStackView.snp.makeConstraints { + $0.top.equalTo(chatStack.snp.bottom).offset(15) + $0.left.equalToSuperview().offset(16) + $0.right.equalToSuperview().offset(-16) + $0.bottom.lessThanOrEqualToSuperview().offset(-20) + } + } +} diff --git a/Sources/SettingsFeature/Views/SettingsSwitcher.swift b/Sources/SettingsFeature/Views/SettingsSwitcher.swift index 8b928746226f12ba5ddc5873de50d66989284f8d..8656cb7861506e22d51ff3dc22d7cde46ef3ac42 100644 --- a/Sources/SettingsFeature/Views/SettingsSwitcher.swift +++ b/Sources/SettingsFeature/Views/SettingsSwitcher.swift @@ -1,219 +1,218 @@ import UIKit import Shared +import AppResources final class SettingsSwitcher: UIView { - let titleLabel = UILabel() - let textLabel = UILabel() - let iconImageView = UIImageView() - let separatorView = UIView() - let switcherView = UISwitch() - let stackView = UIStackView() - let verticalStackView = UIStackView() - - init() { - super.init(frame: .zero) - - textLabel.textColor = Asset.neutralWeak.color - titleLabel.textColor = Asset.neutralActive.color - switcherView.onTintColor = Asset.brandPrimary.color - separatorView.backgroundColor = Asset.neutralLine.color - - iconImageView.contentMode = .center - iconImageView.setContentHuggingPriority(.required, for: .horizontal) - - textLabel.numberOfLines = 0 - textLabel.font = Fonts.Mulish.regular.font(size: 12.0) - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - - addSubview(stackView) - addSubview(separatorView) - - verticalStackView.spacing = 3 - verticalStackView.axis = .vertical - verticalStackView.addArrangedSubview(titleLabel) - verticalStackView.addArrangedSubview(textLabel) - - let icon = iconImageView.pinning(at: .top(0)) - - stackView.spacing = 8 - stackView.addArrangedSubview(icon) - stackView.addArrangedSubview(verticalStackView) - stackView.addArrangedSubview(switcherView.pinning(at: .top(0))) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(-20) - } - - separatorView.snp.makeConstraints { make in - make.height.equalTo(1) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } + let titleLabel = UILabel() + let textLabel = UILabel() + let iconImageView = UIImageView() + let separatorView = UIView() + let switcherView = UISwitch() + let stackView = UIStackView() + let verticalStackView = UIStackView() + + init() { + super.init(frame: .zero) + + textLabel.textColor = Asset.neutralWeak.color + titleLabel.textColor = Asset.neutralActive.color + switcherView.onTintColor = Asset.brandPrimary.color + separatorView.backgroundColor = Asset.neutralLine.color + + iconImageView.contentMode = .center + iconImageView.setContentHuggingPriority(.required, for: .horizontal) + + textLabel.numberOfLines = 0 + textLabel.font = Fonts.Mulish.regular.font(size: 12.0) + titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) + + addSubview(stackView) + addSubview(separatorView) + + verticalStackView.spacing = 3 + verticalStackView.axis = .vertical + verticalStackView.addArrangedSubview(titleLabel) + verticalStackView.addArrangedSubview(textLabel) + + let icon = iconImageView.pinning(at: .top(0)) + + stackView.spacing = 8 + stackView.addArrangedSubview(icon) + stackView.addArrangedSubview(verticalStackView) + stackView.addArrangedSubview(switcherView.pinning(at: .top(0))) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(-20) + } + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func set( + title: String, + text: String? = nil, + icon: UIImage? = nil, + separator: Bool = true, + extraAction: UIButton? = nil + ) { + titleLabel.text = title + + if let content = text { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.5 + + textLabel.attributedText = NSAttributedString( + string: content, attributes: [.paragraphStyle: paragraphStyle] + ) + } else { + verticalStackView.removeArrangedSubview(textLabel) + } + + if let icon = icon { + iconImageView.image = icon + } else { + stackView.removeArrangedSubview(iconImageView) + } + + if let button = extraAction { + stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) } - required init?(coder: NSCoder) { nil } - - func set( - title: String, - text: String? = nil, - icon: UIImage? = nil, - separator: Bool = true, - extraAction: UIButton? = nil - ) { - titleLabel.text = title - - if let content = text { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.5 - - textLabel.attributedText = NSAttributedString( - string: content, attributes: [.paragraphStyle: paragraphStyle] - ) - } else { - verticalStackView.removeArrangedSubview(textLabel) - } - - if let icon = icon { - iconImageView.image = icon - } else { - stackView.removeArrangedSubview(iconImageView) - } - - if let button = extraAction { - stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) - } - - guard separator == true else { - separatorView.removeFromSuperview() - return - } + guard separator == true else { + separatorView.removeFromSuperview() + return } + } } final class SettingsInfoSwitcher: UIView { - let titleView = TextWithInfoView() - let textLabel = UILabel() - let iconImageView = UIImageView() - let separatorView = UIView() - let switcherView = UISwitch() - let stackView = UIStackView() - let verticalStackView = UIStackView() - - var didTapInfo: (() -> Void)? - - init() { - super.init(frame: .zero) - - textLabel.textColor = Asset.neutralWeak.color - switcherView.onTintColor = Asset.brandPrimary.color - separatorView.backgroundColor = Asset.neutralLine.color - - iconImageView.contentMode = .center - iconImageView.setContentHuggingPriority(.required, for: .horizontal) - - textLabel.numberOfLines = 0 - textLabel.font = Fonts.Mulish.regular.font(size: 12.0) - textLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - addSubview(stackView) - addSubview(separatorView) - - let titleContainer = UIView() - titleContainer.addSubview(titleView) - titleView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(-10) - make.left.equalToSuperview().offset(-4) - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(10) - } - - verticalStackView.spacing = 3 - verticalStackView.axis = .vertical - verticalStackView.addArrangedSubview(titleContainer.pinning(at: .left(0))) - verticalStackView.addArrangedSubview(textLabel) - - let icon = iconImageView.pinning(at: .top(0)) - - let switcherContainer = UIView() - switcherContainer.addSubview(switcherView) - switcherView.setContentCompressionResistancePriority(.required, for: .horizontal) - switcherContainer.setContentCompressionResistancePriority(.required, for: .horizontal) - - switcherView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - - stackView.spacing = 8 - stackView.addArrangedSubview(icon) - stackView.addArrangedSubview(verticalStackView) - stackView.addArrangedSubview(switcherContainer) - - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview().offset(-20) - } - - separatorView.snp.makeConstraints { make in - make.height.equalTo(1) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } + let titleView = TextWithInfoView() + let textLabel = UILabel() + let iconImageView = UIImageView() + let separatorView = UIView() + let switcherView = UISwitch() + let stackView = UIStackView() + let verticalStackView = UIStackView() + + var didTapInfo: (() -> Void)? + + init() { + super.init(frame: .zero) + + textLabel.textColor = Asset.neutralWeak.color + switcherView.onTintColor = Asset.brandPrimary.color + separatorView.backgroundColor = Asset.neutralLine.color + + iconImageView.contentMode = .center + iconImageView.setContentHuggingPriority(.required, for: .horizontal) + + textLabel.numberOfLines = 0 + textLabel.font = Fonts.Mulish.regular.font(size: 12.0) + textLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + addSubview(stackView) + addSubview(separatorView) + + let titleContainer = UIView() + titleContainer.addSubview(titleView) + titleView.snp.makeConstraints { + $0.top.equalToSuperview().offset(-10) + $0.left.equalToSuperview().offset(-4) + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(10) + } + + verticalStackView.spacing = 3 + verticalStackView.axis = .vertical + verticalStackView.addArrangedSubview(titleContainer.pinning(at: .left(0))) + verticalStackView.addArrangedSubview(textLabel) + + let icon = iconImageView.pinning(at: .top(0)) + + let switcherContainer = UIView() + switcherContainer.addSubview(switcherView) + switcherView.setContentCompressionResistancePriority(.required, for: .horizontal) + switcherContainer.setContentCompressionResistancePriority(.required, for: .horizontal) + + switcherView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() + } + + stackView.spacing = 8 + stackView.addArrangedSubview(icon) + stackView.addArrangedSubview(verticalStackView) + stackView.addArrangedSubview(switcherContainer) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview().offset(-20) + } + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func set( + title: String, + text: String? = nil, + icon: UIImage? = nil, + separator: Bool = true, + extraAction: UIButton? = nil, + didTapInfo: (() -> Void)? = nil + ) { + self.didTapInfo = didTapInfo + + titleView.setup( + text: title, + attributes: [ + .foregroundColor: Asset.neutralActive.color, + .font: Fonts.Mulish.semiBold.font(size: 14.0) as Any + ], didTapInfo: { self.didTapInfo?() } + ) + + if let content = text { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.5 + + textLabel.attributedText = NSAttributedString( + string: content, attributes: [.paragraphStyle: paragraphStyle] + ) + } else { + verticalStackView.removeArrangedSubview(textLabel) + } + + if let icon = icon { + iconImageView.image = icon + } else { + stackView.removeArrangedSubview(iconImageView) + } + + if let button = extraAction { + stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) } - required init?(coder: NSCoder) { nil } - - func set( - title: String, - text: String? = nil, - icon: UIImage? = nil, - separator: Bool = true, - extraAction: UIButton? = nil, - didTapInfo: (() -> Void)? = nil - ) { - self.didTapInfo = didTapInfo - - titleView.setup( - text: title, - attributes: [ - .foregroundColor: Asset.neutralActive.color, - .font: Fonts.Mulish.semiBold.font(size: 14.0) as Any - ], didTapInfo: { self.didTapInfo?() } - ) - - if let content = text { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.5 - - textLabel.attributedText = NSAttributedString( - string: content, attributes: [.paragraphStyle: paragraphStyle] - ) - } else { - verticalStackView.removeArrangedSubview(textLabel) - } - - if let icon = icon { - iconImageView.image = icon - } else { - stackView.removeArrangedSubview(iconImageView) - } - - if let button = extraAction { - stackView.insertArrangedSubview(button.pinning(at: .top(0)), at: 2) - } - - guard separator == true else { - separatorView.removeFromSuperview() - return - } + guard separator == true else { + separatorView.removeFromSuperview() + return } + } } diff --git a/Sources/SettingsFeature/Views/SettingsView.swift b/Sources/SettingsFeature/Views/SettingsView.swift deleted file mode 100644 index 8f17926c9bc885d82d5625d83eb7cfdf1eae6807..0000000000000000000000000000000000000000 --- a/Sources/SettingsFeature/Views/SettingsView.swift +++ /dev/null @@ -1,182 +0,0 @@ -import UIKit -import Shared - -final class SettingsView: UIView { - enum InfoTapped { - case dummyTraffic - case biometrics - case notifications - case icognitoKeyboard - } - - let generalStack = UIStackView() - let generalTitle = UILabel() - let biometrics = SettingsInfoSwitcher() - let remoteNotifications = SettingsInfoSwitcher() - let dummyTraffic = SettingsInfoSwitcher() - let inAppNotifications = SettingsSwitcher() - - let chatStack = UIStackView() - let chatTitle = UILabel() - let hideActiveApp = SettingsSwitcher() - let icognitoKeyboard = SettingsInfoSwitcher() - - let otherStackView = UIStackView() - let privacyPolicyButton = RowButton() - let disclosuresButton = RowButton() - let advancedButton = RowButton() - let accountBackupButton = RowButton() - let deleteButton = RowButton() - - let didTap: (InfoTapped) -> Void - - init(didTap: @escaping (InfoTapped) -> Void) { - self.didTap = didTap - - super.init(frame: .zero) - backgroundColor = Asset.neutralWhite.color - - setupGeneralStack() - setupChatStack() - setupOtherStack() - } - - required init?(coder: NSCoder) { nil } - - private func setupGeneralStack() { - generalTitle.text = Localized.Settings.general - generalTitle.textColor = Asset.neutralActive.color - generalTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) - - remoteNotifications.set( - title: Localized.Settings.RemoteNotifications.title, - text: Localized.Settings.RemoteNotifications.description, - icon: Asset.settingsNotifications.image - ) { self.didTap(.notifications) } - - inAppNotifications.set( - title: Localized.Settings.InAppNotifications.title, - text: Localized.Settings.InAppNotifications.description, - icon: Asset.settingsNotifications.image - ) - - dummyTraffic.set( - title: Localized.Settings.Traffic.title, - text: Localized.Settings.Traffic.subtitle, - icon: Asset.settingsBiometrics.image, - separator: false, - didTapInfo: { self.didTap(.dummyTraffic) } - ) - - biometrics.set( - title: Localized.Settings.Biometrics.title, - text: Localized.Settings.Biometrics.description, - icon: Asset.settingsBiometrics.image, - separator: false - ) { self.didTap(.biometrics) } - - generalStack.axis = .vertical - generalStack.addArrangedSubview(remoteNotifications) - generalStack.addArrangedSubview(dummyTraffic) - generalStack.addArrangedSubview(inAppNotifications) - generalStack.addArrangedSubview(biometrics) - - addSubview(generalTitle) - addSubview(generalStack) - - generalTitle.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.left.equalToSuperview().offset(21) - } - - generalStack.snp.makeConstraints { make in - make.top.equalTo(generalTitle.snp.bottom).offset(8) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - } - - private func setupChatStack() { - chatTitle.text = Localized.Settings.chat - chatTitle.textColor = Asset.neutralActive.color - chatTitle.font = Fonts.Mulish.semiBold.font(size: 18.0) - - hideActiveApp.set( - title: Localized.Settings.HideActiveApps.title, - text: Localized.Settings.HideActiveApps.description, - icon: Asset.settingsHide.image - ) - - icognitoKeyboard.set( - title: Localized.Settings.IcognitoKeyboard.title, - text: Localized.Settings.IcognitoKeyboard.description, - icon: Asset.settingsKeyboard.image - ) { self.didTap(.icognitoKeyboard) } - - chatStack.axis = .vertical - chatStack.addArrangedSubview(hideActiveApp) - chatStack.addArrangedSubview(icognitoKeyboard) - - addSubview(chatTitle) - addSubview(chatStack) - - chatTitle.snp.makeConstraints { make in - make.top.equalTo(generalStack.snp.bottom).offset(20) - make.left.equalToSuperview().offset(21) - } - - chatStack.snp.makeConstraints { make in - make.top.equalTo(chatTitle.snp.bottom).offset(8) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - } - } - - private func setupOtherStack() { - privacyPolicyButton.setup( - title: Localized.Settings.privacyPolicy, - icon: Asset.settingsPrivacy.image, - separator: false - ) - - disclosuresButton.setup( - title: Localized.Settings.disclosures, - icon: Asset.settingsFolder.image - ) - - advancedButton.setup( - title: Localized.Settings.advanced, - icon: Asset.settingsAdvanced.image - ) - - accountBackupButton.setup( - title: Localized.Settings.Advanced.AccountBackup.title, - icon: Asset.settingsAdvanced.image, - style: .clean, - separator: false - ) - - deleteButton.setup( - title: Localized.Settings.delete, - icon: Asset.settingsDelete.image, - style: .delete, - separator: false - ) - - otherStackView.axis = .vertical - otherStackView.addArrangedSubview(privacyPolicyButton) - otherStackView.addArrangedSubview(disclosuresButton) - otherStackView.addArrangedSubview(accountBackupButton) - otherStackView.addArrangedSubview(advancedButton) - otherStackView.addArrangedSubview(deleteButton) - - addSubview(otherStackView) - - otherStackView.snp.makeConstraints { make in - make.top.equalTo(chatStack.snp.bottom).offset(15) - make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) - make.bottom.lessThanOrEqualToSuperview().offset(-20) - } - } -} diff --git a/Sources/Shared/Aliases.swift b/Sources/Shared/Aliases.swift deleted file mode 100644 index 6b3859ff2b6f5bdbc00a47fb1e352a7378868e04..0000000000000000000000000000000000000000 --- a/Sources/Shared/Aliases.swift +++ /dev/null @@ -1,4 +0,0 @@ -import UIKit - -public typealias EmptyClosure = () -> Void -public typealias StringClosure = (String) -> Void 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/EditStateHandler.swift b/Sources/Shared/EditStateHandler.swift index 1a8d4c7e89edb3449118679c03e87fc4c1afcc08..5b2407d8233a4b17797f43e8a285743e6bbe136e 100644 --- a/Sources/Shared/EditStateHandler.swift +++ b/Sources/Shared/EditStateHandler.swift @@ -1,18 +1,15 @@ import Combine public final class EditStateHandler { - // MARK: Properties + public var isEditing: AnyPublisher<Bool, Never> { + stateSubject.eraseToAnyPublisher() + } - public var isEditing: AnyPublisher<Bool, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<Bool, Never>(false) + private let stateSubject = CurrentValueSubject<Bool, Never>(false) - // MARK: Lifecycle + public init() {} - public init() {} - - // MARK: Public - - public func didSwitchEditing() { - stateRelay.value.toggle() - } + public func didSwitchEditing() { + stateSubject.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 27b041e093c060e28b17b8fc81af0e7fb9355cd4..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: ChatLayout) { - 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 dd0fc91aaa80cfb18e028721d1ae617fd311354b..93d9058f05357fc02caae4ee5a0889bc5bcba43b 100644 --- a/Sources/Shared/Extensions/Date.swift +++ b/Sources/Shared/Extensions/Date.swift @@ -1,56 +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())) - } + 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 new file mode 100644 index 0000000000000000000000000000000000000000..6c2bf25ef593f3a54ced36b4b9aea53cdb602a0f --- /dev/null +++ b/Sources/Shared/FeedbackPlayer.swift @@ -0,0 +1,34 @@ +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 + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..ba2ba1f76c889cd9d35a7a8b012b07ca6125b410 --- /dev/null +++ b/Sources/Shared/Models/Country.swift @@ -0,0 +1,44 @@ +import Foundation + +public struct Country { + public var name: String + public var code: String + public var flag: String + public var regex: String + public var prefix: String + public var example: String + public var prefixWithFlag: String { "\(flag) \(prefix)" } + + public static func fromMyPhone() -> Self { + let all = all() + + guard let country = all.filter({ $0.code == Locale.current.regionCode }).first else { + return all.filter { $0.code == "US" }.first! + } + + return country + } + + public static func all() -> [Self] { + guard let url = Bundle.module.url(forResource: "country_codes", withExtension: "json"), + let data = try? Data(contentsOf: url), + let countries = try? JSONDecoder().decode([Country].self, from: data) else { + fatalError("Can't handle country codes json") + } + + return countries + } + + public static func findFrom(_ number: String) -> Self { + all().first { country in + let start = number.index(number.startIndex, offsetBy: number.count - 2) + let end = number.index(start, offsetBy: number.count - (number.count - 2)) + + return country.code == String(number[start ..< end]) + }! + } +} + +extension Country: Hashable {} +extension Country: Equatable {} +extension Country: Decodable {} diff --git a/Sources/Shared/Models/Reply.swift b/Sources/Shared/Models/Reply.swift new file mode 100644 index 0000000000000000000000000000000000000000..21ffda352cf1631cd638e6a66ef6248a6d55bd87 --- /dev/null +++ b/Sources/Shared/Models/Reply.swift @@ -0,0 +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 + } +} diff --git a/Sources/Models/pbpayload.pb.swift b/Sources/Shared/Models/pbpayload.pb.swift similarity index 100% rename from Sources/Models/pbpayload.pb.swift rename to Sources/Shared/Models/pbpayload.pb.swift 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/Countries/Resources/country_codes.json b/Sources/Shared/Resources/country_codes.json similarity index 100% rename from Sources/Countries/Resources/country_codes.json rename to Sources/Shared/Resources/country_codes.json diff --git a/Sources/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 bfefe3c41d1e3ffdef01e6446e52b2ab20b175f9..9cda32e30765b2220e40aa1aa41d7c68a7e9a044 100644 --- a/Sources/Shared/Views/DotAnimation.swift +++ b/Sources/Shared/Views/DotAnimation.swift @@ -1,100 +1,73 @@ import UIKit -import SnapKit +import AppResources public final class DotAnimation: UIView { - // MARK: UI - - let leftDot = UIView() - let middleDot = UIView() - let rightDot = UIView() - - // MARK: Properties - - var leftInvert = false - var middleInvert = false - var rightInvert = false - - var leftValue: CGFloat = 20 - var middleValue: CGFloat = 45 - var rightValue: CGFloat = 70 - - var displayLink: CADisplayLink? - - // MARK: Lifecycle - - public init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - // MARK: Public - - func setColor(_ color: UIColor = Asset.brandPrimary.color) { - leftDot.backgroundColor = color - middleDot.backgroundColor = color - rightDot.backgroundColor = color + let leftDot = UIView() + let rightDot = UIView() + let middleDot = UIView() + var displayLink: CADisplayLink? + + var leftInvert = false + var rightInvert = false + var middleInvert = false + var leftValue: CGFloat = 20 + var rightValue: CGFloat = 70 + var middleValue: CGFloat = 45 + + public init() { + super.init(frame: .zero) + leftDot.layer.cornerRadius = 7.5 + middleDot.layer.cornerRadius = 7.5 + rightDot.layer.cornerRadius = 7.5 + + setColor() + + addSubview(leftDot) + addSubview(middleDot) + addSubview(rightDot) + + leftDot.snp.makeConstraints { + $0.centerY.equalTo(middleDot) + $0.right.equalTo(middleDot.snp.left).offset(-5) + $0.width.height.equalTo(15) } - // MARK: Private - - private func setup() { - setupCornerRadius() - setColor() - addSubviews() - setupConstraints() - - displayLink = CADisplayLink(target: self, selector: #selector(handleAnimations)) - displayLink!.add(to: RunLoop.main, forMode: .default) + middleDot.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.height.equalTo(15) } - private func setupCornerRadius() { - leftDot.layer.cornerRadius = 4.5 - middleDot.layer.cornerRadius = 4.5 - rightDot.layer.cornerRadius = 4.5 + rightDot.snp.makeConstraints { + $0.centerY.equalTo(middleDot) + $0.left.equalTo(middleDot.snp.right).offset(5) + $0.width.height.equalTo(15) } - private func addSubviews() { - addSubview(leftDot) - addSubview(middleDot) - addSubview(rightDot) - } + displayLink = CADisplayLink(target: self, selector: #selector(handleAnimations)) + displayLink!.add(to: RunLoop.main, forMode: .default) + } - private func setupConstraints() { - leftDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.right.equalTo(middleDot.snp.left).offset(-2) - make.width.height.equalTo(9) - } + required init?(coder: NSCoder) { nil } - middleDot.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.height.equalTo(9) - } + public func setColor(_ color: UIColor = Asset.brandPrimary.color) { + leftDot.backgroundColor = color + middleDot.backgroundColor = color + rightDot.backgroundColor = color + } - rightDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.left.equalTo(middleDot.snp.right).offset(2) - make.width.height.equalTo(9) - } - } - - // MARK: Selectors + @objc private func handleAnimations() { + let factor: CGFloat = 70 - @objc private func handleAnimations() { - let factor: CGFloat = 70 + leftInvert ? (leftValue -= 1) : (leftValue += 1) + middleInvert ? (middleValue -= 1) : (middleValue += 1) + rightInvert ? (rightValue -= 1) : (rightValue += 1) - leftInvert ? (leftValue -= 1) : (leftValue += 1) - middleInvert ? (middleValue -= 1) : (middleValue += 1) - rightInvert ? (rightValue -= 1) : (rightValue += 1) + leftDot.layer.transform = CATransform3DMakeScale(leftValue/factor, leftValue/factor, 1) + middleDot.layer.transform = CATransform3DMakeScale(middleValue/factor, middleValue/factor, 1) + rightDot.layer.transform = CATransform3DMakeScale(rightValue/factor, rightValue/factor, 1) - leftDot.layer.transform = CATransform3DMakeScale(leftValue/factor, leftValue/factor, 1) - middleDot.layer.transform = CATransform3DMakeScale(middleValue/factor, middleValue/factor, 1) - rightDot.layer.transform = CATransform3DMakeScale(rightValue/factor, rightValue/factor, 1) - - if leftValue > factor || leftValue < 10 { leftInvert.toggle() } - if middleValue > factor || middleValue < 10 { middleInvert.toggle() } - if rightValue > factor || rightValue < 10 { rightInvert.toggle() } - } + if leftValue > factor || leftValue < 10 { leftInvert.toggle() } + if middleValue > factor || middleValue < 10 { middleInvert.toggle() } + if rightValue > factor || rightValue < 10 { rightInvert.toggle() } + } } diff --git a/Sources/Shared/Views/ErrorView.swift b/Sources/Shared/Views/ErrorView.swift new file mode 100644 index 0000000000000000000000000000000000000000..e0206ac765d80ce542fbbdd4c821ab45a2189376 --- /dev/null +++ b/Sources/Shared/Views/ErrorView.swift @@ -0,0 +1,57 @@ +//import UIKit +//import SnapKit +// +//final class ErrorView: UIView { +// let title = UILabel() +// let content = UILabel() +// let stack = UIStackView() +// let button = CapsuleButton() +// +// init(with model: HUDError) { +// super.init(frame: .zero) +// setup(with: model) +// } +// +// required init?(coder: NSCoder) { nil } +// +// private func setup(with model: HUDError) { +// layer.cornerRadius = 6 +// backgroundColor = Asset.neutralWhite.color +// +// title.text = model.title +// title.textColor = Asset.neutralBody.color +// title.font = Fonts.Mulish.bold.font(size: 35.0) +// title.textAlignment = .center +// title.numberOfLines = 0 +// +// content.text = model.content +// content.textColor = Asset.neutralBody.color +// content.numberOfLines = 0 +// content.font = Fonts.Mulish.regular.font(size: 14.0) +// content.textAlignment = .center +// +// button.setTitle(model.buttonTitle, for: .normal) +// button.setStyle(.brandColored) +// +// stack.axis = .vertical +// +// stack.addArrangedSubview(title) +// stack.addArrangedSubview(content) +// +// if model.dismissable { +// stack.addArrangedSubview(button) +// } +// +// stack.setCustomSpacing(25, after: title) +// stack.setCustomSpacing(59, after: content) +// +// addSubview(stack) +// +// stack.snp.makeConstraints { make in +// make.top.equalToSuperview().offset(60) +// make.left.equalToSuperview().offset(57) +// make.right.equalToSuperview().offset(-57) +// make.bottom.equalToSuperview().offset(-35) +// } +// } +//} 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/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 b11ef15215945e388c8043ed98e3bc6b5001e142..ce67d0e4e30f1de7a45b7013ab32615319dc799f 100644 --- a/Sources/TermsFeature/TermsConditionsController.swift +++ b/Sources/TermsFeature/TermsConditionsController.swift @@ -1,93 +1,78 @@ import UIKit -import Theme import WebKit import Shared import Combine import Defaults -import DependencyInjection +import AppResources +import AppNavigation +import ComposableArchitecture public final class TermsConditionsController: UIViewController { - @Dependency var coordinator: TermsCoordinator - @Dependency var statusBarController: StatusBarStyleControlling - - @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool - - lazy private var screenView = TermsConditionsView() - - private let ndf: String? - private var cancellables = Set<AnyCancellable>() - - public init(_ ndf: String?) { - self.ndf = ndf - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - view = screenView - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.backButtonTitle = "" - navigationController?.navigationBar.customize( - translucent: true, - tint: Asset.neutralWhite.color - ) - } - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let gradient = CAGradientLayer() - gradient.colors = [ - UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, - UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, - UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, - UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor - ] - - gradient.startPoint = CGPoint(x: 0, y: 0) - gradient.endPoint = CGPoint(x: 1, y: 1) - - gradient.frame = screenView.bounds - screenView.layer.insertSublayer(gradient, at: 0) - } - - public override func viewDidLoad() { - super.viewDidLoad() - - screenView.radioComponent - .radioButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - screenView.radioComponent.isEnabled.toggle() - screenView.nextButton.isEnabled = screenView.radioComponent.isEnabled - UIImpactFeedbackGenerator(style: .heavy).impactOccurred() - }.store(in: &cancellables) - - screenView.nextButton - .publisher(for: .touchUpInside) - .sink { [unowned self] in - didAcceptTerms = true - - if let ndf = ndf { - coordinator.presentUsername(ndf, self) - } else { - coordinator.presentChatList(self) - } - }.store(in: &cancellables) - - screenView.showTermsButton - .publisher(for: .touchUpInside) - .sink { [unowned self] _ in - let webView = WKWebView() - let webController = UIViewController() - webController.view.addSubview(webView) - webView.snp.makeConstraints { $0.edges.equalToSuperview() } - webView.load(URLRequest(url: URL(string: "https://elixxir.io/eula")!)) - present(webController, animated: true) - }.store(in: &cancellables) - } + @Dependency(\.navigator) var navigator: Navigator + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool + + private var cancellables = Set<AnyCancellable>() + private lazy var screenView = TermsConditionsView() + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize( + translucent: true, + tint: Asset.neutralWhite.color + ) + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + let gradient = CAGradientLayer() + gradient.colors = [ + UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor, + UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor, + UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, + UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor + ] + gradient.startPoint = CGPoint(x: 0, y: 0) + gradient.endPoint = CGPoint(x: 1, y: 1) + gradient.frame = screenView.bounds + screenView.layer.insertSublayer(gradient, at: 0) + } + + public override func viewDidLoad() { + super.viewDidLoad() + screenView + .radioComponent + .radioButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + screenView.radioComponent.isEnabled.toggle() + screenView.nextButton.isEnabled = screenView.radioComponent.isEnabled + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + }.store(in: &cancellables) + + screenView + .nextButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + didAcceptTerms = true + if username != nil { + navigator.perform(PresentChatList(on: navigationController!)) + } else { + navigator.perform(PresentOnboardingUsername(on: navigationController!)) + } + }.store(in: &cancellables) + + screenView + .showTermsButton + .publisher(for: .touchUpInside) + .sink { [unowned self] _ in + navigator.perform(PresentWebsite(urlString: "https://elixxir.io/eula", from: self)) + }.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/TermsFeature/TermsCoordinator.swift b/Sources/TermsFeature/TermsCoordinator.swift deleted file mode 100644 index daff90e4b1b81b18e75e3a6e34557a3478e26f57..0000000000000000000000000000000000000000 --- a/Sources/TermsFeature/TermsCoordinator.swift +++ /dev/null @@ -1,25 +0,0 @@ -import UIKit -import Presentation - -public struct TermsCoordinator { - var presentChatList: (UIViewController) -> Void - var presentUsername: (String, UIViewController) -> Void -} - -public extension TermsCoordinator { - static func live( - usernameFactory: @escaping (String) -> UIViewController, - chatListFactory: @escaping () -> UIViewController - ) -> Self { - .init( - presentChatList: { parent in - let presenter = ReplacePresenter() - presenter.present(chatListFactory(), from: parent) - }, - presentUsername: { ndf, parent in - let presenter = PushPresenter() - presenter.present(usernameFactory(ndf), from: parent) - } - ) - } -} diff --git a/Sources/TestHelpers/Dummies.swift b/Sources/TestHelpers/Dummies.swift deleted file mode 100644 index e29064cdd83754306c24dc214ddf3b88d0dd1ab1..0000000000000000000000000000000000000000 --- a/Sources/TestHelpers/Dummies.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Models -import Foundation - -public extension Contact { - static let dummy = Contact( - photo: nil, - userId: Data(), - email: nil, - phone: nil, - status: .friend, - marshaled: Data(), - username: "username", - nickname: nil, - createdAt: Date() - ) -} - -public extension GroupChatInfo { - static let dummy = GroupChatInfo( - group: .dummy, - members: [] - ) -} - -public extension Group { - static let dummy = Group( - leader: Data(), - name: "name", - groupId: Data(), - accepted: true, - serialize: Data() - ) -} - -public extension SingleChatInfo { - static let dummy = SingleChatInfo(contact: .dummy, lastMessage: nil) -} diff --git a/Sources/TestHelpers/PresenterDouble.swift b/Sources/TestHelpers/PresenterDouble.swift deleted file mode 100644 index 603c6a46fc337df3c060451cbb2e83b708e1d065..0000000000000000000000000000000000000000 --- a/Sources/TestHelpers/PresenterDouble.swift +++ /dev/null @@ -1,17 +0,0 @@ -import UIKit -import Presentation - -public final class PresenterDouble: Presenting { - public var didPresentFrom: UIViewController? - public var didPresentTarget: UIViewController? - - public init() {} - - public func present( - _ target: UIViewController, - from parent: UIViewController - ) { - didPresentFrom = parent - didPresentTarget = target - } -} diff --git a/Sources/Theme/StatusBarViewController.swift b/Sources/Theme/StatusBarViewController.swift deleted file mode 100644 index d5dd7b000aa81a0180a517e69dc347016fa7a74c..0000000000000000000000000000000000000000 --- a/Sources/Theme/StatusBarViewController.swift +++ /dev/null @@ -1,59 +0,0 @@ -import UIKit -import Combine -import DependencyInjection - -public protocol StatusBarStyleControlling { - var style: CurrentValueSubject<UIStatusBarStyle, Never> { get } -} - -public struct StatusBarController: StatusBarStyleControlling { - public init() {} - - public let style = CurrentValueSubject<UIStatusBarStyle, Never>(.lightContent) -} - -public final class StatusBarViewController: UIViewController { - private let content: UIViewController? - private var cancellables = Set<AnyCancellable>() - - @Dependency private var statusBarController: StatusBarStyleControlling - - public init(_ content: UIViewController?) { - self.content = content - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override var preferredStatusBarStyle: UIStatusBarStyle { - statusBarController.style.value - } - - public override func loadView() { - let view = UIView() - view.backgroundColor = .clear - self.view = view - } - - public override func viewDidLoad() { - super.viewDidLoad() - - if let content = content { - addChild(content) - view.addSubview(content.view) - content.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - content.view.frame = view.bounds - content.didMove(toParent: self) - } else { - view.isUserInteractionEnabled = false - } - - statusBarController.style - .receive(on: DispatchQueue.main) - .sink { [weak self] style in - UIView.animate(withDuration: 0.2) { - self?.setNeedsStatusBarAppearanceUpdate() - } - }.store(in: &cancellables) - } -} diff --git a/Sources/Theme/ThemeController.swift b/Sources/Theme/ThemeController.swift deleted file mode 100644 index 255f1063f39a94670b4a9e4e7bbbe81e54956a73..0000000000000000000000000000000000000000 --- a/Sources/Theme/ThemeController.swift +++ /dev/null @@ -1,41 +0,0 @@ -import UIKit -import Combine -import Defaults - -public enum Theme: Int { - case system = 0 - case dark - - public var userInterfaceStyle: UIUserInterfaceStyle { - switch self { - case .system: - return .unspecified - case .dark: - return .dark - } - } -} - -public protocol ThemeControlling { - var theme: CurrentValueSubject<Theme, Never> { get } -} - -public final class ThemeController: ThemeControlling { - // MARK: Stored - - @KeyObject(.theme, defaultValue: 0) var storedTheme: Int - - // MARK: Properties - - private var cancellables = Set<AnyCancellable>() - public let theme = CurrentValueSubject<Theme, Never>(.system) - - // MARK: Lifecycle - - public init() { - theme.send(Theme(rawValue: storedTheme) ?? .system) - - theme.sink { [unowned self] in storedTheme = $0.rawValue } - .store(in: &cancellables) - } -} diff --git a/Sources/Theme/Window.swift b/Sources/Theme/Window.swift deleted file mode 100644 index 8afb304005d85a8657b2f6bb2b1b5dba7a039546..0000000000000000000000000000000000000000 --- a/Sources/Theme/Window.swift +++ /dev/null @@ -1,19 +0,0 @@ -import UIKit -import Combine -import DependencyInjection - -public final class Window: UIWindow { - @Dependency private var themeController: ThemeControlling - - private var cancellables = Set<AnyCancellable>() - - public init() { - super.init(frame: UIScreen.main.bounds) - - themeController.theme - .sink { [unowned self] in overrideUserInterfaceStyle = $0.userInterfaceStyle } - .store(in: &cancellables) - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/ToastFeature/ToastController.swift b/Sources/ToastFeature/ToastController.swift deleted file mode 100644 index 1b0fd60e66297ace82beb3a6c1e877185d5ad066..0000000000000000000000000000000000000000 --- a/Sources/ToastFeature/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/ToastFeature/ToastModel.swift b/Sources/ToastFeature/ToastModel.swift deleted file mode 100644 index 06dd2a03fde9743309246e6438cdb9ca37383efc..0000000000000000000000000000000000000000 --- a/Sources/ToastFeature/ToastModel.swift +++ /dev/null @@ -1,36 +0,0 @@ -import UIKit -import Shared - -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/ToastFeature/ToastView.swift b/Sources/ToastFeature/ToastView.swift deleted file mode 100644 index c5c96561df06b942bd640a30cdf308bed014a80f..0000000000000000000000000000000000000000 --- a/Sources/ToastFeature/ToastView.swift +++ /dev/null @@ -1,78 +0,0 @@ -import UIKit -import Shared -import Combine - -final class ToastView: UIView { - private let titleLabel = UILabel() - private let subtitleLabel = UILabel() - private let leftImageView = UIImageView() - private let rightButton = UIButton() - private let verticalStackView = UIStackView() - private let horizontalStackView = UIStackView() - private 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/ToastFeature/ToastViewController.swift b/Sources/ToastFeature/ToastViewController.swift deleted file mode 100644 index 35c68a531da1d7197d3ed709472c07b452d463c6..0000000000000000000000000000000000000000 --- a/Sources/ToastFeature/ToastViewController.swift +++ /dev/null @@ -1,134 +0,0 @@ -import UIKit -import Combine -import DependencyInjection - -public final class ToastViewController: UIViewController { - @Dependency private var controller: ToastController - - private var timer: Timer? - private let content: UIViewController - private let toastTopPadding: CGFloat = 10 - private var cancellables = Set<AnyCancellable>() - private var topToastConstraint: NSLayoutConstraint? - - public init(_ content: UIViewController) { - self.content = content - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { nil } - - public override func loadView() { - let view = UIView() - view.backgroundColor = .clear - self.view = view - } - - override public func viewDidLoad() { - super.viewDidLoad() - - addChild(content) - view.addSubview(content.view) - content.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - content.view.frame = view.bounds - content.didMove(toParent: self) - - controller.currentToast - .receive(on: DispatchQueue.main) - .sink { [unowned self] model in - let toastView = ToastView(model: model) - add(toastView: toastView) - present(toastView: toastView) - }.store(in: &cancellables) - } - - @objc private func didPanToast(_ sender: UIPanGestureRecognizer) { - guard let toastView = sender.view else { return } - - switch sender.state { - case .began, .changed: - timer?.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() - self.view.layoutIfNeeded() - } completion: { _ in - toastView.isUserInteractionEnabled = true - toastView.removeFromSuperview() - self.controller.dismissCurrentToast() - } - } - - 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, - usingSpringWithDamping: 1, - initialSpringVelocity: 0.5, - options: .curveEaseInOut - ) { - self.view.setNeedsLayout() - self.view.layoutIfNeeded() - } completion: { _ in - toastView.isUserInteractionEnabled = true - - self.timer?.invalidate() - self.timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in - guard let self = self else { return } - self.dismiss(toastView: toastView) - } - } - } -} diff --git a/Sources/UpdateErrors/Dependency.swift b/Sources/UpdateErrors/Dependency.swift new file mode 100644 index 0000000000000000000000000000000000000000..7294a48614e8353a88079224bc6cf82f551517ee --- /dev/null +++ b/Sources/UpdateErrors/Dependency.swift @@ -0,0 +1,13 @@ +import Dependencies + +private enum UpdateErrorsDependencyKey: DependencyKey { + static let liveValue: UpdateErrors = .live + static let testValue: UpdateErrors = .unimplemented +} + +extension DependencyValues { + public var updateErrors: UpdateErrors { + get { self[UpdateErrorsDependencyKey.self] } + set { self[UpdateErrorsDependencyKey.self] = newValue } + } +} diff --git a/Sources/UpdateErrors/UpdateErrors.swift b/Sources/UpdateErrors/UpdateErrors.swift new file mode 100644 index 0000000000000000000000000000000000000000..6240d77c68672223e2ed8831fa85a876945bb1cf --- /dev/null +++ b/Sources/UpdateErrors/UpdateErrors.swift @@ -0,0 +1,52 @@ +import XXClient +import Foundation +import XCTestDynamicOverlay + +public struct UpdateErrors { + public enum Error: Swift.Error { + case noData + case decodeFailure + case network(URLError) + case bindingsException + } + + public typealias Completion = (Result<Void, Error>) -> Void + + public var run: (@escaping Completion) -> Void + + public func callAsFunction(_ completion: @escaping Completion) -> Void { + run(completion) + } +} + +extension UpdateErrors { + public static let live = UpdateErrors { completion in + let url = URL(string: "https://git.xx.network/elixxir/client-error-database/-/raw/main/clientErrors.json") + URLSession.shared.dataTask(with: url!) { data, _, error in + if let error { + completion(.failure(.network(error as! URLError))) + return + } + guard let data else { + completion(.failure(.noData)) + return + } + guard let string = String(data: data, encoding: .utf8) else { + completion(.failure(.decodeFailure)) + return + } + do { + try UpdateCommonErrors.live(jsonFile: string) + completion(.success(())) + } catch { + completion(.failure(.bindingsException)) + } + }.resume() + } +} + +extension UpdateErrors { + public static let unimplemented = UpdateErrors( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/VersionChecking/VersionChecking.swift b/Sources/VersionChecking/VersionChecking.swift deleted file mode 100644 index cc5a719cdc5bd28f08ca8aea34dd04a7bcf233de..0000000000000000000000000000000000000000 --- a/Sources/VersionChecking/VersionChecking.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Combine -import Foundation - -#warning("TODO: Unit test this feature") - -public enum VersionInfo { - case upToDate - case failure(Error) - case updateRequired(DappVersionInformation) - case updateRecommended(DappVersionInformation) -} - -public struct VersionDataFetcher { - var run: () -> AnyPublisher<DappVersionInformation, Error> - - public init(run: @escaping () -> AnyPublisher<DappVersionInformation, Error>) { - self.run = run - } - - public func callAsFunction() -> AnyPublisher<DappVersionInformation, Error> { run() } -} - -public struct VersionChecker { - var run: () -> AnyPublisher<VersionInfo, Never> - - public init(run: @escaping () -> AnyPublisher<VersionInfo, Never>) { - self.run = run - } - - public func callAsFunction() -> AnyPublisher<VersionInfo, Never> { run() } -} - -public extension VersionChecker { - - static let mock: Self = .init { Just(.upToDate).eraseToAnyPublisher() } - - static func live( - fetchVersion: VersionDataFetcher = .live(), - bundleVersion: @escaping () -> String = { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String } - ) -> Self { - .init { - fetchVersion() - .map { dappInfo -> VersionInfo in - let version = bundleVersion() - if version >= dappInfo.recommended { - return .upToDate - } else if version >= dappInfo.minimum { - return .updateRecommended(dappInfo) - } else { - return .updateRequired(dappInfo) - } - } - .catch { Just(VersionInfo.failure($0)) } - .eraseToAnyPublisher() - } - } -} - -public extension VersionDataFetcher { - static func mock() -> Self { - .init { - Just(DappVersionInformation( - appUrl: "https://testflight.apple.com/join/L1Rj0so3", - minimum: "1.0", - recommended: "1.0", - minimumMessage: "This app version is not supported anymore, please update to the latest version to keep enjoying our app" - )) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - } - - static func live() -> Self { - .init { - let request = URLRequest( - url: URL(string: "https://elixxir-bins.s3-us-west-1.amazonaws.com/client/dapps/appdb.json")!, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: 5 - ) - - return URLSession.shared - .dataTaskPublisher(for: request) - .map(\.data) - .decode(type: BackendVersionInformation.self, decoder: JSONDecoder()) - .map(\.info) - .eraseToAnyPublisher() - } - } -} - -public struct DappVersionInformation: Codable { - public let appUrl: String - public let minimum: String - public let recommended: String - public let minimumMessage: String - - private enum CodingKeys: String, CodingKey { - case appUrl = "new_ios_app_url" - case minimum = "new_ios_min_version" - case recommended = "new_ios_recommended_version" - case minimumMessage = "new_minimum_popup_msg" - } -} - -private struct BackendVersionInformation: Codable { - let info: DappVersionInformation - - private enum CodingKeys: String, CodingKey { - case info = "dapp-id" - } -} diff --git a/Sources/Voxophone/Voxophone.swift b/Sources/Voxophone/Voxophone.swift index 578a46a85df147350013fc526a5d8cd63c0718dd..61141ffd36ff068e3eb0a8385dd38d76699c9aa1 100644 --- a/Sources/Voxophone/Voxophone.swift +++ b/Sources/Voxophone/Voxophone.swift @@ -1,229 +1,227 @@ -import AVFoundation -import Combine 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) - } - } - - // 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 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) - } - } - } + 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/WebsiteFeature/WebsiteController.swift b/Sources/WebsiteFeature/WebsiteController.swift new file mode 100644 index 0000000000000000000000000000000000000000..dacb0998608f3f43972f01458dcca6e6357c2c8c --- /dev/null +++ b/Sources/WebsiteFeature/WebsiteController.swift @@ -0,0 +1,28 @@ +import UIKit +import WebKit + +public final class WebsiteController: UIViewController { + private lazy var webView = WKWebView() + + private let url: URL + + public init(_ string: String) { + self.url = .init(string: string)! + super.init(nibName: nil, bundle: nil) + } + + public override func loadView() { + view = webView + } + + required init?(coder: NSCoder) { nil } + + public override func viewDidLoad() { + super.viewDidLoad() + + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.webView.load(URLRequest(url: self.url)) + } + } +} 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/Sources/iCloudFeature/iCloudInterface.swift b/Sources/iCloudFeature/iCloudInterface.swift deleted file mode 100644 index 41b3153667013967f53fab9eb01d95a99293297c..0000000000000000000000000000000000000000 --- a/Sources/iCloudFeature/iCloudInterface.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -public protocol iCloudInterface { - func openSettings() - - func isAuthorized() -> Bool - - func downloadMetadata(_: @escaping (Result<iCloudMetadata?, Error>) -> Void) - - func uploadBackup(_: URL, _: @escaping (Result<iCloudMetadata, Error>) -> Void) - - func downloadBackup(_: String, _: @escaping (Result<Data, Error>) -> Void) -} diff --git a/Sources/iCloudFeature/iCloudMetadata.swift b/Sources/iCloudFeature/iCloudMetadata.swift deleted file mode 100644 index 9aa70514badbef94033ec24bc965f49b23567e21..0000000000000000000000000000000000000000 --- a/Sources/iCloudFeature/iCloudMetadata.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -public struct iCloudMetadata: Equatable { - public var size: Float - public var path: String - public var modifiedDate: Date - - public init( - path: String, - size: Float, - modifiedDate: Date - ) { - self.path = path - self.size = size - self.modifiedDate = modifiedDate - } -} diff --git a/Sources/iCloudFeature/iCloudService.swift b/Sources/iCloudFeature/iCloudService.swift deleted file mode 100644 index 2ccf20ba26357c9e3966b88fe17f9941fd7525d4..0000000000000000000000000000000000000000 --- a/Sources/iCloudFeature/iCloudService.swift +++ /dev/null @@ -1,87 +0,0 @@ -import UIKit -import FilesProvider - -public struct iCloudService: iCloudInterface { - private let documentsProvider = CloudFileProvider(containerId: "iCloud.xxm-cloud", scope: .data) - - public init() {} - - public func isAuthorized() -> Bool { - FileManager.default.ubiquityIdentityToken != nil - } - - public func openSettings() { - if let url = URL(string: "App-Prefs:root=CASTLE"), UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - - public func downloadMetadata(_ completion: @escaping (Result<iCloudMetadata?, Error>) -> Void) { - guard let documentsProvider = documentsProvider else { fatalError() } - - documentsProvider.contentsOfDirectory(path: "/", completionHandler: { contents, error in - guard error == nil else { - print(">>> [iCloud] downloadMetadata got error: \(error!.localizedDescription)") - completion(.failure(error!)) - return - } - - print(contents) - - if let file = contents.first(where: { $0.name == "backup.xxm" }) { - completion(.success(.init( - path: file.path, - size: Float(file.size), - modifiedDate: file.modifiedDate! - ))) - } else { - completion(.success(nil)) - } - }) - } - - public func uploadBackup(_ url: URL, _ completion: @escaping (Result<iCloudMetadata, Error>) -> Void) { - guard let documentsProvider = documentsProvider else { fatalError() } - - do { - let data = try Data(contentsOf: url) - - documentsProvider.writeContents(path: "backup.xxm", contents: data, overwrite: true) { error in - guard error == nil else { - print(">>> [iCloud] uploadBackup got error: \(error!.localizedDescription)") - completion(.failure(error!)) - return - } - - completion(.success(.init( - path: "backup.xxm", - size: Float(data.count), - modifiedDate: Date() - ))) - } - } catch { - completion(.failure(error)) - } - } - - public func downloadBackup( - _ path: String, - _ completion: @escaping (Result<Data, Error>) -> Void - ) { - guard let documentsProvider = documentsProvider else { fatalError() } - - documentsProvider.contents(path: path, completionHandler: { contents, error in - guard error == nil else { - print(">>> [iCloud] downloadBackup got error: \(error!.localizedDescription)") - completion(.failure(error!)) - return - } - - if let contents = contents { - completion(.success(contents)) - } else { - completion(.failure(NSError(domain: "Backup file is invalid", code: 0))) - } - }) - } -} diff --git a/Sources/iCloudFeature/iCloudServiceMock.swift b/Sources/iCloudFeature/iCloudServiceMock.swift deleted file mode 100644 index f0bcfca66108e297f5d04de4fb3e5dc0705ae097..0000000000000000000000000000000000000000 --- a/Sources/iCloudFeature/iCloudServiceMock.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation - -public struct iCloudServiceMock: iCloudInterface { - public init() { - // TODO - } - - public func openSettings() { - // TODO - } - - public func isAuthorized() -> Bool { - true - } - - public func downloadBackup( - _: String, - _: @escaping (Result<Data, Error>) -> Void - ) { - // TODO - } - - public func uploadBackup( - _: URL, - _: @escaping (Result<iCloudMetadata, Error>) -> Void - ) { - // TODO - } - - public func downloadMetadata( - _ completion: @escaping (Result<iCloudMetadata?, Error>) -> Void - ) { - completion(.success(.init( - path: "/", - size: 1230000000.0, - modifiedDate: Date() - ))) - } -} 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/Tests/ChatFeatureTests/Coordinator/ChatCoordinatorSpec.swift b/Tests/ChatFeatureTests/Coordinator/ChatCoordinatorSpec.swift deleted file mode 100644 index afc03f67c8065a3a0e76079b02d9aa161af9d73c..0000000000000000000000000000000000000000 --- a/Tests/ChatFeatureTests/Coordinator/ChatCoordinatorSpec.swift +++ /dev/null @@ -1,118 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ChatFeature - -final class ChatCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ChatCoordinator! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - - var retryController: UIViewController! - var contactController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - retryController = UIViewController() - contactController = UIViewController() - - sut = ChatCoordinator( - retryFactory: { retryController }, - contactFactory: { _ in contactController } - ) - - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - bottomPresenter = nil - retryController = nil - contactController = nil - } - - context("when presenting retry sheet") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toRetrySheet(from: parent) - } - - it("should present RetrySheetController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(retryController)) - } - } - - context("when presenting members list") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.toMembersList(target, from: parent) - } - - it("should present MembersController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting Popup") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting Contact") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toContact(.dummy, from: parent) - } - - it("should present ContactController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(contactController)) - } - } - - context("when presenting menu sheet") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.toMenuSheet(target, from: parent) - } - - it("should present SheetController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/ChatListFeatureTests/Coordinator/ChatListCoordinatorSpec.swift b/Tests/ChatListFeatureTests/Coordinator/ChatListCoordinatorSpec.swift deleted file mode 100644 index dc4a069cb02759b7d3eda601d20408e7c075a74b..0000000000000000000000000000000000000000 --- a/Tests/ChatListFeatureTests/Coordinator/ChatListCoordinatorSpec.swift +++ /dev/null @@ -1,243 +0,0 @@ -import UIKit -import Quick -import Nimble -import MenuFeature -import TestHelpers - -@testable import ChatListFeature - -final class ChatListCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ChatListCoordinator! - var sider: PresenterDouble! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - - var scanScreen: UIViewController! - var searchScreen: UIViewController! - var profileScreen: UIViewController! - var settingsScreen: UIViewController! - var contactsScreen: UIViewController! - var requestsScreen: UIViewController! - - beforeEach { - sider = PresenterDouble() - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - - scanScreen = UIViewController() - searchScreen = UIViewController() - profileScreen = UIViewController() - settingsScreen = UIViewController() - contactsScreen = UIViewController() - requestsScreen = UIViewController() - - sut = ChatListCoordinator( - scanFactory: { scanScreen }, - searchFactory: { searchScreen }, - profileFactory: { profileScreen }, - settingsFactory: { settingsScreen }, - contactsFactory: { contactsScreen }, - requestsFactory: { requestsScreen } - ) - - sut.sider = sider - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - sider = nil - scanScreen = nil - searchScreen = nil - profileScreen = nil - settingsScreen = nil - contactsScreen = nil - requestsScreen = nil - bottomPresenter = nil - } - - context("when presenting chat") { - var target: UIViewController! - var parent: UIViewController! - - beforeEach { - target = UIViewController() - parent = UIViewController() - sut.singleChatFactory = { _ in target } - sut.toSingleChat(with: .dummy, from: parent) - } - - it("should present ChatController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting side menu") { - var parent: Delegate! - var target: UIViewController! - - beforeEach { - parent = Delegate() - target = UIViewController() - sut.sideMenuFactory = { _ in target } - sut.toSideMenu(from: parent) - } - - it("should present side menu") { - expect(sider.didPresentFrom).to(be(parent)) - expect(sider.didPresentTarget).to(be(target)) - } - } - - context("when presenting search") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.searchFactory = { target } - sut.toSearch(from: parent) - } - - it("should present SearchController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting scan") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.scanFactory = { target } - sut.toScan(from: parent) - } - - it("should present ScanController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting profile") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.profileFactory = { target } - sut.toProfile(from: parent) - } - - it("should present ProfileController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting contacts") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactsFactory = { target } - sut.toContacts(from: parent) - } - - it("should present ContactListController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting settings") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.settingsFactory = { target } - sut.toSettings(from: parent) - } - - it("should present SettingsController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting group chat") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.groupChatFactory = { _ in target } - sut.toGroupChat(with: .init( - group: .dummy, - members: [], - lastMessage: nil - ), from: parent) - } - - it("should present GroupChatController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting requests") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.requestsFactory = { target } - sut.toRequests(from: parent) - } - - it("should present RequestsContainer") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} - -// MARK: - Delegate - -private final class Delegate: UIViewController, MenuDelegate { - func didSelect(item: MenuItem) {} -} diff --git a/Tests/ContactFeatureTests/Coordinator/ContactCoordinatorSpec.swift b/Tests/ContactFeatureTests/Coordinator/ContactCoordinatorSpec.swift deleted file mode 100644 index 1a4026f8fb294636e838d257a33ef40fbf06aca1..0000000000000000000000000000000000000000 --- a/Tests/ContactFeatureTests/Coordinator/ContactCoordinatorSpec.swift +++ /dev/null @@ -1,129 +0,0 @@ -import UIKit -import Quick -import Nimble -import ChatFeature -import TestHelpers - -@testable import ContactFeature - -final class ContactCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ContactCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - var presenter: PresenterDouble! - var bottomPresenter: PresenterDouble! - - var requestsScreen: UIViewController! - - beforeEach { - pusher = PresenterDouble() - replacer = PresenterDouble() - presenter = PresenterDouble() - requestsScreen = UIViewController() - bottomPresenter = PresenterDouble() - - sut = ContactCoordinator(requestsFactory: { requestsScreen }) - - sut.pusher = pusher - sut.replacer = replacer - sut.presenter = presenter - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - presenter = nil - requestsScreen = nil - bottomPresenter = nil - } - - context("when presenting image picker") { - var parent: UIViewController! - var target: UIImagePickerController! - - beforeEach { - parent = UIViewController() - target = UIImagePickerController() - sut.imagePickerFactory = { target } - sut.toPhotos(from: parent) - } - - it("should present UIImagePickerController") { - expect(presenter.didPresentFrom).to(be(parent)) - expect(presenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting single chat") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.singleChatFactory = { _ in target } - sut.toSingleChat(with: .dummy, from: parent) - } - - it("should present ChatController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting nickname") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.nicknameFactory = { _,_ in target } - sut.toNickname(from: parent, prefilled: "", { _ in }) - } - - it("should present NickameController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting requests") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.requestsFactory = { target } - sut.toRequests(from: parent) - } - - it("should present RequestsController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/ContactListFeatureTests/Coordinator/ContactListCoordinatorSpec.swift b/Tests/ContactListFeatureTests/Coordinator/ContactListCoordinatorSpec.swift deleted file mode 100644 index ce494c1f73b2b8765832d99341f51b37f2fd7676..0000000000000000000000000000000000000000 --- a/Tests/ContactListFeatureTests/Coordinator/ContactListCoordinatorSpec.swift +++ /dev/null @@ -1,162 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ContactListFeature - -final class ContactListCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ContactListCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - var bottomPresenter: PresenterDouble! - var fullscreenPresenter: PresenterDouble! - - var scanScreen: UIViewController! - var groupScreen: UIViewController! - var searchScreen: UIViewController! - var requestsScreen: UIViewController! - - beforeEach { - scanScreen = UIViewController() - searchScreen = UIViewController() - - sut = ContactListCoordinator( - scanFactory: { scanScreen }, - searchFactory: { searchScreen }, - newGroupFactory: { groupScreen }, - requestsFactory: { requestsScreen } - ) - - pusher = PresenterDouble() - replacer = PresenterDouble() - bottomPresenter = PresenterDouble() - fullscreenPresenter = PresenterDouble() - - sut.pusher = pusher - sut.replacer = replacer - sut.bottomPresenter = bottomPresenter - sut.fullscreenPresenter = fullscreenPresenter - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - scanScreen = nil - groupScreen = nil - searchScreen = nil - requestsScreen = nil - bottomPresenter = nil - fullscreenPresenter = nil - } - - context("when presenting contact details") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present contact details screen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting SearchScreen") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.searchFactory = { target } - sut.toSearch(from: parent) - } - - it("should present SearchScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting scan screen") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.scanFactory = { target } - sut.toScan(from: parent) - } - - it("should present qr screen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting new group") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.newGroupFactory = { target } - sut.toNewGroup(from: parent) - } - - it("should present new group") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting group chat") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.groupChatFactory = { _ in target } - sut.toGroupChat(with: .init( - group: .dummy, - members: [], - lastMessage: nil - ), from: parent) - } - - it("should present group chat") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting group popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.groupPopupFactory = { _,_ in target } - sut.toGroupPopup(with: 0, from: parent, { _,_ in }) - } - - it("should present group popup") { - expect(fullscreenPresenter.didPresentFrom).to(be(parent)) - } - } - } - } -} diff --git a/Tests/DefaultsTests/KeyObjectTests.swift b/Tests/DefaultsTests/KeyObjectTests.swift index ffa4794fa650c140e883a19ab7540ae7a8d78192..e35e13ebf2d34edd1b5e5a3e2b1423699be7a21d 100644 --- a/Tests/DefaultsTests/KeyObjectTests.swift +++ b/Tests/DefaultsTests/KeyObjectTests.swift @@ -1,84 +1,83 @@ import XCTest -import DependencyInjection @testable import Defaults final class KeyObjectSpec: XCTestCase { - func testGetCachedValue() { - var didSetObject: Any? - var didSetObjectForKey: String? + func testGetCachedValue() { + var didSetObject: Any? + var didSetObjectForKey: String? - let sut = KeyObjectStore( - objectForKey: { _ in fatalError() }, - setObjectForKey: { object, key in - didSetObject = object - didSetObjectForKey = key - }, removeObjectForKey: { _ in fatalError() } - ) + let sut = KeyObjectStore( + objectForKey: { _ in fatalError() }, + setObjectForKey: { object, key in + didSetObject = object + didSetObjectForKey = key + }, removeObjectForKey: { _ in fatalError() } + ) - DependencyInjection.Container.shared.register(sut) + DI.Container.shared.register(sut) - @KeyObject(.email, defaultValue: "1234") var email: String + @KeyObject(.email, defaultValue: "1234") var email: String - email = "5678" - assert(didSetObject as! String == "5678") - assert(didSetObjectForKey == Key.email.rawValue) - } + email = "5678" + assert(didSetObject as! String == "5678") + assert(didSetObjectForKey == Key.email.rawValue) + } - func testGetDefaultValue() { - var didGetObjectForKey: String? + func testGetDefaultValue() { + var didGetObjectForKey: String? - let sut = KeyObjectStore( - objectForKey: { didGetObjectForKey = $0 }, - setObjectForKey: { _,_ in fatalError() }, - removeObjectForKey: { _ in fatalError() } - ) + let sut = KeyObjectStore( + objectForKey: { didGetObjectForKey = $0 }, + setObjectForKey: { _,_ in fatalError() }, + removeObjectForKey: { _ in fatalError() } + ) - DependencyInjection.Container.shared.register(sut) + DI.Container.shared.register(sut) - let defaultValue = "1234" - @KeyObject(.email, defaultValue: defaultValue) var email: String + let defaultValue = "1234" + @KeyObject(.email, defaultValue: defaultValue) var email: String - assert(email == defaultValue) - assert(didGetObjectForKey == Key.email.rawValue) - } + assert(email == defaultValue) + assert(didGetObjectForKey == Key.email.rawValue) + } - func testSetValue() { - var didSetObject: Any? - var didSetObjectForKey: String? + func testSetValue() { + var didSetObject: Any? + var didSetObjectForKey: String? - let sut = KeyObjectStore( - objectForKey: { _ in fatalError() }, - setObjectForKey: { object, key in - didSetObject = object - didSetObjectForKey = key - }, removeObjectForKey: { _ in fatalError() } - ) + let sut = KeyObjectStore( + objectForKey: { _ in fatalError() }, + setObjectForKey: { object, key in + didSetObject = object + didSetObjectForKey = key + }, removeObjectForKey: { _ in fatalError() } + ) - DependencyInjection.Container.shared.register(sut) + DI.Container.shared.register(sut) - @KeyObject(.phone, defaultValue: "1234") var phone: String - phone = "5678" + @KeyObject(.phone, defaultValue: "1234") var phone: String + phone = "5678" - assert(didSetObject as! String == "5678") - assert(didSetObjectForKey == Key.phone.rawValue) - } + assert(didSetObject as! String == "5678") + assert(didSetObjectForKey == Key.phone.rawValue) + } - func testRemovingValue() { - var didRemoveObjectForKey: String? + func testRemovingValue() { + var didRemoveObjectForKey: String? - let sut = KeyObjectStore( - objectForKey: { _ in fatalError() }, - setObjectForKey: { _,_ in fatalError() }, - removeObjectForKey: { didRemoveObjectForKey = $0 } - ) + let sut = KeyObjectStore( + objectForKey: { _ in fatalError() }, + setObjectForKey: { _,_ in fatalError() }, + removeObjectForKey: { didRemoveObjectForKey = $0 } + ) - DependencyInjection.Container.shared.register(sut) + DI.Container.shared.register(sut) - @KeyObject(.phone, defaultValue: "1234") var phone: String? - phone = nil + @KeyObject(.phone, defaultValue: "1234") var phone: String? + phone = nil - assert(didRemoveObjectForKey == Key.phone.rawValue) - } + assert(didRemoveObjectForKey == Key.phone.rawValue) + } } diff --git a/Tests/DependencyInjectionTests/ContainerTests.swift b/Tests/DependencyInjectionTests/ContainerTests.swift deleted file mode 100644 index b9de7d2eb871fd39d8a3cc948b0ec9a5e2037dec..0000000000000000000000000000000000000000 --- a/Tests/DependencyInjectionTests/ContainerTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -import XCTest - -@testable import DependencyInjection - -final class ContainerTests: XCTestCase { - func testRegisterAndResolveDependency() { - let container = Container() - let dependency = TestDependency() - container.register(dependency as TestDependencyProtocol) - let resolvedDependency: TestDependencyProtocol = try! container.resolve() - - XCTAssert(resolvedDependency === dependency) - } - - func testResolveUnregisterredDependency() { - let container = Container() - do { - let _: TestDependencyProtocol = try container.resolve() - XCTFail("expected to throw an error") - } catch { - XCTAssertEqual( - error as? UnregisteredDependencyError, - UnregisteredDependencyError( - type: String(describing: TestDependencyProtocol.self) - ) - ) - } - } -} - -private protocol TestDependencyProtocol: AnyObject {} - -private class TestDependency: TestDependencyProtocol {} diff --git a/Tests/DependencyInjectionTests/DependencyPropertyWrapperTests.swift b/Tests/DependencyInjectionTests/DependencyPropertyWrapperTests.swift deleted file mode 100644 index 97bf45ef52c1c539b80dbb83292b4c2baa13077c..0000000000000000000000000000000000000000 --- a/Tests/DependencyInjectionTests/DependencyPropertyWrapperTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -import XCTest -@testable import DependencyInjection - -final class DependencyPropertyWrapperTests: XCTestCase { - func testPropertyGetter() { - struct Context { - static let container = Container() - @Dependency(container: container) var property: TestDependencyProtocol - } - - let dependency = TestDependency() - Context.container.register(dependency as TestDependencyProtocol) - - XCTAssert(Context().property === dependency) - } -} - -private protocol TestDependencyProtocol: AnyObject {} - -private class TestDependency: TestDependencyProtocol {} diff --git a/Tests/OnboardingFeatureTests/Coordinator/OnboardingCoordinatorSpec.swift b/Tests/OnboardingFeatureTests/Coordinator/OnboardingCoordinatorSpec.swift deleted file mode 100644 index 2ffb72ee0e6560a6162eace8dcbd0fa788a87197..0000000000000000000000000000000000000000 --- a/Tests/OnboardingFeatureTests/Coordinator/OnboardingCoordinatorSpec.swift +++ /dev/null @@ -1,245 +0,0 @@ -import UIKit -import Quick -import Theme -import Nimble -import Combine -import TestHelpers -import DependencyInjection - -@testable import OnboardingFeature - -final class OnboardingCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: OnboardingCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - var bottomPresenter: PresenterDouble! - var chatsController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - replacer = PresenterDouble() - bottomPresenter = PresenterDouble() - - chatsController = UIViewController() - - DependencyInjection.Container.shared - .register(StatusBarControllerDouble() as StatusBarStyleControlling) - - sut = OnboardingCoordinator(chatListFactory: { chatsController }) - - sut.pusher = pusher - sut.replacer = replacer - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - bottomPresenter = nil - chatsController = nil - } - - context("when presenting success") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.successFactory = { _ in target } - sut.toSuccess(isEmail: false, from: parent) - } - - it("should present OnboardingSuccessController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting username") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.usernameFactory = { _ in target } - sut.toUsername(with: "", from: parent) - } - - it("should present OnboardingUsernameController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting chats") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toChats(from: parent) - } - - it("should present ChatsController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(chatsController)) - } - } - - context("when presenting email") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.emailFactory = { target } - sut.toEmail(from: parent) - } - - it("should present OnboardingEmailController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting phone") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.phoneFactory = { target } - sut.toPhone(from: parent) - } - - it("should present OnboardingPhoneController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting countries") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.countriesFactory = { _ in target } - sut.toCountries(from: parent, { _ in }) - } - - it("should present CountriesController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting welcome") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.welcomeFactory = { target } - sut.toWelcome(from: parent) - } - - it("should present OnboardingWelcomeScreen") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting start") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.startFactory = { _ in target } - sut.toStart(with: "ndf", from: parent) - } - - it("should present OnboardingStartScreen") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(target)) - } - } - - context("when presenting email confirmation") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.emailConfirmationFactory = { _,_ in target } - sut.toEmailConfirmation(with: .init(content: ""), from: parent, completion: { _ in }) - } - - it("should present OnboardingEmailConfirmationScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting phone confirmation") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.phoneConfirmationFactory = { _,_ in target } - sut.toPhoneConfirmation(with: .init(content: ""), from: parent, completion: { _ in }) - } - - it("should present OnboardingPhoneConfirmationScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} - -// MARK: - StatusBarControllerDouble - -private final class StatusBarControllerDouble: StatusBarStyleControlling { - var didSetStyle: UIStatusBarStyle? - - let style = CurrentValueSubject<UIStatusBarStyle, Never>(.lightContent) - var cancellables = Set<AnyCancellable>() - - init() { - style - .receive(on: DispatchQueue.main) - .sink { [unowned self] in didSetStyle = $0 } - .store(in: &cancellables) - } -} diff --git a/Tests/ProfileFeatureTests/Coordinator/ProfileCoordinatorSpec.swift b/Tests/ProfileFeatureTests/Coordinator/ProfileCoordinatorSpec.swift deleted file mode 100644 index 1352033a3a098ffcb773820ff2c4f7cd1defb0e0..0000000000000000000000000000000000000000 --- a/Tests/ProfileFeatureTests/Coordinator/ProfileCoordinatorSpec.swift +++ /dev/null @@ -1,140 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ProfileFeature - -final class ProfileCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ProfileCoordinator! - var pusher: PresenterDouble! - var presenter: PresenterDouble! - var bottomPresenter: PresenterDouble! - - beforeEach { - sut = ProfileCoordinator() - pusher = PresenterDouble() - presenter = PresenterDouble() - bottomPresenter = PresenterDouble() - - sut.pusher = pusher - sut.presenter = presenter - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - presenter = nil - bottomPresenter = nil - } - - context("when presenting image picker") { - var parent: UIViewController! - var target: UIImagePickerController! - - beforeEach { - parent = UIViewController() - target = UIImagePickerController() - sut.imagePickerFactory = { target } - sut.toPhotos(from: parent) - } - - it("should present UIImagePickerController") { - expect(presenter.didPresentFrom).to(be(parent)) - expect(presenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting email") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.emailFactory = { target } - sut.toEmail(from: parent) - } - - it("should present ProfileEmailController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting phone") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.phoneFactory = { target } - sut.toPhone(from: parent) - } - - it("should present ProfilePhoneController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting countries") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.countriesFactory = { _ in target } - sut.toCountries(from: parent, { _ in }) - } - - it("should present CountriesController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting code") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.codeFactory = { _,_ in target } - - sut.toCode( - with: .init(content: ""), - from: parent - ) { _,_ in } - } - - it("should present CodeController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/RequestsFeatureTests/Coordinator/RequestsCoordinatorSpec.swift b/Tests/RequestsFeatureTests/Coordinator/RequestsCoordinatorSpec.swift deleted file mode 100644 index d27a5c47a5c30c6274e9206597058f32f37d4bf4..0000000000000000000000000000000000000000 --- a/Tests/RequestsFeatureTests/Coordinator/RequestsCoordinatorSpec.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Quick -import UIKit -import Nimble -import TestHelpers - -@testable import RequestsFeature - -final class RequestsCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: RequestsCoordinator! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - var searchController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - searchController = UIViewController() - - sut = RequestsCoordinator(searchFactory: { searchController }) - - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - bottomPresenter = nil - searchController = nil - } - - context("when presenting search") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toSearch(from: parent) - } - - it("should present SearchController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(searchController)) - } - } - - context("when presenting contact") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present ContactController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting VerifyingFactory") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.verifyingFactory = { target } - sut.toVerifying(from: parent) - } - - it("should present VerifyingScreen") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting nickname") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.nicknameFactory = { _,_ in target } - sut.toNickname(from: parent, prefilled: "", { _ in }) - } - - it("should present NicknameController") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/ScanFeatureTests/Coordinator/ScanCoordinatorSpec.swift b/Tests/ScanFeatureTests/Coordinator/ScanCoordinatorSpec.swift deleted file mode 100644 index 1d35eb46404cda5ea0de8c9f4328143b48a1a841..0000000000000000000000000000000000000000 --- a/Tests/ScanFeatureTests/Coordinator/ScanCoordinatorSpec.swift +++ /dev/null @@ -1,87 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import ScanFeature - -final class ScanCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: ScanCoordinator! - var pusher: PresenterDouble! - var replacer: PresenterDouble! - - var contactsController: UIViewController! - var requestsController: UIViewController! - - beforeEach { - pusher = PresenterDouble() - replacer = PresenterDouble() - contactsController = UIViewController() - requestsController = UIViewController() - - sut = ScanCoordinator( - contactsFactory: { contactsController }, - requestsFactory: { requestsController } - ) - - sut.pusher = pusher - sut.replacer = replacer - } - - afterEach { - sut = nil - pusher = nil - replacer = nil - contactsController = nil - requestsController = nil - } - - context("when presenting add") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present ContactController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting contacts") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toContacts(from: parent) - } - - it("should present ContactListController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(contactsController)) - } - } - - context("when presenting requests") { - var parent: UIViewController! - - beforeEach { - parent = UIViewController() - sut.toRequests(from: parent) - } - - it("should present RequestsController") { - expect(replacer.didPresentFrom).to(be(parent)) - expect(replacer.didPresentTarget).to(be(requestsController)) - } - } - } - } -} diff --git a/Tests/SearchFeatureTests/Coordinator/SearchCoordinatorSpec.swift b/Tests/SearchFeatureTests/Coordinator/SearchCoordinatorSpec.swift deleted file mode 100644 index 7bb3736b612f2659be50ca77cf72199987ead3fa..0000000000000000000000000000000000000000 --- a/Tests/SearchFeatureTests/Coordinator/SearchCoordinatorSpec.swift +++ /dev/null @@ -1,81 +0,0 @@ -import UIKit -import Quick -import Nimble -import Integration -import TestHelpers - -@testable import SearchFeature - -final class SearchCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: SearchCoordinator! - var pusher: PresenterDouble! - var bottomPresenter: PresenterDouble! - - beforeEach { - sut = SearchCoordinator() - pusher = PresenterDouble() - bottomPresenter = PresenterDouble() - sut.pusher = pusher - sut.bottomPresenter = bottomPresenter - } - - afterEach { - sut = nil - pusher = nil - bottomPresenter = nil - } - - context("when presenting add screen") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.contactFactory = { _ in target } - sut.toContact(.dummy, from: parent) - } - - it("should present AddScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting countries") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.countriesFactory = { _ in target } - sut.toCountries(from: parent, { _ in }) - } - - it("should present CountriesController") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/SettingsFeatureTests/Coordinator/SettingsCoordinatorSpec.swift b/Tests/SettingsFeatureTests/Coordinator/SettingsCoordinatorSpec.swift deleted file mode 100644 index 610e7b9b3eeb85b4bbb3db5c2274efbf0a6d3edf..0000000000000000000000000000000000000000 --- a/Tests/SettingsFeatureTests/Coordinator/SettingsCoordinatorSpec.swift +++ /dev/null @@ -1,79 +0,0 @@ -import UIKit -import Quick -import Nimble -import TestHelpers - -@testable import SettingsFeature - -final class SettingsCoordinatorSpec: QuickSpec { - override func spec() { - context("init") { - var sut: SettingsCoordinator! - var pusher: PresenterDouble! - var presenter: PresenterDouble! - var bottomPresenter: PresenterDouble! - - beforeEach { - pusher = PresenterDouble() - presenter = PresenterDouble() - bottomPresenter = PresenterDouble() - - sut = SettingsCoordinator() - - sut.pusher = pusher - sut.presenter = presenter - sut.bottomPresenter = bottomPresenter - } - - context("when presenting advanced settings") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.advancedFactory = { target } - sut.toAdvanced(from: parent) - } - - it("should present AdvancedScreen") { - expect(pusher.didPresentFrom).to(be(parent)) - expect(pusher.didPresentTarget).to(be(target)) - } - } - - context("when presenting Popup") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.toPopup(target, from: parent) - } - - it("should present Popup") { - expect(bottomPresenter.didPresentFrom).to(be(parent)) - expect(bottomPresenter.didPresentTarget).to(be(target)) - } - } - - context("when presenting ActivityViewController") { - var parent: UIViewController! - var target: UIViewController! - - beforeEach { - parent = UIViewController() - target = UIViewController() - sut.activityControllerFactory = { _ in target } - sut.toActivityController(with: [0], from: parent) - } - - it("should present ActivityViewController") { - expect(presenter.didPresentFrom).to(be(parent)) - expect(presenter.didPresentTarget).to(be(target)) - } - } - } - } -} diff --git a/Tests/ThemeTests/ThemeTests.swift b/Tests/ThemeTests/ThemeTests.swift index 44f88ab56ce69345bf3c5c049a6e7c5137f6f16d..55049b48a6fdd1dd9bdc096336c1ca1da517f3a8 100644 --- a/Tests/ThemeTests/ThemeTests.swift +++ b/Tests/ThemeTests/ThemeTests.swift @@ -2,44 +2,43 @@ import Quick import Nimble import Defaults import Foundation -import DependencyInjection @testable import Theme final class ThemeTests: QuickSpec { - override func spec() { - context("init") { - var sut: ThemeController! - var dictionary: NSMutableDictionary! - - beforeEach { - dictionary = .init() - - DependencyInjection.Container.shared - .register(KeyObjectStore.mock(dictionary: dictionary)) - - sut = ThemeController() - } - - afterEach { - dictionary = nil - } - - it("should load .system a.k.a 0 from defaults") { - let theme = dictionary.value(forKey: Key.theme.rawValue) as? Int - expect(theme).to(equal(0)) - } - - context("when changing theme") { - beforeEach { - sut.theme.send(.dark) - } - - it("should save .dark") { - let theme = dictionary.value(forKey: Key.theme.rawValue) as? Int - expect(theme).to(equal(1)) - } - } + override func spec() { + context("init") { + var sut: ThemeController! + var dictionary: NSMutableDictionary! + + beforeEach { + dictionary = .init() + + DI.Container.shared + .register(KeyObjectStore.mock(dictionary: dictionary)) + + sut = ThemeController() + } + + afterEach { + dictionary = nil + } + + it("should load .system a.k.a 0 from defaults") { + let theme = dictionary.value(forKey: Key.theme.rawValue) as? Int + expect(theme).to(equal(0)) + } + + context("when changing theme") { + beforeEach { + sut.theme.send(.dark) + } + + it("should save .dark") { + let theme = dictionary.value(forKey: Key.theme.rawValue) as? Int + expect(theme).to(equal(1)) } + } } + } } diff --git a/XCFrameworks/Bindings.xcframework/Info.plist b/XCFrameworks/Bindings.xcframework/Info.plist deleted file mode 100644 index 5da456bbdabbf3d610daca4ce17734b523413a53..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/Info.plist +++ /dev/null @@ -1,40 +0,0 @@ -<?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>AvailableLibraries</key> - <array> - <dict> - <key>LibraryIdentifier</key> - <string>ios-arm64</string> - <key>LibraryPath</key> - <string>Bindings.framework</string> - <key>SupportedArchitectures</key> - <array> - <string>arm64</string> - </array> - <key>SupportedPlatform</key> - <string>ios</string> - </dict> - <dict> - <key>LibraryIdentifier</key> - <string>ios-arm64_x86_64-simulator</string> - <key>LibraryPath</key> - <string>Bindings.framework</string> - <key>SupportedArchitectures</key> - <array> - <string>arm64</string> - <string>x86_64</string> - </array> - <key>SupportedPlatform</key> - <string>ios</string> - <key>SupportedPlatformVariant</key> - <string>simulator</string> - </dict> - </array> - <key>CFBundlePackageType</key> - <string>XFWK</string> - <key>XCFrameworkFormatVersion</key> - <string>1.0</string> -</dict> -</plist> diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings deleted file mode 100644 index 8d78f84d50a0b0719b795f7b342f6109c2b848ec..0000000000000000000000000000000000000000 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings and /dev/null differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.h deleted file mode 100644 index 8906a7da239705b790cb2bb64de92f806640cb38..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.h +++ /dev/null @@ -1,13 +0,0 @@ - -// Objective-C API for talking to the following Go packages -// -// gitlab.com/elixxir/client/bindings -// -// File is generated by gomobile bind. Do not edit. -#ifndef __Bindings_FRAMEWORK_H__ -#define __Bindings_FRAMEWORK_H__ - -#include "Bindings.objc.h" -#include "Universe.objc.h" - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.objc.h deleted file mode 100644 index 32bf6d116888f787ced27b01b95cb4e1b2c1138b..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Bindings.objc.h +++ /dev/null @@ -1,2083 +0,0 @@ -// Objective-C API for talking to gitlab.com/elixxir/client/bindings Go package. -// gobind -lang=objc gitlab.com/elixxir/client/bindings -// -// File is generated by gobind. Do not edit. - -#ifndef __Bindings_H__ -#define __Bindings_H__ - -@import Foundation; -#include "ref.h" -#include "Universe.objc.h" - - -@class BindingsBackup; -@class BindingsBackupReport; -@class BindingsClient; -@class BindingsContact; -@class BindingsContactList; -@class BindingsDummyTraffic; -@class BindingsFact; -@class BindingsFactList; -@class BindingsFilePartTracker; -@class BindingsFileTransfer; -@class BindingsGroup; -@class BindingsGroupChat; -@class BindingsGroupMember; -@class BindingsGroupMembership; -@class BindingsGroupMessageReceive; -@class BindingsGroupReportDisk; -@class BindingsGroupSendReport; -@class BindingsIdList; -@class BindingsIntList; -@class BindingsManyNotificationForMeReport; -@class BindingsMessage; -@class BindingsNewGroupReport; -@class BindingsNodeRegistrationsStatus; -@class BindingsNotificationForMeReport; -@class BindingsRestoreContactsReport; -@class BindingsRoundList; -@class BindingsSendReport; -@class BindingsSendReportDisk; -@class BindingsUnregister; -@class BindingsUser; -@class BindingsUserDiscovery; -@protocol BindingsAuthConfirmCallback; -@class BindingsAuthConfirmCallback; -@protocol BindingsAuthRequestCallback; -@class BindingsAuthRequestCallback; -@protocol BindingsAuthResetNotificationCallback; -@class BindingsAuthResetNotificationCallback; -@protocol BindingsClientError; -@class BindingsClientError; -@protocol BindingsEventCallbackFunctionObject; -@class BindingsEventCallbackFunctionObject; -@protocol BindingsFileTransferReceiveFunc; -@class BindingsFileTransferReceiveFunc; -@protocol BindingsFileTransferReceivedProgressFunc; -@class BindingsFileTransferReceivedProgressFunc; -@protocol BindingsFileTransferSentProgressFunc; -@class BindingsFileTransferSentProgressFunc; -@protocol BindingsGroupReceiveFunc; -@class BindingsGroupReceiveFunc; -@protocol BindingsGroupRequestFunc; -@class BindingsGroupRequestFunc; -@protocol BindingsListener; -@class BindingsListener; -@protocol BindingsLogWriter; -@class BindingsLogWriter; -@protocol BindingsLookupCallback; -@class BindingsLookupCallback; -@protocol BindingsMessageDeliveryCallback; -@class BindingsMessageDeliveryCallback; -@protocol BindingsMultiLookupCallback; -@class BindingsMultiLookupCallback; -@protocol BindingsNetworkHealthCallback; -@class BindingsNetworkHealthCallback; -@protocol BindingsPreimageNotification; -@class BindingsPreimageNotification; -@protocol BindingsRestoreContactsUpdater; -@class BindingsRestoreContactsUpdater; -@protocol BindingsRoundCompletionCallback; -@class BindingsRoundCompletionCallback; -@protocol BindingsRoundEventCallback; -@class BindingsRoundEventCallback; -@protocol BindingsSearchCallback; -@class BindingsSearchCallback; -@protocol BindingsSingleSearchCallback; -@class BindingsSingleSearchCallback; -@protocol BindingsTimeSource; -@class BindingsTimeSource; -@protocol BindingsUpdateBackupFunc; -@class BindingsUpdateBackupFunc; - -@protocol BindingsAuthConfirmCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -@protocol BindingsAuthRequestCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsAuthResetNotificationCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsClientError <NSObject> -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -@protocol BindingsEventCallbackFunctionObject <NSObject> -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -@protocol BindingsFileTransferReceiveFunc <NSObject> -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -@protocol BindingsFileTransferReceivedProgressFunc <NSObject> -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsFileTransferSentProgressFunc <NSObject> -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsGroupReceiveFunc <NSObject> -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -@protocol BindingsGroupRequestFunc <NSObject> -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -@protocol BindingsListener <NSObject> -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@protocol BindingsLogWriter <NSObject> -- (void)log:(NSString* _Nullable)p0; -@end - -@protocol BindingsLookupCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsMessageDeliveryCallback <NSObject> -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -@protocol BindingsMultiLookupCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -@protocol BindingsNetworkHealthCallback <NSObject> -- (void)callback:(BOOL)p0; -@end - -@protocol BindingsPreimageNotification <NSObject> -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -@protocol BindingsRestoreContactsUpdater <NSObject> -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -@protocol BindingsRoundCompletionCallback <NSObject> -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -@protocol BindingsRoundEventCallback <NSObject> -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -@protocol BindingsSearchCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -@protocol BindingsSingleSearchCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsTimeSource <NSObject> -- (int64_t)nowMs; -@end - -@protocol BindingsUpdateBackupFunc <NSObject> -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -@interface BindingsBackup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * AddJson stores a passed in json string in the backup structure - */ -- (void)addJson:(NSString* _Nullable)json; -/** - * IsBackupRunning returns true if the backup has been initialized and is -running. Returns false if it has been stopped. - */ -- (BOOL)isBackupRunning; -/** - * StopBackup stops the backup processes and deletes the user's password from -storage. To enable backups again, call InitializeBackup. - */ -- (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsBackupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID - -@property (nonatomic) NSString* _Nonnull params; -@end - -/** - * BindingsClient wraps the api.Client, implementing additional functions -to support the gomobile Client interface - */ -@interface BindingsClient : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)confirmAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteAllRequests clears all requests from Client's auth storage. - */ -- (BOOL)deleteAllRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteContact is a function which removes a contact from Client's storage - */ -- (BOOL)deleteContact:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteReceiveRequests clears receive requests from Client's auth storage. - */ -- (BOOL)deleteReceiveRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteRequest will delete a request, agnostic of request type -for the given partner ID. If no request exists for this -partner ID an error will be returned. - */ -- (BOOL)deleteRequest:(NSData* _Nullable)requesterUserId error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteSentRequests clears sent requests from Client's auth storage. - */ -- (BOOL)deleteSentRequests:(NSError* _Nullable* _Nullable)error; -// skipped method Client.GetInternalClient with unsupported parameter or return types - -/** - * GetNodeRegistrationStatus returns a struct with the number of nodes the -client is registered with and the number total. - */ -- (BindingsNodeRegistrationsStatus* _Nullable)getNodeRegistrationStatus:(NSError* _Nullable* _Nullable)error; -/** - * GetPartners returns a list of - */ -- (NSData* _Nullable)getPartners:(NSError* _Nullable* _Nullable)error; -/** - * GetPreferredBins returns the geographic bin or bins that the provided two -character country code is a part of. The bins are returned as CSV. - */ -- (NSString* _Nonnull)getPreferredBins:(NSString* _Nullable)countryCode error:(NSError* _Nullable* _Nullable)error; -- (NSString* _Nonnull)getPreimages:(NSData* _Nullable)identity; -// skipped method Client.GetRateLimitParams with unsupported parameter or return types - -- (NSString* _Nonnull)getRelationshipFingerprint:(NSData* _Nullable)partnerID error:(NSError* _Nullable* _Nullable)error; -/** - * Returns a user object from which all information about the current user -can be gleaned - */ -- (BindingsUser* _Nullable)getUser; -/** - * HasRunningProcessies checks if any background threads are running. -returns true if none are running. This is meant to be -used when NetworkFollowerStatus() returns Stopping. -Due to the handling of comms on iOS, where the OS can -block indefiently, it may not enter the stopped -state apropreatly. This can be used instead. - */ -- (BOOL)hasRunningProcessies; -/** - * returns true if the network is read to be in a healthy state where -messages can be sent - */ -- (BOOL)isNetworkHealthy; -- (BindingsContact* _Nullable)makePrecannedAuthenticatedChannel:(long)precannedID error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the state of the network follower. Returns: -Stopped - 0 -Starting - 1000 -Running - 2000 -Stopping - 3000 - */ -- (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; -/** - * RegisterClientErrorCallback registers the callback to handle errors from the -long running threads controlled by StartNetworkFollower and StopNetworkFollower - */ -- (void)registerClientErrorCallback:(id<BindingsClientError> _Nullable)clientError; -/** - * RegisterEventCallback records the given function to receive -ReportableEvent objects. It returns the internal index -of the callback so that it can be deleted later. - */ -- (BOOL)registerEventCallback:(NSString* _Nullable)name myObj:(id<BindingsEventCallbackFunctionObject> _Nullable)myObj error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterForNotifications accepts firebase messaging token - */ -- (BOOL)registerForNotifications:(NSString* _Nullable)token error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterListener records and installs a listener for messages -matching specific uid, msgType, and/or username -Returns a ListenerUnregister interface which can be - -to register for any userID, pass in an id with length 0 or an id with -all zeroes - -to register for any message type, pass in a message type of 0 - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsUnregister* _Nullable)registerListener:(NSData* _Nullable)uid msgType:(long)msgType listener:(id<BindingsListener> _Nullable)listener error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterNetworkHealthCB registers the network health callback to be called -any time the network health changes. Returns a unique ID that can be used to -unregister the network health callback. - */ -- (int64_t)registerNetworkHealthCB:(id<BindingsNetworkHealthCallback> _Nullable)nhc; -- (void)registerPreimageCallback:(NSData* _Nullable)identity pin:(id<BindingsPreimageNotification> _Nullable)pin; -/** - * RegisterRoundEventsHandler registers a callback interface for round -events. -The rid is the round the event attaches to -The timeoutMS is the number of milliseconds until the event fails, and the -validStates are a list of states (one per byte) on which the event gets -triggered -States: - 0x00 - PENDING (Never seen by client) - 0x01 - PRECOMPUTING - 0x02 - STANDBY - 0x03 - QUEUED - 0x04 - REALTIME - 0x05 - COMPLETED - 0x06 - FAILED -These states are defined in elixxir/primitives/states/state.go - */ -- (BindingsUnregister* _Nullable)registerRoundEventsHandler:(long)rid cb:(id<BindingsRoundEventCallback> _Nullable)cb timeoutMS:(long)timeoutMS il:(BindingsIntList* _Nullable)il; -- (void)replayRequests; -- (BOOL)requestAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (BOOL)resetSession:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * This will return the round the message was sent on if it is successfully sent -This can be used to register a round event to learn about message delivery. -on failure a round id of -1 is returned - */ -- (BOOL)sendCmix:(NSData* _Nullable)recipient contents:(NSData* _Nullable)contents parameters:(NSString* _Nullable)parameters ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * SendE2E sends an end-to-end payload to the provided recipient with -the provided msgType. Returns the list of rounds in which parts of -the message were sent or an error if it fails. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsSendReport* _Nullable)sendE2E:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SendUnsafe sends an unencrypted payload to the provided recipient -with the provided msgType. Returns the list of rounds in which parts -of the message were sent or an error if it fails. -NOTE: Do not use this function unless you know what you are doing. -This function always produces an error message in client logging. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types with custom types - */ -- (BindingsRoundList* _Nullable)sendUnsafe:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SetProxiedBins updates the host pool filter that filters out gateways that -are not in one of the specified bins. The provided bins should be CSV. - */ -- (BOOL)setProxiedBins:(NSString* _Nullable)binStringsCSV error:(NSError* _Nullable* _Nullable)error; -/** - * StartNetworkFollower kicks off the tracking of the network. It starts -long running network client threads and returns an object for checking -state and stopping those threads. -Call this when returning from sleep and close when going back to -sleep. -These threads may become a significant drain on battery when offline, ensure -they are stopped if there is no internet access -Threads Started: - - Network Follower (/network/follow.go) - tracks the network events and hands them off to workers for handling - - Historical Round Retrieval (/network/rounds/historical.go) - Retrieves data about rounds which are too old to be stored by the client - - Message Retrieval Worker Group (/network/rounds/retrieve.go) - Requests all messages in a given round from the gateway of the last node - - Message Handling Worker Group (/network/message/handle.go) - Decrypts and partitions messages when signals via the Switchboard - - Health Tracker (/network/health) - Via the network instance tracks the state of the network - - Garbled Messages (/network/message/garbled.go) - Can be signaled to check all recent messages which could be be decoded - Uses a message store on disk for persistence - - Critical Messages (/network/message/critical.go) - Ensures all protocol layer mandatory messages are sent - Uses a message store on disk for persistence - - KeyExchange Trigger (/keyExchange/trigger.go) - Responds to sent rekeys and executes them - - KeyExchange Confirm (/keyExchange/confirm.go) - Responds to confirmations of successful rekey operations - */ -- (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * StopNetworkFollower stops the network follower if it is running. -It returns errors if the Follower is in the wrong status to stop or if it -fails to stop it. -if the network follower is running and this fails, the client object will -most likely be in an unrecoverable state and need to be trashed. - */ -- (BOOL)stopNetworkFollower:(NSError* _Nullable* _Nullable)error; -/** - * UnregisterEventCallback deletes the callback identified by the -index. It returns an error if it fails. - */ -- (void)unregisterEventCallback:(NSString* _Nullable)name; -/** - * UnregisterForNotifications unregister user for notifications - */ -- (BOOL)unregisterForNotifications:(NSError* _Nullable* _Nullable)error; -- (void)unregisterNetworkHealthCB:(int64_t)funcID; -- (BOOL)verifyOwnership:(NSData* _Nullable)receivedMarshaled verifiedMarshaled:(NSData* _Nullable)verifiedMarshaled ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForMessageDelivery allows the caller to get notified if the rounds a -message was sent in successfully completed. Under the hood, this uses an API -which uses the internal round data, network historical round lookup, and -waiting on network events to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - -This function takes the marshaled send report to ensure a memory leak does -not occur as a result of both sides of the bindings holding a reference to -the same pointer. - */ -- (BOOL)waitForMessageDelivery:(NSData* _Nullable)marshaledSendReport mdc:(id<BindingsMessageDeliveryCallback> _Nullable)mdc timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForNewtwork will block until either the network is healthy or the -passed timeout. It will return true if the network is healthy - */ -- (BOOL)waitForNetwork:(long)timeoutMS; -/** - * WaitForRoundCompletion allows the caller to get notified if a round -has completed (or failed). Under the hood, this uses an API which uses the internal -round data, network historical round lookup, and waiting on network events -to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - */ -- (BOOL)waitForRoundCompletion:(long)roundID rec:(id<BindingsRoundCompletionCallback> _Nullable)rec timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * contact object - */ -@interface BindingsContact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped method Contact.GetAPIContact with unsupported parameter or return types - -/** - * GetDHPublicKey returns the public key associated with the Contact. - */ -- (NSData* _Nullable)getDHPublicKey; -/** - * Returns a fact list for adding and getting facts to and from the contact - */ -- (BindingsFactList* _Nullable)getFactList; -/** - * GetID returns the user ID for this user. - */ -- (NSData* _Nullable)getID; -/** - * GetDHPublicKey returns hash of a DH proof of key ownership. - */ -- (NSData* _Nullable)getOwnershipProof; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsContactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BindingsContact* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * DummyTraffic contains the file dummy traffic manager. The manager can be used -to set and get the status of the send thread. - */ -@interface BindingsDummyTraffic : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client maxNumMessages:(long)maxNumMessages avgSendDeltaMS:(long)avgSendDeltaMS randomRangeMS:(long)randomRangeMS; -/** - * GetStatus returns the current state of the dummy traffic send thread. It has -the following return values: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Note that this function does not return the status set by SetStatus directly; -it returns the current status of the send thread, which means any call to -SetStatus will have a small delay before it is returned by GetStatus. - */ -- (BOOL)getStatus; -/** - * SetStatus sets the state of the dummy traffic send thread, which determines -if the thread is running or paused. The possible statuses are: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Returns an error if the channel is full. -Note that this function cannot change the status of the send thread if it has -yet to be started or stopped. - */ -- (BOOL)setStatus:(BOOL)status error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsFact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -- (nullable instancetype)init:(long)factType factStr:(NSString* _Nullable)factStr; -- (NSString* _Nonnull)get; -- (NSString* _Nonnull)stringify; -- (long)type; -@end - -@interface BindingsFactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * FactList - */ -- (nullable instancetype)init; -- (BOOL)add:(NSString* _Nullable)factData factType:(long)factType error:(NSError* _Nullable* _Nullable)error; -- (BindingsFact* _Nullable)get:(long)i; -- (long)num; -- (NSString* _Nonnull)stringify:(NSError* _Nullable* _Nullable)error; -@end - -/** - * FilePartTracker contains the interfaces.FilePartTracker. - */ -@interface BindingsFilePartTracker : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetNumParts returns the total number of file parts in the transfer. - */ -- (long)getNumParts; -/** - * GetPartStatus returns the status of the file part with the given part number. -The possible values for the status are: -0 = unsent -1 = sent (sender has sent a part, but it has not arrived) -2 = arrived (sender has sent a part, and it has arrived) -3 = received (receiver has received a part) - */ -- (long)getPartStatus:(long)partNum; -@end - -/** - * FileTransfer contains the file transfer manager. - */ -@interface BindingsFileTransfer : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client receiveFunc:(id<BindingsFileTransferReceiveFunc> _Nullable)receiveFunc parameters:(NSString* _Nullable)parameters; -/** - * CloseSend deletes a sent file transfer from the sent transfer map and from -storage once a transfer has completed or reached the retry limit. Returns an -error if the transfer has not run out of retries. - */ -- (BOOL)closeSend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * GetMaxFileNameByteLength returns the maximum length, in bytes, allowed for a -file name. - */ -- (long)getMaxFileNameByteLength; -/** - * GetMaxFilePreviewSize returns the maximum file preview size, in bytes. - */ -- (long)getMaxFilePreviewSize; -/** - * GetMaxFileSize returns the maximum file size, in bytes, allowed to be -transferred. - */ -- (long)getMaxFileSize; -/** - * GetMaxFileTypeByteLength returns the maximum length, in bytes, allowed for a -file type. - */ -- (long)getMaxFileTypeByteLength; -/** - * Receive returns the fully assembled file on the completion of the transfer. -It deletes the transfer from the received transfer map and from storage. -Returns an error if the transfer is not complete, the full file cannot be -verified, or if the transfer cannot be found. - */ -- (NSData* _Nullable)receive:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterReceiveProgressCallback allows for the registration of a callback to -track the progress of an individual received file transfer. The callback will -be called immediately when added to report the current status of the -transfer. It will then call every time a file part is received, the transfer -completes, or an error occurs. It is called at most once ever period, which -means if events occur faster than the period, then they will not be reported -and instead, the progress will be reported once at the end of the period. -Once the callback reports that the transfer has completed, the recipient -can get the full file by calling Receive. -The period is specified in milliseconds. - */ -- (BOOL)registerReceiveProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferReceivedProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterSendProgressCallback allows for the registration of a callback to -track the progress of an individual sent file transfer. The callback will be -called immediately when added to report the current status of the transfer. -It will then call every time a file part is sent, a file part arrives, the -transfer completes, or an error occurs. It is called at most once every -period, which means if events occur faster than the period, then they will -not be reported and instead, the progress will be reported once at the end of -the period. -The period is specified in milliseconds. - */ -- (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends a file to the recipient. The sender must have an E2E relationship -with the recipient. -The file name is the name of the file to show a user. It has a max length of -48 bytes. -The file type identifies what type of file is being sent. It has a max length -of 8 bytes. -The file data cannot be larger than 256 kB -The retry float is the total amount of data to send relative to the data -size. Data will be resent on error and will resend up to [(1 + retry) * -fileSize]. -The preview stores a preview of the data (such as a thumbnail) and is -capped at 4 kB in size. -Returns a unique transfer ID used to identify the transfer. -PeriodMS is the duration, in milliseconds, to wait between progress callback -calls. Set this large enough to prevent spamming. - */ -- (NSData* _Nullable)send:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType fileData:(NSData* _Nullable)fileData recipientID:(NSData* _Nullable)recipientID retry:(float)retry preview:(NSData* _Nullable)preview progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Group structure contains the identifying and membership information of a -group chat. - */ -@interface BindingsGroup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetCreatedMS returns the time the group was created in milliseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedMS; -/** - * GetCreatedNano returns the time the group was created in nanoseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedNano; -/** - * GetID return the 33-byte unique group ID. - */ -- (NSData* _Nullable)getID; -/** - * GetInitMessage returns initial message sent with the group request. - */ -- (NSData* _Nullable)getInitMessage; -/** - * GetMembership returns a list of contacts, one for each member in the group. -The list is in order; the first contact is the leader/creator of the group. -All subsequent members are ordered by their ID. - */ -- (BindingsGroupMembership* _Nullable)getMembership; -/** - * GetName returns the name set by the user for the group. - */ -- (NSData* _Nullable)getName; -/** - * Serialize serializes the Group. - */ -- (NSData* _Nullable)serialize; -@end - -/** - * GroupChat object contains the group chat manager. - */ -@interface BindingsGroupChat : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetGroup returns the group with the group ID. If no group exists, then the -error "failed to find group" is returned. - */ -- (BindingsGroup* _Nullable)getGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * GetGroups returns an IdList containing a list of group IDs that the user is a -part of. - */ -- (BindingsIdList* _Nullable)getGroups; -/** - * JoinGroup allows a user to join a group when they receive a request. The -caller must pass in the serialized bytes of a Group. - */ -- (BOOL)joinGroup:(NSData* _Nullable)serializedGroupData error:(NSError* _Nullable* _Nullable)error; -/** - * LeaveGroup deletes a group so a user no longer has access. - */ -- (BOOL)leaveGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * MakeGroup creates a new group and sends a group request to all members in the -group. The ID of the new group, the rounds the requests were sent on, and the -status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)makeGroup:(BindingsIdList* _Nullable)membership name:(NSData* _Nullable)name message:(NSData* _Nullable)message; -/** - * NumGroups returns the number of groups the user is a part of. - */ -- (long)numGroups; -/** - * ResendRequest resends a group request to all members in the group. The rounds -they were sent on and the status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)resendRequest:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends the message to the specified group. Returns the round the messages -were sent on. - */ -- (BindingsGroupSendReport* _Nullable)send:(NSData* _Nullable)groupIdBytes message:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * // -Member Structure -// -GroupMember represents a member in the group membership list. - */ -@interface BindingsGroupMember : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMember.Member with unsupported type: gitlab.com/elixxir/crypto/group.Member - -// skipped method GroupMember.DeepCopy with unsupported parameter or return types - -// skipped method GroupMember.Equal with unsupported parameter or return types - -/** - * GetDhKey returns the byte representation of the public Diffie–Hellman key of -the member. - */ -- (NSData* _Nullable)getDhKey; -/** - * GetID returns the 33-byte user ID of the member. - */ -- (NSData* _Nullable)getID; -- (NSString* _Nonnull)goString; -- (NSData* _Nullable)serialize; -- (NSString* _Nonnull)string; -@end - -/** - * GroupMembership structure contains a list of members that are part of a -group. The first member is the group leader. - */ -@interface BindingsGroupMembership : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Get returns the member at the index. The member at index 0 is always the -group leader. An error is returned if the index is out of range. - */ -- (BindingsGroupMember* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of members in the group membership. - */ -- (long)len; -@end - -/** - * GroupMessageReceive contains a group message, its ID, and its data that a -user receives. - */ -@interface BindingsGroupMessageReceive : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMessageReceive.MessageReceive with unsupported type: gitlab.com/elixxir/client/groupChat.MessageReceive - -/** - * GetEphemeralID returns the ephemeral ID of the recipient. - */ -- (int64_t)getEphemeralID; -/** - * GetGroupID returns the 33-byte group ID. - */ -- (NSData* _Nullable)getGroupID; -/** - * GetMessageID returns the message ID. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetPayload returns the message payload. - */ -- (NSData* _Nullable)getPayload; -/** - * GetRecipientID returns the 33-byte user ID of the recipient. - */ -- (NSData* _Nullable)getRecipientID; -/** - * GetRoundID returns the ID of the round the message was sent on. - */ -- (int64_t)getRoundID; -/** - * GetRoundTimestampMS returns the timestamp, in milliseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the timestamp, in nanoseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the ID of the round the message was sent on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSenderID returns the 33-byte user ID of the sender. - */ -- (NSData* _Nullable)getSenderID; -/** - * GetTimestampMS returns the message timestamp in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message timestamp in nanoseconds. - */ -- (int64_t)getTimestampNano; -- (NSString* _Nonnull)string; -@end - -@interface BindingsGroupReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable grpId; -@property (nonatomic) long status; -@end - -/** - * GroupSendReport is returned when sending a group message. It contains the -round ID sent on and the timestamp of the send. - */ -@interface BindingsGroupSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetMessageID returns the ID of the round that the send occurred on. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetRoundID returns the ID of the round that the send occurred on. - */ -- (int64_t)getRoundID; -/** - * GetRoundURL returns the URL of the round that the send occurred on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the timestamp of the send in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the timestamp of the send in nanoseconds. - */ -- (int64_t)getTimestampNano; -@end - -/** - * ID list -IdList contains a list of IDs. - */ -@interface BindingsIdList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Add appends the ID bytes to the end of the list. - */ -- (BOOL)add:(NSData* _Nullable)idBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Get returns the ID at the index. An error is returned if the index is out of -range. - */ -- (NSData* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of IDs in the list. - */ -- (long)len; -@end - -@interface BindingsIntList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (void)add:(long)i; -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -@interface BindingsManyNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsNotificationForMeReport* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -/** - * Message is a message received from the cMix network in the clear -or that has been decrypted using established E2E keys. - */ -@interface BindingsMessage : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetID returns the id of the message - */ -- (NSData* _Nullable)getID; -/** - * GetMessageType returns the message's type - */ -- (long)getMessageType; -/** - * GetPayload returns the message's payload/contents - */ -- (NSData* _Nullable)getPayload; -/** - * GetRoundId returns the message's round ID - */ -- (int64_t)getRoundId; -/** - * GetRoundTimestampMS returns the message's round timestamp in milliseconds - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the message's round timestamp in nanoseconds - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the message's round URL - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSender returns the message's sender ID, if available - */ -- (NSData* _Nullable)getSender; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -@end - -/** - * NewGroupReport is returned when creating a new group and contains the ID of -the group, a list of rounds that the group requests were sent on, and the -status of the send. - */ -@interface BindingsNewGroupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetError returns the string of an error. -Will be an empty string if no error occured - */ -- (NSString* _Nonnull)getError; -/** - * GetGroup returns the Group. - */ -- (BindingsGroup* _Nullable)getGroup; -/** - * GetRoundList returns the RoundList containing a list of rounds requests were -sent on. - */ -- (BindingsRoundList* _Nullable)getRoundList; -/** - * GetStatus returns the status of the requests sent when creating a new group. -status = 0 an error occurred before any requests could be sent - 1 all requests failed to send (call Resend Group) - 2 some request failed and some succeeded (call Resend Group) - 3, all requests sent successfully (call Resend Group) - */ -- (long)getStatus; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * NodeRegistrationsStatus structure for returning node registration statuses -for bindings. - */ -@interface BindingsNodeRegistrationsStatus : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetRegistered returns the number of nodes registered with the client. - */ -- (long)getRegistered; -/** - * GetTotal return the total of nodes currently in the network. - */ -- (long)getTotal; -@end - -@interface BindingsNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)forMe; -- (NSData* _Nullable)source; -- (NSString* _Nonnull)type; -@end - -/** - * RestoreContactsReport is a gomobile friendly report structure -for determining which IDs restored, which failed, and why. - */ -@interface BindingsRestoreContactsReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetErrorAt returns the error string at index - */ -- (NSString* _Nonnull)getErrorAt:(long)index; -/** - * GetFailedAt returns the failed ID at index - */ -- (NSData* _Nullable)getFailedAt:(long)index; -/** - * GetRestoreContactsError returns an error string. Empty if no error. - */ -- (NSString* _Nonnull)getRestoreContactsError; -/** - * GetRestoredAt returns the restored ID at index - */ -- (NSData* _Nullable)getRestoredAt:(long)index; -/** - * LenFailed returns the length of the ID's failed. - */ -- (long)lenFailed; -/** - * LenRestored returns the length of ID's restored. - */ -- (long)lenRestored; -@end - -@interface BindingsRoundList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * the send report is the mechanisim by which sendE2E returns a single - */ -@interface BindingsSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (NSData* _Nullable)getMessageID; -- (BindingsRoundList* _Nullable)getRoundList; -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsSendReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field SendReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable mid; -@property (nonatomic) int64_t ts; -@end - -/** - * Generic Unregister - a generic return used for all callbacks which can be -unregistered -Interface which allows the un-registration of a listener - */ -@interface BindingsUnregister : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Call unregisters a callback - */ -- (void)unregister; -@end - -@interface BindingsUser : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsContact* _Nullable)getContact; -- (NSData* _Nullable)getE2EDhPrivateKey; -- (NSData* _Nullable)getE2EDhPublicKey; -- (NSData* _Nullable)getReceptionID; -- (NSData* _Nullable)getReceptionRSAPrivateKeyPem; -- (NSData* _Nullable)getReceptionRSAPublicKeyPem; -- (NSData* _Nullable)getReceptionSalt; -- (NSData* _Nullable)getTransmissionID; -- (NSData* _Nullable)getTransmissionRSAPrivateKeyPem; -- (NSData* _Nullable)getTransmissionRSAPublicKeyPem; -- (NSData* _Nullable)getTransmissionSalt; -- (BOOL)isPrecanned; -@end - -@interface BindingsUserDiscovery : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)init:(BindingsClient* _Nullable)client; -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; -/** - * AddFact adds a fact for the user to user discovery. Will only succeed if the -user is already registered and the system does not have the fact currently -registered for any user. -Will fail if the fact string is not well formed. -This does not complete the fact registration process, it returns a -confirmation id instead. Over the communications system the fact is -associated with, a code will be sent. This confirmation ID needs to be -called along with the code to finalize the fact. - */ -- (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from -AddFact while the code will come over the associated communications system - */ -- (BOOL)confirmFact:(NSString* _Nullable)confirmationID code:(NSString* _Nullable)code error:(NSError* _Nullable* _Nullable)error; -/** - * Lookup the contact object associated with the given userID. The -id is the byte representation of an id. -This will reject if that id is malformed. The LookupCallback will return -the associated contact if it exists. - */ -- (BOOL)lookup:(NSData* _Nullable)idBytes callback:(id<BindingsLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * MultiLookup Looks for the contact object associated with all given userIDs. -The ids are the byte representation of an id stored in an IDList object. -This will reject if that id is malformed or if the indexing on the IDList -object is wrong. The MultiLookupCallback will return with all contacts -returned within the timeout. - */ -- (BOOL)multiLookup:(BindingsIdList* _Nullable)ids callback:(id<BindingsMultiLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * Register registers a user with user discovery. Will return an error if the -network signatures are malformed or if the username is taken. Usernames -cannot be changed after registration at this time. Will fail if the user is -already registered. -Identity does not go over cmix, it occurs over normal communications - */ -- (BOOL)register:(NSString* _Nullable)username error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveFact removes a previously confirmed fact. Will fail if the passed fact string is -not well-formed or if the fact is not associated with this client. -Users cannot remove username facts and must instead remove the user. - */ -- (BOOL)removeFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveUser deletes a user. The fact sent must be the username. -This function preserves the username forever and makes it -unusable. - */ -- (BOOL)removeUser:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * Search for the passed Facts. The factList is the stringification of a -fact list object, look at /bindings/list.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This is NOT intended to be used to search for multiple users at once, that -can have a privacy reduction. Instead, it is intended to be used to search -for a user where multiple pieces of information is known. - */ -- (BOOL)search:(NSString* _Nullable)fl callback:(id<BindingsSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SearchSingle searches for the passed Facts. The fact is the stringification of a -fact object, look at /bindings/contact.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This only searches for a single fact at a time. It is intended to make some -simple use cases of the API easier. - */ -- (BOOL)searchSingle:(NSString* _Nullable)f callback:(id<BindingsSingleSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SetAlternativeUserDiscovery sets the alternativeUd object within manager. -Once set, any user discovery operation will go through the alternative -user discovery service. -To undo this operation, use UnsetAlternativeUserDiscovery. -The contact file is the already read in bytes, not the file path for the contact file. - */ -- (BOOL)setAlternativeUserDiscovery:(NSData* _Nullable)address cert:(NSData* _Nullable)cert contactFile:(NSData* _Nullable)contactFile error:(NSError* _Nullable* _Nullable)error; -/** - * UnsetAlternativeUserDiscovery clears out the information from -the Manager object. - */ -- (BOOL)unsetAlternativeUserDiscovery:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Error codes - */ -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedCode; -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedMessage; - -/** - * CompressJpeg takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpeg(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * CompressJpegForPreview takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpegForPreview(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * DownloadAndVerifySignedNdfWithUrl retrieves the NDF from a specified URL. -The NDF is processed into a protobuf containing a signature which -is verified using the cert string passed in. The NDF is returned as marshaled -byte data which may be used to start a client. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadAndVerifySignedNdfWithUrl(NSString* _Nullable url, NSString* _Nullable cert, NSError* _Nullable* _Nullable error); - -/** - * DownloadDAppRegistrationDB returns a []byte containing -the JSON data describing registered dApps. -See https://git.xx.network/elixxir/registered-dapps - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadDAppRegistrationDB(NSError* _Nullable* _Nullable error); - -/** - * DownloadErrorDB returns a []byte containing the JSON data -describing client errors. -See https://git.xx.network/elixxir/client-error-database/ - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadErrorDB(NSError* _Nullable* _Nullable error); - -/** - * DumpStack returns a string with the stack trace of every running thread. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsDumpStack(NSError* _Nullable* _Nullable error); - -/** - * EnableGrpcLogs sets GRPC trace logging - */ -FOUNDATION_EXPORT void BindingsEnableGrpcLogs(id<BindingsLogWriter> _Nullable writer); - -/** - * ErrorStringToUserFriendlyMessage takes a passed in errStr which will be -a backend generated error. These may be error specifically written by -the backend team or lower level errors gotten from low level dependencies. -This function will parse the error string for common errors provided from -errToUserErr to provide a more user-friendly error message for the front end. -If the error is not common, some simple parsing is done on the error message -to make it more user-accessible, removing backend specific jargon. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsErrorStringToUserFriendlyMessage(NSString* _Nullable errStr); - -/** - * GenerateSecret creates a secret password using a system-based -pseudorandom number generator. It takes 1 parameter, `numBytes`, -which should be set to 32, but can be set higher in certain cases. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateSecret(long numBytes); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetCMIXParams(NSError* _Nullable* _Nullable error); - -/** - * returns a previously created client. IF be used if the garbage collector -removes the client instance on the app side. Is NOT thread safe relative to -login, newClient, or newPrecannedClient - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsGetClientSingleton(void); - -/** - * GetDependencies returns the api DEPENDENCIES - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetDependencies(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetE2EParams(NSError* _Nullable* _Nullable error); - -/** - * GetGitVersion rturns the api GITVERSION - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetGitVersion(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetNetworkParams(NSError* _Nullable* _Nullable error); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetUnsafeParams(NSError* _Nullable* _Nullable error); - -/** - * GetVersion returns the api SEMVER - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void); - -/** - * InitializeBackup starts the backup processes that returns backup updates when -they occur. Any time an event occurs that changes the contents of the backup, -such as adding or deleting a contact, the backup is triggered and an -encrypted backup is generated and returned on the updateBackupCb callback. -Call this function only when enabling backup if it has not already been -initialized or when the user wants to change their password. -To resume backup process on app recovery, use ResumeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsInitializeBackup(NSString* _Nullable password, id<BindingsUpdateBackupFunc> _Nullable updateBackupCb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * LoadSecretWithMnemonic loads the secret stored from the call to -StoreSecretWithMnemonic. The path given should be the same filepath -as the path given in StoreSecretWithMnemonic. There should be a file -in this path called ".recovery". This operation is not tied -to client operations, as the user will not have a client when trying to -recover their account. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsLoadSecretWithMnemonic(NSString* _Nullable mnemonic, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * sets level of logging. All logs the set level and above will be displayed -options are: - TRACE - 0 - DEBUG - 1 - INFO - 2 - WARN - 3 - ERROR - 4 - CRITICAL - 5 - FATAL - 6 -The default state without updates is: INFO - */ -FOUNDATION_EXPORT BOOL BindingsLogLevel(long level, NSError* _Nullable* _Nullable error); - -/** - * Login will load an existing client from the storageDir -using the password. This will fail if the client doesn't exist or -the password is incorrect. -The password is passed as a byte array so that it can be cleared from -memory and stored as securely as possible using the memguard library. -Login does not block on network connection, and instead loads and -starts subprocesses to perform network operations. - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsLogin(NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * MakeIdList creates a new empty IdList. - */ -FOUNDATION_EXPORT BindingsIdList* _Nullable BindingsMakeIdList(void); - -FOUNDATION_EXPORT BindingsIntList* _Nullable BindingsMakeIntList(void); - -/** - * NewClient creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable regCode, NSError* _Nullable* _Nullable error); - -/** - * NewClientFromBackup constructs a new Client from an encrypted backup. The backup -is decrypted using the backupPassphrase. On success a successful client creation, -the function will return a JSON encoded list of the E2E partners -contained in the backup and a json-encoded string of the parameters stored in the backup - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); - -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -FOUNDATION_EXPORT BindingsDummyTraffic* _Nullable BindingsNewDummyTrafficManager(BindingsClient* _Nullable client, long maxNumMessages, long avgSendDeltaMS, long randomRangeMS, NSError* _Nullable* _Nullable error); - -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -FOUNDATION_EXPORT BindingsFact* _Nullable BindingsNewFact(long factType, NSString* _Nullable factStr, NSError* _Nullable* _Nullable error); - -/** - * FactList - */ -FOUNDATION_EXPORT BindingsFactList* _Nullable BindingsNewFactList(void); - -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -FOUNDATION_EXPORT BindingsFileTransfer* _Nullable BindingsNewFileTransferManager(BindingsClient* _Nullable client, id<BindingsFileTransferReceiveFunc> _Nullable receiveFunc, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * NewGroupManager creates a new group chat manager. - */ -FOUNDATION_EXPORT BindingsGroupChat* _Nullable BindingsNewGroupManager(BindingsClient* _Nullable client, id<BindingsGroupRequestFunc> _Nullable requestFunc, id<BindingsGroupReceiveFunc> _Nullable receiveFunc, NSError* _Nullable* _Nullable error); - -/** - * NewPrecannedClient creates an insecure user with predetermined keys with nodes -It creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewPrecannedClient(long precannedID, NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); - -/** - * NotificationsForMe Check if a notification received is for me -It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, -a Type, and a source. These are as follows: - TYPE SOURCE DESCRIPTION - "default" recipient user ID A message with no association - "request" sender user ID A channel request has been received - "reset" sender user ID A channel reset has been received - "confirm" sender user ID A channel request has been accepted - "silent" sender user ID A message which should not be notified on - "e2e" sender user ID reception of an E2E message - "group" group ID reception of a group chat message - "endFT" sender user ID Last message sent confirming end of file transfer - "groupRQ" sender user ID Request from sender to join a group chat - */ -FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotificationsForMe(NSString* _Nullable notifCSV, NSString* _Nullable preimages, NSError* _Nullable* _Nullable error); - -/** - * RegisterLogWriter registers a callback on which logs are written. - */ -FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); - -/** - * RestoreContactsFromBackup takes as input the jason output of the -`NewClientFromBackup` function, unmarshals it into IDs, looks up -each ID in user discovery, and initiates a session reset request. -This function will not return until every id in the list has been sent a -request. It should be called again and again until it completes. -xxDK users should not use this function. This function is used by -the mobile phone apps and are not intended to be part of the xxDK. It -should be treated as internal functions specific to the phone apps. - */ -FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); - -/** - * ResumeBackup starts the backup processes back up with a new callback after it -has been initialized. -Call this function only when resuming a backup that has already been -initialized or to replace the callback. -To start the backup for the first time or to use a new password, use -InitializeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsResumeBackup(id<BindingsUpdateBackupFunc> _Nullable cb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * SetTimeSource sets the network time to a custom source. - */ -FOUNDATION_EXPORT void BindingsSetTimeSource(id<BindingsTimeSource> _Nullable timeNow); - -/** - * StoreSecretWithMnemonic stores the secret tied with the mnemonic to storage. -Unlike other storage operations, this does not use EKV, as that is -intrinsically tied to client operations, which the user will not have while -trying to recover their account. As such, we store the encrypted data -directly, with a specified path. Path will be a valid filepath in which the -recover file will be stored as ".recovery". - -As an example, given "home/user/xxmessenger/storagePath", -the recovery file will be stored at -"home/user/xxmessenger/storagePath/.recovery" - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsStoreSecretWithMnemonic(NSData* _Nullable secret, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled contact object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsContact* _Nullable BindingsUnmarshalContact(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled send report object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsSendReport* _Nullable BindingsUnmarshalSendReport(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * UpdateCommonErrors takes the passed in contents of a JSON file and updates the -errToUserErr map with the contents of the json file. The JSON's expected format -conform with the commented examples provides in errToUserErr above. -NOTE that you should not pass in a file path, but a preloaded JSON file - */ -FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, NSError* _Nullable* _Nullable error); - -// skipped function WrapAPIClient with unsupported parameter or return types - - -// skipped function WrapUserDiscovery with unsupported parameter or return types - - -@class BindingsAuthConfirmCallback; - -@class BindingsAuthRequestCallback; - -@class BindingsAuthResetNotificationCallback; - -@class BindingsClientError; - -@class BindingsEventCallbackFunctionObject; - -@class BindingsFileTransferReceiveFunc; - -@class BindingsFileTransferReceivedProgressFunc; - -@class BindingsFileTransferSentProgressFunc; - -@class BindingsGroupReceiveFunc; - -@class BindingsGroupRequestFunc; - -@class BindingsListener; - -@class BindingsLogWriter; - -@class BindingsLookupCallback; - -@class BindingsMessageDeliveryCallback; - -@class BindingsMultiLookupCallback; - -@class BindingsNetworkHealthCallback; - -@class BindingsPreimageNotification; - -@class BindingsRestoreContactsUpdater; - -@class BindingsRoundCompletionCallback; - -@class BindingsRoundEventCallback; - -@class BindingsSearchCallback; - -@class BindingsSingleSearchCallback; - -@class BindingsTimeSource; - -@class BindingsUpdateBackupFunc; - -/** - * AuthConfirmCallback notifies the register whenever they receive an auth -request confirmation - */ -@interface BindingsAuthConfirmCallback : NSObject <goSeqRefInterface, BindingsAuthConfirmCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthRequestCallback : NSObject <goSeqRefInterface, BindingsAuthRequestCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@interface BindingsClientError : NSObject <goSeqRefInterface, BindingsClientError> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -/** - * EventCallbackFunctionObject bindings interface which contains function -that implements the EventCallbackFunction - */ -@interface BindingsEventCallbackFunctionObject : NSObject <goSeqRefInterface, BindingsEventCallbackFunctionObject> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -/** - * FileTransferReceiveFunc contains a function callback that notifies the -receiver of an incoming file transfer. It is called on the reception of the -initial file transfer message. - */ -@interface BindingsFileTransferReceiveFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -/** - * FileTransferReceivedProgressFunc contains a function callback that tracks the -progress of receiving a file. It is called when a file part is received, the -transfer completes, or on error. - */ -@interface BindingsFileTransferReceivedProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceivedProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * FileTransferSentProgressFunc contains a function callback that tracks the -progress of sending a file. It is called when a file part is sent, a file -part arrives, the transfer completes, or on error. - */ -@interface BindingsFileTransferSentProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferSentProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * GroupReceiveFunc contains a function callback that is called when a group -message is received. - */ -@interface BindingsGroupReceiveFunc : NSObject <goSeqRefInterface, BindingsGroupReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -/** - * GroupRequestFunc contains a function callback that is called when a group -request is received. - */ -@interface BindingsGroupRequestFunc : NSObject <goSeqRefInterface, BindingsGroupRequestFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -/** - * Listener provides a callback to hear a message -An object implementing this interface can be called back when the client -gets a message of the type that the registerer specified at registration -time. - */ -@interface BindingsListener : NSObject <goSeqRefInterface, BindingsListener> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@interface BindingsLogWriter : NSObject <goSeqRefInterface, BindingsLogWriter> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)log:(NSString* _Nullable)p0; -@end - -/** - * LookupCallback returns the result of a single lookup - */ -@interface BindingsLookupCallback : NSObject <goSeqRefInterface, BindingsLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -/** - * MessageDeliveryCallback gets called on the determination if all events -related to a message send were successful. - */ -@interface BindingsMessageDeliveryCallback : NSObject <goSeqRefInterface, BindingsMessageDeliveryCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -/** - * MultiLookupCallback returns the result of many parallel lookups - */ -@interface BindingsMultiLookupCallback : NSObject <goSeqRefInterface, BindingsMultiLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -/** - * A callback when which is used to receive notification if network health -changes - */ -@interface BindingsNetworkHealthCallback : NSObject <goSeqRefInterface, BindingsNetworkHealthCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BOOL)p0; -@end - -@interface BindingsPreimageNotification : NSObject <goSeqRefInterface, BindingsPreimageNotification> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -/** - * RestoreContactsUpdater interface provides a callback function -for receiving update information from RestoreContactsFromBackup. - */ -@interface BindingsRestoreContactsUpdater : NSObject <goSeqRefInterface, BindingsRestoreContactsUpdater> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -/** - * RoundCompletionCallback is returned when the completion of a round is known. - */ -@interface BindingsRoundCompletionCallback : NSObject <goSeqRefInterface, BindingsRoundCompletionCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -/** - * RoundEventCallback handles waiting on the exact state of a round on -the cMix network. - */ -@interface BindingsRoundEventCallback : NSObject <goSeqRefInterface, BindingsRoundEventCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -/** - * SearchCallback returns the result of a search - */ -@interface BindingsSearchCallback : NSObject <goSeqRefInterface, BindingsSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -/** - * SingleSearchCallback returns the result of a single search - */ -@interface BindingsSingleSearchCallback : NSObject <goSeqRefInterface, BindingsSingleSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@interface BindingsTimeSource : NSObject <goSeqRefInterface, BindingsTimeSource> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (int64_t)nowMs; -@end - -/** - * UpdateBackupFunc contains a function callback that returns new backups. - */ -@interface BindingsUpdateBackupFunc : NSObject <goSeqRefInterface, BindingsUpdateBackupFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Universe.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Universe.objc.h deleted file mode 100644 index 019e7502d581983722a15bf30799e85cbc5dd766..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/Universe.objc.h +++ /dev/null @@ -1,29 +0,0 @@ -// Objective-C API for talking to Go package. -// gobind -lang=objc -// -// File is generated by gobind. Do not edit. - -#ifndef __Universe_H__ -#define __Universe_H__ - -@import Foundation; -#include "ref.h" - -@protocol Universeerror; -@class Universeerror; - -@protocol Universeerror <NSObject> -- (NSString* _Nonnull)error; -@end - -@class Universeerror; - -@interface Universeerror : NSError <goSeqRefInterface, Universeerror> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (NSString* _Nonnull)error; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/ref.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/ref.h deleted file mode 100644 index b8036a4d85c7387f3def61473a071b5d8c4c8208..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Headers/ref.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef __GO_REF_HDR__ -#define __GO_REF_HDR__ - -#include <Foundation/Foundation.h> - -// GoSeqRef is an object tagged with an integer for passing back and -// forth across the language boundary. A GoSeqRef may represent either -// an instance of a Go object, or an Objective-C object passed to Go. -// The explicit allocation of a GoSeqRef is used to pin a Go object -// when it is passed to Objective-C. The Go seq package maintains a -// reference to the Go object in a map keyed by the refnum along with -// a reference count. When the reference count reaches zero, the Go -// seq package will clear the corresponding entry in the map. -@interface GoSeqRef : NSObject { -} -@property(readonly) int32_t refnum; -@property(strong) id obj; // NULL when representing a Go object. - -// new GoSeqRef object to proxy a Go object. The refnum must be -// provided from Go side. -- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; - -- (int32_t)incNum; - -@end - -@protocol goSeqRefInterface --(GoSeqRef*) _ref; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Modules/module.modulemap b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Modules/module.modulemap deleted file mode 100644 index 4316a5b24058edfc18ffb2dc7f7a982e8353441a..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Modules/module.modulemap +++ /dev/null @@ -1,8 +0,0 @@ -framework module "Bindings" { - header "ref.h" - header "Bindings.objc.h" - header "Universe.objc.h" - header "Bindings.h" - - export * -} \ No newline at end of file diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Resources/Info.plist b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Resources/Info.plist deleted file mode 100644 index 0d1a4b8ab9b1fc8e9357197398f73353470cb636..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Resources/Info.plist +++ /dev/null @@ -1,6 +0,0 @@ -<?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> - </dict> - </plist> diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings deleted file mode 100644 index 8d78f84d50a0b0719b795f7b342f6109c2b848ec..0000000000000000000000000000000000000000 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings and /dev/null differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.h deleted file mode 100644 index 8906a7da239705b790cb2bb64de92f806640cb38..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.h +++ /dev/null @@ -1,13 +0,0 @@ - -// Objective-C API for talking to the following Go packages -// -// gitlab.com/elixxir/client/bindings -// -// File is generated by gomobile bind. Do not edit. -#ifndef __Bindings_FRAMEWORK_H__ -#define __Bindings_FRAMEWORK_H__ - -#include "Bindings.objc.h" -#include "Universe.objc.h" - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h deleted file mode 100644 index 32bf6d116888f787ced27b01b95cb4e1b2c1138b..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h +++ /dev/null @@ -1,2083 +0,0 @@ -// Objective-C API for talking to gitlab.com/elixxir/client/bindings Go package. -// gobind -lang=objc gitlab.com/elixxir/client/bindings -// -// File is generated by gobind. Do not edit. - -#ifndef __Bindings_H__ -#define __Bindings_H__ - -@import Foundation; -#include "ref.h" -#include "Universe.objc.h" - - -@class BindingsBackup; -@class BindingsBackupReport; -@class BindingsClient; -@class BindingsContact; -@class BindingsContactList; -@class BindingsDummyTraffic; -@class BindingsFact; -@class BindingsFactList; -@class BindingsFilePartTracker; -@class BindingsFileTransfer; -@class BindingsGroup; -@class BindingsGroupChat; -@class BindingsGroupMember; -@class BindingsGroupMembership; -@class BindingsGroupMessageReceive; -@class BindingsGroupReportDisk; -@class BindingsGroupSendReport; -@class BindingsIdList; -@class BindingsIntList; -@class BindingsManyNotificationForMeReport; -@class BindingsMessage; -@class BindingsNewGroupReport; -@class BindingsNodeRegistrationsStatus; -@class BindingsNotificationForMeReport; -@class BindingsRestoreContactsReport; -@class BindingsRoundList; -@class BindingsSendReport; -@class BindingsSendReportDisk; -@class BindingsUnregister; -@class BindingsUser; -@class BindingsUserDiscovery; -@protocol BindingsAuthConfirmCallback; -@class BindingsAuthConfirmCallback; -@protocol BindingsAuthRequestCallback; -@class BindingsAuthRequestCallback; -@protocol BindingsAuthResetNotificationCallback; -@class BindingsAuthResetNotificationCallback; -@protocol BindingsClientError; -@class BindingsClientError; -@protocol BindingsEventCallbackFunctionObject; -@class BindingsEventCallbackFunctionObject; -@protocol BindingsFileTransferReceiveFunc; -@class BindingsFileTransferReceiveFunc; -@protocol BindingsFileTransferReceivedProgressFunc; -@class BindingsFileTransferReceivedProgressFunc; -@protocol BindingsFileTransferSentProgressFunc; -@class BindingsFileTransferSentProgressFunc; -@protocol BindingsGroupReceiveFunc; -@class BindingsGroupReceiveFunc; -@protocol BindingsGroupRequestFunc; -@class BindingsGroupRequestFunc; -@protocol BindingsListener; -@class BindingsListener; -@protocol BindingsLogWriter; -@class BindingsLogWriter; -@protocol BindingsLookupCallback; -@class BindingsLookupCallback; -@protocol BindingsMessageDeliveryCallback; -@class BindingsMessageDeliveryCallback; -@protocol BindingsMultiLookupCallback; -@class BindingsMultiLookupCallback; -@protocol BindingsNetworkHealthCallback; -@class BindingsNetworkHealthCallback; -@protocol BindingsPreimageNotification; -@class BindingsPreimageNotification; -@protocol BindingsRestoreContactsUpdater; -@class BindingsRestoreContactsUpdater; -@protocol BindingsRoundCompletionCallback; -@class BindingsRoundCompletionCallback; -@protocol BindingsRoundEventCallback; -@class BindingsRoundEventCallback; -@protocol BindingsSearchCallback; -@class BindingsSearchCallback; -@protocol BindingsSingleSearchCallback; -@class BindingsSingleSearchCallback; -@protocol BindingsTimeSource; -@class BindingsTimeSource; -@protocol BindingsUpdateBackupFunc; -@class BindingsUpdateBackupFunc; - -@protocol BindingsAuthConfirmCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -@protocol BindingsAuthRequestCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsAuthResetNotificationCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsClientError <NSObject> -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -@protocol BindingsEventCallbackFunctionObject <NSObject> -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -@protocol BindingsFileTransferReceiveFunc <NSObject> -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -@protocol BindingsFileTransferReceivedProgressFunc <NSObject> -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsFileTransferSentProgressFunc <NSObject> -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsGroupReceiveFunc <NSObject> -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -@protocol BindingsGroupRequestFunc <NSObject> -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -@protocol BindingsListener <NSObject> -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@protocol BindingsLogWriter <NSObject> -- (void)log:(NSString* _Nullable)p0; -@end - -@protocol BindingsLookupCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsMessageDeliveryCallback <NSObject> -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -@protocol BindingsMultiLookupCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -@protocol BindingsNetworkHealthCallback <NSObject> -- (void)callback:(BOOL)p0; -@end - -@protocol BindingsPreimageNotification <NSObject> -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -@protocol BindingsRestoreContactsUpdater <NSObject> -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -@protocol BindingsRoundCompletionCallback <NSObject> -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -@protocol BindingsRoundEventCallback <NSObject> -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -@protocol BindingsSearchCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -@protocol BindingsSingleSearchCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsTimeSource <NSObject> -- (int64_t)nowMs; -@end - -@protocol BindingsUpdateBackupFunc <NSObject> -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -@interface BindingsBackup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * AddJson stores a passed in json string in the backup structure - */ -- (void)addJson:(NSString* _Nullable)json; -/** - * IsBackupRunning returns true if the backup has been initialized and is -running. Returns false if it has been stopped. - */ -- (BOOL)isBackupRunning; -/** - * StopBackup stops the backup processes and deletes the user's password from -storage. To enable backups again, call InitializeBackup. - */ -- (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsBackupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID - -@property (nonatomic) NSString* _Nonnull params; -@end - -/** - * BindingsClient wraps the api.Client, implementing additional functions -to support the gomobile Client interface - */ -@interface BindingsClient : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)confirmAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteAllRequests clears all requests from Client's auth storage. - */ -- (BOOL)deleteAllRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteContact is a function which removes a contact from Client's storage - */ -- (BOOL)deleteContact:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteReceiveRequests clears receive requests from Client's auth storage. - */ -- (BOOL)deleteReceiveRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteRequest will delete a request, agnostic of request type -for the given partner ID. If no request exists for this -partner ID an error will be returned. - */ -- (BOOL)deleteRequest:(NSData* _Nullable)requesterUserId error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteSentRequests clears sent requests from Client's auth storage. - */ -- (BOOL)deleteSentRequests:(NSError* _Nullable* _Nullable)error; -// skipped method Client.GetInternalClient with unsupported parameter or return types - -/** - * GetNodeRegistrationStatus returns a struct with the number of nodes the -client is registered with and the number total. - */ -- (BindingsNodeRegistrationsStatus* _Nullable)getNodeRegistrationStatus:(NSError* _Nullable* _Nullable)error; -/** - * GetPartners returns a list of - */ -- (NSData* _Nullable)getPartners:(NSError* _Nullable* _Nullable)error; -/** - * GetPreferredBins returns the geographic bin or bins that the provided two -character country code is a part of. The bins are returned as CSV. - */ -- (NSString* _Nonnull)getPreferredBins:(NSString* _Nullable)countryCode error:(NSError* _Nullable* _Nullable)error; -- (NSString* _Nonnull)getPreimages:(NSData* _Nullable)identity; -// skipped method Client.GetRateLimitParams with unsupported parameter or return types - -- (NSString* _Nonnull)getRelationshipFingerprint:(NSData* _Nullable)partnerID error:(NSError* _Nullable* _Nullable)error; -/** - * Returns a user object from which all information about the current user -can be gleaned - */ -- (BindingsUser* _Nullable)getUser; -/** - * HasRunningProcessies checks if any background threads are running. -returns true if none are running. This is meant to be -used when NetworkFollowerStatus() returns Stopping. -Due to the handling of comms on iOS, where the OS can -block indefiently, it may not enter the stopped -state apropreatly. This can be used instead. - */ -- (BOOL)hasRunningProcessies; -/** - * returns true if the network is read to be in a healthy state where -messages can be sent - */ -- (BOOL)isNetworkHealthy; -- (BindingsContact* _Nullable)makePrecannedAuthenticatedChannel:(long)precannedID error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the state of the network follower. Returns: -Stopped - 0 -Starting - 1000 -Running - 2000 -Stopping - 3000 - */ -- (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; -/** - * RegisterClientErrorCallback registers the callback to handle errors from the -long running threads controlled by StartNetworkFollower and StopNetworkFollower - */ -- (void)registerClientErrorCallback:(id<BindingsClientError> _Nullable)clientError; -/** - * RegisterEventCallback records the given function to receive -ReportableEvent objects. It returns the internal index -of the callback so that it can be deleted later. - */ -- (BOOL)registerEventCallback:(NSString* _Nullable)name myObj:(id<BindingsEventCallbackFunctionObject> _Nullable)myObj error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterForNotifications accepts firebase messaging token - */ -- (BOOL)registerForNotifications:(NSString* _Nullable)token error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterListener records and installs a listener for messages -matching specific uid, msgType, and/or username -Returns a ListenerUnregister interface which can be - -to register for any userID, pass in an id with length 0 or an id with -all zeroes - -to register for any message type, pass in a message type of 0 - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsUnregister* _Nullable)registerListener:(NSData* _Nullable)uid msgType:(long)msgType listener:(id<BindingsListener> _Nullable)listener error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterNetworkHealthCB registers the network health callback to be called -any time the network health changes. Returns a unique ID that can be used to -unregister the network health callback. - */ -- (int64_t)registerNetworkHealthCB:(id<BindingsNetworkHealthCallback> _Nullable)nhc; -- (void)registerPreimageCallback:(NSData* _Nullable)identity pin:(id<BindingsPreimageNotification> _Nullable)pin; -/** - * RegisterRoundEventsHandler registers a callback interface for round -events. -The rid is the round the event attaches to -The timeoutMS is the number of milliseconds until the event fails, and the -validStates are a list of states (one per byte) on which the event gets -triggered -States: - 0x00 - PENDING (Never seen by client) - 0x01 - PRECOMPUTING - 0x02 - STANDBY - 0x03 - QUEUED - 0x04 - REALTIME - 0x05 - COMPLETED - 0x06 - FAILED -These states are defined in elixxir/primitives/states/state.go - */ -- (BindingsUnregister* _Nullable)registerRoundEventsHandler:(long)rid cb:(id<BindingsRoundEventCallback> _Nullable)cb timeoutMS:(long)timeoutMS il:(BindingsIntList* _Nullable)il; -- (void)replayRequests; -- (BOOL)requestAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (BOOL)resetSession:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * This will return the round the message was sent on if it is successfully sent -This can be used to register a round event to learn about message delivery. -on failure a round id of -1 is returned - */ -- (BOOL)sendCmix:(NSData* _Nullable)recipient contents:(NSData* _Nullable)contents parameters:(NSString* _Nullable)parameters ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * SendE2E sends an end-to-end payload to the provided recipient with -the provided msgType. Returns the list of rounds in which parts of -the message were sent or an error if it fails. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsSendReport* _Nullable)sendE2E:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SendUnsafe sends an unencrypted payload to the provided recipient -with the provided msgType. Returns the list of rounds in which parts -of the message were sent or an error if it fails. -NOTE: Do not use this function unless you know what you are doing. -This function always produces an error message in client logging. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types with custom types - */ -- (BindingsRoundList* _Nullable)sendUnsafe:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SetProxiedBins updates the host pool filter that filters out gateways that -are not in one of the specified bins. The provided bins should be CSV. - */ -- (BOOL)setProxiedBins:(NSString* _Nullable)binStringsCSV error:(NSError* _Nullable* _Nullable)error; -/** - * StartNetworkFollower kicks off the tracking of the network. It starts -long running network client threads and returns an object for checking -state and stopping those threads. -Call this when returning from sleep and close when going back to -sleep. -These threads may become a significant drain on battery when offline, ensure -they are stopped if there is no internet access -Threads Started: - - Network Follower (/network/follow.go) - tracks the network events and hands them off to workers for handling - - Historical Round Retrieval (/network/rounds/historical.go) - Retrieves data about rounds which are too old to be stored by the client - - Message Retrieval Worker Group (/network/rounds/retrieve.go) - Requests all messages in a given round from the gateway of the last node - - Message Handling Worker Group (/network/message/handle.go) - Decrypts and partitions messages when signals via the Switchboard - - Health Tracker (/network/health) - Via the network instance tracks the state of the network - - Garbled Messages (/network/message/garbled.go) - Can be signaled to check all recent messages which could be be decoded - Uses a message store on disk for persistence - - Critical Messages (/network/message/critical.go) - Ensures all protocol layer mandatory messages are sent - Uses a message store on disk for persistence - - KeyExchange Trigger (/keyExchange/trigger.go) - Responds to sent rekeys and executes them - - KeyExchange Confirm (/keyExchange/confirm.go) - Responds to confirmations of successful rekey operations - */ -- (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * StopNetworkFollower stops the network follower if it is running. -It returns errors if the Follower is in the wrong status to stop or if it -fails to stop it. -if the network follower is running and this fails, the client object will -most likely be in an unrecoverable state and need to be trashed. - */ -- (BOOL)stopNetworkFollower:(NSError* _Nullable* _Nullable)error; -/** - * UnregisterEventCallback deletes the callback identified by the -index. It returns an error if it fails. - */ -- (void)unregisterEventCallback:(NSString* _Nullable)name; -/** - * UnregisterForNotifications unregister user for notifications - */ -- (BOOL)unregisterForNotifications:(NSError* _Nullable* _Nullable)error; -- (void)unregisterNetworkHealthCB:(int64_t)funcID; -- (BOOL)verifyOwnership:(NSData* _Nullable)receivedMarshaled verifiedMarshaled:(NSData* _Nullable)verifiedMarshaled ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForMessageDelivery allows the caller to get notified if the rounds a -message was sent in successfully completed. Under the hood, this uses an API -which uses the internal round data, network historical round lookup, and -waiting on network events to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - -This function takes the marshaled send report to ensure a memory leak does -not occur as a result of both sides of the bindings holding a reference to -the same pointer. - */ -- (BOOL)waitForMessageDelivery:(NSData* _Nullable)marshaledSendReport mdc:(id<BindingsMessageDeliveryCallback> _Nullable)mdc timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForNewtwork will block until either the network is healthy or the -passed timeout. It will return true if the network is healthy - */ -- (BOOL)waitForNetwork:(long)timeoutMS; -/** - * WaitForRoundCompletion allows the caller to get notified if a round -has completed (or failed). Under the hood, this uses an API which uses the internal -round data, network historical round lookup, and waiting on network events -to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - */ -- (BOOL)waitForRoundCompletion:(long)roundID rec:(id<BindingsRoundCompletionCallback> _Nullable)rec timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * contact object - */ -@interface BindingsContact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped method Contact.GetAPIContact with unsupported parameter or return types - -/** - * GetDHPublicKey returns the public key associated with the Contact. - */ -- (NSData* _Nullable)getDHPublicKey; -/** - * Returns a fact list for adding and getting facts to and from the contact - */ -- (BindingsFactList* _Nullable)getFactList; -/** - * GetID returns the user ID for this user. - */ -- (NSData* _Nullable)getID; -/** - * GetDHPublicKey returns hash of a DH proof of key ownership. - */ -- (NSData* _Nullable)getOwnershipProof; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsContactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BindingsContact* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * DummyTraffic contains the file dummy traffic manager. The manager can be used -to set and get the status of the send thread. - */ -@interface BindingsDummyTraffic : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client maxNumMessages:(long)maxNumMessages avgSendDeltaMS:(long)avgSendDeltaMS randomRangeMS:(long)randomRangeMS; -/** - * GetStatus returns the current state of the dummy traffic send thread. It has -the following return values: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Note that this function does not return the status set by SetStatus directly; -it returns the current status of the send thread, which means any call to -SetStatus will have a small delay before it is returned by GetStatus. - */ -- (BOOL)getStatus; -/** - * SetStatus sets the state of the dummy traffic send thread, which determines -if the thread is running or paused. The possible statuses are: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Returns an error if the channel is full. -Note that this function cannot change the status of the send thread if it has -yet to be started or stopped. - */ -- (BOOL)setStatus:(BOOL)status error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsFact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -- (nullable instancetype)init:(long)factType factStr:(NSString* _Nullable)factStr; -- (NSString* _Nonnull)get; -- (NSString* _Nonnull)stringify; -- (long)type; -@end - -@interface BindingsFactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * FactList - */ -- (nullable instancetype)init; -- (BOOL)add:(NSString* _Nullable)factData factType:(long)factType error:(NSError* _Nullable* _Nullable)error; -- (BindingsFact* _Nullable)get:(long)i; -- (long)num; -- (NSString* _Nonnull)stringify:(NSError* _Nullable* _Nullable)error; -@end - -/** - * FilePartTracker contains the interfaces.FilePartTracker. - */ -@interface BindingsFilePartTracker : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetNumParts returns the total number of file parts in the transfer. - */ -- (long)getNumParts; -/** - * GetPartStatus returns the status of the file part with the given part number. -The possible values for the status are: -0 = unsent -1 = sent (sender has sent a part, but it has not arrived) -2 = arrived (sender has sent a part, and it has arrived) -3 = received (receiver has received a part) - */ -- (long)getPartStatus:(long)partNum; -@end - -/** - * FileTransfer contains the file transfer manager. - */ -@interface BindingsFileTransfer : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client receiveFunc:(id<BindingsFileTransferReceiveFunc> _Nullable)receiveFunc parameters:(NSString* _Nullable)parameters; -/** - * CloseSend deletes a sent file transfer from the sent transfer map and from -storage once a transfer has completed or reached the retry limit. Returns an -error if the transfer has not run out of retries. - */ -- (BOOL)closeSend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * GetMaxFileNameByteLength returns the maximum length, in bytes, allowed for a -file name. - */ -- (long)getMaxFileNameByteLength; -/** - * GetMaxFilePreviewSize returns the maximum file preview size, in bytes. - */ -- (long)getMaxFilePreviewSize; -/** - * GetMaxFileSize returns the maximum file size, in bytes, allowed to be -transferred. - */ -- (long)getMaxFileSize; -/** - * GetMaxFileTypeByteLength returns the maximum length, in bytes, allowed for a -file type. - */ -- (long)getMaxFileTypeByteLength; -/** - * Receive returns the fully assembled file on the completion of the transfer. -It deletes the transfer from the received transfer map and from storage. -Returns an error if the transfer is not complete, the full file cannot be -verified, or if the transfer cannot be found. - */ -- (NSData* _Nullable)receive:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterReceiveProgressCallback allows for the registration of a callback to -track the progress of an individual received file transfer. The callback will -be called immediately when added to report the current status of the -transfer. It will then call every time a file part is received, the transfer -completes, or an error occurs. It is called at most once ever period, which -means if events occur faster than the period, then they will not be reported -and instead, the progress will be reported once at the end of the period. -Once the callback reports that the transfer has completed, the recipient -can get the full file by calling Receive. -The period is specified in milliseconds. - */ -- (BOOL)registerReceiveProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferReceivedProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterSendProgressCallback allows for the registration of a callback to -track the progress of an individual sent file transfer. The callback will be -called immediately when added to report the current status of the transfer. -It will then call every time a file part is sent, a file part arrives, the -transfer completes, or an error occurs. It is called at most once every -period, which means if events occur faster than the period, then they will -not be reported and instead, the progress will be reported once at the end of -the period. -The period is specified in milliseconds. - */ -- (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends a file to the recipient. The sender must have an E2E relationship -with the recipient. -The file name is the name of the file to show a user. It has a max length of -48 bytes. -The file type identifies what type of file is being sent. It has a max length -of 8 bytes. -The file data cannot be larger than 256 kB -The retry float is the total amount of data to send relative to the data -size. Data will be resent on error and will resend up to [(1 + retry) * -fileSize]. -The preview stores a preview of the data (such as a thumbnail) and is -capped at 4 kB in size. -Returns a unique transfer ID used to identify the transfer. -PeriodMS is the duration, in milliseconds, to wait between progress callback -calls. Set this large enough to prevent spamming. - */ -- (NSData* _Nullable)send:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType fileData:(NSData* _Nullable)fileData recipientID:(NSData* _Nullable)recipientID retry:(float)retry preview:(NSData* _Nullable)preview progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Group structure contains the identifying and membership information of a -group chat. - */ -@interface BindingsGroup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetCreatedMS returns the time the group was created in milliseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedMS; -/** - * GetCreatedNano returns the time the group was created in nanoseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedNano; -/** - * GetID return the 33-byte unique group ID. - */ -- (NSData* _Nullable)getID; -/** - * GetInitMessage returns initial message sent with the group request. - */ -- (NSData* _Nullable)getInitMessage; -/** - * GetMembership returns a list of contacts, one for each member in the group. -The list is in order; the first contact is the leader/creator of the group. -All subsequent members are ordered by their ID. - */ -- (BindingsGroupMembership* _Nullable)getMembership; -/** - * GetName returns the name set by the user for the group. - */ -- (NSData* _Nullable)getName; -/** - * Serialize serializes the Group. - */ -- (NSData* _Nullable)serialize; -@end - -/** - * GroupChat object contains the group chat manager. - */ -@interface BindingsGroupChat : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetGroup returns the group with the group ID. If no group exists, then the -error "failed to find group" is returned. - */ -- (BindingsGroup* _Nullable)getGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * GetGroups returns an IdList containing a list of group IDs that the user is a -part of. - */ -- (BindingsIdList* _Nullable)getGroups; -/** - * JoinGroup allows a user to join a group when they receive a request. The -caller must pass in the serialized bytes of a Group. - */ -- (BOOL)joinGroup:(NSData* _Nullable)serializedGroupData error:(NSError* _Nullable* _Nullable)error; -/** - * LeaveGroup deletes a group so a user no longer has access. - */ -- (BOOL)leaveGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * MakeGroup creates a new group and sends a group request to all members in the -group. The ID of the new group, the rounds the requests were sent on, and the -status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)makeGroup:(BindingsIdList* _Nullable)membership name:(NSData* _Nullable)name message:(NSData* _Nullable)message; -/** - * NumGroups returns the number of groups the user is a part of. - */ -- (long)numGroups; -/** - * ResendRequest resends a group request to all members in the group. The rounds -they were sent on and the status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)resendRequest:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends the message to the specified group. Returns the round the messages -were sent on. - */ -- (BindingsGroupSendReport* _Nullable)send:(NSData* _Nullable)groupIdBytes message:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * // -Member Structure -// -GroupMember represents a member in the group membership list. - */ -@interface BindingsGroupMember : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMember.Member with unsupported type: gitlab.com/elixxir/crypto/group.Member - -// skipped method GroupMember.DeepCopy with unsupported parameter or return types - -// skipped method GroupMember.Equal with unsupported parameter or return types - -/** - * GetDhKey returns the byte representation of the public Diffie–Hellman key of -the member. - */ -- (NSData* _Nullable)getDhKey; -/** - * GetID returns the 33-byte user ID of the member. - */ -- (NSData* _Nullable)getID; -- (NSString* _Nonnull)goString; -- (NSData* _Nullable)serialize; -- (NSString* _Nonnull)string; -@end - -/** - * GroupMembership structure contains a list of members that are part of a -group. The first member is the group leader. - */ -@interface BindingsGroupMembership : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Get returns the member at the index. The member at index 0 is always the -group leader. An error is returned if the index is out of range. - */ -- (BindingsGroupMember* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of members in the group membership. - */ -- (long)len; -@end - -/** - * GroupMessageReceive contains a group message, its ID, and its data that a -user receives. - */ -@interface BindingsGroupMessageReceive : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMessageReceive.MessageReceive with unsupported type: gitlab.com/elixxir/client/groupChat.MessageReceive - -/** - * GetEphemeralID returns the ephemeral ID of the recipient. - */ -- (int64_t)getEphemeralID; -/** - * GetGroupID returns the 33-byte group ID. - */ -- (NSData* _Nullable)getGroupID; -/** - * GetMessageID returns the message ID. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetPayload returns the message payload. - */ -- (NSData* _Nullable)getPayload; -/** - * GetRecipientID returns the 33-byte user ID of the recipient. - */ -- (NSData* _Nullable)getRecipientID; -/** - * GetRoundID returns the ID of the round the message was sent on. - */ -- (int64_t)getRoundID; -/** - * GetRoundTimestampMS returns the timestamp, in milliseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the timestamp, in nanoseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the ID of the round the message was sent on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSenderID returns the 33-byte user ID of the sender. - */ -- (NSData* _Nullable)getSenderID; -/** - * GetTimestampMS returns the message timestamp in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message timestamp in nanoseconds. - */ -- (int64_t)getTimestampNano; -- (NSString* _Nonnull)string; -@end - -@interface BindingsGroupReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable grpId; -@property (nonatomic) long status; -@end - -/** - * GroupSendReport is returned when sending a group message. It contains the -round ID sent on and the timestamp of the send. - */ -@interface BindingsGroupSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetMessageID returns the ID of the round that the send occurred on. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetRoundID returns the ID of the round that the send occurred on. - */ -- (int64_t)getRoundID; -/** - * GetRoundURL returns the URL of the round that the send occurred on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the timestamp of the send in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the timestamp of the send in nanoseconds. - */ -- (int64_t)getTimestampNano; -@end - -/** - * ID list -IdList contains a list of IDs. - */ -@interface BindingsIdList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Add appends the ID bytes to the end of the list. - */ -- (BOOL)add:(NSData* _Nullable)idBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Get returns the ID at the index. An error is returned if the index is out of -range. - */ -- (NSData* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of IDs in the list. - */ -- (long)len; -@end - -@interface BindingsIntList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (void)add:(long)i; -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -@interface BindingsManyNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsNotificationForMeReport* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -/** - * Message is a message received from the cMix network in the clear -or that has been decrypted using established E2E keys. - */ -@interface BindingsMessage : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetID returns the id of the message - */ -- (NSData* _Nullable)getID; -/** - * GetMessageType returns the message's type - */ -- (long)getMessageType; -/** - * GetPayload returns the message's payload/contents - */ -- (NSData* _Nullable)getPayload; -/** - * GetRoundId returns the message's round ID - */ -- (int64_t)getRoundId; -/** - * GetRoundTimestampMS returns the message's round timestamp in milliseconds - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the message's round timestamp in nanoseconds - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the message's round URL - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSender returns the message's sender ID, if available - */ -- (NSData* _Nullable)getSender; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -@end - -/** - * NewGroupReport is returned when creating a new group and contains the ID of -the group, a list of rounds that the group requests were sent on, and the -status of the send. - */ -@interface BindingsNewGroupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetError returns the string of an error. -Will be an empty string if no error occured - */ -- (NSString* _Nonnull)getError; -/** - * GetGroup returns the Group. - */ -- (BindingsGroup* _Nullable)getGroup; -/** - * GetRoundList returns the RoundList containing a list of rounds requests were -sent on. - */ -- (BindingsRoundList* _Nullable)getRoundList; -/** - * GetStatus returns the status of the requests sent when creating a new group. -status = 0 an error occurred before any requests could be sent - 1 all requests failed to send (call Resend Group) - 2 some request failed and some succeeded (call Resend Group) - 3, all requests sent successfully (call Resend Group) - */ -- (long)getStatus; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * NodeRegistrationsStatus structure for returning node registration statuses -for bindings. - */ -@interface BindingsNodeRegistrationsStatus : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetRegistered returns the number of nodes registered with the client. - */ -- (long)getRegistered; -/** - * GetTotal return the total of nodes currently in the network. - */ -- (long)getTotal; -@end - -@interface BindingsNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)forMe; -- (NSData* _Nullable)source; -- (NSString* _Nonnull)type; -@end - -/** - * RestoreContactsReport is a gomobile friendly report structure -for determining which IDs restored, which failed, and why. - */ -@interface BindingsRestoreContactsReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetErrorAt returns the error string at index - */ -- (NSString* _Nonnull)getErrorAt:(long)index; -/** - * GetFailedAt returns the failed ID at index - */ -- (NSData* _Nullable)getFailedAt:(long)index; -/** - * GetRestoreContactsError returns an error string. Empty if no error. - */ -- (NSString* _Nonnull)getRestoreContactsError; -/** - * GetRestoredAt returns the restored ID at index - */ -- (NSData* _Nullable)getRestoredAt:(long)index; -/** - * LenFailed returns the length of the ID's failed. - */ -- (long)lenFailed; -/** - * LenRestored returns the length of ID's restored. - */ -- (long)lenRestored; -@end - -@interface BindingsRoundList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * the send report is the mechanisim by which sendE2E returns a single - */ -@interface BindingsSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (NSData* _Nullable)getMessageID; -- (BindingsRoundList* _Nullable)getRoundList; -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsSendReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field SendReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable mid; -@property (nonatomic) int64_t ts; -@end - -/** - * Generic Unregister - a generic return used for all callbacks which can be -unregistered -Interface which allows the un-registration of a listener - */ -@interface BindingsUnregister : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Call unregisters a callback - */ -- (void)unregister; -@end - -@interface BindingsUser : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsContact* _Nullable)getContact; -- (NSData* _Nullable)getE2EDhPrivateKey; -- (NSData* _Nullable)getE2EDhPublicKey; -- (NSData* _Nullable)getReceptionID; -- (NSData* _Nullable)getReceptionRSAPrivateKeyPem; -- (NSData* _Nullable)getReceptionRSAPublicKeyPem; -- (NSData* _Nullable)getReceptionSalt; -- (NSData* _Nullable)getTransmissionID; -- (NSData* _Nullable)getTransmissionRSAPrivateKeyPem; -- (NSData* _Nullable)getTransmissionRSAPublicKeyPem; -- (NSData* _Nullable)getTransmissionSalt; -- (BOOL)isPrecanned; -@end - -@interface BindingsUserDiscovery : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)init:(BindingsClient* _Nullable)client; -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; -/** - * AddFact adds a fact for the user to user discovery. Will only succeed if the -user is already registered and the system does not have the fact currently -registered for any user. -Will fail if the fact string is not well formed. -This does not complete the fact registration process, it returns a -confirmation id instead. Over the communications system the fact is -associated with, a code will be sent. This confirmation ID needs to be -called along with the code to finalize the fact. - */ -- (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from -AddFact while the code will come over the associated communications system - */ -- (BOOL)confirmFact:(NSString* _Nullable)confirmationID code:(NSString* _Nullable)code error:(NSError* _Nullable* _Nullable)error; -/** - * Lookup the contact object associated with the given userID. The -id is the byte representation of an id. -This will reject if that id is malformed. The LookupCallback will return -the associated contact if it exists. - */ -- (BOOL)lookup:(NSData* _Nullable)idBytes callback:(id<BindingsLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * MultiLookup Looks for the contact object associated with all given userIDs. -The ids are the byte representation of an id stored in an IDList object. -This will reject if that id is malformed or if the indexing on the IDList -object is wrong. The MultiLookupCallback will return with all contacts -returned within the timeout. - */ -- (BOOL)multiLookup:(BindingsIdList* _Nullable)ids callback:(id<BindingsMultiLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * Register registers a user with user discovery. Will return an error if the -network signatures are malformed or if the username is taken. Usernames -cannot be changed after registration at this time. Will fail if the user is -already registered. -Identity does not go over cmix, it occurs over normal communications - */ -- (BOOL)register:(NSString* _Nullable)username error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveFact removes a previously confirmed fact. Will fail if the passed fact string is -not well-formed or if the fact is not associated with this client. -Users cannot remove username facts and must instead remove the user. - */ -- (BOOL)removeFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveUser deletes a user. The fact sent must be the username. -This function preserves the username forever and makes it -unusable. - */ -- (BOOL)removeUser:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * Search for the passed Facts. The factList is the stringification of a -fact list object, look at /bindings/list.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This is NOT intended to be used to search for multiple users at once, that -can have a privacy reduction. Instead, it is intended to be used to search -for a user where multiple pieces of information is known. - */ -- (BOOL)search:(NSString* _Nullable)fl callback:(id<BindingsSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SearchSingle searches for the passed Facts. The fact is the stringification of a -fact object, look at /bindings/contact.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This only searches for a single fact at a time. It is intended to make some -simple use cases of the API easier. - */ -- (BOOL)searchSingle:(NSString* _Nullable)f callback:(id<BindingsSingleSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SetAlternativeUserDiscovery sets the alternativeUd object within manager. -Once set, any user discovery operation will go through the alternative -user discovery service. -To undo this operation, use UnsetAlternativeUserDiscovery. -The contact file is the already read in bytes, not the file path for the contact file. - */ -- (BOOL)setAlternativeUserDiscovery:(NSData* _Nullable)address cert:(NSData* _Nullable)cert contactFile:(NSData* _Nullable)contactFile error:(NSError* _Nullable* _Nullable)error; -/** - * UnsetAlternativeUserDiscovery clears out the information from -the Manager object. - */ -- (BOOL)unsetAlternativeUserDiscovery:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Error codes - */ -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedCode; -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedMessage; - -/** - * CompressJpeg takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpeg(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * CompressJpegForPreview takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpegForPreview(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * DownloadAndVerifySignedNdfWithUrl retrieves the NDF from a specified URL. -The NDF is processed into a protobuf containing a signature which -is verified using the cert string passed in. The NDF is returned as marshaled -byte data which may be used to start a client. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadAndVerifySignedNdfWithUrl(NSString* _Nullable url, NSString* _Nullable cert, NSError* _Nullable* _Nullable error); - -/** - * DownloadDAppRegistrationDB returns a []byte containing -the JSON data describing registered dApps. -See https://git.xx.network/elixxir/registered-dapps - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadDAppRegistrationDB(NSError* _Nullable* _Nullable error); - -/** - * DownloadErrorDB returns a []byte containing the JSON data -describing client errors. -See https://git.xx.network/elixxir/client-error-database/ - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadErrorDB(NSError* _Nullable* _Nullable error); - -/** - * DumpStack returns a string with the stack trace of every running thread. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsDumpStack(NSError* _Nullable* _Nullable error); - -/** - * EnableGrpcLogs sets GRPC trace logging - */ -FOUNDATION_EXPORT void BindingsEnableGrpcLogs(id<BindingsLogWriter> _Nullable writer); - -/** - * ErrorStringToUserFriendlyMessage takes a passed in errStr which will be -a backend generated error. These may be error specifically written by -the backend team or lower level errors gotten from low level dependencies. -This function will parse the error string for common errors provided from -errToUserErr to provide a more user-friendly error message for the front end. -If the error is not common, some simple parsing is done on the error message -to make it more user-accessible, removing backend specific jargon. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsErrorStringToUserFriendlyMessage(NSString* _Nullable errStr); - -/** - * GenerateSecret creates a secret password using a system-based -pseudorandom number generator. It takes 1 parameter, `numBytes`, -which should be set to 32, but can be set higher in certain cases. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateSecret(long numBytes); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetCMIXParams(NSError* _Nullable* _Nullable error); - -/** - * returns a previously created client. IF be used if the garbage collector -removes the client instance on the app side. Is NOT thread safe relative to -login, newClient, or newPrecannedClient - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsGetClientSingleton(void); - -/** - * GetDependencies returns the api DEPENDENCIES - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetDependencies(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetE2EParams(NSError* _Nullable* _Nullable error); - -/** - * GetGitVersion rturns the api GITVERSION - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetGitVersion(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetNetworkParams(NSError* _Nullable* _Nullable error); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetUnsafeParams(NSError* _Nullable* _Nullable error); - -/** - * GetVersion returns the api SEMVER - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void); - -/** - * InitializeBackup starts the backup processes that returns backup updates when -they occur. Any time an event occurs that changes the contents of the backup, -such as adding or deleting a contact, the backup is triggered and an -encrypted backup is generated and returned on the updateBackupCb callback. -Call this function only when enabling backup if it has not already been -initialized or when the user wants to change their password. -To resume backup process on app recovery, use ResumeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsInitializeBackup(NSString* _Nullable password, id<BindingsUpdateBackupFunc> _Nullable updateBackupCb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * LoadSecretWithMnemonic loads the secret stored from the call to -StoreSecretWithMnemonic. The path given should be the same filepath -as the path given in StoreSecretWithMnemonic. There should be a file -in this path called ".recovery". This operation is not tied -to client operations, as the user will not have a client when trying to -recover their account. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsLoadSecretWithMnemonic(NSString* _Nullable mnemonic, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * sets level of logging. All logs the set level and above will be displayed -options are: - TRACE - 0 - DEBUG - 1 - INFO - 2 - WARN - 3 - ERROR - 4 - CRITICAL - 5 - FATAL - 6 -The default state without updates is: INFO - */ -FOUNDATION_EXPORT BOOL BindingsLogLevel(long level, NSError* _Nullable* _Nullable error); - -/** - * Login will load an existing client from the storageDir -using the password. This will fail if the client doesn't exist or -the password is incorrect. -The password is passed as a byte array so that it can be cleared from -memory and stored as securely as possible using the memguard library. -Login does not block on network connection, and instead loads and -starts subprocesses to perform network operations. - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsLogin(NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * MakeIdList creates a new empty IdList. - */ -FOUNDATION_EXPORT BindingsIdList* _Nullable BindingsMakeIdList(void); - -FOUNDATION_EXPORT BindingsIntList* _Nullable BindingsMakeIntList(void); - -/** - * NewClient creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable regCode, NSError* _Nullable* _Nullable error); - -/** - * NewClientFromBackup constructs a new Client from an encrypted backup. The backup -is decrypted using the backupPassphrase. On success a successful client creation, -the function will return a JSON encoded list of the E2E partners -contained in the backup and a json-encoded string of the parameters stored in the backup - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); - -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -FOUNDATION_EXPORT BindingsDummyTraffic* _Nullable BindingsNewDummyTrafficManager(BindingsClient* _Nullable client, long maxNumMessages, long avgSendDeltaMS, long randomRangeMS, NSError* _Nullable* _Nullable error); - -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -FOUNDATION_EXPORT BindingsFact* _Nullable BindingsNewFact(long factType, NSString* _Nullable factStr, NSError* _Nullable* _Nullable error); - -/** - * FactList - */ -FOUNDATION_EXPORT BindingsFactList* _Nullable BindingsNewFactList(void); - -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -FOUNDATION_EXPORT BindingsFileTransfer* _Nullable BindingsNewFileTransferManager(BindingsClient* _Nullable client, id<BindingsFileTransferReceiveFunc> _Nullable receiveFunc, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * NewGroupManager creates a new group chat manager. - */ -FOUNDATION_EXPORT BindingsGroupChat* _Nullable BindingsNewGroupManager(BindingsClient* _Nullable client, id<BindingsGroupRequestFunc> _Nullable requestFunc, id<BindingsGroupReceiveFunc> _Nullable receiveFunc, NSError* _Nullable* _Nullable error); - -/** - * NewPrecannedClient creates an insecure user with predetermined keys with nodes -It creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewPrecannedClient(long precannedID, NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); - -/** - * NotificationsForMe Check if a notification received is for me -It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, -a Type, and a source. These are as follows: - TYPE SOURCE DESCRIPTION - "default" recipient user ID A message with no association - "request" sender user ID A channel request has been received - "reset" sender user ID A channel reset has been received - "confirm" sender user ID A channel request has been accepted - "silent" sender user ID A message which should not be notified on - "e2e" sender user ID reception of an E2E message - "group" group ID reception of a group chat message - "endFT" sender user ID Last message sent confirming end of file transfer - "groupRQ" sender user ID Request from sender to join a group chat - */ -FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotificationsForMe(NSString* _Nullable notifCSV, NSString* _Nullable preimages, NSError* _Nullable* _Nullable error); - -/** - * RegisterLogWriter registers a callback on which logs are written. - */ -FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); - -/** - * RestoreContactsFromBackup takes as input the jason output of the -`NewClientFromBackup` function, unmarshals it into IDs, looks up -each ID in user discovery, and initiates a session reset request. -This function will not return until every id in the list has been sent a -request. It should be called again and again until it completes. -xxDK users should not use this function. This function is used by -the mobile phone apps and are not intended to be part of the xxDK. It -should be treated as internal functions specific to the phone apps. - */ -FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); - -/** - * ResumeBackup starts the backup processes back up with a new callback after it -has been initialized. -Call this function only when resuming a backup that has already been -initialized or to replace the callback. -To start the backup for the first time or to use a new password, use -InitializeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsResumeBackup(id<BindingsUpdateBackupFunc> _Nullable cb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * SetTimeSource sets the network time to a custom source. - */ -FOUNDATION_EXPORT void BindingsSetTimeSource(id<BindingsTimeSource> _Nullable timeNow); - -/** - * StoreSecretWithMnemonic stores the secret tied with the mnemonic to storage. -Unlike other storage operations, this does not use EKV, as that is -intrinsically tied to client operations, which the user will not have while -trying to recover their account. As such, we store the encrypted data -directly, with a specified path. Path will be a valid filepath in which the -recover file will be stored as ".recovery". - -As an example, given "home/user/xxmessenger/storagePath", -the recovery file will be stored at -"home/user/xxmessenger/storagePath/.recovery" - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsStoreSecretWithMnemonic(NSData* _Nullable secret, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled contact object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsContact* _Nullable BindingsUnmarshalContact(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled send report object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsSendReport* _Nullable BindingsUnmarshalSendReport(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * UpdateCommonErrors takes the passed in contents of a JSON file and updates the -errToUserErr map with the contents of the json file. The JSON's expected format -conform with the commented examples provides in errToUserErr above. -NOTE that you should not pass in a file path, but a preloaded JSON file - */ -FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, NSError* _Nullable* _Nullable error); - -// skipped function WrapAPIClient with unsupported parameter or return types - - -// skipped function WrapUserDiscovery with unsupported parameter or return types - - -@class BindingsAuthConfirmCallback; - -@class BindingsAuthRequestCallback; - -@class BindingsAuthResetNotificationCallback; - -@class BindingsClientError; - -@class BindingsEventCallbackFunctionObject; - -@class BindingsFileTransferReceiveFunc; - -@class BindingsFileTransferReceivedProgressFunc; - -@class BindingsFileTransferSentProgressFunc; - -@class BindingsGroupReceiveFunc; - -@class BindingsGroupRequestFunc; - -@class BindingsListener; - -@class BindingsLogWriter; - -@class BindingsLookupCallback; - -@class BindingsMessageDeliveryCallback; - -@class BindingsMultiLookupCallback; - -@class BindingsNetworkHealthCallback; - -@class BindingsPreimageNotification; - -@class BindingsRestoreContactsUpdater; - -@class BindingsRoundCompletionCallback; - -@class BindingsRoundEventCallback; - -@class BindingsSearchCallback; - -@class BindingsSingleSearchCallback; - -@class BindingsTimeSource; - -@class BindingsUpdateBackupFunc; - -/** - * AuthConfirmCallback notifies the register whenever they receive an auth -request confirmation - */ -@interface BindingsAuthConfirmCallback : NSObject <goSeqRefInterface, BindingsAuthConfirmCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthRequestCallback : NSObject <goSeqRefInterface, BindingsAuthRequestCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@interface BindingsClientError : NSObject <goSeqRefInterface, BindingsClientError> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -/** - * EventCallbackFunctionObject bindings interface which contains function -that implements the EventCallbackFunction - */ -@interface BindingsEventCallbackFunctionObject : NSObject <goSeqRefInterface, BindingsEventCallbackFunctionObject> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -/** - * FileTransferReceiveFunc contains a function callback that notifies the -receiver of an incoming file transfer. It is called on the reception of the -initial file transfer message. - */ -@interface BindingsFileTransferReceiveFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -/** - * FileTransferReceivedProgressFunc contains a function callback that tracks the -progress of receiving a file. It is called when a file part is received, the -transfer completes, or on error. - */ -@interface BindingsFileTransferReceivedProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceivedProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * FileTransferSentProgressFunc contains a function callback that tracks the -progress of sending a file. It is called when a file part is sent, a file -part arrives, the transfer completes, or on error. - */ -@interface BindingsFileTransferSentProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferSentProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * GroupReceiveFunc contains a function callback that is called when a group -message is received. - */ -@interface BindingsGroupReceiveFunc : NSObject <goSeqRefInterface, BindingsGroupReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -/** - * GroupRequestFunc contains a function callback that is called when a group -request is received. - */ -@interface BindingsGroupRequestFunc : NSObject <goSeqRefInterface, BindingsGroupRequestFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -/** - * Listener provides a callback to hear a message -An object implementing this interface can be called back when the client -gets a message of the type that the registerer specified at registration -time. - */ -@interface BindingsListener : NSObject <goSeqRefInterface, BindingsListener> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@interface BindingsLogWriter : NSObject <goSeqRefInterface, BindingsLogWriter> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)log:(NSString* _Nullable)p0; -@end - -/** - * LookupCallback returns the result of a single lookup - */ -@interface BindingsLookupCallback : NSObject <goSeqRefInterface, BindingsLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -/** - * MessageDeliveryCallback gets called on the determination if all events -related to a message send were successful. - */ -@interface BindingsMessageDeliveryCallback : NSObject <goSeqRefInterface, BindingsMessageDeliveryCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -/** - * MultiLookupCallback returns the result of many parallel lookups - */ -@interface BindingsMultiLookupCallback : NSObject <goSeqRefInterface, BindingsMultiLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -/** - * A callback when which is used to receive notification if network health -changes - */ -@interface BindingsNetworkHealthCallback : NSObject <goSeqRefInterface, BindingsNetworkHealthCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BOOL)p0; -@end - -@interface BindingsPreimageNotification : NSObject <goSeqRefInterface, BindingsPreimageNotification> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -/** - * RestoreContactsUpdater interface provides a callback function -for receiving update information from RestoreContactsFromBackup. - */ -@interface BindingsRestoreContactsUpdater : NSObject <goSeqRefInterface, BindingsRestoreContactsUpdater> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -/** - * RoundCompletionCallback is returned when the completion of a round is known. - */ -@interface BindingsRoundCompletionCallback : NSObject <goSeqRefInterface, BindingsRoundCompletionCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -/** - * RoundEventCallback handles waiting on the exact state of a round on -the cMix network. - */ -@interface BindingsRoundEventCallback : NSObject <goSeqRefInterface, BindingsRoundEventCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -/** - * SearchCallback returns the result of a search - */ -@interface BindingsSearchCallback : NSObject <goSeqRefInterface, BindingsSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -/** - * SingleSearchCallback returns the result of a single search - */ -@interface BindingsSingleSearchCallback : NSObject <goSeqRefInterface, BindingsSingleSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@interface BindingsTimeSource : NSObject <goSeqRefInterface, BindingsTimeSource> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (int64_t)nowMs; -@end - -/** - * UpdateBackupFunc contains a function callback that returns new backups. - */ -@interface BindingsUpdateBackupFunc : NSObject <goSeqRefInterface, BindingsUpdateBackupFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Universe.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Universe.objc.h deleted file mode 100644 index 019e7502d581983722a15bf30799e85cbc5dd766..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Universe.objc.h +++ /dev/null @@ -1,29 +0,0 @@ -// Objective-C API for talking to Go package. -// gobind -lang=objc -// -// File is generated by gobind. Do not edit. - -#ifndef __Universe_H__ -#define __Universe_H__ - -@import Foundation; -#include "ref.h" - -@protocol Universeerror; -@class Universeerror; - -@protocol Universeerror <NSObject> -- (NSString* _Nonnull)error; -@end - -@class Universeerror; - -@interface Universeerror : NSError <goSeqRefInterface, Universeerror> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (NSString* _Nonnull)error; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/ref.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/ref.h deleted file mode 100644 index b8036a4d85c7387f3def61473a071b5d8c4c8208..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/ref.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef __GO_REF_HDR__ -#define __GO_REF_HDR__ - -#include <Foundation/Foundation.h> - -// GoSeqRef is an object tagged with an integer for passing back and -// forth across the language boundary. A GoSeqRef may represent either -// an instance of a Go object, or an Objective-C object passed to Go. -// The explicit allocation of a GoSeqRef is used to pin a Go object -// when it is passed to Objective-C. The Go seq package maintains a -// reference to the Go object in a map keyed by the refnum along with -// a reference count. When the reference count reaches zero, the Go -// seq package will clear the corresponding entry in the map. -@interface GoSeqRef : NSObject { -} -@property(readonly) int32_t refnum; -@property(strong) id obj; // NULL when representing a Go object. - -// new GoSeqRef object to proxy a Go object. The refnum must be -// provided from Go side. -- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; - -- (int32_t)incNum; - -@end - -@protocol goSeqRefInterface --(GoSeqRef*) _ref; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Modules/module.modulemap b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Modules/module.modulemap deleted file mode 100644 index 4316a5b24058edfc18ffb2dc7f7a982e8353441a..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Modules/module.modulemap +++ /dev/null @@ -1,8 +0,0 @@ -framework module "Bindings" { - header "ref.h" - header "Bindings.objc.h" - header "Universe.objc.h" - header "Bindings.h" - - export * -} \ No newline at end of file diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Resources/Info.plist b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Resources/Info.plist deleted file mode 100644 index 0d1a4b8ab9b1fc8e9357197398f73353470cb636..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Resources/Info.plist +++ /dev/null @@ -1,6 +0,0 @@ -<?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> - </dict> - </plist> diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings deleted file mode 100644 index 8d78f84d50a0b0719b795f7b342f6109c2b848ec..0000000000000000000000000000000000000000 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings and /dev/null differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.h deleted file mode 100644 index 8906a7da239705b790cb2bb64de92f806640cb38..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.h +++ /dev/null @@ -1,13 +0,0 @@ - -// Objective-C API for talking to the following Go packages -// -// gitlab.com/elixxir/client/bindings -// -// File is generated by gomobile bind. Do not edit. -#ifndef __Bindings_FRAMEWORK_H__ -#define __Bindings_FRAMEWORK_H__ - -#include "Bindings.objc.h" -#include "Universe.objc.h" - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.objc.h deleted file mode 100644 index 32bf6d116888f787ced27b01b95cb4e1b2c1138b..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Bindings.objc.h +++ /dev/null @@ -1,2083 +0,0 @@ -// Objective-C API for talking to gitlab.com/elixxir/client/bindings Go package. -// gobind -lang=objc gitlab.com/elixxir/client/bindings -// -// File is generated by gobind. Do not edit. - -#ifndef __Bindings_H__ -#define __Bindings_H__ - -@import Foundation; -#include "ref.h" -#include "Universe.objc.h" - - -@class BindingsBackup; -@class BindingsBackupReport; -@class BindingsClient; -@class BindingsContact; -@class BindingsContactList; -@class BindingsDummyTraffic; -@class BindingsFact; -@class BindingsFactList; -@class BindingsFilePartTracker; -@class BindingsFileTransfer; -@class BindingsGroup; -@class BindingsGroupChat; -@class BindingsGroupMember; -@class BindingsGroupMembership; -@class BindingsGroupMessageReceive; -@class BindingsGroupReportDisk; -@class BindingsGroupSendReport; -@class BindingsIdList; -@class BindingsIntList; -@class BindingsManyNotificationForMeReport; -@class BindingsMessage; -@class BindingsNewGroupReport; -@class BindingsNodeRegistrationsStatus; -@class BindingsNotificationForMeReport; -@class BindingsRestoreContactsReport; -@class BindingsRoundList; -@class BindingsSendReport; -@class BindingsSendReportDisk; -@class BindingsUnregister; -@class BindingsUser; -@class BindingsUserDiscovery; -@protocol BindingsAuthConfirmCallback; -@class BindingsAuthConfirmCallback; -@protocol BindingsAuthRequestCallback; -@class BindingsAuthRequestCallback; -@protocol BindingsAuthResetNotificationCallback; -@class BindingsAuthResetNotificationCallback; -@protocol BindingsClientError; -@class BindingsClientError; -@protocol BindingsEventCallbackFunctionObject; -@class BindingsEventCallbackFunctionObject; -@protocol BindingsFileTransferReceiveFunc; -@class BindingsFileTransferReceiveFunc; -@protocol BindingsFileTransferReceivedProgressFunc; -@class BindingsFileTransferReceivedProgressFunc; -@protocol BindingsFileTransferSentProgressFunc; -@class BindingsFileTransferSentProgressFunc; -@protocol BindingsGroupReceiveFunc; -@class BindingsGroupReceiveFunc; -@protocol BindingsGroupRequestFunc; -@class BindingsGroupRequestFunc; -@protocol BindingsListener; -@class BindingsListener; -@protocol BindingsLogWriter; -@class BindingsLogWriter; -@protocol BindingsLookupCallback; -@class BindingsLookupCallback; -@protocol BindingsMessageDeliveryCallback; -@class BindingsMessageDeliveryCallback; -@protocol BindingsMultiLookupCallback; -@class BindingsMultiLookupCallback; -@protocol BindingsNetworkHealthCallback; -@class BindingsNetworkHealthCallback; -@protocol BindingsPreimageNotification; -@class BindingsPreimageNotification; -@protocol BindingsRestoreContactsUpdater; -@class BindingsRestoreContactsUpdater; -@protocol BindingsRoundCompletionCallback; -@class BindingsRoundCompletionCallback; -@protocol BindingsRoundEventCallback; -@class BindingsRoundEventCallback; -@protocol BindingsSearchCallback; -@class BindingsSearchCallback; -@protocol BindingsSingleSearchCallback; -@class BindingsSingleSearchCallback; -@protocol BindingsTimeSource; -@class BindingsTimeSource; -@protocol BindingsUpdateBackupFunc; -@class BindingsUpdateBackupFunc; - -@protocol BindingsAuthConfirmCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -@protocol BindingsAuthRequestCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsAuthResetNotificationCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsClientError <NSObject> -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -@protocol BindingsEventCallbackFunctionObject <NSObject> -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -@protocol BindingsFileTransferReceiveFunc <NSObject> -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -@protocol BindingsFileTransferReceivedProgressFunc <NSObject> -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsFileTransferSentProgressFunc <NSObject> -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsGroupReceiveFunc <NSObject> -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -@protocol BindingsGroupRequestFunc <NSObject> -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -@protocol BindingsListener <NSObject> -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@protocol BindingsLogWriter <NSObject> -- (void)log:(NSString* _Nullable)p0; -@end - -@protocol BindingsLookupCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsMessageDeliveryCallback <NSObject> -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -@protocol BindingsMultiLookupCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -@protocol BindingsNetworkHealthCallback <NSObject> -- (void)callback:(BOOL)p0; -@end - -@protocol BindingsPreimageNotification <NSObject> -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -@protocol BindingsRestoreContactsUpdater <NSObject> -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -@protocol BindingsRoundCompletionCallback <NSObject> -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -@protocol BindingsRoundEventCallback <NSObject> -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -@protocol BindingsSearchCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -@protocol BindingsSingleSearchCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsTimeSource <NSObject> -- (int64_t)nowMs; -@end - -@protocol BindingsUpdateBackupFunc <NSObject> -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -@interface BindingsBackup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * AddJson stores a passed in json string in the backup structure - */ -- (void)addJson:(NSString* _Nullable)json; -/** - * IsBackupRunning returns true if the backup has been initialized and is -running. Returns false if it has been stopped. - */ -- (BOOL)isBackupRunning; -/** - * StopBackup stops the backup processes and deletes the user's password from -storage. To enable backups again, call InitializeBackup. - */ -- (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsBackupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID - -@property (nonatomic) NSString* _Nonnull params; -@end - -/** - * BindingsClient wraps the api.Client, implementing additional functions -to support the gomobile Client interface - */ -@interface BindingsClient : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)confirmAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteAllRequests clears all requests from Client's auth storage. - */ -- (BOOL)deleteAllRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteContact is a function which removes a contact from Client's storage - */ -- (BOOL)deleteContact:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteReceiveRequests clears receive requests from Client's auth storage. - */ -- (BOOL)deleteReceiveRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteRequest will delete a request, agnostic of request type -for the given partner ID. If no request exists for this -partner ID an error will be returned. - */ -- (BOOL)deleteRequest:(NSData* _Nullable)requesterUserId error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteSentRequests clears sent requests from Client's auth storage. - */ -- (BOOL)deleteSentRequests:(NSError* _Nullable* _Nullable)error; -// skipped method Client.GetInternalClient with unsupported parameter or return types - -/** - * GetNodeRegistrationStatus returns a struct with the number of nodes the -client is registered with and the number total. - */ -- (BindingsNodeRegistrationsStatus* _Nullable)getNodeRegistrationStatus:(NSError* _Nullable* _Nullable)error; -/** - * GetPartners returns a list of - */ -- (NSData* _Nullable)getPartners:(NSError* _Nullable* _Nullable)error; -/** - * GetPreferredBins returns the geographic bin or bins that the provided two -character country code is a part of. The bins are returned as CSV. - */ -- (NSString* _Nonnull)getPreferredBins:(NSString* _Nullable)countryCode error:(NSError* _Nullable* _Nullable)error; -- (NSString* _Nonnull)getPreimages:(NSData* _Nullable)identity; -// skipped method Client.GetRateLimitParams with unsupported parameter or return types - -- (NSString* _Nonnull)getRelationshipFingerprint:(NSData* _Nullable)partnerID error:(NSError* _Nullable* _Nullable)error; -/** - * Returns a user object from which all information about the current user -can be gleaned - */ -- (BindingsUser* _Nullable)getUser; -/** - * HasRunningProcessies checks if any background threads are running. -returns true if none are running. This is meant to be -used when NetworkFollowerStatus() returns Stopping. -Due to the handling of comms on iOS, where the OS can -block indefiently, it may not enter the stopped -state apropreatly. This can be used instead. - */ -- (BOOL)hasRunningProcessies; -/** - * returns true if the network is read to be in a healthy state where -messages can be sent - */ -- (BOOL)isNetworkHealthy; -- (BindingsContact* _Nullable)makePrecannedAuthenticatedChannel:(long)precannedID error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the state of the network follower. Returns: -Stopped - 0 -Starting - 1000 -Running - 2000 -Stopping - 3000 - */ -- (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; -/** - * RegisterClientErrorCallback registers the callback to handle errors from the -long running threads controlled by StartNetworkFollower and StopNetworkFollower - */ -- (void)registerClientErrorCallback:(id<BindingsClientError> _Nullable)clientError; -/** - * RegisterEventCallback records the given function to receive -ReportableEvent objects. It returns the internal index -of the callback so that it can be deleted later. - */ -- (BOOL)registerEventCallback:(NSString* _Nullable)name myObj:(id<BindingsEventCallbackFunctionObject> _Nullable)myObj error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterForNotifications accepts firebase messaging token - */ -- (BOOL)registerForNotifications:(NSString* _Nullable)token error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterListener records and installs a listener for messages -matching specific uid, msgType, and/or username -Returns a ListenerUnregister interface which can be - -to register for any userID, pass in an id with length 0 or an id with -all zeroes - -to register for any message type, pass in a message type of 0 - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsUnregister* _Nullable)registerListener:(NSData* _Nullable)uid msgType:(long)msgType listener:(id<BindingsListener> _Nullable)listener error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterNetworkHealthCB registers the network health callback to be called -any time the network health changes. Returns a unique ID that can be used to -unregister the network health callback. - */ -- (int64_t)registerNetworkHealthCB:(id<BindingsNetworkHealthCallback> _Nullable)nhc; -- (void)registerPreimageCallback:(NSData* _Nullable)identity pin:(id<BindingsPreimageNotification> _Nullable)pin; -/** - * RegisterRoundEventsHandler registers a callback interface for round -events. -The rid is the round the event attaches to -The timeoutMS is the number of milliseconds until the event fails, and the -validStates are a list of states (one per byte) on which the event gets -triggered -States: - 0x00 - PENDING (Never seen by client) - 0x01 - PRECOMPUTING - 0x02 - STANDBY - 0x03 - QUEUED - 0x04 - REALTIME - 0x05 - COMPLETED - 0x06 - FAILED -These states are defined in elixxir/primitives/states/state.go - */ -- (BindingsUnregister* _Nullable)registerRoundEventsHandler:(long)rid cb:(id<BindingsRoundEventCallback> _Nullable)cb timeoutMS:(long)timeoutMS il:(BindingsIntList* _Nullable)il; -- (void)replayRequests; -- (BOOL)requestAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (BOOL)resetSession:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * This will return the round the message was sent on if it is successfully sent -This can be used to register a round event to learn about message delivery. -on failure a round id of -1 is returned - */ -- (BOOL)sendCmix:(NSData* _Nullable)recipient contents:(NSData* _Nullable)contents parameters:(NSString* _Nullable)parameters ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * SendE2E sends an end-to-end payload to the provided recipient with -the provided msgType. Returns the list of rounds in which parts of -the message were sent or an error if it fails. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsSendReport* _Nullable)sendE2E:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SendUnsafe sends an unencrypted payload to the provided recipient -with the provided msgType. Returns the list of rounds in which parts -of the message were sent or an error if it fails. -NOTE: Do not use this function unless you know what you are doing. -This function always produces an error message in client logging. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types with custom types - */ -- (BindingsRoundList* _Nullable)sendUnsafe:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SetProxiedBins updates the host pool filter that filters out gateways that -are not in one of the specified bins. The provided bins should be CSV. - */ -- (BOOL)setProxiedBins:(NSString* _Nullable)binStringsCSV error:(NSError* _Nullable* _Nullable)error; -/** - * StartNetworkFollower kicks off the tracking of the network. It starts -long running network client threads and returns an object for checking -state and stopping those threads. -Call this when returning from sleep and close when going back to -sleep. -These threads may become a significant drain on battery when offline, ensure -they are stopped if there is no internet access -Threads Started: - - Network Follower (/network/follow.go) - tracks the network events and hands them off to workers for handling - - Historical Round Retrieval (/network/rounds/historical.go) - Retrieves data about rounds which are too old to be stored by the client - - Message Retrieval Worker Group (/network/rounds/retrieve.go) - Requests all messages in a given round from the gateway of the last node - - Message Handling Worker Group (/network/message/handle.go) - Decrypts and partitions messages when signals via the Switchboard - - Health Tracker (/network/health) - Via the network instance tracks the state of the network - - Garbled Messages (/network/message/garbled.go) - Can be signaled to check all recent messages which could be be decoded - Uses a message store on disk for persistence - - Critical Messages (/network/message/critical.go) - Ensures all protocol layer mandatory messages are sent - Uses a message store on disk for persistence - - KeyExchange Trigger (/keyExchange/trigger.go) - Responds to sent rekeys and executes them - - KeyExchange Confirm (/keyExchange/confirm.go) - Responds to confirmations of successful rekey operations - */ -- (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * StopNetworkFollower stops the network follower if it is running. -It returns errors if the Follower is in the wrong status to stop or if it -fails to stop it. -if the network follower is running and this fails, the client object will -most likely be in an unrecoverable state and need to be trashed. - */ -- (BOOL)stopNetworkFollower:(NSError* _Nullable* _Nullable)error; -/** - * UnregisterEventCallback deletes the callback identified by the -index. It returns an error if it fails. - */ -- (void)unregisterEventCallback:(NSString* _Nullable)name; -/** - * UnregisterForNotifications unregister user for notifications - */ -- (BOOL)unregisterForNotifications:(NSError* _Nullable* _Nullable)error; -- (void)unregisterNetworkHealthCB:(int64_t)funcID; -- (BOOL)verifyOwnership:(NSData* _Nullable)receivedMarshaled verifiedMarshaled:(NSData* _Nullable)verifiedMarshaled ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForMessageDelivery allows the caller to get notified if the rounds a -message was sent in successfully completed. Under the hood, this uses an API -which uses the internal round data, network historical round lookup, and -waiting on network events to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - -This function takes the marshaled send report to ensure a memory leak does -not occur as a result of both sides of the bindings holding a reference to -the same pointer. - */ -- (BOOL)waitForMessageDelivery:(NSData* _Nullable)marshaledSendReport mdc:(id<BindingsMessageDeliveryCallback> _Nullable)mdc timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForNewtwork will block until either the network is healthy or the -passed timeout. It will return true if the network is healthy - */ -- (BOOL)waitForNetwork:(long)timeoutMS; -/** - * WaitForRoundCompletion allows the caller to get notified if a round -has completed (or failed). Under the hood, this uses an API which uses the internal -round data, network historical round lookup, and waiting on network events -to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - */ -- (BOOL)waitForRoundCompletion:(long)roundID rec:(id<BindingsRoundCompletionCallback> _Nullable)rec timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * contact object - */ -@interface BindingsContact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped method Contact.GetAPIContact with unsupported parameter or return types - -/** - * GetDHPublicKey returns the public key associated with the Contact. - */ -- (NSData* _Nullable)getDHPublicKey; -/** - * Returns a fact list for adding and getting facts to and from the contact - */ -- (BindingsFactList* _Nullable)getFactList; -/** - * GetID returns the user ID for this user. - */ -- (NSData* _Nullable)getID; -/** - * GetDHPublicKey returns hash of a DH proof of key ownership. - */ -- (NSData* _Nullable)getOwnershipProof; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsContactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BindingsContact* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * DummyTraffic contains the file dummy traffic manager. The manager can be used -to set and get the status of the send thread. - */ -@interface BindingsDummyTraffic : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client maxNumMessages:(long)maxNumMessages avgSendDeltaMS:(long)avgSendDeltaMS randomRangeMS:(long)randomRangeMS; -/** - * GetStatus returns the current state of the dummy traffic send thread. It has -the following return values: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Note that this function does not return the status set by SetStatus directly; -it returns the current status of the send thread, which means any call to -SetStatus will have a small delay before it is returned by GetStatus. - */ -- (BOOL)getStatus; -/** - * SetStatus sets the state of the dummy traffic send thread, which determines -if the thread is running or paused. The possible statuses are: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Returns an error if the channel is full. -Note that this function cannot change the status of the send thread if it has -yet to be started or stopped. - */ -- (BOOL)setStatus:(BOOL)status error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsFact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -- (nullable instancetype)init:(long)factType factStr:(NSString* _Nullable)factStr; -- (NSString* _Nonnull)get; -- (NSString* _Nonnull)stringify; -- (long)type; -@end - -@interface BindingsFactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * FactList - */ -- (nullable instancetype)init; -- (BOOL)add:(NSString* _Nullable)factData factType:(long)factType error:(NSError* _Nullable* _Nullable)error; -- (BindingsFact* _Nullable)get:(long)i; -- (long)num; -- (NSString* _Nonnull)stringify:(NSError* _Nullable* _Nullable)error; -@end - -/** - * FilePartTracker contains the interfaces.FilePartTracker. - */ -@interface BindingsFilePartTracker : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetNumParts returns the total number of file parts in the transfer. - */ -- (long)getNumParts; -/** - * GetPartStatus returns the status of the file part with the given part number. -The possible values for the status are: -0 = unsent -1 = sent (sender has sent a part, but it has not arrived) -2 = arrived (sender has sent a part, and it has arrived) -3 = received (receiver has received a part) - */ -- (long)getPartStatus:(long)partNum; -@end - -/** - * FileTransfer contains the file transfer manager. - */ -@interface BindingsFileTransfer : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client receiveFunc:(id<BindingsFileTransferReceiveFunc> _Nullable)receiveFunc parameters:(NSString* _Nullable)parameters; -/** - * CloseSend deletes a sent file transfer from the sent transfer map and from -storage once a transfer has completed or reached the retry limit. Returns an -error if the transfer has not run out of retries. - */ -- (BOOL)closeSend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * GetMaxFileNameByteLength returns the maximum length, in bytes, allowed for a -file name. - */ -- (long)getMaxFileNameByteLength; -/** - * GetMaxFilePreviewSize returns the maximum file preview size, in bytes. - */ -- (long)getMaxFilePreviewSize; -/** - * GetMaxFileSize returns the maximum file size, in bytes, allowed to be -transferred. - */ -- (long)getMaxFileSize; -/** - * GetMaxFileTypeByteLength returns the maximum length, in bytes, allowed for a -file type. - */ -- (long)getMaxFileTypeByteLength; -/** - * Receive returns the fully assembled file on the completion of the transfer. -It deletes the transfer from the received transfer map and from storage. -Returns an error if the transfer is not complete, the full file cannot be -verified, or if the transfer cannot be found. - */ -- (NSData* _Nullable)receive:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterReceiveProgressCallback allows for the registration of a callback to -track the progress of an individual received file transfer. The callback will -be called immediately when added to report the current status of the -transfer. It will then call every time a file part is received, the transfer -completes, or an error occurs. It is called at most once ever period, which -means if events occur faster than the period, then they will not be reported -and instead, the progress will be reported once at the end of the period. -Once the callback reports that the transfer has completed, the recipient -can get the full file by calling Receive. -The period is specified in milliseconds. - */ -- (BOOL)registerReceiveProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferReceivedProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterSendProgressCallback allows for the registration of a callback to -track the progress of an individual sent file transfer. The callback will be -called immediately when added to report the current status of the transfer. -It will then call every time a file part is sent, a file part arrives, the -transfer completes, or an error occurs. It is called at most once every -period, which means if events occur faster than the period, then they will -not be reported and instead, the progress will be reported once at the end of -the period. -The period is specified in milliseconds. - */ -- (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends a file to the recipient. The sender must have an E2E relationship -with the recipient. -The file name is the name of the file to show a user. It has a max length of -48 bytes. -The file type identifies what type of file is being sent. It has a max length -of 8 bytes. -The file data cannot be larger than 256 kB -The retry float is the total amount of data to send relative to the data -size. Data will be resent on error and will resend up to [(1 + retry) * -fileSize]. -The preview stores a preview of the data (such as a thumbnail) and is -capped at 4 kB in size. -Returns a unique transfer ID used to identify the transfer. -PeriodMS is the duration, in milliseconds, to wait between progress callback -calls. Set this large enough to prevent spamming. - */ -- (NSData* _Nullable)send:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType fileData:(NSData* _Nullable)fileData recipientID:(NSData* _Nullable)recipientID retry:(float)retry preview:(NSData* _Nullable)preview progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Group structure contains the identifying and membership information of a -group chat. - */ -@interface BindingsGroup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetCreatedMS returns the time the group was created in milliseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedMS; -/** - * GetCreatedNano returns the time the group was created in nanoseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedNano; -/** - * GetID return the 33-byte unique group ID. - */ -- (NSData* _Nullable)getID; -/** - * GetInitMessage returns initial message sent with the group request. - */ -- (NSData* _Nullable)getInitMessage; -/** - * GetMembership returns a list of contacts, one for each member in the group. -The list is in order; the first contact is the leader/creator of the group. -All subsequent members are ordered by their ID. - */ -- (BindingsGroupMembership* _Nullable)getMembership; -/** - * GetName returns the name set by the user for the group. - */ -- (NSData* _Nullable)getName; -/** - * Serialize serializes the Group. - */ -- (NSData* _Nullable)serialize; -@end - -/** - * GroupChat object contains the group chat manager. - */ -@interface BindingsGroupChat : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetGroup returns the group with the group ID. If no group exists, then the -error "failed to find group" is returned. - */ -- (BindingsGroup* _Nullable)getGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * GetGroups returns an IdList containing a list of group IDs that the user is a -part of. - */ -- (BindingsIdList* _Nullable)getGroups; -/** - * JoinGroup allows a user to join a group when they receive a request. The -caller must pass in the serialized bytes of a Group. - */ -- (BOOL)joinGroup:(NSData* _Nullable)serializedGroupData error:(NSError* _Nullable* _Nullable)error; -/** - * LeaveGroup deletes a group so a user no longer has access. - */ -- (BOOL)leaveGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * MakeGroup creates a new group and sends a group request to all members in the -group. The ID of the new group, the rounds the requests were sent on, and the -status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)makeGroup:(BindingsIdList* _Nullable)membership name:(NSData* _Nullable)name message:(NSData* _Nullable)message; -/** - * NumGroups returns the number of groups the user is a part of. - */ -- (long)numGroups; -/** - * ResendRequest resends a group request to all members in the group. The rounds -they were sent on and the status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)resendRequest:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends the message to the specified group. Returns the round the messages -were sent on. - */ -- (BindingsGroupSendReport* _Nullable)send:(NSData* _Nullable)groupIdBytes message:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * // -Member Structure -// -GroupMember represents a member in the group membership list. - */ -@interface BindingsGroupMember : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMember.Member with unsupported type: gitlab.com/elixxir/crypto/group.Member - -// skipped method GroupMember.DeepCopy with unsupported parameter or return types - -// skipped method GroupMember.Equal with unsupported parameter or return types - -/** - * GetDhKey returns the byte representation of the public Diffie–Hellman key of -the member. - */ -- (NSData* _Nullable)getDhKey; -/** - * GetID returns the 33-byte user ID of the member. - */ -- (NSData* _Nullable)getID; -- (NSString* _Nonnull)goString; -- (NSData* _Nullable)serialize; -- (NSString* _Nonnull)string; -@end - -/** - * GroupMembership structure contains a list of members that are part of a -group. The first member is the group leader. - */ -@interface BindingsGroupMembership : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Get returns the member at the index. The member at index 0 is always the -group leader. An error is returned if the index is out of range. - */ -- (BindingsGroupMember* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of members in the group membership. - */ -- (long)len; -@end - -/** - * GroupMessageReceive contains a group message, its ID, and its data that a -user receives. - */ -@interface BindingsGroupMessageReceive : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMessageReceive.MessageReceive with unsupported type: gitlab.com/elixxir/client/groupChat.MessageReceive - -/** - * GetEphemeralID returns the ephemeral ID of the recipient. - */ -- (int64_t)getEphemeralID; -/** - * GetGroupID returns the 33-byte group ID. - */ -- (NSData* _Nullable)getGroupID; -/** - * GetMessageID returns the message ID. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetPayload returns the message payload. - */ -- (NSData* _Nullable)getPayload; -/** - * GetRecipientID returns the 33-byte user ID of the recipient. - */ -- (NSData* _Nullable)getRecipientID; -/** - * GetRoundID returns the ID of the round the message was sent on. - */ -- (int64_t)getRoundID; -/** - * GetRoundTimestampMS returns the timestamp, in milliseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the timestamp, in nanoseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the ID of the round the message was sent on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSenderID returns the 33-byte user ID of the sender. - */ -- (NSData* _Nullable)getSenderID; -/** - * GetTimestampMS returns the message timestamp in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message timestamp in nanoseconds. - */ -- (int64_t)getTimestampNano; -- (NSString* _Nonnull)string; -@end - -@interface BindingsGroupReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable grpId; -@property (nonatomic) long status; -@end - -/** - * GroupSendReport is returned when sending a group message. It contains the -round ID sent on and the timestamp of the send. - */ -@interface BindingsGroupSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetMessageID returns the ID of the round that the send occurred on. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetRoundID returns the ID of the round that the send occurred on. - */ -- (int64_t)getRoundID; -/** - * GetRoundURL returns the URL of the round that the send occurred on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the timestamp of the send in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the timestamp of the send in nanoseconds. - */ -- (int64_t)getTimestampNano; -@end - -/** - * ID list -IdList contains a list of IDs. - */ -@interface BindingsIdList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Add appends the ID bytes to the end of the list. - */ -- (BOOL)add:(NSData* _Nullable)idBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Get returns the ID at the index. An error is returned if the index is out of -range. - */ -- (NSData* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of IDs in the list. - */ -- (long)len; -@end - -@interface BindingsIntList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (void)add:(long)i; -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -@interface BindingsManyNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsNotificationForMeReport* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -/** - * Message is a message received from the cMix network in the clear -or that has been decrypted using established E2E keys. - */ -@interface BindingsMessage : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetID returns the id of the message - */ -- (NSData* _Nullable)getID; -/** - * GetMessageType returns the message's type - */ -- (long)getMessageType; -/** - * GetPayload returns the message's payload/contents - */ -- (NSData* _Nullable)getPayload; -/** - * GetRoundId returns the message's round ID - */ -- (int64_t)getRoundId; -/** - * GetRoundTimestampMS returns the message's round timestamp in milliseconds - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the message's round timestamp in nanoseconds - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the message's round URL - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSender returns the message's sender ID, if available - */ -- (NSData* _Nullable)getSender; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -@end - -/** - * NewGroupReport is returned when creating a new group and contains the ID of -the group, a list of rounds that the group requests were sent on, and the -status of the send. - */ -@interface BindingsNewGroupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetError returns the string of an error. -Will be an empty string if no error occured - */ -- (NSString* _Nonnull)getError; -/** - * GetGroup returns the Group. - */ -- (BindingsGroup* _Nullable)getGroup; -/** - * GetRoundList returns the RoundList containing a list of rounds requests were -sent on. - */ -- (BindingsRoundList* _Nullable)getRoundList; -/** - * GetStatus returns the status of the requests sent when creating a new group. -status = 0 an error occurred before any requests could be sent - 1 all requests failed to send (call Resend Group) - 2 some request failed and some succeeded (call Resend Group) - 3, all requests sent successfully (call Resend Group) - */ -- (long)getStatus; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * NodeRegistrationsStatus structure for returning node registration statuses -for bindings. - */ -@interface BindingsNodeRegistrationsStatus : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetRegistered returns the number of nodes registered with the client. - */ -- (long)getRegistered; -/** - * GetTotal return the total of nodes currently in the network. - */ -- (long)getTotal; -@end - -@interface BindingsNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)forMe; -- (NSData* _Nullable)source; -- (NSString* _Nonnull)type; -@end - -/** - * RestoreContactsReport is a gomobile friendly report structure -for determining which IDs restored, which failed, and why. - */ -@interface BindingsRestoreContactsReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetErrorAt returns the error string at index - */ -- (NSString* _Nonnull)getErrorAt:(long)index; -/** - * GetFailedAt returns the failed ID at index - */ -- (NSData* _Nullable)getFailedAt:(long)index; -/** - * GetRestoreContactsError returns an error string. Empty if no error. - */ -- (NSString* _Nonnull)getRestoreContactsError; -/** - * GetRestoredAt returns the restored ID at index - */ -- (NSData* _Nullable)getRestoredAt:(long)index; -/** - * LenFailed returns the length of the ID's failed. - */ -- (long)lenFailed; -/** - * LenRestored returns the length of ID's restored. - */ -- (long)lenRestored; -@end - -@interface BindingsRoundList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * the send report is the mechanisim by which sendE2E returns a single - */ -@interface BindingsSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (NSData* _Nullable)getMessageID; -- (BindingsRoundList* _Nullable)getRoundList; -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsSendReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field SendReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable mid; -@property (nonatomic) int64_t ts; -@end - -/** - * Generic Unregister - a generic return used for all callbacks which can be -unregistered -Interface which allows the un-registration of a listener - */ -@interface BindingsUnregister : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Call unregisters a callback - */ -- (void)unregister; -@end - -@interface BindingsUser : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsContact* _Nullable)getContact; -- (NSData* _Nullable)getE2EDhPrivateKey; -- (NSData* _Nullable)getE2EDhPublicKey; -- (NSData* _Nullable)getReceptionID; -- (NSData* _Nullable)getReceptionRSAPrivateKeyPem; -- (NSData* _Nullable)getReceptionRSAPublicKeyPem; -- (NSData* _Nullable)getReceptionSalt; -- (NSData* _Nullable)getTransmissionID; -- (NSData* _Nullable)getTransmissionRSAPrivateKeyPem; -- (NSData* _Nullable)getTransmissionRSAPublicKeyPem; -- (NSData* _Nullable)getTransmissionSalt; -- (BOOL)isPrecanned; -@end - -@interface BindingsUserDiscovery : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)init:(BindingsClient* _Nullable)client; -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; -/** - * AddFact adds a fact for the user to user discovery. Will only succeed if the -user is already registered and the system does not have the fact currently -registered for any user. -Will fail if the fact string is not well formed. -This does not complete the fact registration process, it returns a -confirmation id instead. Over the communications system the fact is -associated with, a code will be sent. This confirmation ID needs to be -called along with the code to finalize the fact. - */ -- (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from -AddFact while the code will come over the associated communications system - */ -- (BOOL)confirmFact:(NSString* _Nullable)confirmationID code:(NSString* _Nullable)code error:(NSError* _Nullable* _Nullable)error; -/** - * Lookup the contact object associated with the given userID. The -id is the byte representation of an id. -This will reject if that id is malformed. The LookupCallback will return -the associated contact if it exists. - */ -- (BOOL)lookup:(NSData* _Nullable)idBytes callback:(id<BindingsLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * MultiLookup Looks for the contact object associated with all given userIDs. -The ids are the byte representation of an id stored in an IDList object. -This will reject if that id is malformed or if the indexing on the IDList -object is wrong. The MultiLookupCallback will return with all contacts -returned within the timeout. - */ -- (BOOL)multiLookup:(BindingsIdList* _Nullable)ids callback:(id<BindingsMultiLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * Register registers a user with user discovery. Will return an error if the -network signatures are malformed or if the username is taken. Usernames -cannot be changed after registration at this time. Will fail if the user is -already registered. -Identity does not go over cmix, it occurs over normal communications - */ -- (BOOL)register:(NSString* _Nullable)username error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveFact removes a previously confirmed fact. Will fail if the passed fact string is -not well-formed or if the fact is not associated with this client. -Users cannot remove username facts and must instead remove the user. - */ -- (BOOL)removeFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveUser deletes a user. The fact sent must be the username. -This function preserves the username forever and makes it -unusable. - */ -- (BOOL)removeUser:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * Search for the passed Facts. The factList is the stringification of a -fact list object, look at /bindings/list.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This is NOT intended to be used to search for multiple users at once, that -can have a privacy reduction. Instead, it is intended to be used to search -for a user where multiple pieces of information is known. - */ -- (BOOL)search:(NSString* _Nullable)fl callback:(id<BindingsSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SearchSingle searches for the passed Facts. The fact is the stringification of a -fact object, look at /bindings/contact.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This only searches for a single fact at a time. It is intended to make some -simple use cases of the API easier. - */ -- (BOOL)searchSingle:(NSString* _Nullable)f callback:(id<BindingsSingleSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SetAlternativeUserDiscovery sets the alternativeUd object within manager. -Once set, any user discovery operation will go through the alternative -user discovery service. -To undo this operation, use UnsetAlternativeUserDiscovery. -The contact file is the already read in bytes, not the file path for the contact file. - */ -- (BOOL)setAlternativeUserDiscovery:(NSData* _Nullable)address cert:(NSData* _Nullable)cert contactFile:(NSData* _Nullable)contactFile error:(NSError* _Nullable* _Nullable)error; -/** - * UnsetAlternativeUserDiscovery clears out the information from -the Manager object. - */ -- (BOOL)unsetAlternativeUserDiscovery:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Error codes - */ -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedCode; -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedMessage; - -/** - * CompressJpeg takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpeg(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * CompressJpegForPreview takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpegForPreview(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * DownloadAndVerifySignedNdfWithUrl retrieves the NDF from a specified URL. -The NDF is processed into a protobuf containing a signature which -is verified using the cert string passed in. The NDF is returned as marshaled -byte data which may be used to start a client. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadAndVerifySignedNdfWithUrl(NSString* _Nullable url, NSString* _Nullable cert, NSError* _Nullable* _Nullable error); - -/** - * DownloadDAppRegistrationDB returns a []byte containing -the JSON data describing registered dApps. -See https://git.xx.network/elixxir/registered-dapps - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadDAppRegistrationDB(NSError* _Nullable* _Nullable error); - -/** - * DownloadErrorDB returns a []byte containing the JSON data -describing client errors. -See https://git.xx.network/elixxir/client-error-database/ - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadErrorDB(NSError* _Nullable* _Nullable error); - -/** - * DumpStack returns a string with the stack trace of every running thread. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsDumpStack(NSError* _Nullable* _Nullable error); - -/** - * EnableGrpcLogs sets GRPC trace logging - */ -FOUNDATION_EXPORT void BindingsEnableGrpcLogs(id<BindingsLogWriter> _Nullable writer); - -/** - * ErrorStringToUserFriendlyMessage takes a passed in errStr which will be -a backend generated error. These may be error specifically written by -the backend team or lower level errors gotten from low level dependencies. -This function will parse the error string for common errors provided from -errToUserErr to provide a more user-friendly error message for the front end. -If the error is not common, some simple parsing is done on the error message -to make it more user-accessible, removing backend specific jargon. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsErrorStringToUserFriendlyMessage(NSString* _Nullable errStr); - -/** - * GenerateSecret creates a secret password using a system-based -pseudorandom number generator. It takes 1 parameter, `numBytes`, -which should be set to 32, but can be set higher in certain cases. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateSecret(long numBytes); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetCMIXParams(NSError* _Nullable* _Nullable error); - -/** - * returns a previously created client. IF be used if the garbage collector -removes the client instance on the app side. Is NOT thread safe relative to -login, newClient, or newPrecannedClient - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsGetClientSingleton(void); - -/** - * GetDependencies returns the api DEPENDENCIES - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetDependencies(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetE2EParams(NSError* _Nullable* _Nullable error); - -/** - * GetGitVersion rturns the api GITVERSION - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetGitVersion(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetNetworkParams(NSError* _Nullable* _Nullable error); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetUnsafeParams(NSError* _Nullable* _Nullable error); - -/** - * GetVersion returns the api SEMVER - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void); - -/** - * InitializeBackup starts the backup processes that returns backup updates when -they occur. Any time an event occurs that changes the contents of the backup, -such as adding or deleting a contact, the backup is triggered and an -encrypted backup is generated and returned on the updateBackupCb callback. -Call this function only when enabling backup if it has not already been -initialized or when the user wants to change their password. -To resume backup process on app recovery, use ResumeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsInitializeBackup(NSString* _Nullable password, id<BindingsUpdateBackupFunc> _Nullable updateBackupCb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * LoadSecretWithMnemonic loads the secret stored from the call to -StoreSecretWithMnemonic. The path given should be the same filepath -as the path given in StoreSecretWithMnemonic. There should be a file -in this path called ".recovery". This operation is not tied -to client operations, as the user will not have a client when trying to -recover their account. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsLoadSecretWithMnemonic(NSString* _Nullable mnemonic, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * sets level of logging. All logs the set level and above will be displayed -options are: - TRACE - 0 - DEBUG - 1 - INFO - 2 - WARN - 3 - ERROR - 4 - CRITICAL - 5 - FATAL - 6 -The default state without updates is: INFO - */ -FOUNDATION_EXPORT BOOL BindingsLogLevel(long level, NSError* _Nullable* _Nullable error); - -/** - * Login will load an existing client from the storageDir -using the password. This will fail if the client doesn't exist or -the password is incorrect. -The password is passed as a byte array so that it can be cleared from -memory and stored as securely as possible using the memguard library. -Login does not block on network connection, and instead loads and -starts subprocesses to perform network operations. - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsLogin(NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * MakeIdList creates a new empty IdList. - */ -FOUNDATION_EXPORT BindingsIdList* _Nullable BindingsMakeIdList(void); - -FOUNDATION_EXPORT BindingsIntList* _Nullable BindingsMakeIntList(void); - -/** - * NewClient creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable regCode, NSError* _Nullable* _Nullable error); - -/** - * NewClientFromBackup constructs a new Client from an encrypted backup. The backup -is decrypted using the backupPassphrase. On success a successful client creation, -the function will return a JSON encoded list of the E2E partners -contained in the backup and a json-encoded string of the parameters stored in the backup - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); - -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -FOUNDATION_EXPORT BindingsDummyTraffic* _Nullable BindingsNewDummyTrafficManager(BindingsClient* _Nullable client, long maxNumMessages, long avgSendDeltaMS, long randomRangeMS, NSError* _Nullable* _Nullable error); - -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -FOUNDATION_EXPORT BindingsFact* _Nullable BindingsNewFact(long factType, NSString* _Nullable factStr, NSError* _Nullable* _Nullable error); - -/** - * FactList - */ -FOUNDATION_EXPORT BindingsFactList* _Nullable BindingsNewFactList(void); - -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -FOUNDATION_EXPORT BindingsFileTransfer* _Nullable BindingsNewFileTransferManager(BindingsClient* _Nullable client, id<BindingsFileTransferReceiveFunc> _Nullable receiveFunc, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * NewGroupManager creates a new group chat manager. - */ -FOUNDATION_EXPORT BindingsGroupChat* _Nullable BindingsNewGroupManager(BindingsClient* _Nullable client, id<BindingsGroupRequestFunc> _Nullable requestFunc, id<BindingsGroupReceiveFunc> _Nullable receiveFunc, NSError* _Nullable* _Nullable error); - -/** - * NewPrecannedClient creates an insecure user with predetermined keys with nodes -It creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewPrecannedClient(long precannedID, NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); - -/** - * NotificationsForMe Check if a notification received is for me -It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, -a Type, and a source. These are as follows: - TYPE SOURCE DESCRIPTION - "default" recipient user ID A message with no association - "request" sender user ID A channel request has been received - "reset" sender user ID A channel reset has been received - "confirm" sender user ID A channel request has been accepted - "silent" sender user ID A message which should not be notified on - "e2e" sender user ID reception of an E2E message - "group" group ID reception of a group chat message - "endFT" sender user ID Last message sent confirming end of file transfer - "groupRQ" sender user ID Request from sender to join a group chat - */ -FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotificationsForMe(NSString* _Nullable notifCSV, NSString* _Nullable preimages, NSError* _Nullable* _Nullable error); - -/** - * RegisterLogWriter registers a callback on which logs are written. - */ -FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); - -/** - * RestoreContactsFromBackup takes as input the jason output of the -`NewClientFromBackup` function, unmarshals it into IDs, looks up -each ID in user discovery, and initiates a session reset request. -This function will not return until every id in the list has been sent a -request. It should be called again and again until it completes. -xxDK users should not use this function. This function is used by -the mobile phone apps and are not intended to be part of the xxDK. It -should be treated as internal functions specific to the phone apps. - */ -FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); - -/** - * ResumeBackup starts the backup processes back up with a new callback after it -has been initialized. -Call this function only when resuming a backup that has already been -initialized or to replace the callback. -To start the backup for the first time or to use a new password, use -InitializeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsResumeBackup(id<BindingsUpdateBackupFunc> _Nullable cb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * SetTimeSource sets the network time to a custom source. - */ -FOUNDATION_EXPORT void BindingsSetTimeSource(id<BindingsTimeSource> _Nullable timeNow); - -/** - * StoreSecretWithMnemonic stores the secret tied with the mnemonic to storage. -Unlike other storage operations, this does not use EKV, as that is -intrinsically tied to client operations, which the user will not have while -trying to recover their account. As such, we store the encrypted data -directly, with a specified path. Path will be a valid filepath in which the -recover file will be stored as ".recovery". - -As an example, given "home/user/xxmessenger/storagePath", -the recovery file will be stored at -"home/user/xxmessenger/storagePath/.recovery" - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsStoreSecretWithMnemonic(NSData* _Nullable secret, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled contact object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsContact* _Nullable BindingsUnmarshalContact(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled send report object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsSendReport* _Nullable BindingsUnmarshalSendReport(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * UpdateCommonErrors takes the passed in contents of a JSON file and updates the -errToUserErr map with the contents of the json file. The JSON's expected format -conform with the commented examples provides in errToUserErr above. -NOTE that you should not pass in a file path, but a preloaded JSON file - */ -FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, NSError* _Nullable* _Nullable error); - -// skipped function WrapAPIClient with unsupported parameter or return types - - -// skipped function WrapUserDiscovery with unsupported parameter or return types - - -@class BindingsAuthConfirmCallback; - -@class BindingsAuthRequestCallback; - -@class BindingsAuthResetNotificationCallback; - -@class BindingsClientError; - -@class BindingsEventCallbackFunctionObject; - -@class BindingsFileTransferReceiveFunc; - -@class BindingsFileTransferReceivedProgressFunc; - -@class BindingsFileTransferSentProgressFunc; - -@class BindingsGroupReceiveFunc; - -@class BindingsGroupRequestFunc; - -@class BindingsListener; - -@class BindingsLogWriter; - -@class BindingsLookupCallback; - -@class BindingsMessageDeliveryCallback; - -@class BindingsMultiLookupCallback; - -@class BindingsNetworkHealthCallback; - -@class BindingsPreimageNotification; - -@class BindingsRestoreContactsUpdater; - -@class BindingsRoundCompletionCallback; - -@class BindingsRoundEventCallback; - -@class BindingsSearchCallback; - -@class BindingsSingleSearchCallback; - -@class BindingsTimeSource; - -@class BindingsUpdateBackupFunc; - -/** - * AuthConfirmCallback notifies the register whenever they receive an auth -request confirmation - */ -@interface BindingsAuthConfirmCallback : NSObject <goSeqRefInterface, BindingsAuthConfirmCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthRequestCallback : NSObject <goSeqRefInterface, BindingsAuthRequestCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@interface BindingsClientError : NSObject <goSeqRefInterface, BindingsClientError> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -/** - * EventCallbackFunctionObject bindings interface which contains function -that implements the EventCallbackFunction - */ -@interface BindingsEventCallbackFunctionObject : NSObject <goSeqRefInterface, BindingsEventCallbackFunctionObject> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -/** - * FileTransferReceiveFunc contains a function callback that notifies the -receiver of an incoming file transfer. It is called on the reception of the -initial file transfer message. - */ -@interface BindingsFileTransferReceiveFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -/** - * FileTransferReceivedProgressFunc contains a function callback that tracks the -progress of receiving a file. It is called when a file part is received, the -transfer completes, or on error. - */ -@interface BindingsFileTransferReceivedProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceivedProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * FileTransferSentProgressFunc contains a function callback that tracks the -progress of sending a file. It is called when a file part is sent, a file -part arrives, the transfer completes, or on error. - */ -@interface BindingsFileTransferSentProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferSentProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * GroupReceiveFunc contains a function callback that is called when a group -message is received. - */ -@interface BindingsGroupReceiveFunc : NSObject <goSeqRefInterface, BindingsGroupReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -/** - * GroupRequestFunc contains a function callback that is called when a group -request is received. - */ -@interface BindingsGroupRequestFunc : NSObject <goSeqRefInterface, BindingsGroupRequestFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -/** - * Listener provides a callback to hear a message -An object implementing this interface can be called back when the client -gets a message of the type that the registerer specified at registration -time. - */ -@interface BindingsListener : NSObject <goSeqRefInterface, BindingsListener> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@interface BindingsLogWriter : NSObject <goSeqRefInterface, BindingsLogWriter> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)log:(NSString* _Nullable)p0; -@end - -/** - * LookupCallback returns the result of a single lookup - */ -@interface BindingsLookupCallback : NSObject <goSeqRefInterface, BindingsLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -/** - * MessageDeliveryCallback gets called on the determination if all events -related to a message send were successful. - */ -@interface BindingsMessageDeliveryCallback : NSObject <goSeqRefInterface, BindingsMessageDeliveryCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -/** - * MultiLookupCallback returns the result of many parallel lookups - */ -@interface BindingsMultiLookupCallback : NSObject <goSeqRefInterface, BindingsMultiLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -/** - * A callback when which is used to receive notification if network health -changes - */ -@interface BindingsNetworkHealthCallback : NSObject <goSeqRefInterface, BindingsNetworkHealthCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BOOL)p0; -@end - -@interface BindingsPreimageNotification : NSObject <goSeqRefInterface, BindingsPreimageNotification> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -/** - * RestoreContactsUpdater interface provides a callback function -for receiving update information from RestoreContactsFromBackup. - */ -@interface BindingsRestoreContactsUpdater : NSObject <goSeqRefInterface, BindingsRestoreContactsUpdater> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -/** - * RoundCompletionCallback is returned when the completion of a round is known. - */ -@interface BindingsRoundCompletionCallback : NSObject <goSeqRefInterface, BindingsRoundCompletionCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -/** - * RoundEventCallback handles waiting on the exact state of a round on -the cMix network. - */ -@interface BindingsRoundEventCallback : NSObject <goSeqRefInterface, BindingsRoundEventCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -/** - * SearchCallback returns the result of a search - */ -@interface BindingsSearchCallback : NSObject <goSeqRefInterface, BindingsSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -/** - * SingleSearchCallback returns the result of a single search - */ -@interface BindingsSingleSearchCallback : NSObject <goSeqRefInterface, BindingsSingleSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@interface BindingsTimeSource : NSObject <goSeqRefInterface, BindingsTimeSource> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (int64_t)nowMs; -@end - -/** - * UpdateBackupFunc contains a function callback that returns new backups. - */ -@interface BindingsUpdateBackupFunc : NSObject <goSeqRefInterface, BindingsUpdateBackupFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Universe.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Universe.objc.h deleted file mode 100644 index 019e7502d581983722a15bf30799e85cbc5dd766..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/Universe.objc.h +++ /dev/null @@ -1,29 +0,0 @@ -// Objective-C API for talking to Go package. -// gobind -lang=objc -// -// File is generated by gobind. Do not edit. - -#ifndef __Universe_H__ -#define __Universe_H__ - -@import Foundation; -#include "ref.h" - -@protocol Universeerror; -@class Universeerror; - -@protocol Universeerror <NSObject> -- (NSString* _Nonnull)error; -@end - -@class Universeerror; - -@interface Universeerror : NSError <goSeqRefInterface, Universeerror> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (NSString* _Nonnull)error; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/ref.h b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/ref.h deleted file mode 100644 index b8036a4d85c7387f3def61473a071b5d8c4c8208..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Headers/ref.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef __GO_REF_HDR__ -#define __GO_REF_HDR__ - -#include <Foundation/Foundation.h> - -// GoSeqRef is an object tagged with an integer for passing back and -// forth across the language boundary. A GoSeqRef may represent either -// an instance of a Go object, or an Objective-C object passed to Go. -// The explicit allocation of a GoSeqRef is used to pin a Go object -// when it is passed to Objective-C. The Go seq package maintains a -// reference to the Go object in a map keyed by the refnum along with -// a reference count. When the reference count reaches zero, the Go -// seq package will clear the corresponding entry in the map. -@interface GoSeqRef : NSObject { -} -@property(readonly) int32_t refnum; -@property(strong) id obj; // NULL when representing a Go object. - -// new GoSeqRef object to proxy a Go object. The refnum must be -// provided from Go side. -- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; - -- (int32_t)incNum; - -@end - -@protocol goSeqRefInterface --(GoSeqRef*) _ref; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Modules/module.modulemap b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Modules/module.modulemap deleted file mode 100644 index 4316a5b24058edfc18ffb2dc7f7a982e8353441a..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Modules/module.modulemap +++ /dev/null @@ -1,8 +0,0 @@ -framework module "Bindings" { - header "ref.h" - header "Bindings.objc.h" - header "Universe.objc.h" - header "Bindings.h" - - export * -} \ No newline at end of file diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Resources/Info.plist b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Resources/Info.plist deleted file mode 100644 index 0d1a4b8ab9b1fc8e9357197398f73353470cb636..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Resources/Info.plist +++ /dev/null @@ -1,6 +0,0 @@ -<?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> - </dict> - </plist> diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings deleted file mode 100644 index 1e3b1dc25b4d65bf1e21a5d169dc3ae4de6c4fd7..0000000000000000000000000000000000000000 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings and /dev/null differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.h deleted file mode 100644 index 8906a7da239705b790cb2bb64de92f806640cb38..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.h +++ /dev/null @@ -1,13 +0,0 @@ - -// Objective-C API for talking to the following Go packages -// -// gitlab.com/elixxir/client/bindings -// -// File is generated by gomobile bind. Do not edit. -#ifndef __Bindings_FRAMEWORK_H__ -#define __Bindings_FRAMEWORK_H__ - -#include "Bindings.objc.h" -#include "Universe.objc.h" - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.objc.h deleted file mode 100644 index 32bf6d116888f787ced27b01b95cb4e1b2c1138b..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Bindings.objc.h +++ /dev/null @@ -1,2083 +0,0 @@ -// Objective-C API for talking to gitlab.com/elixxir/client/bindings Go package. -// gobind -lang=objc gitlab.com/elixxir/client/bindings -// -// File is generated by gobind. Do not edit. - -#ifndef __Bindings_H__ -#define __Bindings_H__ - -@import Foundation; -#include "ref.h" -#include "Universe.objc.h" - - -@class BindingsBackup; -@class BindingsBackupReport; -@class BindingsClient; -@class BindingsContact; -@class BindingsContactList; -@class BindingsDummyTraffic; -@class BindingsFact; -@class BindingsFactList; -@class BindingsFilePartTracker; -@class BindingsFileTransfer; -@class BindingsGroup; -@class BindingsGroupChat; -@class BindingsGroupMember; -@class BindingsGroupMembership; -@class BindingsGroupMessageReceive; -@class BindingsGroupReportDisk; -@class BindingsGroupSendReport; -@class BindingsIdList; -@class BindingsIntList; -@class BindingsManyNotificationForMeReport; -@class BindingsMessage; -@class BindingsNewGroupReport; -@class BindingsNodeRegistrationsStatus; -@class BindingsNotificationForMeReport; -@class BindingsRestoreContactsReport; -@class BindingsRoundList; -@class BindingsSendReport; -@class BindingsSendReportDisk; -@class BindingsUnregister; -@class BindingsUser; -@class BindingsUserDiscovery; -@protocol BindingsAuthConfirmCallback; -@class BindingsAuthConfirmCallback; -@protocol BindingsAuthRequestCallback; -@class BindingsAuthRequestCallback; -@protocol BindingsAuthResetNotificationCallback; -@class BindingsAuthResetNotificationCallback; -@protocol BindingsClientError; -@class BindingsClientError; -@protocol BindingsEventCallbackFunctionObject; -@class BindingsEventCallbackFunctionObject; -@protocol BindingsFileTransferReceiveFunc; -@class BindingsFileTransferReceiveFunc; -@protocol BindingsFileTransferReceivedProgressFunc; -@class BindingsFileTransferReceivedProgressFunc; -@protocol BindingsFileTransferSentProgressFunc; -@class BindingsFileTransferSentProgressFunc; -@protocol BindingsGroupReceiveFunc; -@class BindingsGroupReceiveFunc; -@protocol BindingsGroupRequestFunc; -@class BindingsGroupRequestFunc; -@protocol BindingsListener; -@class BindingsListener; -@protocol BindingsLogWriter; -@class BindingsLogWriter; -@protocol BindingsLookupCallback; -@class BindingsLookupCallback; -@protocol BindingsMessageDeliveryCallback; -@class BindingsMessageDeliveryCallback; -@protocol BindingsMultiLookupCallback; -@class BindingsMultiLookupCallback; -@protocol BindingsNetworkHealthCallback; -@class BindingsNetworkHealthCallback; -@protocol BindingsPreimageNotification; -@class BindingsPreimageNotification; -@protocol BindingsRestoreContactsUpdater; -@class BindingsRestoreContactsUpdater; -@protocol BindingsRoundCompletionCallback; -@class BindingsRoundCompletionCallback; -@protocol BindingsRoundEventCallback; -@class BindingsRoundEventCallback; -@protocol BindingsSearchCallback; -@class BindingsSearchCallback; -@protocol BindingsSingleSearchCallback; -@class BindingsSingleSearchCallback; -@protocol BindingsTimeSource; -@class BindingsTimeSource; -@protocol BindingsUpdateBackupFunc; -@class BindingsUpdateBackupFunc; - -@protocol BindingsAuthConfirmCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -@protocol BindingsAuthRequestCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsAuthResetNotificationCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsClientError <NSObject> -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -@protocol BindingsEventCallbackFunctionObject <NSObject> -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -@protocol BindingsFileTransferReceiveFunc <NSObject> -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -@protocol BindingsFileTransferReceivedProgressFunc <NSObject> -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsFileTransferSentProgressFunc <NSObject> -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsGroupReceiveFunc <NSObject> -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -@protocol BindingsGroupRequestFunc <NSObject> -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -@protocol BindingsListener <NSObject> -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@protocol BindingsLogWriter <NSObject> -- (void)log:(NSString* _Nullable)p0; -@end - -@protocol BindingsLookupCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsMessageDeliveryCallback <NSObject> -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -@protocol BindingsMultiLookupCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -@protocol BindingsNetworkHealthCallback <NSObject> -- (void)callback:(BOOL)p0; -@end - -@protocol BindingsPreimageNotification <NSObject> -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -@protocol BindingsRestoreContactsUpdater <NSObject> -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -@protocol BindingsRoundCompletionCallback <NSObject> -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -@protocol BindingsRoundEventCallback <NSObject> -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -@protocol BindingsSearchCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -@protocol BindingsSingleSearchCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsTimeSource <NSObject> -- (int64_t)nowMs; -@end - -@protocol BindingsUpdateBackupFunc <NSObject> -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -@interface BindingsBackup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * AddJson stores a passed in json string in the backup structure - */ -- (void)addJson:(NSString* _Nullable)json; -/** - * IsBackupRunning returns true if the backup has been initialized and is -running. Returns false if it has been stopped. - */ -- (BOOL)isBackupRunning; -/** - * StopBackup stops the backup processes and deletes the user's password from -storage. To enable backups again, call InitializeBackup. - */ -- (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsBackupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID - -@property (nonatomic) NSString* _Nonnull params; -@end - -/** - * BindingsClient wraps the api.Client, implementing additional functions -to support the gomobile Client interface - */ -@interface BindingsClient : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)confirmAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteAllRequests clears all requests from Client's auth storage. - */ -- (BOOL)deleteAllRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteContact is a function which removes a contact from Client's storage - */ -- (BOOL)deleteContact:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteReceiveRequests clears receive requests from Client's auth storage. - */ -- (BOOL)deleteReceiveRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteRequest will delete a request, agnostic of request type -for the given partner ID. If no request exists for this -partner ID an error will be returned. - */ -- (BOOL)deleteRequest:(NSData* _Nullable)requesterUserId error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteSentRequests clears sent requests from Client's auth storage. - */ -- (BOOL)deleteSentRequests:(NSError* _Nullable* _Nullable)error; -// skipped method Client.GetInternalClient with unsupported parameter or return types - -/** - * GetNodeRegistrationStatus returns a struct with the number of nodes the -client is registered with and the number total. - */ -- (BindingsNodeRegistrationsStatus* _Nullable)getNodeRegistrationStatus:(NSError* _Nullable* _Nullable)error; -/** - * GetPartners returns a list of - */ -- (NSData* _Nullable)getPartners:(NSError* _Nullable* _Nullable)error; -/** - * GetPreferredBins returns the geographic bin or bins that the provided two -character country code is a part of. The bins are returned as CSV. - */ -- (NSString* _Nonnull)getPreferredBins:(NSString* _Nullable)countryCode error:(NSError* _Nullable* _Nullable)error; -- (NSString* _Nonnull)getPreimages:(NSData* _Nullable)identity; -// skipped method Client.GetRateLimitParams with unsupported parameter or return types - -- (NSString* _Nonnull)getRelationshipFingerprint:(NSData* _Nullable)partnerID error:(NSError* _Nullable* _Nullable)error; -/** - * Returns a user object from which all information about the current user -can be gleaned - */ -- (BindingsUser* _Nullable)getUser; -/** - * HasRunningProcessies checks if any background threads are running. -returns true if none are running. This is meant to be -used when NetworkFollowerStatus() returns Stopping. -Due to the handling of comms on iOS, where the OS can -block indefiently, it may not enter the stopped -state apropreatly. This can be used instead. - */ -- (BOOL)hasRunningProcessies; -/** - * returns true if the network is read to be in a healthy state where -messages can be sent - */ -- (BOOL)isNetworkHealthy; -- (BindingsContact* _Nullable)makePrecannedAuthenticatedChannel:(long)precannedID error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the state of the network follower. Returns: -Stopped - 0 -Starting - 1000 -Running - 2000 -Stopping - 3000 - */ -- (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; -/** - * RegisterClientErrorCallback registers the callback to handle errors from the -long running threads controlled by StartNetworkFollower and StopNetworkFollower - */ -- (void)registerClientErrorCallback:(id<BindingsClientError> _Nullable)clientError; -/** - * RegisterEventCallback records the given function to receive -ReportableEvent objects. It returns the internal index -of the callback so that it can be deleted later. - */ -- (BOOL)registerEventCallback:(NSString* _Nullable)name myObj:(id<BindingsEventCallbackFunctionObject> _Nullable)myObj error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterForNotifications accepts firebase messaging token - */ -- (BOOL)registerForNotifications:(NSString* _Nullable)token error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterListener records and installs a listener for messages -matching specific uid, msgType, and/or username -Returns a ListenerUnregister interface which can be - -to register for any userID, pass in an id with length 0 or an id with -all zeroes - -to register for any message type, pass in a message type of 0 - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsUnregister* _Nullable)registerListener:(NSData* _Nullable)uid msgType:(long)msgType listener:(id<BindingsListener> _Nullable)listener error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterNetworkHealthCB registers the network health callback to be called -any time the network health changes. Returns a unique ID that can be used to -unregister the network health callback. - */ -- (int64_t)registerNetworkHealthCB:(id<BindingsNetworkHealthCallback> _Nullable)nhc; -- (void)registerPreimageCallback:(NSData* _Nullable)identity pin:(id<BindingsPreimageNotification> _Nullable)pin; -/** - * RegisterRoundEventsHandler registers a callback interface for round -events. -The rid is the round the event attaches to -The timeoutMS is the number of milliseconds until the event fails, and the -validStates are a list of states (one per byte) on which the event gets -triggered -States: - 0x00 - PENDING (Never seen by client) - 0x01 - PRECOMPUTING - 0x02 - STANDBY - 0x03 - QUEUED - 0x04 - REALTIME - 0x05 - COMPLETED - 0x06 - FAILED -These states are defined in elixxir/primitives/states/state.go - */ -- (BindingsUnregister* _Nullable)registerRoundEventsHandler:(long)rid cb:(id<BindingsRoundEventCallback> _Nullable)cb timeoutMS:(long)timeoutMS il:(BindingsIntList* _Nullable)il; -- (void)replayRequests; -- (BOOL)requestAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (BOOL)resetSession:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * This will return the round the message was sent on if it is successfully sent -This can be used to register a round event to learn about message delivery. -on failure a round id of -1 is returned - */ -- (BOOL)sendCmix:(NSData* _Nullable)recipient contents:(NSData* _Nullable)contents parameters:(NSString* _Nullable)parameters ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * SendE2E sends an end-to-end payload to the provided recipient with -the provided msgType. Returns the list of rounds in which parts of -the message were sent or an error if it fails. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsSendReport* _Nullable)sendE2E:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SendUnsafe sends an unencrypted payload to the provided recipient -with the provided msgType. Returns the list of rounds in which parts -of the message were sent or an error if it fails. -NOTE: Do not use this function unless you know what you are doing. -This function always produces an error message in client logging. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types with custom types - */ -- (BindingsRoundList* _Nullable)sendUnsafe:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SetProxiedBins updates the host pool filter that filters out gateways that -are not in one of the specified bins. The provided bins should be CSV. - */ -- (BOOL)setProxiedBins:(NSString* _Nullable)binStringsCSV error:(NSError* _Nullable* _Nullable)error; -/** - * StartNetworkFollower kicks off the tracking of the network. It starts -long running network client threads and returns an object for checking -state and stopping those threads. -Call this when returning from sleep and close when going back to -sleep. -These threads may become a significant drain on battery when offline, ensure -they are stopped if there is no internet access -Threads Started: - - Network Follower (/network/follow.go) - tracks the network events and hands them off to workers for handling - - Historical Round Retrieval (/network/rounds/historical.go) - Retrieves data about rounds which are too old to be stored by the client - - Message Retrieval Worker Group (/network/rounds/retrieve.go) - Requests all messages in a given round from the gateway of the last node - - Message Handling Worker Group (/network/message/handle.go) - Decrypts and partitions messages when signals via the Switchboard - - Health Tracker (/network/health) - Via the network instance tracks the state of the network - - Garbled Messages (/network/message/garbled.go) - Can be signaled to check all recent messages which could be be decoded - Uses a message store on disk for persistence - - Critical Messages (/network/message/critical.go) - Ensures all protocol layer mandatory messages are sent - Uses a message store on disk for persistence - - KeyExchange Trigger (/keyExchange/trigger.go) - Responds to sent rekeys and executes them - - KeyExchange Confirm (/keyExchange/confirm.go) - Responds to confirmations of successful rekey operations - */ -- (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * StopNetworkFollower stops the network follower if it is running. -It returns errors if the Follower is in the wrong status to stop or if it -fails to stop it. -if the network follower is running and this fails, the client object will -most likely be in an unrecoverable state and need to be trashed. - */ -- (BOOL)stopNetworkFollower:(NSError* _Nullable* _Nullable)error; -/** - * UnregisterEventCallback deletes the callback identified by the -index. It returns an error if it fails. - */ -- (void)unregisterEventCallback:(NSString* _Nullable)name; -/** - * UnregisterForNotifications unregister user for notifications - */ -- (BOOL)unregisterForNotifications:(NSError* _Nullable* _Nullable)error; -- (void)unregisterNetworkHealthCB:(int64_t)funcID; -- (BOOL)verifyOwnership:(NSData* _Nullable)receivedMarshaled verifiedMarshaled:(NSData* _Nullable)verifiedMarshaled ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForMessageDelivery allows the caller to get notified if the rounds a -message was sent in successfully completed. Under the hood, this uses an API -which uses the internal round data, network historical round lookup, and -waiting on network events to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - -This function takes the marshaled send report to ensure a memory leak does -not occur as a result of both sides of the bindings holding a reference to -the same pointer. - */ -- (BOOL)waitForMessageDelivery:(NSData* _Nullable)marshaledSendReport mdc:(id<BindingsMessageDeliveryCallback> _Nullable)mdc timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForNewtwork will block until either the network is healthy or the -passed timeout. It will return true if the network is healthy - */ -- (BOOL)waitForNetwork:(long)timeoutMS; -/** - * WaitForRoundCompletion allows the caller to get notified if a round -has completed (or failed). Under the hood, this uses an API which uses the internal -round data, network historical round lookup, and waiting on network events -to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - */ -- (BOOL)waitForRoundCompletion:(long)roundID rec:(id<BindingsRoundCompletionCallback> _Nullable)rec timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * contact object - */ -@interface BindingsContact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped method Contact.GetAPIContact with unsupported parameter or return types - -/** - * GetDHPublicKey returns the public key associated with the Contact. - */ -- (NSData* _Nullable)getDHPublicKey; -/** - * Returns a fact list for adding and getting facts to and from the contact - */ -- (BindingsFactList* _Nullable)getFactList; -/** - * GetID returns the user ID for this user. - */ -- (NSData* _Nullable)getID; -/** - * GetDHPublicKey returns hash of a DH proof of key ownership. - */ -- (NSData* _Nullable)getOwnershipProof; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsContactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BindingsContact* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * DummyTraffic contains the file dummy traffic manager. The manager can be used -to set and get the status of the send thread. - */ -@interface BindingsDummyTraffic : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client maxNumMessages:(long)maxNumMessages avgSendDeltaMS:(long)avgSendDeltaMS randomRangeMS:(long)randomRangeMS; -/** - * GetStatus returns the current state of the dummy traffic send thread. It has -the following return values: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Note that this function does not return the status set by SetStatus directly; -it returns the current status of the send thread, which means any call to -SetStatus will have a small delay before it is returned by GetStatus. - */ -- (BOOL)getStatus; -/** - * SetStatus sets the state of the dummy traffic send thread, which determines -if the thread is running or paused. The possible statuses are: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Returns an error if the channel is full. -Note that this function cannot change the status of the send thread if it has -yet to be started or stopped. - */ -- (BOOL)setStatus:(BOOL)status error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsFact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -- (nullable instancetype)init:(long)factType factStr:(NSString* _Nullable)factStr; -- (NSString* _Nonnull)get; -- (NSString* _Nonnull)stringify; -- (long)type; -@end - -@interface BindingsFactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * FactList - */ -- (nullable instancetype)init; -- (BOOL)add:(NSString* _Nullable)factData factType:(long)factType error:(NSError* _Nullable* _Nullable)error; -- (BindingsFact* _Nullable)get:(long)i; -- (long)num; -- (NSString* _Nonnull)stringify:(NSError* _Nullable* _Nullable)error; -@end - -/** - * FilePartTracker contains the interfaces.FilePartTracker. - */ -@interface BindingsFilePartTracker : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetNumParts returns the total number of file parts in the transfer. - */ -- (long)getNumParts; -/** - * GetPartStatus returns the status of the file part with the given part number. -The possible values for the status are: -0 = unsent -1 = sent (sender has sent a part, but it has not arrived) -2 = arrived (sender has sent a part, and it has arrived) -3 = received (receiver has received a part) - */ -- (long)getPartStatus:(long)partNum; -@end - -/** - * FileTransfer contains the file transfer manager. - */ -@interface BindingsFileTransfer : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client receiveFunc:(id<BindingsFileTransferReceiveFunc> _Nullable)receiveFunc parameters:(NSString* _Nullable)parameters; -/** - * CloseSend deletes a sent file transfer from the sent transfer map and from -storage once a transfer has completed or reached the retry limit. Returns an -error if the transfer has not run out of retries. - */ -- (BOOL)closeSend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * GetMaxFileNameByteLength returns the maximum length, in bytes, allowed for a -file name. - */ -- (long)getMaxFileNameByteLength; -/** - * GetMaxFilePreviewSize returns the maximum file preview size, in bytes. - */ -- (long)getMaxFilePreviewSize; -/** - * GetMaxFileSize returns the maximum file size, in bytes, allowed to be -transferred. - */ -- (long)getMaxFileSize; -/** - * GetMaxFileTypeByteLength returns the maximum length, in bytes, allowed for a -file type. - */ -- (long)getMaxFileTypeByteLength; -/** - * Receive returns the fully assembled file on the completion of the transfer. -It deletes the transfer from the received transfer map and from storage. -Returns an error if the transfer is not complete, the full file cannot be -verified, or if the transfer cannot be found. - */ -- (NSData* _Nullable)receive:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterReceiveProgressCallback allows for the registration of a callback to -track the progress of an individual received file transfer. The callback will -be called immediately when added to report the current status of the -transfer. It will then call every time a file part is received, the transfer -completes, or an error occurs. It is called at most once ever period, which -means if events occur faster than the period, then they will not be reported -and instead, the progress will be reported once at the end of the period. -Once the callback reports that the transfer has completed, the recipient -can get the full file by calling Receive. -The period is specified in milliseconds. - */ -- (BOOL)registerReceiveProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferReceivedProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterSendProgressCallback allows for the registration of a callback to -track the progress of an individual sent file transfer. The callback will be -called immediately when added to report the current status of the transfer. -It will then call every time a file part is sent, a file part arrives, the -transfer completes, or an error occurs. It is called at most once every -period, which means if events occur faster than the period, then they will -not be reported and instead, the progress will be reported once at the end of -the period. -The period is specified in milliseconds. - */ -- (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends a file to the recipient. The sender must have an E2E relationship -with the recipient. -The file name is the name of the file to show a user. It has a max length of -48 bytes. -The file type identifies what type of file is being sent. It has a max length -of 8 bytes. -The file data cannot be larger than 256 kB -The retry float is the total amount of data to send relative to the data -size. Data will be resent on error and will resend up to [(1 + retry) * -fileSize]. -The preview stores a preview of the data (such as a thumbnail) and is -capped at 4 kB in size. -Returns a unique transfer ID used to identify the transfer. -PeriodMS is the duration, in milliseconds, to wait between progress callback -calls. Set this large enough to prevent spamming. - */ -- (NSData* _Nullable)send:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType fileData:(NSData* _Nullable)fileData recipientID:(NSData* _Nullable)recipientID retry:(float)retry preview:(NSData* _Nullable)preview progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Group structure contains the identifying and membership information of a -group chat. - */ -@interface BindingsGroup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetCreatedMS returns the time the group was created in milliseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedMS; -/** - * GetCreatedNano returns the time the group was created in nanoseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedNano; -/** - * GetID return the 33-byte unique group ID. - */ -- (NSData* _Nullable)getID; -/** - * GetInitMessage returns initial message sent with the group request. - */ -- (NSData* _Nullable)getInitMessage; -/** - * GetMembership returns a list of contacts, one for each member in the group. -The list is in order; the first contact is the leader/creator of the group. -All subsequent members are ordered by their ID. - */ -- (BindingsGroupMembership* _Nullable)getMembership; -/** - * GetName returns the name set by the user for the group. - */ -- (NSData* _Nullable)getName; -/** - * Serialize serializes the Group. - */ -- (NSData* _Nullable)serialize; -@end - -/** - * GroupChat object contains the group chat manager. - */ -@interface BindingsGroupChat : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetGroup returns the group with the group ID. If no group exists, then the -error "failed to find group" is returned. - */ -- (BindingsGroup* _Nullable)getGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * GetGroups returns an IdList containing a list of group IDs that the user is a -part of. - */ -- (BindingsIdList* _Nullable)getGroups; -/** - * JoinGroup allows a user to join a group when they receive a request. The -caller must pass in the serialized bytes of a Group. - */ -- (BOOL)joinGroup:(NSData* _Nullable)serializedGroupData error:(NSError* _Nullable* _Nullable)error; -/** - * LeaveGroup deletes a group so a user no longer has access. - */ -- (BOOL)leaveGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * MakeGroup creates a new group and sends a group request to all members in the -group. The ID of the new group, the rounds the requests were sent on, and the -status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)makeGroup:(BindingsIdList* _Nullable)membership name:(NSData* _Nullable)name message:(NSData* _Nullable)message; -/** - * NumGroups returns the number of groups the user is a part of. - */ -- (long)numGroups; -/** - * ResendRequest resends a group request to all members in the group. The rounds -they were sent on and the status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)resendRequest:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends the message to the specified group. Returns the round the messages -were sent on. - */ -- (BindingsGroupSendReport* _Nullable)send:(NSData* _Nullable)groupIdBytes message:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * // -Member Structure -// -GroupMember represents a member in the group membership list. - */ -@interface BindingsGroupMember : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMember.Member with unsupported type: gitlab.com/elixxir/crypto/group.Member - -// skipped method GroupMember.DeepCopy with unsupported parameter or return types - -// skipped method GroupMember.Equal with unsupported parameter or return types - -/** - * GetDhKey returns the byte representation of the public Diffie–Hellman key of -the member. - */ -- (NSData* _Nullable)getDhKey; -/** - * GetID returns the 33-byte user ID of the member. - */ -- (NSData* _Nullable)getID; -- (NSString* _Nonnull)goString; -- (NSData* _Nullable)serialize; -- (NSString* _Nonnull)string; -@end - -/** - * GroupMembership structure contains a list of members that are part of a -group. The first member is the group leader. - */ -@interface BindingsGroupMembership : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Get returns the member at the index. The member at index 0 is always the -group leader. An error is returned if the index is out of range. - */ -- (BindingsGroupMember* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of members in the group membership. - */ -- (long)len; -@end - -/** - * GroupMessageReceive contains a group message, its ID, and its data that a -user receives. - */ -@interface BindingsGroupMessageReceive : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMessageReceive.MessageReceive with unsupported type: gitlab.com/elixxir/client/groupChat.MessageReceive - -/** - * GetEphemeralID returns the ephemeral ID of the recipient. - */ -- (int64_t)getEphemeralID; -/** - * GetGroupID returns the 33-byte group ID. - */ -- (NSData* _Nullable)getGroupID; -/** - * GetMessageID returns the message ID. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetPayload returns the message payload. - */ -- (NSData* _Nullable)getPayload; -/** - * GetRecipientID returns the 33-byte user ID of the recipient. - */ -- (NSData* _Nullable)getRecipientID; -/** - * GetRoundID returns the ID of the round the message was sent on. - */ -- (int64_t)getRoundID; -/** - * GetRoundTimestampMS returns the timestamp, in milliseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the timestamp, in nanoseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the ID of the round the message was sent on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSenderID returns the 33-byte user ID of the sender. - */ -- (NSData* _Nullable)getSenderID; -/** - * GetTimestampMS returns the message timestamp in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message timestamp in nanoseconds. - */ -- (int64_t)getTimestampNano; -- (NSString* _Nonnull)string; -@end - -@interface BindingsGroupReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable grpId; -@property (nonatomic) long status; -@end - -/** - * GroupSendReport is returned when sending a group message. It contains the -round ID sent on and the timestamp of the send. - */ -@interface BindingsGroupSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetMessageID returns the ID of the round that the send occurred on. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetRoundID returns the ID of the round that the send occurred on. - */ -- (int64_t)getRoundID; -/** - * GetRoundURL returns the URL of the round that the send occurred on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the timestamp of the send in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the timestamp of the send in nanoseconds. - */ -- (int64_t)getTimestampNano; -@end - -/** - * ID list -IdList contains a list of IDs. - */ -@interface BindingsIdList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Add appends the ID bytes to the end of the list. - */ -- (BOOL)add:(NSData* _Nullable)idBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Get returns the ID at the index. An error is returned if the index is out of -range. - */ -- (NSData* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of IDs in the list. - */ -- (long)len; -@end - -@interface BindingsIntList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (void)add:(long)i; -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -@interface BindingsManyNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsNotificationForMeReport* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -/** - * Message is a message received from the cMix network in the clear -or that has been decrypted using established E2E keys. - */ -@interface BindingsMessage : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetID returns the id of the message - */ -- (NSData* _Nullable)getID; -/** - * GetMessageType returns the message's type - */ -- (long)getMessageType; -/** - * GetPayload returns the message's payload/contents - */ -- (NSData* _Nullable)getPayload; -/** - * GetRoundId returns the message's round ID - */ -- (int64_t)getRoundId; -/** - * GetRoundTimestampMS returns the message's round timestamp in milliseconds - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the message's round timestamp in nanoseconds - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the message's round URL - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSender returns the message's sender ID, if available - */ -- (NSData* _Nullable)getSender; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -@end - -/** - * NewGroupReport is returned when creating a new group and contains the ID of -the group, a list of rounds that the group requests were sent on, and the -status of the send. - */ -@interface BindingsNewGroupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetError returns the string of an error. -Will be an empty string if no error occured - */ -- (NSString* _Nonnull)getError; -/** - * GetGroup returns the Group. - */ -- (BindingsGroup* _Nullable)getGroup; -/** - * GetRoundList returns the RoundList containing a list of rounds requests were -sent on. - */ -- (BindingsRoundList* _Nullable)getRoundList; -/** - * GetStatus returns the status of the requests sent when creating a new group. -status = 0 an error occurred before any requests could be sent - 1 all requests failed to send (call Resend Group) - 2 some request failed and some succeeded (call Resend Group) - 3, all requests sent successfully (call Resend Group) - */ -- (long)getStatus; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * NodeRegistrationsStatus structure for returning node registration statuses -for bindings. - */ -@interface BindingsNodeRegistrationsStatus : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetRegistered returns the number of nodes registered with the client. - */ -- (long)getRegistered; -/** - * GetTotal return the total of nodes currently in the network. - */ -- (long)getTotal; -@end - -@interface BindingsNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)forMe; -- (NSData* _Nullable)source; -- (NSString* _Nonnull)type; -@end - -/** - * RestoreContactsReport is a gomobile friendly report structure -for determining which IDs restored, which failed, and why. - */ -@interface BindingsRestoreContactsReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetErrorAt returns the error string at index - */ -- (NSString* _Nonnull)getErrorAt:(long)index; -/** - * GetFailedAt returns the failed ID at index - */ -- (NSData* _Nullable)getFailedAt:(long)index; -/** - * GetRestoreContactsError returns an error string. Empty if no error. - */ -- (NSString* _Nonnull)getRestoreContactsError; -/** - * GetRestoredAt returns the restored ID at index - */ -- (NSData* _Nullable)getRestoredAt:(long)index; -/** - * LenFailed returns the length of the ID's failed. - */ -- (long)lenFailed; -/** - * LenRestored returns the length of ID's restored. - */ -- (long)lenRestored; -@end - -@interface BindingsRoundList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * the send report is the mechanisim by which sendE2E returns a single - */ -@interface BindingsSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (NSData* _Nullable)getMessageID; -- (BindingsRoundList* _Nullable)getRoundList; -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsSendReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field SendReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable mid; -@property (nonatomic) int64_t ts; -@end - -/** - * Generic Unregister - a generic return used for all callbacks which can be -unregistered -Interface which allows the un-registration of a listener - */ -@interface BindingsUnregister : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Call unregisters a callback - */ -- (void)unregister; -@end - -@interface BindingsUser : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsContact* _Nullable)getContact; -- (NSData* _Nullable)getE2EDhPrivateKey; -- (NSData* _Nullable)getE2EDhPublicKey; -- (NSData* _Nullable)getReceptionID; -- (NSData* _Nullable)getReceptionRSAPrivateKeyPem; -- (NSData* _Nullable)getReceptionRSAPublicKeyPem; -- (NSData* _Nullable)getReceptionSalt; -- (NSData* _Nullable)getTransmissionID; -- (NSData* _Nullable)getTransmissionRSAPrivateKeyPem; -- (NSData* _Nullable)getTransmissionRSAPublicKeyPem; -- (NSData* _Nullable)getTransmissionSalt; -- (BOOL)isPrecanned; -@end - -@interface BindingsUserDiscovery : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)init:(BindingsClient* _Nullable)client; -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; -/** - * AddFact adds a fact for the user to user discovery. Will only succeed if the -user is already registered and the system does not have the fact currently -registered for any user. -Will fail if the fact string is not well formed. -This does not complete the fact registration process, it returns a -confirmation id instead. Over the communications system the fact is -associated with, a code will be sent. This confirmation ID needs to be -called along with the code to finalize the fact. - */ -- (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from -AddFact while the code will come over the associated communications system - */ -- (BOOL)confirmFact:(NSString* _Nullable)confirmationID code:(NSString* _Nullable)code error:(NSError* _Nullable* _Nullable)error; -/** - * Lookup the contact object associated with the given userID. The -id is the byte representation of an id. -This will reject if that id is malformed. The LookupCallback will return -the associated contact if it exists. - */ -- (BOOL)lookup:(NSData* _Nullable)idBytes callback:(id<BindingsLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * MultiLookup Looks for the contact object associated with all given userIDs. -The ids are the byte representation of an id stored in an IDList object. -This will reject if that id is malformed or if the indexing on the IDList -object is wrong. The MultiLookupCallback will return with all contacts -returned within the timeout. - */ -- (BOOL)multiLookup:(BindingsIdList* _Nullable)ids callback:(id<BindingsMultiLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * Register registers a user with user discovery. Will return an error if the -network signatures are malformed or if the username is taken. Usernames -cannot be changed after registration at this time. Will fail if the user is -already registered. -Identity does not go over cmix, it occurs over normal communications - */ -- (BOOL)register:(NSString* _Nullable)username error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveFact removes a previously confirmed fact. Will fail if the passed fact string is -not well-formed or if the fact is not associated with this client. -Users cannot remove username facts and must instead remove the user. - */ -- (BOOL)removeFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveUser deletes a user. The fact sent must be the username. -This function preserves the username forever and makes it -unusable. - */ -- (BOOL)removeUser:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * Search for the passed Facts. The factList is the stringification of a -fact list object, look at /bindings/list.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This is NOT intended to be used to search for multiple users at once, that -can have a privacy reduction. Instead, it is intended to be used to search -for a user where multiple pieces of information is known. - */ -- (BOOL)search:(NSString* _Nullable)fl callback:(id<BindingsSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SearchSingle searches for the passed Facts. The fact is the stringification of a -fact object, look at /bindings/contact.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This only searches for a single fact at a time. It is intended to make some -simple use cases of the API easier. - */ -- (BOOL)searchSingle:(NSString* _Nullable)f callback:(id<BindingsSingleSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SetAlternativeUserDiscovery sets the alternativeUd object within manager. -Once set, any user discovery operation will go through the alternative -user discovery service. -To undo this operation, use UnsetAlternativeUserDiscovery. -The contact file is the already read in bytes, not the file path for the contact file. - */ -- (BOOL)setAlternativeUserDiscovery:(NSData* _Nullable)address cert:(NSData* _Nullable)cert contactFile:(NSData* _Nullable)contactFile error:(NSError* _Nullable* _Nullable)error; -/** - * UnsetAlternativeUserDiscovery clears out the information from -the Manager object. - */ -- (BOOL)unsetAlternativeUserDiscovery:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Error codes - */ -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedCode; -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedMessage; - -/** - * CompressJpeg takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpeg(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * CompressJpegForPreview takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpegForPreview(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * DownloadAndVerifySignedNdfWithUrl retrieves the NDF from a specified URL. -The NDF is processed into a protobuf containing a signature which -is verified using the cert string passed in. The NDF is returned as marshaled -byte data which may be used to start a client. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadAndVerifySignedNdfWithUrl(NSString* _Nullable url, NSString* _Nullable cert, NSError* _Nullable* _Nullable error); - -/** - * DownloadDAppRegistrationDB returns a []byte containing -the JSON data describing registered dApps. -See https://git.xx.network/elixxir/registered-dapps - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadDAppRegistrationDB(NSError* _Nullable* _Nullable error); - -/** - * DownloadErrorDB returns a []byte containing the JSON data -describing client errors. -See https://git.xx.network/elixxir/client-error-database/ - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadErrorDB(NSError* _Nullable* _Nullable error); - -/** - * DumpStack returns a string with the stack trace of every running thread. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsDumpStack(NSError* _Nullable* _Nullable error); - -/** - * EnableGrpcLogs sets GRPC trace logging - */ -FOUNDATION_EXPORT void BindingsEnableGrpcLogs(id<BindingsLogWriter> _Nullable writer); - -/** - * ErrorStringToUserFriendlyMessage takes a passed in errStr which will be -a backend generated error. These may be error specifically written by -the backend team or lower level errors gotten from low level dependencies. -This function will parse the error string for common errors provided from -errToUserErr to provide a more user-friendly error message for the front end. -If the error is not common, some simple parsing is done on the error message -to make it more user-accessible, removing backend specific jargon. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsErrorStringToUserFriendlyMessage(NSString* _Nullable errStr); - -/** - * GenerateSecret creates a secret password using a system-based -pseudorandom number generator. It takes 1 parameter, `numBytes`, -which should be set to 32, but can be set higher in certain cases. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateSecret(long numBytes); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetCMIXParams(NSError* _Nullable* _Nullable error); - -/** - * returns a previously created client. IF be used if the garbage collector -removes the client instance on the app side. Is NOT thread safe relative to -login, newClient, or newPrecannedClient - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsGetClientSingleton(void); - -/** - * GetDependencies returns the api DEPENDENCIES - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetDependencies(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetE2EParams(NSError* _Nullable* _Nullable error); - -/** - * GetGitVersion rturns the api GITVERSION - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetGitVersion(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetNetworkParams(NSError* _Nullable* _Nullable error); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetUnsafeParams(NSError* _Nullable* _Nullable error); - -/** - * GetVersion returns the api SEMVER - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void); - -/** - * InitializeBackup starts the backup processes that returns backup updates when -they occur. Any time an event occurs that changes the contents of the backup, -such as adding or deleting a contact, the backup is triggered and an -encrypted backup is generated and returned on the updateBackupCb callback. -Call this function only when enabling backup if it has not already been -initialized or when the user wants to change their password. -To resume backup process on app recovery, use ResumeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsInitializeBackup(NSString* _Nullable password, id<BindingsUpdateBackupFunc> _Nullable updateBackupCb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * LoadSecretWithMnemonic loads the secret stored from the call to -StoreSecretWithMnemonic. The path given should be the same filepath -as the path given in StoreSecretWithMnemonic. There should be a file -in this path called ".recovery". This operation is not tied -to client operations, as the user will not have a client when trying to -recover their account. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsLoadSecretWithMnemonic(NSString* _Nullable mnemonic, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * sets level of logging. All logs the set level and above will be displayed -options are: - TRACE - 0 - DEBUG - 1 - INFO - 2 - WARN - 3 - ERROR - 4 - CRITICAL - 5 - FATAL - 6 -The default state without updates is: INFO - */ -FOUNDATION_EXPORT BOOL BindingsLogLevel(long level, NSError* _Nullable* _Nullable error); - -/** - * Login will load an existing client from the storageDir -using the password. This will fail if the client doesn't exist or -the password is incorrect. -The password is passed as a byte array so that it can be cleared from -memory and stored as securely as possible using the memguard library. -Login does not block on network connection, and instead loads and -starts subprocesses to perform network operations. - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsLogin(NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * MakeIdList creates a new empty IdList. - */ -FOUNDATION_EXPORT BindingsIdList* _Nullable BindingsMakeIdList(void); - -FOUNDATION_EXPORT BindingsIntList* _Nullable BindingsMakeIntList(void); - -/** - * NewClient creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable regCode, NSError* _Nullable* _Nullable error); - -/** - * NewClientFromBackup constructs a new Client from an encrypted backup. The backup -is decrypted using the backupPassphrase. On success a successful client creation, -the function will return a JSON encoded list of the E2E partners -contained in the backup and a json-encoded string of the parameters stored in the backup - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); - -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -FOUNDATION_EXPORT BindingsDummyTraffic* _Nullable BindingsNewDummyTrafficManager(BindingsClient* _Nullable client, long maxNumMessages, long avgSendDeltaMS, long randomRangeMS, NSError* _Nullable* _Nullable error); - -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -FOUNDATION_EXPORT BindingsFact* _Nullable BindingsNewFact(long factType, NSString* _Nullable factStr, NSError* _Nullable* _Nullable error); - -/** - * FactList - */ -FOUNDATION_EXPORT BindingsFactList* _Nullable BindingsNewFactList(void); - -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -FOUNDATION_EXPORT BindingsFileTransfer* _Nullable BindingsNewFileTransferManager(BindingsClient* _Nullable client, id<BindingsFileTransferReceiveFunc> _Nullable receiveFunc, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * NewGroupManager creates a new group chat manager. - */ -FOUNDATION_EXPORT BindingsGroupChat* _Nullable BindingsNewGroupManager(BindingsClient* _Nullable client, id<BindingsGroupRequestFunc> _Nullable requestFunc, id<BindingsGroupReceiveFunc> _Nullable receiveFunc, NSError* _Nullable* _Nullable error); - -/** - * NewPrecannedClient creates an insecure user with predetermined keys with nodes -It creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewPrecannedClient(long precannedID, NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); - -/** - * NotificationsForMe Check if a notification received is for me -It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, -a Type, and a source. These are as follows: - TYPE SOURCE DESCRIPTION - "default" recipient user ID A message with no association - "request" sender user ID A channel request has been received - "reset" sender user ID A channel reset has been received - "confirm" sender user ID A channel request has been accepted - "silent" sender user ID A message which should not be notified on - "e2e" sender user ID reception of an E2E message - "group" group ID reception of a group chat message - "endFT" sender user ID Last message sent confirming end of file transfer - "groupRQ" sender user ID Request from sender to join a group chat - */ -FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotificationsForMe(NSString* _Nullable notifCSV, NSString* _Nullable preimages, NSError* _Nullable* _Nullable error); - -/** - * RegisterLogWriter registers a callback on which logs are written. - */ -FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); - -/** - * RestoreContactsFromBackup takes as input the jason output of the -`NewClientFromBackup` function, unmarshals it into IDs, looks up -each ID in user discovery, and initiates a session reset request. -This function will not return until every id in the list has been sent a -request. It should be called again and again until it completes. -xxDK users should not use this function. This function is used by -the mobile phone apps and are not intended to be part of the xxDK. It -should be treated as internal functions specific to the phone apps. - */ -FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); - -/** - * ResumeBackup starts the backup processes back up with a new callback after it -has been initialized. -Call this function only when resuming a backup that has already been -initialized or to replace the callback. -To start the backup for the first time or to use a new password, use -InitializeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsResumeBackup(id<BindingsUpdateBackupFunc> _Nullable cb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * SetTimeSource sets the network time to a custom source. - */ -FOUNDATION_EXPORT void BindingsSetTimeSource(id<BindingsTimeSource> _Nullable timeNow); - -/** - * StoreSecretWithMnemonic stores the secret tied with the mnemonic to storage. -Unlike other storage operations, this does not use EKV, as that is -intrinsically tied to client operations, which the user will not have while -trying to recover their account. As such, we store the encrypted data -directly, with a specified path. Path will be a valid filepath in which the -recover file will be stored as ".recovery". - -As an example, given "home/user/xxmessenger/storagePath", -the recovery file will be stored at -"home/user/xxmessenger/storagePath/.recovery" - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsStoreSecretWithMnemonic(NSData* _Nullable secret, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled contact object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsContact* _Nullable BindingsUnmarshalContact(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled send report object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsSendReport* _Nullable BindingsUnmarshalSendReport(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * UpdateCommonErrors takes the passed in contents of a JSON file and updates the -errToUserErr map with the contents of the json file. The JSON's expected format -conform with the commented examples provides in errToUserErr above. -NOTE that you should not pass in a file path, but a preloaded JSON file - */ -FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, NSError* _Nullable* _Nullable error); - -// skipped function WrapAPIClient with unsupported parameter or return types - - -// skipped function WrapUserDiscovery with unsupported parameter or return types - - -@class BindingsAuthConfirmCallback; - -@class BindingsAuthRequestCallback; - -@class BindingsAuthResetNotificationCallback; - -@class BindingsClientError; - -@class BindingsEventCallbackFunctionObject; - -@class BindingsFileTransferReceiveFunc; - -@class BindingsFileTransferReceivedProgressFunc; - -@class BindingsFileTransferSentProgressFunc; - -@class BindingsGroupReceiveFunc; - -@class BindingsGroupRequestFunc; - -@class BindingsListener; - -@class BindingsLogWriter; - -@class BindingsLookupCallback; - -@class BindingsMessageDeliveryCallback; - -@class BindingsMultiLookupCallback; - -@class BindingsNetworkHealthCallback; - -@class BindingsPreimageNotification; - -@class BindingsRestoreContactsUpdater; - -@class BindingsRoundCompletionCallback; - -@class BindingsRoundEventCallback; - -@class BindingsSearchCallback; - -@class BindingsSingleSearchCallback; - -@class BindingsTimeSource; - -@class BindingsUpdateBackupFunc; - -/** - * AuthConfirmCallback notifies the register whenever they receive an auth -request confirmation - */ -@interface BindingsAuthConfirmCallback : NSObject <goSeqRefInterface, BindingsAuthConfirmCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthRequestCallback : NSObject <goSeqRefInterface, BindingsAuthRequestCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@interface BindingsClientError : NSObject <goSeqRefInterface, BindingsClientError> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -/** - * EventCallbackFunctionObject bindings interface which contains function -that implements the EventCallbackFunction - */ -@interface BindingsEventCallbackFunctionObject : NSObject <goSeqRefInterface, BindingsEventCallbackFunctionObject> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -/** - * FileTransferReceiveFunc contains a function callback that notifies the -receiver of an incoming file transfer. It is called on the reception of the -initial file transfer message. - */ -@interface BindingsFileTransferReceiveFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -/** - * FileTransferReceivedProgressFunc contains a function callback that tracks the -progress of receiving a file. It is called when a file part is received, the -transfer completes, or on error. - */ -@interface BindingsFileTransferReceivedProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceivedProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * FileTransferSentProgressFunc contains a function callback that tracks the -progress of sending a file. It is called when a file part is sent, a file -part arrives, the transfer completes, or on error. - */ -@interface BindingsFileTransferSentProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferSentProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * GroupReceiveFunc contains a function callback that is called when a group -message is received. - */ -@interface BindingsGroupReceiveFunc : NSObject <goSeqRefInterface, BindingsGroupReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -/** - * GroupRequestFunc contains a function callback that is called when a group -request is received. - */ -@interface BindingsGroupRequestFunc : NSObject <goSeqRefInterface, BindingsGroupRequestFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -/** - * Listener provides a callback to hear a message -An object implementing this interface can be called back when the client -gets a message of the type that the registerer specified at registration -time. - */ -@interface BindingsListener : NSObject <goSeqRefInterface, BindingsListener> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@interface BindingsLogWriter : NSObject <goSeqRefInterface, BindingsLogWriter> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)log:(NSString* _Nullable)p0; -@end - -/** - * LookupCallback returns the result of a single lookup - */ -@interface BindingsLookupCallback : NSObject <goSeqRefInterface, BindingsLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -/** - * MessageDeliveryCallback gets called on the determination if all events -related to a message send were successful. - */ -@interface BindingsMessageDeliveryCallback : NSObject <goSeqRefInterface, BindingsMessageDeliveryCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -/** - * MultiLookupCallback returns the result of many parallel lookups - */ -@interface BindingsMultiLookupCallback : NSObject <goSeqRefInterface, BindingsMultiLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -/** - * A callback when which is used to receive notification if network health -changes - */ -@interface BindingsNetworkHealthCallback : NSObject <goSeqRefInterface, BindingsNetworkHealthCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BOOL)p0; -@end - -@interface BindingsPreimageNotification : NSObject <goSeqRefInterface, BindingsPreimageNotification> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -/** - * RestoreContactsUpdater interface provides a callback function -for receiving update information from RestoreContactsFromBackup. - */ -@interface BindingsRestoreContactsUpdater : NSObject <goSeqRefInterface, BindingsRestoreContactsUpdater> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -/** - * RoundCompletionCallback is returned when the completion of a round is known. - */ -@interface BindingsRoundCompletionCallback : NSObject <goSeqRefInterface, BindingsRoundCompletionCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -/** - * RoundEventCallback handles waiting on the exact state of a round on -the cMix network. - */ -@interface BindingsRoundEventCallback : NSObject <goSeqRefInterface, BindingsRoundEventCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -/** - * SearchCallback returns the result of a search - */ -@interface BindingsSearchCallback : NSObject <goSeqRefInterface, BindingsSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -/** - * SingleSearchCallback returns the result of a single search - */ -@interface BindingsSingleSearchCallback : NSObject <goSeqRefInterface, BindingsSingleSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@interface BindingsTimeSource : NSObject <goSeqRefInterface, BindingsTimeSource> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (int64_t)nowMs; -@end - -/** - * UpdateBackupFunc contains a function callback that returns new backups. - */ -@interface BindingsUpdateBackupFunc : NSObject <goSeqRefInterface, BindingsUpdateBackupFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Universe.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Universe.objc.h deleted file mode 100644 index 019e7502d581983722a15bf30799e85cbc5dd766..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/Universe.objc.h +++ /dev/null @@ -1,29 +0,0 @@ -// Objective-C API for talking to Go package. -// gobind -lang=objc -// -// File is generated by gobind. Do not edit. - -#ifndef __Universe_H__ -#define __Universe_H__ - -@import Foundation; -#include "ref.h" - -@protocol Universeerror; -@class Universeerror; - -@protocol Universeerror <NSObject> -- (NSString* _Nonnull)error; -@end - -@class Universeerror; - -@interface Universeerror : NSError <goSeqRefInterface, Universeerror> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (NSString* _Nonnull)error; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/ref.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/ref.h deleted file mode 100644 index b8036a4d85c7387f3def61473a071b5d8c4c8208..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Headers/ref.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef __GO_REF_HDR__ -#define __GO_REF_HDR__ - -#include <Foundation/Foundation.h> - -// GoSeqRef is an object tagged with an integer for passing back and -// forth across the language boundary. A GoSeqRef may represent either -// an instance of a Go object, or an Objective-C object passed to Go. -// The explicit allocation of a GoSeqRef is used to pin a Go object -// when it is passed to Objective-C. The Go seq package maintains a -// reference to the Go object in a map keyed by the refnum along with -// a reference count. When the reference count reaches zero, the Go -// seq package will clear the corresponding entry in the map. -@interface GoSeqRef : NSObject { -} -@property(readonly) int32_t refnum; -@property(strong) id obj; // NULL when representing a Go object. - -// new GoSeqRef object to proxy a Go object. The refnum must be -// provided from Go side. -- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; - -- (int32_t)incNum; - -@end - -@protocol goSeqRefInterface --(GoSeqRef*) _ref; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Modules/module.modulemap b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Modules/module.modulemap deleted file mode 100644 index 4316a5b24058edfc18ffb2dc7f7a982e8353441a..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Modules/module.modulemap +++ /dev/null @@ -1,8 +0,0 @@ -framework module "Bindings" { - header "ref.h" - header "Bindings.objc.h" - header "Universe.objc.h" - header "Bindings.h" - - export * -} \ No newline at end of file diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Resources/Info.plist b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Resources/Info.plist deleted file mode 100644 index 0d1a4b8ab9b1fc8e9357197398f73353470cb636..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Resources/Info.plist +++ /dev/null @@ -1,6 +0,0 @@ -<?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> - </dict> - </plist> diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings deleted file mode 100644 index 1e3b1dc25b4d65bf1e21a5d169dc3ae4de6c4fd7..0000000000000000000000000000000000000000 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings and /dev/null differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.h deleted file mode 100644 index 8906a7da239705b790cb2bb64de92f806640cb38..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.h +++ /dev/null @@ -1,13 +0,0 @@ - -// Objective-C API for talking to the following Go packages -// -// gitlab.com/elixxir/client/bindings -// -// File is generated by gomobile bind. Do not edit. -#ifndef __Bindings_FRAMEWORK_H__ -#define __Bindings_FRAMEWORK_H__ - -#include "Bindings.objc.h" -#include "Universe.objc.h" - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h deleted file mode 100644 index 32bf6d116888f787ced27b01b95cb4e1b2c1138b..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h +++ /dev/null @@ -1,2083 +0,0 @@ -// Objective-C API for talking to gitlab.com/elixxir/client/bindings Go package. -// gobind -lang=objc gitlab.com/elixxir/client/bindings -// -// File is generated by gobind. Do not edit. - -#ifndef __Bindings_H__ -#define __Bindings_H__ - -@import Foundation; -#include "ref.h" -#include "Universe.objc.h" - - -@class BindingsBackup; -@class BindingsBackupReport; -@class BindingsClient; -@class BindingsContact; -@class BindingsContactList; -@class BindingsDummyTraffic; -@class BindingsFact; -@class BindingsFactList; -@class BindingsFilePartTracker; -@class BindingsFileTransfer; -@class BindingsGroup; -@class BindingsGroupChat; -@class BindingsGroupMember; -@class BindingsGroupMembership; -@class BindingsGroupMessageReceive; -@class BindingsGroupReportDisk; -@class BindingsGroupSendReport; -@class BindingsIdList; -@class BindingsIntList; -@class BindingsManyNotificationForMeReport; -@class BindingsMessage; -@class BindingsNewGroupReport; -@class BindingsNodeRegistrationsStatus; -@class BindingsNotificationForMeReport; -@class BindingsRestoreContactsReport; -@class BindingsRoundList; -@class BindingsSendReport; -@class BindingsSendReportDisk; -@class BindingsUnregister; -@class BindingsUser; -@class BindingsUserDiscovery; -@protocol BindingsAuthConfirmCallback; -@class BindingsAuthConfirmCallback; -@protocol BindingsAuthRequestCallback; -@class BindingsAuthRequestCallback; -@protocol BindingsAuthResetNotificationCallback; -@class BindingsAuthResetNotificationCallback; -@protocol BindingsClientError; -@class BindingsClientError; -@protocol BindingsEventCallbackFunctionObject; -@class BindingsEventCallbackFunctionObject; -@protocol BindingsFileTransferReceiveFunc; -@class BindingsFileTransferReceiveFunc; -@protocol BindingsFileTransferReceivedProgressFunc; -@class BindingsFileTransferReceivedProgressFunc; -@protocol BindingsFileTransferSentProgressFunc; -@class BindingsFileTransferSentProgressFunc; -@protocol BindingsGroupReceiveFunc; -@class BindingsGroupReceiveFunc; -@protocol BindingsGroupRequestFunc; -@class BindingsGroupRequestFunc; -@protocol BindingsListener; -@class BindingsListener; -@protocol BindingsLogWriter; -@class BindingsLogWriter; -@protocol BindingsLookupCallback; -@class BindingsLookupCallback; -@protocol BindingsMessageDeliveryCallback; -@class BindingsMessageDeliveryCallback; -@protocol BindingsMultiLookupCallback; -@class BindingsMultiLookupCallback; -@protocol BindingsNetworkHealthCallback; -@class BindingsNetworkHealthCallback; -@protocol BindingsPreimageNotification; -@class BindingsPreimageNotification; -@protocol BindingsRestoreContactsUpdater; -@class BindingsRestoreContactsUpdater; -@protocol BindingsRoundCompletionCallback; -@class BindingsRoundCompletionCallback; -@protocol BindingsRoundEventCallback; -@class BindingsRoundEventCallback; -@protocol BindingsSearchCallback; -@class BindingsSearchCallback; -@protocol BindingsSingleSearchCallback; -@class BindingsSingleSearchCallback; -@protocol BindingsTimeSource; -@class BindingsTimeSource; -@protocol BindingsUpdateBackupFunc; -@class BindingsUpdateBackupFunc; - -@protocol BindingsAuthConfirmCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -@protocol BindingsAuthRequestCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsAuthResetNotificationCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsClientError <NSObject> -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -@protocol BindingsEventCallbackFunctionObject <NSObject> -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -@protocol BindingsFileTransferReceiveFunc <NSObject> -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -@protocol BindingsFileTransferReceivedProgressFunc <NSObject> -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsFileTransferSentProgressFunc <NSObject> -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsGroupReceiveFunc <NSObject> -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -@protocol BindingsGroupRequestFunc <NSObject> -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -@protocol BindingsListener <NSObject> -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@protocol BindingsLogWriter <NSObject> -- (void)log:(NSString* _Nullable)p0; -@end - -@protocol BindingsLookupCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsMessageDeliveryCallback <NSObject> -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -@protocol BindingsMultiLookupCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -@protocol BindingsNetworkHealthCallback <NSObject> -- (void)callback:(BOOL)p0; -@end - -@protocol BindingsPreimageNotification <NSObject> -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -@protocol BindingsRestoreContactsUpdater <NSObject> -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -@protocol BindingsRoundCompletionCallback <NSObject> -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -@protocol BindingsRoundEventCallback <NSObject> -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -@protocol BindingsSearchCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -@protocol BindingsSingleSearchCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsTimeSource <NSObject> -- (int64_t)nowMs; -@end - -@protocol BindingsUpdateBackupFunc <NSObject> -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -@interface BindingsBackup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * AddJson stores a passed in json string in the backup structure - */ -- (void)addJson:(NSString* _Nullable)json; -/** - * IsBackupRunning returns true if the backup has been initialized and is -running. Returns false if it has been stopped. - */ -- (BOOL)isBackupRunning; -/** - * StopBackup stops the backup processes and deletes the user's password from -storage. To enable backups again, call InitializeBackup. - */ -- (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsBackupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID - -@property (nonatomic) NSString* _Nonnull params; -@end - -/** - * BindingsClient wraps the api.Client, implementing additional functions -to support the gomobile Client interface - */ -@interface BindingsClient : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)confirmAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteAllRequests clears all requests from Client's auth storage. - */ -- (BOOL)deleteAllRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteContact is a function which removes a contact from Client's storage - */ -- (BOOL)deleteContact:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteReceiveRequests clears receive requests from Client's auth storage. - */ -- (BOOL)deleteReceiveRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteRequest will delete a request, agnostic of request type -for the given partner ID. If no request exists for this -partner ID an error will be returned. - */ -- (BOOL)deleteRequest:(NSData* _Nullable)requesterUserId error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteSentRequests clears sent requests from Client's auth storage. - */ -- (BOOL)deleteSentRequests:(NSError* _Nullable* _Nullable)error; -// skipped method Client.GetInternalClient with unsupported parameter or return types - -/** - * GetNodeRegistrationStatus returns a struct with the number of nodes the -client is registered with and the number total. - */ -- (BindingsNodeRegistrationsStatus* _Nullable)getNodeRegistrationStatus:(NSError* _Nullable* _Nullable)error; -/** - * GetPartners returns a list of - */ -- (NSData* _Nullable)getPartners:(NSError* _Nullable* _Nullable)error; -/** - * GetPreferredBins returns the geographic bin or bins that the provided two -character country code is a part of. The bins are returned as CSV. - */ -- (NSString* _Nonnull)getPreferredBins:(NSString* _Nullable)countryCode error:(NSError* _Nullable* _Nullable)error; -- (NSString* _Nonnull)getPreimages:(NSData* _Nullable)identity; -// skipped method Client.GetRateLimitParams with unsupported parameter or return types - -- (NSString* _Nonnull)getRelationshipFingerprint:(NSData* _Nullable)partnerID error:(NSError* _Nullable* _Nullable)error; -/** - * Returns a user object from which all information about the current user -can be gleaned - */ -- (BindingsUser* _Nullable)getUser; -/** - * HasRunningProcessies checks if any background threads are running. -returns true if none are running. This is meant to be -used when NetworkFollowerStatus() returns Stopping. -Due to the handling of comms on iOS, where the OS can -block indefiently, it may not enter the stopped -state apropreatly. This can be used instead. - */ -- (BOOL)hasRunningProcessies; -/** - * returns true if the network is read to be in a healthy state where -messages can be sent - */ -- (BOOL)isNetworkHealthy; -- (BindingsContact* _Nullable)makePrecannedAuthenticatedChannel:(long)precannedID error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the state of the network follower. Returns: -Stopped - 0 -Starting - 1000 -Running - 2000 -Stopping - 3000 - */ -- (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; -/** - * RegisterClientErrorCallback registers the callback to handle errors from the -long running threads controlled by StartNetworkFollower and StopNetworkFollower - */ -- (void)registerClientErrorCallback:(id<BindingsClientError> _Nullable)clientError; -/** - * RegisterEventCallback records the given function to receive -ReportableEvent objects. It returns the internal index -of the callback so that it can be deleted later. - */ -- (BOOL)registerEventCallback:(NSString* _Nullable)name myObj:(id<BindingsEventCallbackFunctionObject> _Nullable)myObj error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterForNotifications accepts firebase messaging token - */ -- (BOOL)registerForNotifications:(NSString* _Nullable)token error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterListener records and installs a listener for messages -matching specific uid, msgType, and/or username -Returns a ListenerUnregister interface which can be - -to register for any userID, pass in an id with length 0 or an id with -all zeroes - -to register for any message type, pass in a message type of 0 - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsUnregister* _Nullable)registerListener:(NSData* _Nullable)uid msgType:(long)msgType listener:(id<BindingsListener> _Nullable)listener error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterNetworkHealthCB registers the network health callback to be called -any time the network health changes. Returns a unique ID that can be used to -unregister the network health callback. - */ -- (int64_t)registerNetworkHealthCB:(id<BindingsNetworkHealthCallback> _Nullable)nhc; -- (void)registerPreimageCallback:(NSData* _Nullable)identity pin:(id<BindingsPreimageNotification> _Nullable)pin; -/** - * RegisterRoundEventsHandler registers a callback interface for round -events. -The rid is the round the event attaches to -The timeoutMS is the number of milliseconds until the event fails, and the -validStates are a list of states (one per byte) on which the event gets -triggered -States: - 0x00 - PENDING (Never seen by client) - 0x01 - PRECOMPUTING - 0x02 - STANDBY - 0x03 - QUEUED - 0x04 - REALTIME - 0x05 - COMPLETED - 0x06 - FAILED -These states are defined in elixxir/primitives/states/state.go - */ -- (BindingsUnregister* _Nullable)registerRoundEventsHandler:(long)rid cb:(id<BindingsRoundEventCallback> _Nullable)cb timeoutMS:(long)timeoutMS il:(BindingsIntList* _Nullable)il; -- (void)replayRequests; -- (BOOL)requestAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (BOOL)resetSession:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * This will return the round the message was sent on if it is successfully sent -This can be used to register a round event to learn about message delivery. -on failure a round id of -1 is returned - */ -- (BOOL)sendCmix:(NSData* _Nullable)recipient contents:(NSData* _Nullable)contents parameters:(NSString* _Nullable)parameters ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * SendE2E sends an end-to-end payload to the provided recipient with -the provided msgType. Returns the list of rounds in which parts of -the message were sent or an error if it fails. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsSendReport* _Nullable)sendE2E:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SendUnsafe sends an unencrypted payload to the provided recipient -with the provided msgType. Returns the list of rounds in which parts -of the message were sent or an error if it fails. -NOTE: Do not use this function unless you know what you are doing. -This function always produces an error message in client logging. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types with custom types - */ -- (BindingsRoundList* _Nullable)sendUnsafe:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SetProxiedBins updates the host pool filter that filters out gateways that -are not in one of the specified bins. The provided bins should be CSV. - */ -- (BOOL)setProxiedBins:(NSString* _Nullable)binStringsCSV error:(NSError* _Nullable* _Nullable)error; -/** - * StartNetworkFollower kicks off the tracking of the network. It starts -long running network client threads and returns an object for checking -state and stopping those threads. -Call this when returning from sleep and close when going back to -sleep. -These threads may become a significant drain on battery when offline, ensure -they are stopped if there is no internet access -Threads Started: - - Network Follower (/network/follow.go) - tracks the network events and hands them off to workers for handling - - Historical Round Retrieval (/network/rounds/historical.go) - Retrieves data about rounds which are too old to be stored by the client - - Message Retrieval Worker Group (/network/rounds/retrieve.go) - Requests all messages in a given round from the gateway of the last node - - Message Handling Worker Group (/network/message/handle.go) - Decrypts and partitions messages when signals via the Switchboard - - Health Tracker (/network/health) - Via the network instance tracks the state of the network - - Garbled Messages (/network/message/garbled.go) - Can be signaled to check all recent messages which could be be decoded - Uses a message store on disk for persistence - - Critical Messages (/network/message/critical.go) - Ensures all protocol layer mandatory messages are sent - Uses a message store on disk for persistence - - KeyExchange Trigger (/keyExchange/trigger.go) - Responds to sent rekeys and executes them - - KeyExchange Confirm (/keyExchange/confirm.go) - Responds to confirmations of successful rekey operations - */ -- (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * StopNetworkFollower stops the network follower if it is running. -It returns errors if the Follower is in the wrong status to stop or if it -fails to stop it. -if the network follower is running and this fails, the client object will -most likely be in an unrecoverable state and need to be trashed. - */ -- (BOOL)stopNetworkFollower:(NSError* _Nullable* _Nullable)error; -/** - * UnregisterEventCallback deletes the callback identified by the -index. It returns an error if it fails. - */ -- (void)unregisterEventCallback:(NSString* _Nullable)name; -/** - * UnregisterForNotifications unregister user for notifications - */ -- (BOOL)unregisterForNotifications:(NSError* _Nullable* _Nullable)error; -- (void)unregisterNetworkHealthCB:(int64_t)funcID; -- (BOOL)verifyOwnership:(NSData* _Nullable)receivedMarshaled verifiedMarshaled:(NSData* _Nullable)verifiedMarshaled ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForMessageDelivery allows the caller to get notified if the rounds a -message was sent in successfully completed. Under the hood, this uses an API -which uses the internal round data, network historical round lookup, and -waiting on network events to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - -This function takes the marshaled send report to ensure a memory leak does -not occur as a result of both sides of the bindings holding a reference to -the same pointer. - */ -- (BOOL)waitForMessageDelivery:(NSData* _Nullable)marshaledSendReport mdc:(id<BindingsMessageDeliveryCallback> _Nullable)mdc timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForNewtwork will block until either the network is healthy or the -passed timeout. It will return true if the network is healthy - */ -- (BOOL)waitForNetwork:(long)timeoutMS; -/** - * WaitForRoundCompletion allows the caller to get notified if a round -has completed (or failed). Under the hood, this uses an API which uses the internal -round data, network historical round lookup, and waiting on network events -to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - */ -- (BOOL)waitForRoundCompletion:(long)roundID rec:(id<BindingsRoundCompletionCallback> _Nullable)rec timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * contact object - */ -@interface BindingsContact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped method Contact.GetAPIContact with unsupported parameter or return types - -/** - * GetDHPublicKey returns the public key associated with the Contact. - */ -- (NSData* _Nullable)getDHPublicKey; -/** - * Returns a fact list for adding and getting facts to and from the contact - */ -- (BindingsFactList* _Nullable)getFactList; -/** - * GetID returns the user ID for this user. - */ -- (NSData* _Nullable)getID; -/** - * GetDHPublicKey returns hash of a DH proof of key ownership. - */ -- (NSData* _Nullable)getOwnershipProof; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsContactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BindingsContact* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * DummyTraffic contains the file dummy traffic manager. The manager can be used -to set and get the status of the send thread. - */ -@interface BindingsDummyTraffic : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client maxNumMessages:(long)maxNumMessages avgSendDeltaMS:(long)avgSendDeltaMS randomRangeMS:(long)randomRangeMS; -/** - * GetStatus returns the current state of the dummy traffic send thread. It has -the following return values: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Note that this function does not return the status set by SetStatus directly; -it returns the current status of the send thread, which means any call to -SetStatus will have a small delay before it is returned by GetStatus. - */ -- (BOOL)getStatus; -/** - * SetStatus sets the state of the dummy traffic send thread, which determines -if the thread is running or paused. The possible statuses are: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Returns an error if the channel is full. -Note that this function cannot change the status of the send thread if it has -yet to be started or stopped. - */ -- (BOOL)setStatus:(BOOL)status error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsFact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -- (nullable instancetype)init:(long)factType factStr:(NSString* _Nullable)factStr; -- (NSString* _Nonnull)get; -- (NSString* _Nonnull)stringify; -- (long)type; -@end - -@interface BindingsFactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * FactList - */ -- (nullable instancetype)init; -- (BOOL)add:(NSString* _Nullable)factData factType:(long)factType error:(NSError* _Nullable* _Nullable)error; -- (BindingsFact* _Nullable)get:(long)i; -- (long)num; -- (NSString* _Nonnull)stringify:(NSError* _Nullable* _Nullable)error; -@end - -/** - * FilePartTracker contains the interfaces.FilePartTracker. - */ -@interface BindingsFilePartTracker : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetNumParts returns the total number of file parts in the transfer. - */ -- (long)getNumParts; -/** - * GetPartStatus returns the status of the file part with the given part number. -The possible values for the status are: -0 = unsent -1 = sent (sender has sent a part, but it has not arrived) -2 = arrived (sender has sent a part, and it has arrived) -3 = received (receiver has received a part) - */ -- (long)getPartStatus:(long)partNum; -@end - -/** - * FileTransfer contains the file transfer manager. - */ -@interface BindingsFileTransfer : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client receiveFunc:(id<BindingsFileTransferReceiveFunc> _Nullable)receiveFunc parameters:(NSString* _Nullable)parameters; -/** - * CloseSend deletes a sent file transfer from the sent transfer map and from -storage once a transfer has completed or reached the retry limit. Returns an -error if the transfer has not run out of retries. - */ -- (BOOL)closeSend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * GetMaxFileNameByteLength returns the maximum length, in bytes, allowed for a -file name. - */ -- (long)getMaxFileNameByteLength; -/** - * GetMaxFilePreviewSize returns the maximum file preview size, in bytes. - */ -- (long)getMaxFilePreviewSize; -/** - * GetMaxFileSize returns the maximum file size, in bytes, allowed to be -transferred. - */ -- (long)getMaxFileSize; -/** - * GetMaxFileTypeByteLength returns the maximum length, in bytes, allowed for a -file type. - */ -- (long)getMaxFileTypeByteLength; -/** - * Receive returns the fully assembled file on the completion of the transfer. -It deletes the transfer from the received transfer map and from storage. -Returns an error if the transfer is not complete, the full file cannot be -verified, or if the transfer cannot be found. - */ -- (NSData* _Nullable)receive:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterReceiveProgressCallback allows for the registration of a callback to -track the progress of an individual received file transfer. The callback will -be called immediately when added to report the current status of the -transfer. It will then call every time a file part is received, the transfer -completes, or an error occurs. It is called at most once ever period, which -means if events occur faster than the period, then they will not be reported -and instead, the progress will be reported once at the end of the period. -Once the callback reports that the transfer has completed, the recipient -can get the full file by calling Receive. -The period is specified in milliseconds. - */ -- (BOOL)registerReceiveProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferReceivedProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterSendProgressCallback allows for the registration of a callback to -track the progress of an individual sent file transfer. The callback will be -called immediately when added to report the current status of the transfer. -It will then call every time a file part is sent, a file part arrives, the -transfer completes, or an error occurs. It is called at most once every -period, which means if events occur faster than the period, then they will -not be reported and instead, the progress will be reported once at the end of -the period. -The period is specified in milliseconds. - */ -- (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends a file to the recipient. The sender must have an E2E relationship -with the recipient. -The file name is the name of the file to show a user. It has a max length of -48 bytes. -The file type identifies what type of file is being sent. It has a max length -of 8 bytes. -The file data cannot be larger than 256 kB -The retry float is the total amount of data to send relative to the data -size. Data will be resent on error and will resend up to [(1 + retry) * -fileSize]. -The preview stores a preview of the data (such as a thumbnail) and is -capped at 4 kB in size. -Returns a unique transfer ID used to identify the transfer. -PeriodMS is the duration, in milliseconds, to wait between progress callback -calls. Set this large enough to prevent spamming. - */ -- (NSData* _Nullable)send:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType fileData:(NSData* _Nullable)fileData recipientID:(NSData* _Nullable)recipientID retry:(float)retry preview:(NSData* _Nullable)preview progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Group structure contains the identifying and membership information of a -group chat. - */ -@interface BindingsGroup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetCreatedMS returns the time the group was created in milliseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedMS; -/** - * GetCreatedNano returns the time the group was created in nanoseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedNano; -/** - * GetID return the 33-byte unique group ID. - */ -- (NSData* _Nullable)getID; -/** - * GetInitMessage returns initial message sent with the group request. - */ -- (NSData* _Nullable)getInitMessage; -/** - * GetMembership returns a list of contacts, one for each member in the group. -The list is in order; the first contact is the leader/creator of the group. -All subsequent members are ordered by their ID. - */ -- (BindingsGroupMembership* _Nullable)getMembership; -/** - * GetName returns the name set by the user for the group. - */ -- (NSData* _Nullable)getName; -/** - * Serialize serializes the Group. - */ -- (NSData* _Nullable)serialize; -@end - -/** - * GroupChat object contains the group chat manager. - */ -@interface BindingsGroupChat : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetGroup returns the group with the group ID. If no group exists, then the -error "failed to find group" is returned. - */ -- (BindingsGroup* _Nullable)getGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * GetGroups returns an IdList containing a list of group IDs that the user is a -part of. - */ -- (BindingsIdList* _Nullable)getGroups; -/** - * JoinGroup allows a user to join a group when they receive a request. The -caller must pass in the serialized bytes of a Group. - */ -- (BOOL)joinGroup:(NSData* _Nullable)serializedGroupData error:(NSError* _Nullable* _Nullable)error; -/** - * LeaveGroup deletes a group so a user no longer has access. - */ -- (BOOL)leaveGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * MakeGroup creates a new group and sends a group request to all members in the -group. The ID of the new group, the rounds the requests were sent on, and the -status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)makeGroup:(BindingsIdList* _Nullable)membership name:(NSData* _Nullable)name message:(NSData* _Nullable)message; -/** - * NumGroups returns the number of groups the user is a part of. - */ -- (long)numGroups; -/** - * ResendRequest resends a group request to all members in the group. The rounds -they were sent on and the status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)resendRequest:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends the message to the specified group. Returns the round the messages -were sent on. - */ -- (BindingsGroupSendReport* _Nullable)send:(NSData* _Nullable)groupIdBytes message:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * // -Member Structure -// -GroupMember represents a member in the group membership list. - */ -@interface BindingsGroupMember : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMember.Member with unsupported type: gitlab.com/elixxir/crypto/group.Member - -// skipped method GroupMember.DeepCopy with unsupported parameter or return types - -// skipped method GroupMember.Equal with unsupported parameter or return types - -/** - * GetDhKey returns the byte representation of the public Diffie–Hellman key of -the member. - */ -- (NSData* _Nullable)getDhKey; -/** - * GetID returns the 33-byte user ID of the member. - */ -- (NSData* _Nullable)getID; -- (NSString* _Nonnull)goString; -- (NSData* _Nullable)serialize; -- (NSString* _Nonnull)string; -@end - -/** - * GroupMembership structure contains a list of members that are part of a -group. The first member is the group leader. - */ -@interface BindingsGroupMembership : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Get returns the member at the index. The member at index 0 is always the -group leader. An error is returned if the index is out of range. - */ -- (BindingsGroupMember* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of members in the group membership. - */ -- (long)len; -@end - -/** - * GroupMessageReceive contains a group message, its ID, and its data that a -user receives. - */ -@interface BindingsGroupMessageReceive : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMessageReceive.MessageReceive with unsupported type: gitlab.com/elixxir/client/groupChat.MessageReceive - -/** - * GetEphemeralID returns the ephemeral ID of the recipient. - */ -- (int64_t)getEphemeralID; -/** - * GetGroupID returns the 33-byte group ID. - */ -- (NSData* _Nullable)getGroupID; -/** - * GetMessageID returns the message ID. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetPayload returns the message payload. - */ -- (NSData* _Nullable)getPayload; -/** - * GetRecipientID returns the 33-byte user ID of the recipient. - */ -- (NSData* _Nullable)getRecipientID; -/** - * GetRoundID returns the ID of the round the message was sent on. - */ -- (int64_t)getRoundID; -/** - * GetRoundTimestampMS returns the timestamp, in milliseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the timestamp, in nanoseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the ID of the round the message was sent on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSenderID returns the 33-byte user ID of the sender. - */ -- (NSData* _Nullable)getSenderID; -/** - * GetTimestampMS returns the message timestamp in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message timestamp in nanoseconds. - */ -- (int64_t)getTimestampNano; -- (NSString* _Nonnull)string; -@end - -@interface BindingsGroupReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable grpId; -@property (nonatomic) long status; -@end - -/** - * GroupSendReport is returned when sending a group message. It contains the -round ID sent on and the timestamp of the send. - */ -@interface BindingsGroupSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetMessageID returns the ID of the round that the send occurred on. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetRoundID returns the ID of the round that the send occurred on. - */ -- (int64_t)getRoundID; -/** - * GetRoundURL returns the URL of the round that the send occurred on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the timestamp of the send in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the timestamp of the send in nanoseconds. - */ -- (int64_t)getTimestampNano; -@end - -/** - * ID list -IdList contains a list of IDs. - */ -@interface BindingsIdList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Add appends the ID bytes to the end of the list. - */ -- (BOOL)add:(NSData* _Nullable)idBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Get returns the ID at the index. An error is returned if the index is out of -range. - */ -- (NSData* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of IDs in the list. - */ -- (long)len; -@end - -@interface BindingsIntList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (void)add:(long)i; -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -@interface BindingsManyNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsNotificationForMeReport* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -/** - * Message is a message received from the cMix network in the clear -or that has been decrypted using established E2E keys. - */ -@interface BindingsMessage : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetID returns the id of the message - */ -- (NSData* _Nullable)getID; -/** - * GetMessageType returns the message's type - */ -- (long)getMessageType; -/** - * GetPayload returns the message's payload/contents - */ -- (NSData* _Nullable)getPayload; -/** - * GetRoundId returns the message's round ID - */ -- (int64_t)getRoundId; -/** - * GetRoundTimestampMS returns the message's round timestamp in milliseconds - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the message's round timestamp in nanoseconds - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the message's round URL - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSender returns the message's sender ID, if available - */ -- (NSData* _Nullable)getSender; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -@end - -/** - * NewGroupReport is returned when creating a new group and contains the ID of -the group, a list of rounds that the group requests were sent on, and the -status of the send. - */ -@interface BindingsNewGroupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetError returns the string of an error. -Will be an empty string if no error occured - */ -- (NSString* _Nonnull)getError; -/** - * GetGroup returns the Group. - */ -- (BindingsGroup* _Nullable)getGroup; -/** - * GetRoundList returns the RoundList containing a list of rounds requests were -sent on. - */ -- (BindingsRoundList* _Nullable)getRoundList; -/** - * GetStatus returns the status of the requests sent when creating a new group. -status = 0 an error occurred before any requests could be sent - 1 all requests failed to send (call Resend Group) - 2 some request failed and some succeeded (call Resend Group) - 3, all requests sent successfully (call Resend Group) - */ -- (long)getStatus; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * NodeRegistrationsStatus structure for returning node registration statuses -for bindings. - */ -@interface BindingsNodeRegistrationsStatus : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetRegistered returns the number of nodes registered with the client. - */ -- (long)getRegistered; -/** - * GetTotal return the total of nodes currently in the network. - */ -- (long)getTotal; -@end - -@interface BindingsNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)forMe; -- (NSData* _Nullable)source; -- (NSString* _Nonnull)type; -@end - -/** - * RestoreContactsReport is a gomobile friendly report structure -for determining which IDs restored, which failed, and why. - */ -@interface BindingsRestoreContactsReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetErrorAt returns the error string at index - */ -- (NSString* _Nonnull)getErrorAt:(long)index; -/** - * GetFailedAt returns the failed ID at index - */ -- (NSData* _Nullable)getFailedAt:(long)index; -/** - * GetRestoreContactsError returns an error string. Empty if no error. - */ -- (NSString* _Nonnull)getRestoreContactsError; -/** - * GetRestoredAt returns the restored ID at index - */ -- (NSData* _Nullable)getRestoredAt:(long)index; -/** - * LenFailed returns the length of the ID's failed. - */ -- (long)lenFailed; -/** - * LenRestored returns the length of ID's restored. - */ -- (long)lenRestored; -@end - -@interface BindingsRoundList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * the send report is the mechanisim by which sendE2E returns a single - */ -@interface BindingsSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (NSData* _Nullable)getMessageID; -- (BindingsRoundList* _Nullable)getRoundList; -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsSendReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field SendReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable mid; -@property (nonatomic) int64_t ts; -@end - -/** - * Generic Unregister - a generic return used for all callbacks which can be -unregistered -Interface which allows the un-registration of a listener - */ -@interface BindingsUnregister : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Call unregisters a callback - */ -- (void)unregister; -@end - -@interface BindingsUser : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsContact* _Nullable)getContact; -- (NSData* _Nullable)getE2EDhPrivateKey; -- (NSData* _Nullable)getE2EDhPublicKey; -- (NSData* _Nullable)getReceptionID; -- (NSData* _Nullable)getReceptionRSAPrivateKeyPem; -- (NSData* _Nullable)getReceptionRSAPublicKeyPem; -- (NSData* _Nullable)getReceptionSalt; -- (NSData* _Nullable)getTransmissionID; -- (NSData* _Nullable)getTransmissionRSAPrivateKeyPem; -- (NSData* _Nullable)getTransmissionRSAPublicKeyPem; -- (NSData* _Nullable)getTransmissionSalt; -- (BOOL)isPrecanned; -@end - -@interface BindingsUserDiscovery : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)init:(BindingsClient* _Nullable)client; -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; -/** - * AddFact adds a fact for the user to user discovery. Will only succeed if the -user is already registered and the system does not have the fact currently -registered for any user. -Will fail if the fact string is not well formed. -This does not complete the fact registration process, it returns a -confirmation id instead. Over the communications system the fact is -associated with, a code will be sent. This confirmation ID needs to be -called along with the code to finalize the fact. - */ -- (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from -AddFact while the code will come over the associated communications system - */ -- (BOOL)confirmFact:(NSString* _Nullable)confirmationID code:(NSString* _Nullable)code error:(NSError* _Nullable* _Nullable)error; -/** - * Lookup the contact object associated with the given userID. The -id is the byte representation of an id. -This will reject if that id is malformed. The LookupCallback will return -the associated contact if it exists. - */ -- (BOOL)lookup:(NSData* _Nullable)idBytes callback:(id<BindingsLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * MultiLookup Looks for the contact object associated with all given userIDs. -The ids are the byte representation of an id stored in an IDList object. -This will reject if that id is malformed or if the indexing on the IDList -object is wrong. The MultiLookupCallback will return with all contacts -returned within the timeout. - */ -- (BOOL)multiLookup:(BindingsIdList* _Nullable)ids callback:(id<BindingsMultiLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * Register registers a user with user discovery. Will return an error if the -network signatures are malformed or if the username is taken. Usernames -cannot be changed after registration at this time. Will fail if the user is -already registered. -Identity does not go over cmix, it occurs over normal communications - */ -- (BOOL)register:(NSString* _Nullable)username error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveFact removes a previously confirmed fact. Will fail if the passed fact string is -not well-formed or if the fact is not associated with this client. -Users cannot remove username facts and must instead remove the user. - */ -- (BOOL)removeFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveUser deletes a user. The fact sent must be the username. -This function preserves the username forever and makes it -unusable. - */ -- (BOOL)removeUser:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * Search for the passed Facts. The factList is the stringification of a -fact list object, look at /bindings/list.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This is NOT intended to be used to search for multiple users at once, that -can have a privacy reduction. Instead, it is intended to be used to search -for a user where multiple pieces of information is known. - */ -- (BOOL)search:(NSString* _Nullable)fl callback:(id<BindingsSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SearchSingle searches for the passed Facts. The fact is the stringification of a -fact object, look at /bindings/contact.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This only searches for a single fact at a time. It is intended to make some -simple use cases of the API easier. - */ -- (BOOL)searchSingle:(NSString* _Nullable)f callback:(id<BindingsSingleSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SetAlternativeUserDiscovery sets the alternativeUd object within manager. -Once set, any user discovery operation will go through the alternative -user discovery service. -To undo this operation, use UnsetAlternativeUserDiscovery. -The contact file is the already read in bytes, not the file path for the contact file. - */ -- (BOOL)setAlternativeUserDiscovery:(NSData* _Nullable)address cert:(NSData* _Nullable)cert contactFile:(NSData* _Nullable)contactFile error:(NSError* _Nullable* _Nullable)error; -/** - * UnsetAlternativeUserDiscovery clears out the information from -the Manager object. - */ -- (BOOL)unsetAlternativeUserDiscovery:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Error codes - */ -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedCode; -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedMessage; - -/** - * CompressJpeg takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpeg(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * CompressJpegForPreview takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpegForPreview(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * DownloadAndVerifySignedNdfWithUrl retrieves the NDF from a specified URL. -The NDF is processed into a protobuf containing a signature which -is verified using the cert string passed in. The NDF is returned as marshaled -byte data which may be used to start a client. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadAndVerifySignedNdfWithUrl(NSString* _Nullable url, NSString* _Nullable cert, NSError* _Nullable* _Nullable error); - -/** - * DownloadDAppRegistrationDB returns a []byte containing -the JSON data describing registered dApps. -See https://git.xx.network/elixxir/registered-dapps - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadDAppRegistrationDB(NSError* _Nullable* _Nullable error); - -/** - * DownloadErrorDB returns a []byte containing the JSON data -describing client errors. -See https://git.xx.network/elixxir/client-error-database/ - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadErrorDB(NSError* _Nullable* _Nullable error); - -/** - * DumpStack returns a string with the stack trace of every running thread. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsDumpStack(NSError* _Nullable* _Nullable error); - -/** - * EnableGrpcLogs sets GRPC trace logging - */ -FOUNDATION_EXPORT void BindingsEnableGrpcLogs(id<BindingsLogWriter> _Nullable writer); - -/** - * ErrorStringToUserFriendlyMessage takes a passed in errStr which will be -a backend generated error. These may be error specifically written by -the backend team or lower level errors gotten from low level dependencies. -This function will parse the error string for common errors provided from -errToUserErr to provide a more user-friendly error message for the front end. -If the error is not common, some simple parsing is done on the error message -to make it more user-accessible, removing backend specific jargon. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsErrorStringToUserFriendlyMessage(NSString* _Nullable errStr); - -/** - * GenerateSecret creates a secret password using a system-based -pseudorandom number generator. It takes 1 parameter, `numBytes`, -which should be set to 32, but can be set higher in certain cases. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateSecret(long numBytes); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetCMIXParams(NSError* _Nullable* _Nullable error); - -/** - * returns a previously created client. IF be used if the garbage collector -removes the client instance on the app side. Is NOT thread safe relative to -login, newClient, or newPrecannedClient - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsGetClientSingleton(void); - -/** - * GetDependencies returns the api DEPENDENCIES - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetDependencies(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetE2EParams(NSError* _Nullable* _Nullable error); - -/** - * GetGitVersion rturns the api GITVERSION - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetGitVersion(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetNetworkParams(NSError* _Nullable* _Nullable error); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetUnsafeParams(NSError* _Nullable* _Nullable error); - -/** - * GetVersion returns the api SEMVER - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void); - -/** - * InitializeBackup starts the backup processes that returns backup updates when -they occur. Any time an event occurs that changes the contents of the backup, -such as adding or deleting a contact, the backup is triggered and an -encrypted backup is generated and returned on the updateBackupCb callback. -Call this function only when enabling backup if it has not already been -initialized or when the user wants to change their password. -To resume backup process on app recovery, use ResumeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsInitializeBackup(NSString* _Nullable password, id<BindingsUpdateBackupFunc> _Nullable updateBackupCb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * LoadSecretWithMnemonic loads the secret stored from the call to -StoreSecretWithMnemonic. The path given should be the same filepath -as the path given in StoreSecretWithMnemonic. There should be a file -in this path called ".recovery". This operation is not tied -to client operations, as the user will not have a client when trying to -recover their account. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsLoadSecretWithMnemonic(NSString* _Nullable mnemonic, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * sets level of logging. All logs the set level and above will be displayed -options are: - TRACE - 0 - DEBUG - 1 - INFO - 2 - WARN - 3 - ERROR - 4 - CRITICAL - 5 - FATAL - 6 -The default state without updates is: INFO - */ -FOUNDATION_EXPORT BOOL BindingsLogLevel(long level, NSError* _Nullable* _Nullable error); - -/** - * Login will load an existing client from the storageDir -using the password. This will fail if the client doesn't exist or -the password is incorrect. -The password is passed as a byte array so that it can be cleared from -memory and stored as securely as possible using the memguard library. -Login does not block on network connection, and instead loads and -starts subprocesses to perform network operations. - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsLogin(NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * MakeIdList creates a new empty IdList. - */ -FOUNDATION_EXPORT BindingsIdList* _Nullable BindingsMakeIdList(void); - -FOUNDATION_EXPORT BindingsIntList* _Nullable BindingsMakeIntList(void); - -/** - * NewClient creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable regCode, NSError* _Nullable* _Nullable error); - -/** - * NewClientFromBackup constructs a new Client from an encrypted backup. The backup -is decrypted using the backupPassphrase. On success a successful client creation, -the function will return a JSON encoded list of the E2E partners -contained in the backup and a json-encoded string of the parameters stored in the backup - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); - -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -FOUNDATION_EXPORT BindingsDummyTraffic* _Nullable BindingsNewDummyTrafficManager(BindingsClient* _Nullable client, long maxNumMessages, long avgSendDeltaMS, long randomRangeMS, NSError* _Nullable* _Nullable error); - -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -FOUNDATION_EXPORT BindingsFact* _Nullable BindingsNewFact(long factType, NSString* _Nullable factStr, NSError* _Nullable* _Nullable error); - -/** - * FactList - */ -FOUNDATION_EXPORT BindingsFactList* _Nullable BindingsNewFactList(void); - -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -FOUNDATION_EXPORT BindingsFileTransfer* _Nullable BindingsNewFileTransferManager(BindingsClient* _Nullable client, id<BindingsFileTransferReceiveFunc> _Nullable receiveFunc, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * NewGroupManager creates a new group chat manager. - */ -FOUNDATION_EXPORT BindingsGroupChat* _Nullable BindingsNewGroupManager(BindingsClient* _Nullable client, id<BindingsGroupRequestFunc> _Nullable requestFunc, id<BindingsGroupReceiveFunc> _Nullable receiveFunc, NSError* _Nullable* _Nullable error); - -/** - * NewPrecannedClient creates an insecure user with predetermined keys with nodes -It creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewPrecannedClient(long precannedID, NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); - -/** - * NotificationsForMe Check if a notification received is for me -It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, -a Type, and a source. These are as follows: - TYPE SOURCE DESCRIPTION - "default" recipient user ID A message with no association - "request" sender user ID A channel request has been received - "reset" sender user ID A channel reset has been received - "confirm" sender user ID A channel request has been accepted - "silent" sender user ID A message which should not be notified on - "e2e" sender user ID reception of an E2E message - "group" group ID reception of a group chat message - "endFT" sender user ID Last message sent confirming end of file transfer - "groupRQ" sender user ID Request from sender to join a group chat - */ -FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotificationsForMe(NSString* _Nullable notifCSV, NSString* _Nullable preimages, NSError* _Nullable* _Nullable error); - -/** - * RegisterLogWriter registers a callback on which logs are written. - */ -FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); - -/** - * RestoreContactsFromBackup takes as input the jason output of the -`NewClientFromBackup` function, unmarshals it into IDs, looks up -each ID in user discovery, and initiates a session reset request. -This function will not return until every id in the list has been sent a -request. It should be called again and again until it completes. -xxDK users should not use this function. This function is used by -the mobile phone apps and are not intended to be part of the xxDK. It -should be treated as internal functions specific to the phone apps. - */ -FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); - -/** - * ResumeBackup starts the backup processes back up with a new callback after it -has been initialized. -Call this function only when resuming a backup that has already been -initialized or to replace the callback. -To start the backup for the first time or to use a new password, use -InitializeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsResumeBackup(id<BindingsUpdateBackupFunc> _Nullable cb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * SetTimeSource sets the network time to a custom source. - */ -FOUNDATION_EXPORT void BindingsSetTimeSource(id<BindingsTimeSource> _Nullable timeNow); - -/** - * StoreSecretWithMnemonic stores the secret tied with the mnemonic to storage. -Unlike other storage operations, this does not use EKV, as that is -intrinsically tied to client operations, which the user will not have while -trying to recover their account. As such, we store the encrypted data -directly, with a specified path. Path will be a valid filepath in which the -recover file will be stored as ".recovery". - -As an example, given "home/user/xxmessenger/storagePath", -the recovery file will be stored at -"home/user/xxmessenger/storagePath/.recovery" - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsStoreSecretWithMnemonic(NSData* _Nullable secret, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled contact object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsContact* _Nullable BindingsUnmarshalContact(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled send report object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsSendReport* _Nullable BindingsUnmarshalSendReport(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * UpdateCommonErrors takes the passed in contents of a JSON file and updates the -errToUserErr map with the contents of the json file. The JSON's expected format -conform with the commented examples provides in errToUserErr above. -NOTE that you should not pass in a file path, but a preloaded JSON file - */ -FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, NSError* _Nullable* _Nullable error); - -// skipped function WrapAPIClient with unsupported parameter or return types - - -// skipped function WrapUserDiscovery with unsupported parameter or return types - - -@class BindingsAuthConfirmCallback; - -@class BindingsAuthRequestCallback; - -@class BindingsAuthResetNotificationCallback; - -@class BindingsClientError; - -@class BindingsEventCallbackFunctionObject; - -@class BindingsFileTransferReceiveFunc; - -@class BindingsFileTransferReceivedProgressFunc; - -@class BindingsFileTransferSentProgressFunc; - -@class BindingsGroupReceiveFunc; - -@class BindingsGroupRequestFunc; - -@class BindingsListener; - -@class BindingsLogWriter; - -@class BindingsLookupCallback; - -@class BindingsMessageDeliveryCallback; - -@class BindingsMultiLookupCallback; - -@class BindingsNetworkHealthCallback; - -@class BindingsPreimageNotification; - -@class BindingsRestoreContactsUpdater; - -@class BindingsRoundCompletionCallback; - -@class BindingsRoundEventCallback; - -@class BindingsSearchCallback; - -@class BindingsSingleSearchCallback; - -@class BindingsTimeSource; - -@class BindingsUpdateBackupFunc; - -/** - * AuthConfirmCallback notifies the register whenever they receive an auth -request confirmation - */ -@interface BindingsAuthConfirmCallback : NSObject <goSeqRefInterface, BindingsAuthConfirmCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthRequestCallback : NSObject <goSeqRefInterface, BindingsAuthRequestCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@interface BindingsClientError : NSObject <goSeqRefInterface, BindingsClientError> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -/** - * EventCallbackFunctionObject bindings interface which contains function -that implements the EventCallbackFunction - */ -@interface BindingsEventCallbackFunctionObject : NSObject <goSeqRefInterface, BindingsEventCallbackFunctionObject> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -/** - * FileTransferReceiveFunc contains a function callback that notifies the -receiver of an incoming file transfer. It is called on the reception of the -initial file transfer message. - */ -@interface BindingsFileTransferReceiveFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -/** - * FileTransferReceivedProgressFunc contains a function callback that tracks the -progress of receiving a file. It is called when a file part is received, the -transfer completes, or on error. - */ -@interface BindingsFileTransferReceivedProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceivedProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * FileTransferSentProgressFunc contains a function callback that tracks the -progress of sending a file. It is called when a file part is sent, a file -part arrives, the transfer completes, or on error. - */ -@interface BindingsFileTransferSentProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferSentProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * GroupReceiveFunc contains a function callback that is called when a group -message is received. - */ -@interface BindingsGroupReceiveFunc : NSObject <goSeqRefInterface, BindingsGroupReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -/** - * GroupRequestFunc contains a function callback that is called when a group -request is received. - */ -@interface BindingsGroupRequestFunc : NSObject <goSeqRefInterface, BindingsGroupRequestFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -/** - * Listener provides a callback to hear a message -An object implementing this interface can be called back when the client -gets a message of the type that the registerer specified at registration -time. - */ -@interface BindingsListener : NSObject <goSeqRefInterface, BindingsListener> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@interface BindingsLogWriter : NSObject <goSeqRefInterface, BindingsLogWriter> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)log:(NSString* _Nullable)p0; -@end - -/** - * LookupCallback returns the result of a single lookup - */ -@interface BindingsLookupCallback : NSObject <goSeqRefInterface, BindingsLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -/** - * MessageDeliveryCallback gets called on the determination if all events -related to a message send were successful. - */ -@interface BindingsMessageDeliveryCallback : NSObject <goSeqRefInterface, BindingsMessageDeliveryCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -/** - * MultiLookupCallback returns the result of many parallel lookups - */ -@interface BindingsMultiLookupCallback : NSObject <goSeqRefInterface, BindingsMultiLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -/** - * A callback when which is used to receive notification if network health -changes - */ -@interface BindingsNetworkHealthCallback : NSObject <goSeqRefInterface, BindingsNetworkHealthCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BOOL)p0; -@end - -@interface BindingsPreimageNotification : NSObject <goSeqRefInterface, BindingsPreimageNotification> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -/** - * RestoreContactsUpdater interface provides a callback function -for receiving update information from RestoreContactsFromBackup. - */ -@interface BindingsRestoreContactsUpdater : NSObject <goSeqRefInterface, BindingsRestoreContactsUpdater> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -/** - * RoundCompletionCallback is returned when the completion of a round is known. - */ -@interface BindingsRoundCompletionCallback : NSObject <goSeqRefInterface, BindingsRoundCompletionCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -/** - * RoundEventCallback handles waiting on the exact state of a round on -the cMix network. - */ -@interface BindingsRoundEventCallback : NSObject <goSeqRefInterface, BindingsRoundEventCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -/** - * SearchCallback returns the result of a search - */ -@interface BindingsSearchCallback : NSObject <goSeqRefInterface, BindingsSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -/** - * SingleSearchCallback returns the result of a single search - */ -@interface BindingsSingleSearchCallback : NSObject <goSeqRefInterface, BindingsSingleSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@interface BindingsTimeSource : NSObject <goSeqRefInterface, BindingsTimeSource> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (int64_t)nowMs; -@end - -/** - * UpdateBackupFunc contains a function callback that returns new backups. - */ -@interface BindingsUpdateBackupFunc : NSObject <goSeqRefInterface, BindingsUpdateBackupFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Universe.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Universe.objc.h deleted file mode 100644 index 019e7502d581983722a15bf30799e85cbc5dd766..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Universe.objc.h +++ /dev/null @@ -1,29 +0,0 @@ -// Objective-C API for talking to Go package. -// gobind -lang=objc -// -// File is generated by gobind. Do not edit. - -#ifndef __Universe_H__ -#define __Universe_H__ - -@import Foundation; -#include "ref.h" - -@protocol Universeerror; -@class Universeerror; - -@protocol Universeerror <NSObject> -- (NSString* _Nonnull)error; -@end - -@class Universeerror; - -@interface Universeerror : NSError <goSeqRefInterface, Universeerror> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (NSString* _Nonnull)error; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/ref.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/ref.h deleted file mode 100644 index b8036a4d85c7387f3def61473a071b5d8c4c8208..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/ref.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef __GO_REF_HDR__ -#define __GO_REF_HDR__ - -#include <Foundation/Foundation.h> - -// GoSeqRef is an object tagged with an integer for passing back and -// forth across the language boundary. A GoSeqRef may represent either -// an instance of a Go object, or an Objective-C object passed to Go. -// The explicit allocation of a GoSeqRef is used to pin a Go object -// when it is passed to Objective-C. The Go seq package maintains a -// reference to the Go object in a map keyed by the refnum along with -// a reference count. When the reference count reaches zero, the Go -// seq package will clear the corresponding entry in the map. -@interface GoSeqRef : NSObject { -} -@property(readonly) int32_t refnum; -@property(strong) id obj; // NULL when representing a Go object. - -// new GoSeqRef object to proxy a Go object. The refnum must be -// provided from Go side. -- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; - -- (int32_t)incNum; - -@end - -@protocol goSeqRefInterface --(GoSeqRef*) _ref; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Modules/module.modulemap b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Modules/module.modulemap deleted file mode 100644 index 4316a5b24058edfc18ffb2dc7f7a982e8353441a..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Modules/module.modulemap +++ /dev/null @@ -1,8 +0,0 @@ -framework module "Bindings" { - header "ref.h" - header "Bindings.objc.h" - header "Universe.objc.h" - header "Bindings.h" - - export * -} \ No newline at end of file diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Resources/Info.plist b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Resources/Info.plist deleted file mode 100644 index 0d1a4b8ab9b1fc8e9357197398f73353470cb636..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Resources/Info.plist +++ /dev/null @@ -1,6 +0,0 @@ -<?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> - </dict> - </plist> diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings deleted file mode 100644 index 1e3b1dc25b4d65bf1e21a5d169dc3ae4de6c4fd7..0000000000000000000000000000000000000000 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings and /dev/null differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.h deleted file mode 100644 index 8906a7da239705b790cb2bb64de92f806640cb38..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.h +++ /dev/null @@ -1,13 +0,0 @@ - -// Objective-C API for talking to the following Go packages -// -// gitlab.com/elixxir/client/bindings -// -// File is generated by gomobile bind. Do not edit. -#ifndef __Bindings_FRAMEWORK_H__ -#define __Bindings_FRAMEWORK_H__ - -#include "Bindings.objc.h" -#include "Universe.objc.h" - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.objc.h deleted file mode 100644 index 32bf6d116888f787ced27b01b95cb4e1b2c1138b..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Bindings.objc.h +++ /dev/null @@ -1,2083 +0,0 @@ -// Objective-C API for talking to gitlab.com/elixxir/client/bindings Go package. -// gobind -lang=objc gitlab.com/elixxir/client/bindings -// -// File is generated by gobind. Do not edit. - -#ifndef __Bindings_H__ -#define __Bindings_H__ - -@import Foundation; -#include "ref.h" -#include "Universe.objc.h" - - -@class BindingsBackup; -@class BindingsBackupReport; -@class BindingsClient; -@class BindingsContact; -@class BindingsContactList; -@class BindingsDummyTraffic; -@class BindingsFact; -@class BindingsFactList; -@class BindingsFilePartTracker; -@class BindingsFileTransfer; -@class BindingsGroup; -@class BindingsGroupChat; -@class BindingsGroupMember; -@class BindingsGroupMembership; -@class BindingsGroupMessageReceive; -@class BindingsGroupReportDisk; -@class BindingsGroupSendReport; -@class BindingsIdList; -@class BindingsIntList; -@class BindingsManyNotificationForMeReport; -@class BindingsMessage; -@class BindingsNewGroupReport; -@class BindingsNodeRegistrationsStatus; -@class BindingsNotificationForMeReport; -@class BindingsRestoreContactsReport; -@class BindingsRoundList; -@class BindingsSendReport; -@class BindingsSendReportDisk; -@class BindingsUnregister; -@class BindingsUser; -@class BindingsUserDiscovery; -@protocol BindingsAuthConfirmCallback; -@class BindingsAuthConfirmCallback; -@protocol BindingsAuthRequestCallback; -@class BindingsAuthRequestCallback; -@protocol BindingsAuthResetNotificationCallback; -@class BindingsAuthResetNotificationCallback; -@protocol BindingsClientError; -@class BindingsClientError; -@protocol BindingsEventCallbackFunctionObject; -@class BindingsEventCallbackFunctionObject; -@protocol BindingsFileTransferReceiveFunc; -@class BindingsFileTransferReceiveFunc; -@protocol BindingsFileTransferReceivedProgressFunc; -@class BindingsFileTransferReceivedProgressFunc; -@protocol BindingsFileTransferSentProgressFunc; -@class BindingsFileTransferSentProgressFunc; -@protocol BindingsGroupReceiveFunc; -@class BindingsGroupReceiveFunc; -@protocol BindingsGroupRequestFunc; -@class BindingsGroupRequestFunc; -@protocol BindingsListener; -@class BindingsListener; -@protocol BindingsLogWriter; -@class BindingsLogWriter; -@protocol BindingsLookupCallback; -@class BindingsLookupCallback; -@protocol BindingsMessageDeliveryCallback; -@class BindingsMessageDeliveryCallback; -@protocol BindingsMultiLookupCallback; -@class BindingsMultiLookupCallback; -@protocol BindingsNetworkHealthCallback; -@class BindingsNetworkHealthCallback; -@protocol BindingsPreimageNotification; -@class BindingsPreimageNotification; -@protocol BindingsRestoreContactsUpdater; -@class BindingsRestoreContactsUpdater; -@protocol BindingsRoundCompletionCallback; -@class BindingsRoundCompletionCallback; -@protocol BindingsRoundEventCallback; -@class BindingsRoundEventCallback; -@protocol BindingsSearchCallback; -@class BindingsSearchCallback; -@protocol BindingsSingleSearchCallback; -@class BindingsSingleSearchCallback; -@protocol BindingsTimeSource; -@class BindingsTimeSource; -@protocol BindingsUpdateBackupFunc; -@class BindingsUpdateBackupFunc; - -@protocol BindingsAuthConfirmCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -@protocol BindingsAuthRequestCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsAuthResetNotificationCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@protocol BindingsClientError <NSObject> -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -@protocol BindingsEventCallbackFunctionObject <NSObject> -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -@protocol BindingsFileTransferReceiveFunc <NSObject> -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -@protocol BindingsFileTransferReceivedProgressFunc <NSObject> -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsFileTransferSentProgressFunc <NSObject> -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -@protocol BindingsGroupReceiveFunc <NSObject> -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -@protocol BindingsGroupRequestFunc <NSObject> -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -@protocol BindingsListener <NSObject> -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@protocol BindingsLogWriter <NSObject> -- (void)log:(NSString* _Nullable)p0; -@end - -@protocol BindingsLookupCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsMessageDeliveryCallback <NSObject> -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -@protocol BindingsMultiLookupCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -@protocol BindingsNetworkHealthCallback <NSObject> -- (void)callback:(BOOL)p0; -@end - -@protocol BindingsPreimageNotification <NSObject> -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -@protocol BindingsRestoreContactsUpdater <NSObject> -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -@protocol BindingsRoundCompletionCallback <NSObject> -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -@protocol BindingsRoundEventCallback <NSObject> -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -@protocol BindingsSearchCallback <NSObject> -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -@protocol BindingsSingleSearchCallback <NSObject> -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@protocol BindingsTimeSource <NSObject> -- (int64_t)nowMs; -@end - -@protocol BindingsUpdateBackupFunc <NSObject> -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -@interface BindingsBackup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * AddJson stores a passed in json string in the backup structure - */ -- (void)addJson:(NSString* _Nullable)json; -/** - * IsBackupRunning returns true if the backup has been initialized and is -running. Returns false if it has been stopped. - */ -- (BOOL)isBackupRunning; -/** - * StopBackup stops the backup processes and deletes the user's password from -storage. To enable backups again, call InitializeBackup. - */ -- (BOOL)stopBackup:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsBackupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field BackupReport.RestoredContacts with unsupported type: []*gitlab.com/xx_network/primitives/id.ID - -@property (nonatomic) NSString* _Nonnull params; -@end - -/** - * BindingsClient wraps the api.Client, implementing additional functions -to support the gomobile Client interface - */ -@interface BindingsClient : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)confirmAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteAllRequests clears all requests from Client's auth storage. - */ -- (BOOL)deleteAllRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteContact is a function which removes a contact from Client's storage - */ -- (BOOL)deleteContact:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteReceiveRequests clears receive requests from Client's auth storage. - */ -- (BOOL)deleteReceiveRequests:(NSError* _Nullable* _Nullable)error; -/** - * DeleteRequest will delete a request, agnostic of request type -for the given partner ID. If no request exists for this -partner ID an error will be returned. - */ -- (BOOL)deleteRequest:(NSData* _Nullable)requesterUserId error:(NSError* _Nullable* _Nullable)error; -/** - * DeleteSentRequests clears sent requests from Client's auth storage. - */ -- (BOOL)deleteSentRequests:(NSError* _Nullable* _Nullable)error; -// skipped method Client.GetInternalClient with unsupported parameter or return types - -/** - * GetNodeRegistrationStatus returns a struct with the number of nodes the -client is registered with and the number total. - */ -- (BindingsNodeRegistrationsStatus* _Nullable)getNodeRegistrationStatus:(NSError* _Nullable* _Nullable)error; -/** - * GetPartners returns a list of - */ -- (NSData* _Nullable)getPartners:(NSError* _Nullable* _Nullable)error; -/** - * GetPreferredBins returns the geographic bin or bins that the provided two -character country code is a part of. The bins are returned as CSV. - */ -- (NSString* _Nonnull)getPreferredBins:(NSString* _Nullable)countryCode error:(NSError* _Nullable* _Nullable)error; -- (NSString* _Nonnull)getPreimages:(NSData* _Nullable)identity; -// skipped method Client.GetRateLimitParams with unsupported parameter or return types - -- (NSString* _Nonnull)getRelationshipFingerprint:(NSData* _Nullable)partnerID error:(NSError* _Nullable* _Nullable)error; -/** - * Returns a user object from which all information about the current user -can be gleaned - */ -- (BindingsUser* _Nullable)getUser; -/** - * HasRunningProcessies checks if any background threads are running. -returns true if none are running. This is meant to be -used when NetworkFollowerStatus() returns Stopping. -Due to the handling of comms on iOS, where the OS can -block indefiently, it may not enter the stopped -state apropreatly. This can be used instead. - */ -- (BOOL)hasRunningProcessies; -/** - * returns true if the network is read to be in a healthy state where -messages can be sent - */ -- (BOOL)isNetworkHealthy; -- (BindingsContact* _Nullable)makePrecannedAuthenticatedChannel:(long)precannedID error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the state of the network follower. Returns: -Stopped - 0 -Starting - 1000 -Running - 2000 -Stopping - 3000 - */ -- (long)networkFollowerStatus; -- (void)registerAuthCallbacks:(id<BindingsAuthRequestCallback> _Nullable)request confirm:(id<BindingsAuthConfirmCallback> _Nullable)confirm reset:(id<BindingsAuthResetNotificationCallback> _Nullable)reset; -/** - * RegisterClientErrorCallback registers the callback to handle errors from the -long running threads controlled by StartNetworkFollower and StopNetworkFollower - */ -- (void)registerClientErrorCallback:(id<BindingsClientError> _Nullable)clientError; -/** - * RegisterEventCallback records the given function to receive -ReportableEvent objects. It returns the internal index -of the callback so that it can be deleted later. - */ -- (BOOL)registerEventCallback:(NSString* _Nullable)name myObj:(id<BindingsEventCallbackFunctionObject> _Nullable)myObj error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterForNotifications accepts firebase messaging token - */ -- (BOOL)registerForNotifications:(NSString* _Nullable)token error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterListener records and installs a listener for messages -matching specific uid, msgType, and/or username -Returns a ListenerUnregister interface which can be - -to register for any userID, pass in an id with length 0 or an id with -all zeroes - -to register for any message type, pass in a message type of 0 - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsUnregister* _Nullable)registerListener:(NSData* _Nullable)uid msgType:(long)msgType listener:(id<BindingsListener> _Nullable)listener error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterNetworkHealthCB registers the network health callback to be called -any time the network health changes. Returns a unique ID that can be used to -unregister the network health callback. - */ -- (int64_t)registerNetworkHealthCB:(id<BindingsNetworkHealthCallback> _Nullable)nhc; -- (void)registerPreimageCallback:(NSData* _Nullable)identity pin:(id<BindingsPreimageNotification> _Nullable)pin; -/** - * RegisterRoundEventsHandler registers a callback interface for round -events. -The rid is the round the event attaches to -The timeoutMS is the number of milliseconds until the event fails, and the -validStates are a list of states (one per byte) on which the event gets -triggered -States: - 0x00 - PENDING (Never seen by client) - 0x01 - PRECOMPUTING - 0x02 - STANDBY - 0x03 - QUEUED - 0x04 - REALTIME - 0x05 - COMPLETED - 0x06 - FAILED -These states are defined in elixxir/primitives/states/state.go - */ -- (BindingsUnregister* _Nullable)registerRoundEventsHandler:(long)rid cb:(id<BindingsRoundEventCallback> _Nullable)cb timeoutMS:(long)timeoutMS il:(BindingsIntList* _Nullable)il; -- (void)replayRequests; -- (BOOL)requestAuthenticatedChannel:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (BOOL)resetSession:(NSData* _Nullable)recipientMarshaled meMarshaled:(NSData* _Nullable)meMarshaled message:(NSString* _Nullable)message ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * This will return the round the message was sent on if it is successfully sent -This can be used to register a round event to learn about message delivery. -on failure a round id of -1 is returned - */ -- (BOOL)sendCmix:(NSData* _Nullable)recipient contents:(NSData* _Nullable)contents parameters:(NSString* _Nullable)parameters ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * SendE2E sends an end-to-end payload to the provided recipient with -the provided msgType. Returns the list of rounds in which parts of -the message were sent or an error if it fails. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types - */ -- (BindingsSendReport* _Nullable)sendE2E:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SendUnsafe sends an unencrypted payload to the provided recipient -with the provided msgType. Returns the list of rounds in which parts -of the message were sent or an error if it fails. -NOTE: Do not use this function unless you know what you are doing. -This function always produces an error message in client logging. - -Message Types can be found in client/interfaces/message/type.go -Make sure to not conflict with ANY default message types with custom types - */ -- (BindingsRoundList* _Nullable)sendUnsafe:(NSData* _Nullable)recipient payload:(NSData* _Nullable)payload messageType:(long)messageType parameters:(NSString* _Nullable)parameters error:(NSError* _Nullable* _Nullable)error; -/** - * SetProxiedBins updates the host pool filter that filters out gateways that -are not in one of the specified bins. The provided bins should be CSV. - */ -- (BOOL)setProxiedBins:(NSString* _Nullable)binStringsCSV error:(NSError* _Nullable* _Nullable)error; -/** - * StartNetworkFollower kicks off the tracking of the network. It starts -long running network client threads and returns an object for checking -state and stopping those threads. -Call this when returning from sleep and close when going back to -sleep. -These threads may become a significant drain on battery when offline, ensure -they are stopped if there is no internet access -Threads Started: - - Network Follower (/network/follow.go) - tracks the network events and hands them off to workers for handling - - Historical Round Retrieval (/network/rounds/historical.go) - Retrieves data about rounds which are too old to be stored by the client - - Message Retrieval Worker Group (/network/rounds/retrieve.go) - Requests all messages in a given round from the gateway of the last node - - Message Handling Worker Group (/network/message/handle.go) - Decrypts and partitions messages when signals via the Switchboard - - Health Tracker (/network/health) - Via the network instance tracks the state of the network - - Garbled Messages (/network/message/garbled.go) - Can be signaled to check all recent messages which could be be decoded - Uses a message store on disk for persistence - - Critical Messages (/network/message/critical.go) - Ensures all protocol layer mandatory messages are sent - Uses a message store on disk for persistence - - KeyExchange Trigger (/keyExchange/trigger.go) - Responds to sent rekeys and executes them - - KeyExchange Confirm (/keyExchange/confirm.go) - Responds to confirmations of successful rekey operations - */ -- (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * StopNetworkFollower stops the network follower if it is running. -It returns errors if the Follower is in the wrong status to stop or if it -fails to stop it. -if the network follower is running and this fails, the client object will -most likely be in an unrecoverable state and need to be trashed. - */ -- (BOOL)stopNetworkFollower:(NSError* _Nullable* _Nullable)error; -/** - * UnregisterEventCallback deletes the callback identified by the -index. It returns an error if it fails. - */ -- (void)unregisterEventCallback:(NSString* _Nullable)name; -/** - * UnregisterForNotifications unregister user for notifications - */ -- (BOOL)unregisterForNotifications:(NSError* _Nullable* _Nullable)error; -- (void)unregisterNetworkHealthCB:(int64_t)funcID; -- (BOOL)verifyOwnership:(NSData* _Nullable)receivedMarshaled verifiedMarshaled:(NSData* _Nullable)verifiedMarshaled ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForMessageDelivery allows the caller to get notified if the rounds a -message was sent in successfully completed. Under the hood, this uses an API -which uses the internal round data, network historical round lookup, and -waiting on network events to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - -This function takes the marshaled send report to ensure a memory leak does -not occur as a result of both sides of the bindings holding a reference to -the same pointer. - */ -- (BOOL)waitForMessageDelivery:(NSData* _Nullable)marshaledSendReport mdc:(id<BindingsMessageDeliveryCallback> _Nullable)mdc timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * WaitForNewtwork will block until either the network is healthy or the -passed timeout. It will return true if the network is healthy - */ -- (BOOL)waitForNetwork:(long)timeoutMS; -/** - * WaitForRoundCompletion allows the caller to get notified if a round -has completed (or failed). Under the hood, this uses an API which uses the internal -round data, network historical round lookup, and waiting on network events -to determine what has (or will) occur. - -The callbacks will return at timeoutMS if no state update occurs - */ -- (BOOL)waitForRoundCompletion:(long)roundID rec:(id<BindingsRoundCompletionCallback> _Nullable)rec timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * contact object - */ -@interface BindingsContact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped method Contact.GetAPIContact with unsupported parameter or return types - -/** - * GetDHPublicKey returns the public key associated with the Contact. - */ -- (NSData* _Nullable)getDHPublicKey; -/** - * Returns a fact list for adding and getting facts to and from the contact - */ -- (BindingsFactList* _Nullable)getFactList; -/** - * GetID returns the user ID for this user. - */ -- (NSData* _Nullable)getID; -/** - * GetDHPublicKey returns hash of a DH proof of key ownership. - */ -- (NSData* _Nullable)getOwnershipProof; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsContactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BindingsContact* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * DummyTraffic contains the file dummy traffic manager. The manager can be used -to set and get the status of the send thread. - */ -@interface BindingsDummyTraffic : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client maxNumMessages:(long)maxNumMessages avgSendDeltaMS:(long)avgSendDeltaMS randomRangeMS:(long)randomRangeMS; -/** - * GetStatus returns the current state of the dummy traffic send thread. It has -the following return values: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Note that this function does not return the status set by SetStatus directly; -it returns the current status of the send thread, which means any call to -SetStatus will have a small delay before it is returned by GetStatus. - */ -- (BOOL)getStatus; -/** - * SetStatus sets the state of the dummy traffic send thread, which determines -if the thread is running or paused. The possible statuses are: - true = send thread is sending dummy messages - false = send thread is paused/stopped and not sending dummy messages -Returns an error if the channel is full. -Note that this function cannot change the status of the send thread if it has -yet to be started or stopped. - */ -- (BOOL)setStatus:(BOOL)status error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsFact : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -- (nullable instancetype)init:(long)factType factStr:(NSString* _Nullable)factStr; -- (NSString* _Nonnull)get; -- (NSString* _Nonnull)stringify; -- (long)type; -@end - -@interface BindingsFactList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * FactList - */ -- (nullable instancetype)init; -- (BOOL)add:(NSString* _Nullable)factData factType:(long)factType error:(NSError* _Nullable* _Nullable)error; -- (BindingsFact* _Nullable)get:(long)i; -- (long)num; -- (NSString* _Nonnull)stringify:(NSError* _Nullable* _Nullable)error; -@end - -/** - * FilePartTracker contains the interfaces.FilePartTracker. - */ -@interface BindingsFilePartTracker : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetNumParts returns the total number of file parts in the transfer. - */ -- (long)getNumParts; -/** - * GetPartStatus returns the status of the file part with the given part number. -The possible values for the status are: -0 = unsent -1 = sent (sender has sent a part, but it has not arrived) -2 = arrived (sender has sent a part, and it has arrived) -3 = received (receiver has received a part) - */ -- (long)getPartStatus:(long)partNum; -@end - -/** - * FileTransfer contains the file transfer manager. - */ -@interface BindingsFileTransfer : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -- (nullable instancetype)initManager:(BindingsClient* _Nullable)client receiveFunc:(id<BindingsFileTransferReceiveFunc> _Nullable)receiveFunc parameters:(NSString* _Nullable)parameters; -/** - * CloseSend deletes a sent file transfer from the sent transfer map and from -storage once a transfer has completed or reached the retry limit. Returns an -error if the transfer has not run out of retries. - */ -- (BOOL)closeSend:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * GetMaxFileNameByteLength returns the maximum length, in bytes, allowed for a -file name. - */ -- (long)getMaxFileNameByteLength; -/** - * GetMaxFilePreviewSize returns the maximum file preview size, in bytes. - */ -- (long)getMaxFilePreviewSize; -/** - * GetMaxFileSize returns the maximum file size, in bytes, allowed to be -transferred. - */ -- (long)getMaxFileSize; -/** - * GetMaxFileTypeByteLength returns the maximum length, in bytes, allowed for a -file type. - */ -- (long)getMaxFileTypeByteLength; -/** - * Receive returns the fully assembled file on the completion of the transfer. -It deletes the transfer from the received transfer map and from storage. -Returns an error if the transfer is not complete, the full file cannot be -verified, or if the transfer cannot be found. - */ -- (NSData* _Nullable)receive:(NSData* _Nullable)transferID error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterReceiveProgressCallback allows for the registration of a callback to -track the progress of an individual received file transfer. The callback will -be called immediately when added to report the current status of the -transfer. It will then call every time a file part is received, the transfer -completes, or an error occurs. It is called at most once ever period, which -means if events occur faster than the period, then they will not be reported -and instead, the progress will be reported once at the end of the period. -Once the callback reports that the transfer has completed, the recipient -can get the full file by calling Receive. -The period is specified in milliseconds. - */ -- (BOOL)registerReceiveProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferReceivedProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * RegisterSendProgressCallback allows for the registration of a callback to -track the progress of an individual sent file transfer. The callback will be -called immediately when added to report the current status of the transfer. -It will then call every time a file part is sent, a file part arrives, the -transfer completes, or an error occurs. It is called at most once every -period, which means if events occur faster than the period, then they will -not be reported and instead, the progress will be reported once at the end of -the period. -The period is specified in milliseconds. - */ -- (BOOL)registerSendProgressCallback:(NSData* _Nullable)transferID progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends a file to the recipient. The sender must have an E2E relationship -with the recipient. -The file name is the name of the file to show a user. It has a max length of -48 bytes. -The file type identifies what type of file is being sent. It has a max length -of 8 bytes. -The file data cannot be larger than 256 kB -The retry float is the total amount of data to send relative to the data -size. Data will be resent on error and will resend up to [(1 + retry) * -fileSize]. -The preview stores a preview of the data (such as a thumbnail) and is -capped at 4 kB in size. -Returns a unique transfer ID used to identify the transfer. -PeriodMS is the duration, in milliseconds, to wait between progress callback -calls. Set this large enough to prevent spamming. - */ -- (NSData* _Nullable)send:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType fileData:(NSData* _Nullable)fileData recipientID:(NSData* _Nullable)recipientID retry:(float)retry preview:(NSData* _Nullable)preview progressFunc:(id<BindingsFileTransferSentProgressFunc> _Nullable)progressFunc periodMS:(long)periodMS error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Group structure contains the identifying and membership information of a -group chat. - */ -@interface BindingsGroup : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetCreatedMS returns the time the group was created in milliseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedMS; -/** - * GetCreatedNano returns the time the group was created in nanoseconds. This is -also the time the group requests were sent. - */ -- (int64_t)getCreatedNano; -/** - * GetID return the 33-byte unique group ID. - */ -- (NSData* _Nullable)getID; -/** - * GetInitMessage returns initial message sent with the group request. - */ -- (NSData* _Nullable)getInitMessage; -/** - * GetMembership returns a list of contacts, one for each member in the group. -The list is in order; the first contact is the leader/creator of the group. -All subsequent members are ordered by their ID. - */ -- (BindingsGroupMembership* _Nullable)getMembership; -/** - * GetName returns the name set by the user for the group. - */ -- (NSData* _Nullable)getName; -/** - * Serialize serializes the Group. - */ -- (NSData* _Nullable)serialize; -@end - -/** - * GroupChat object contains the group chat manager. - */ -@interface BindingsGroupChat : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetGroup returns the group with the group ID. If no group exists, then the -error "failed to find group" is returned. - */ -- (BindingsGroup* _Nullable)getGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * GetGroups returns an IdList containing a list of group IDs that the user is a -part of. - */ -- (BindingsIdList* _Nullable)getGroups; -/** - * JoinGroup allows a user to join a group when they receive a request. The -caller must pass in the serialized bytes of a Group. - */ -- (BOOL)joinGroup:(NSData* _Nullable)serializedGroupData error:(NSError* _Nullable* _Nullable)error; -/** - * LeaveGroup deletes a group so a user no longer has access. - */ -- (BOOL)leaveGroup:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * MakeGroup creates a new group and sends a group request to all members in the -group. The ID of the new group, the rounds the requests were sent on, and the -status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)makeGroup:(BindingsIdList* _Nullable)membership name:(NSData* _Nullable)name message:(NSData* _Nullable)message; -/** - * NumGroups returns the number of groups the user is a part of. - */ -- (long)numGroups; -/** - * ResendRequest resends a group request to all members in the group. The rounds -they were sent on and the status of the send are contained in NewGroupReport. - */ -- (BindingsNewGroupReport* _Nullable)resendRequest:(NSData* _Nullable)groupIdBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Send sends the message to the specified group. Returns the round the messages -were sent on. - */ -- (BindingsGroupSendReport* _Nullable)send:(NSData* _Nullable)groupIdBytes message:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * // -Member Structure -// -GroupMember represents a member in the group membership list. - */ -@interface BindingsGroupMember : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMember.Member with unsupported type: gitlab.com/elixxir/crypto/group.Member - -// skipped method GroupMember.DeepCopy with unsupported parameter or return types - -// skipped method GroupMember.Equal with unsupported parameter or return types - -/** - * GetDhKey returns the byte representation of the public Diffie–Hellman key of -the member. - */ -- (NSData* _Nullable)getDhKey; -/** - * GetID returns the 33-byte user ID of the member. - */ -- (NSData* _Nullable)getID; -- (NSString* _Nonnull)goString; -- (NSData* _Nullable)serialize; -- (NSString* _Nonnull)string; -@end - -/** - * GroupMembership structure contains a list of members that are part of a -group. The first member is the group leader. - */ -@interface BindingsGroupMembership : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Get returns the member at the index. The member at index 0 is always the -group leader. An error is returned if the index is out of range. - */ -- (BindingsGroupMember* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of members in the group membership. - */ -- (long)len; -@end - -/** - * GroupMessageReceive contains a group message, its ID, and its data that a -user receives. - */ -@interface BindingsGroupMessageReceive : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupMessageReceive.MessageReceive with unsupported type: gitlab.com/elixxir/client/groupChat.MessageReceive - -/** - * GetEphemeralID returns the ephemeral ID of the recipient. - */ -- (int64_t)getEphemeralID; -/** - * GetGroupID returns the 33-byte group ID. - */ -- (NSData* _Nullable)getGroupID; -/** - * GetMessageID returns the message ID. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetPayload returns the message payload. - */ -- (NSData* _Nullable)getPayload; -/** - * GetRecipientID returns the 33-byte user ID of the recipient. - */ -- (NSData* _Nullable)getRecipientID; -/** - * GetRoundID returns the ID of the round the message was sent on. - */ -- (int64_t)getRoundID; -/** - * GetRoundTimestampMS returns the timestamp, in milliseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the timestamp, in nanoseconds, of the round the -message was sent on. - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the ID of the round the message was sent on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSenderID returns the 33-byte user ID of the sender. - */ -- (NSData* _Nullable)getSenderID; -/** - * GetTimestampMS returns the message timestamp in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message timestamp in nanoseconds. - */ -- (int64_t)getTimestampNano; -- (NSString* _Nonnull)string; -@end - -@interface BindingsGroupReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field GroupReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable grpId; -@property (nonatomic) long status; -@end - -/** - * GroupSendReport is returned when sending a group message. It contains the -round ID sent on and the timestamp of the send. - */ -@interface BindingsGroupSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetMessageID returns the ID of the round that the send occurred on. - */ -- (NSData* _Nullable)getMessageID; -/** - * GetRoundID returns the ID of the round that the send occurred on. - */ -- (int64_t)getRoundID; -/** - * GetRoundURL returns the URL of the round that the send occurred on. - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the timestamp of the send in milliseconds. - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the timestamp of the send in nanoseconds. - */ -- (int64_t)getTimestampNano; -@end - -/** - * ID list -IdList contains a list of IDs. - */ -@interface BindingsIdList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Add appends the ID bytes to the end of the list. - */ -- (BOOL)add:(NSData* _Nullable)idBytes error:(NSError* _Nullable* _Nullable)error; -/** - * Get returns the ID at the index. An error is returned if the index is out of -range. - */ -- (NSData* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -/** - * Len returns the number of IDs in the list. - */ -- (long)len; -@end - -@interface BindingsIntList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (void)add:(long)i; -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -@interface BindingsManyNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsNotificationForMeReport* _Nullable)get:(long)i error:(NSError* _Nullable* _Nullable)error; -- (long)len; -@end - -/** - * Message is a message received from the cMix network in the clear -or that has been decrypted using established E2E keys. - */ -@interface BindingsMessage : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetID returns the id of the message - */ -- (NSData* _Nullable)getID; -/** - * GetMessageType returns the message's type - */ -- (long)getMessageType; -/** - * GetPayload returns the message's payload/contents - */ -- (NSData* _Nullable)getPayload; -/** - * GetRoundId returns the message's round ID - */ -- (int64_t)getRoundId; -/** - * GetRoundTimestampMS returns the message's round timestamp in milliseconds - */ -- (int64_t)getRoundTimestampMS; -/** - * GetRoundTimestampNano returns the message's round timestamp in nanoseconds - */ -- (int64_t)getRoundTimestampNano; -/** - * GetRoundURL returns the message's round URL - */ -- (NSString* _Nonnull)getRoundURL; -/** - * GetSender returns the message's sender ID, if available - */ -- (NSData* _Nullable)getSender; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -@end - -/** - * NewGroupReport is returned when creating a new group and contains the ID of -the group, a list of rounds that the group requests were sent on, and the -status of the send. - */ -@interface BindingsNewGroupReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetError returns the string of an error. -Will be an empty string if no error occured - */ -- (NSString* _Nonnull)getError; -/** - * GetGroup returns the Group. - */ -- (BindingsGroup* _Nullable)getGroup; -/** - * GetRoundList returns the RoundList containing a list of rounds requests were -sent on. - */ -- (BindingsRoundList* _Nullable)getRoundList; -/** - * GetStatus returns the status of the requests sent when creating a new group. -status = 0 an error occurred before any requests could be sent - 1 all requests failed to send (call Resend Group) - 2 some request failed and some succeeded (call Resend Group) - 3, all requests sent successfully (call Resend Group) - */ -- (long)getStatus; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -/** - * NodeRegistrationsStatus structure for returning node registration statuses -for bindings. - */ -@interface BindingsNodeRegistrationsStatus : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetRegistered returns the number of nodes registered with the client. - */ -- (long)getRegistered; -/** - * GetTotal return the total of nodes currently in the network. - */ -- (long)getTotal; -@end - -@interface BindingsNotificationForMeReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BOOL)forMe; -- (NSData* _Nullable)source; -- (NSString* _Nonnull)type; -@end - -/** - * RestoreContactsReport is a gomobile friendly report structure -for determining which IDs restored, which failed, and why. - */ -@interface BindingsRestoreContactsReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * GetErrorAt returns the error string at index - */ -- (NSString* _Nonnull)getErrorAt:(long)index; -/** - * GetFailedAt returns the failed ID at index - */ -- (NSData* _Nullable)getFailedAt:(long)index; -/** - * GetRestoreContactsError returns an error string. Empty if no error. - */ -- (NSString* _Nonnull)getRestoreContactsError; -/** - * GetRestoredAt returns the restored ID at index - */ -- (NSData* _Nullable)getRestoredAt:(long)index; -/** - * LenFailed returns the length of the ID's failed. - */ -- (long)lenFailed; -/** - * LenRestored returns the length of ID's restored. - */ -- (long)lenRestored; -@end - -@interface BindingsRoundList : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Gets a stored round ID at the given index - */ -- (BOOL)get:(long)i ret0_:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; -/** - * Gets the number of round IDs stored - */ -- (long)len; -@end - -/** - * the send report is the mechanisim by which sendE2E returns a single - */ -@interface BindingsSendReport : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (NSData* _Nullable)getMessageID; -- (BindingsRoundList* _Nullable)getRoundList; -- (NSString* _Nonnull)getRoundURL; -/** - * GetTimestampMS returns the message's timestamp in milliseconds - */ -- (int64_t)getTimestampMS; -/** - * GetTimestampNano returns the message's timestamp in nanoseconds - */ -- (int64_t)getTimestampNano; -- (NSData* _Nullable)marshal:(NSError* _Nullable* _Nullable)error; -- (BOOL)unmarshal:(NSData* _Nullable)b error:(NSError* _Nullable* _Nullable)error; -@end - -@interface BindingsSendReportDisk : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -// skipped field SendReportDisk.List with unsupported type: []gitlab.com/xx_network/primitives/id.Round - -@property (nonatomic) NSData* _Nullable mid; -@property (nonatomic) int64_t ts; -@end - -/** - * Generic Unregister - a generic return used for all callbacks which can be -unregistered -Interface which allows the un-registration of a listener - */ -@interface BindingsUnregister : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -/** - * Call unregisters a callback - */ -- (void)unregister; -@end - -@interface BindingsUser : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (nonnull instancetype)init; -- (BindingsContact* _Nullable)getContact; -- (NSData* _Nullable)getE2EDhPrivateKey; -- (NSData* _Nullable)getE2EDhPublicKey; -- (NSData* _Nullable)getReceptionID; -- (NSData* _Nullable)getReceptionRSAPrivateKeyPem; -- (NSData* _Nullable)getReceptionRSAPublicKeyPem; -- (NSData* _Nullable)getReceptionSalt; -- (NSData* _Nullable)getTransmissionID; -- (NSData* _Nullable)getTransmissionRSAPrivateKeyPem; -- (NSData* _Nullable)getTransmissionRSAPublicKeyPem; -- (NSData* _Nullable)getTransmissionSalt; -- (BOOL)isPrecanned; -@end - -@interface BindingsUserDiscovery : NSObject <goSeqRefInterface> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)init:(BindingsClient* _Nullable)client; -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -- (nullable instancetype)initFromBackup:(BindingsClient* _Nullable)client email:(NSString* _Nullable)email phone:(NSString* _Nullable)phone; -/** - * AddFact adds a fact for the user to user discovery. Will only succeed if the -user is already registered and the system does not have the fact currently -registered for any user. -Will fail if the fact string is not well formed. -This does not complete the fact registration process, it returns a -confirmation id instead. Over the communications system the fact is -associated with, a code will be sent. This confirmation ID needs to be -called along with the code to finalize the fact. - */ -- (NSString* _Nonnull)addFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from -AddFact while the code will come over the associated communications system - */ -- (BOOL)confirmFact:(NSString* _Nullable)confirmationID code:(NSString* _Nullable)code error:(NSError* _Nullable* _Nullable)error; -/** - * Lookup the contact object associated with the given userID. The -id is the byte representation of an id. -This will reject if that id is malformed. The LookupCallback will return -the associated contact if it exists. - */ -- (BOOL)lookup:(NSData* _Nullable)idBytes callback:(id<BindingsLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * MultiLookup Looks for the contact object associated with all given userIDs. -The ids are the byte representation of an id stored in an IDList object. -This will reject if that id is malformed or if the indexing on the IDList -object is wrong. The MultiLookupCallback will return with all contacts -returned within the timeout. - */ -- (BOOL)multiLookup:(BindingsIdList* _Nullable)ids callback:(id<BindingsMultiLookupCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * Register registers a user with user discovery. Will return an error if the -network signatures are malformed or if the username is taken. Usernames -cannot be changed after registration at this time. Will fail if the user is -already registered. -Identity does not go over cmix, it occurs over normal communications - */ -- (BOOL)register:(NSString* _Nullable)username error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveFact removes a previously confirmed fact. Will fail if the passed fact string is -not well-formed or if the fact is not associated with this client. -Users cannot remove username facts and must instead remove the user. - */ -- (BOOL)removeFact:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * RemoveUser deletes a user. The fact sent must be the username. -This function preserves the username forever and makes it -unusable. - */ -- (BOOL)removeUser:(NSString* _Nullable)fStr error:(NSError* _Nullable* _Nullable)error; -/** - * Search for the passed Facts. The factList is the stringification of a -fact list object, look at /bindings/list.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This is NOT intended to be used to search for multiple users at once, that -can have a privacy reduction. Instead, it is intended to be used to search -for a user where multiple pieces of information is known. - */ -- (BOOL)search:(NSString* _Nullable)fl callback:(id<BindingsSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SearchSingle searches for the passed Facts. The fact is the stringification of a -fact object, look at /bindings/contact.go for more on that object. -This will reject if that object is malformed. The SearchCallback will return -a list of contacts, each having the facts it hit against. -This only searches for a single fact at a time. It is intended to make some -simple use cases of the API easier. - */ -- (BOOL)searchSingle:(NSString* _Nullable)f callback:(id<BindingsSingleSearchCallback> _Nullable)callback timeoutMS:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error; -/** - * SetAlternativeUserDiscovery sets the alternativeUd object within manager. -Once set, any user discovery operation will go through the alternative -user discovery service. -To undo this operation, use UnsetAlternativeUserDiscovery. -The contact file is the already read in bytes, not the file path for the contact file. - */ -- (BOOL)setAlternativeUserDiscovery:(NSData* _Nullable)address cert:(NSData* _Nullable)cert contactFile:(NSData* _Nullable)contactFile error:(NSError* _Nullable* _Nullable)error; -/** - * UnsetAlternativeUserDiscovery clears out the information from -the Manager object. - */ -- (BOOL)unsetAlternativeUserDiscovery:(NSError* _Nullable* _Nullable)error; -@end - -/** - * Error codes - */ -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedCode; -FOUNDATION_EXPORT NSString* _Nonnull const BindingsUnrecognizedMessage; - -/** - * CompressJpeg takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpeg(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * CompressJpegForPreview takes a JPEG image in byte format -and compresses it based on desired output size - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsCompressJpegForPreview(NSData* _Nullable imgBytes, NSError* _Nullable* _Nullable error); - -/** - * DownloadAndVerifySignedNdfWithUrl retrieves the NDF from a specified URL. -The NDF is processed into a protobuf containing a signature which -is verified using the cert string passed in. The NDF is returned as marshaled -byte data which may be used to start a client. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadAndVerifySignedNdfWithUrl(NSString* _Nullable url, NSString* _Nullable cert, NSError* _Nullable* _Nullable error); - -/** - * DownloadDAppRegistrationDB returns a []byte containing -the JSON data describing registered dApps. -See https://git.xx.network/elixxir/registered-dapps - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadDAppRegistrationDB(NSError* _Nullable* _Nullable error); - -/** - * DownloadErrorDB returns a []byte containing the JSON data -describing client errors. -See https://git.xx.network/elixxir/client-error-database/ - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsDownloadErrorDB(NSError* _Nullable* _Nullable error); - -/** - * DumpStack returns a string with the stack trace of every running thread. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsDumpStack(NSError* _Nullable* _Nullable error); - -/** - * EnableGrpcLogs sets GRPC trace logging - */ -FOUNDATION_EXPORT void BindingsEnableGrpcLogs(id<BindingsLogWriter> _Nullable writer); - -/** - * ErrorStringToUserFriendlyMessage takes a passed in errStr which will be -a backend generated error. These may be error specifically written by -the backend team or lower level errors gotten from low level dependencies. -This function will parse the error string for common errors provided from -errToUserErr to provide a more user-friendly error message for the front end. -If the error is not common, some simple parsing is done on the error message -to make it more user-accessible, removing backend specific jargon. - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsErrorStringToUserFriendlyMessage(NSString* _Nullable errStr); - -/** - * GenerateSecret creates a secret password using a system-based -pseudorandom number generator. It takes 1 parameter, `numBytes`, -which should be set to 32, but can be set higher in certain cases. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateSecret(long numBytes); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetCMIXParams(NSError* _Nullable* _Nullable error); - -/** - * returns a previously created client. IF be used if the garbage collector -removes the client instance on the app side. Is NOT thread safe relative to -login, newClient, or newPrecannedClient - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsGetClientSingleton(void); - -/** - * GetDependencies returns the api DEPENDENCIES - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetDependencies(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetE2EParams(NSError* _Nullable* _Nullable error); - -/** - * GetGitVersion rturns the api GITVERSION - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetGitVersion(void); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetNetworkParams(NSError* _Nullable* _Nullable error); - -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetUnsafeParams(NSError* _Nullable* _Nullable error); - -/** - * GetVersion returns the api SEMVER - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void); - -/** - * InitializeBackup starts the backup processes that returns backup updates when -they occur. Any time an event occurs that changes the contents of the backup, -such as adding or deleting a contact, the backup is triggered and an -encrypted backup is generated and returned on the updateBackupCb callback. -Call this function only when enabling backup if it has not already been -initialized or when the user wants to change their password. -To resume backup process on app recovery, use ResumeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsInitializeBackup(NSString* _Nullable password, id<BindingsUpdateBackupFunc> _Nullable updateBackupCb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * LoadSecretWithMnemonic loads the secret stored from the call to -StoreSecretWithMnemonic. The path given should be the same filepath -as the path given in StoreSecretWithMnemonic. There should be a file -in this path called ".recovery". This operation is not tied -to client operations, as the user will not have a client when trying to -recover their account. - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsLoadSecretWithMnemonic(NSString* _Nullable mnemonic, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * sets level of logging. All logs the set level and above will be displayed -options are: - TRACE - 0 - DEBUG - 1 - INFO - 2 - WARN - 3 - ERROR - 4 - CRITICAL - 5 - FATAL - 6 -The default state without updates is: INFO - */ -FOUNDATION_EXPORT BOOL BindingsLogLevel(long level, NSError* _Nullable* _Nullable error); - -/** - * Login will load an existing client from the storageDir -using the password. This will fail if the client doesn't exist or -the password is incorrect. -The password is passed as a byte array so that it can be cleared from -memory and stored as securely as possible using the memguard library. -Login does not block on network connection, and instead loads and -starts subprocesses to perform network operations. - */ -FOUNDATION_EXPORT BindingsClient* _Nullable BindingsLogin(NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * MakeIdList creates a new empty IdList. - */ -FOUNDATION_EXPORT BindingsIdList* _Nullable BindingsMakeIdList(void); - -FOUNDATION_EXPORT BindingsIntList* _Nullable BindingsMakeIntList(void); - -/** - * NewClient creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewClient(NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSString* _Nullable regCode, NSError* _Nullable* _Nullable error); - -/** - * NewClientFromBackup constructs a new Client from an encrypted backup. The backup -is decrypted using the backupPassphrase. On success a successful client creation, -the function will return a JSON encoded list of the E2E partners -contained in the backup and a json-encoded string of the parameters stored in the backup - */ -FOUNDATION_EXPORT NSData* _Nullable BindingsNewClientFromBackup(NSString* _Nullable ndfJSON, NSString* _Nullable storageDir, NSData* _Nullable sessionPassword, NSData* _Nullable backupPassphrase, NSData* _Nullable backupFileContents, NSError* _Nullable* _Nullable error); - -/** - * NewDummyTrafficManager creates a DummyTraffic manager and initialises the -dummy traffic send thread. Note that the manager does not start sending dummy -traffic until its status is set to true using DummyTraffic.SetStatus. -The maxNumMessages is the upper bound of the random number of messages sent -each send. avgSendDeltaMS is the average duration, in milliseconds, to wait -between sends. Sends occur every avgSendDeltaMS +/- a random duration with an -upper bound of randomRangeMS. - */ -FOUNDATION_EXPORT BindingsDummyTraffic* _Nullable BindingsNewDummyTrafficManager(BindingsClient* _Nullable client, long maxNumMessages, long avgSendDeltaMS, long randomRangeMS, NSError* _Nullable* _Nullable error); - -/** - * fact object -creates a new fact. The factType must be either: - 0 - Username - 1 - Email - 2 - Phone Number -The fact must be well formed for the type and must not include commas or -semicolons. If it is not well formed, it will be rejected. Phone numbers -must have the two letter country codes appended. For the complete set of -validation, see /elixxir/primitives/fact/fact.go - */ -FOUNDATION_EXPORT BindingsFact* _Nullable BindingsNewFact(long factType, NSString* _Nullable factStr, NSError* _Nullable* _Nullable error); - -/** - * FactList - */ -FOUNDATION_EXPORT BindingsFactList* _Nullable BindingsNewFactList(void); - -/** - * NewFileTransferManager creates a new file transfer manager and starts the -sending and receiving threads. The receiveFunc is called everytime a new file -transfer is received. -The parameters string contains file transfer network configuration options -and is a JSON formatted string of the fileTransfer.Params object. If it is -left empty, then defaults are used. It is highly recommended that defaults -are used. If it is set, it must match the following format: - {"MaxThroughput":150000,"SendTimeout":500000000} -MaxThroughput is in bytes/sec and SendTimeout is in nanoseconds. - */ -FOUNDATION_EXPORT BindingsFileTransfer* _Nullable BindingsNewFileTransferManager(BindingsClient* _Nullable client, id<BindingsFileTransferReceiveFunc> _Nullable receiveFunc, NSString* _Nullable parameters, NSError* _Nullable* _Nullable error); - -/** - * NewGroupManager creates a new group chat manager. - */ -FOUNDATION_EXPORT BindingsGroupChat* _Nullable BindingsNewGroupManager(BindingsClient* _Nullable client, id<BindingsGroupRequestFunc> _Nullable requestFunc, id<BindingsGroupReceiveFunc> _Nullable receiveFunc, NSError* _Nullable* _Nullable error); - -/** - * NewPrecannedClient creates an insecure user with predetermined keys with nodes -It creates client storage, generates keys, connects, and registers -with the network. Note that this does not register a username/identity, but -merely creates a new cryptographic identity for adding such information -at a later date. - -Users of this function should delete the storage directory on error. - */ -FOUNDATION_EXPORT BOOL BindingsNewPrecannedClient(long precannedID, NSString* _Nullable network, NSString* _Nullable storageDir, NSData* _Nullable password, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscovery returns a new user discovery object. Only call this once. It must be called -after StartNetworkFollower is called and will fail if the network has never -been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscovery(BindingsClient* _Nullable client, NSError* _Nullable* _Nullable error); - -/** - * NewUserDiscoveryFromBackup returns a new user discovery object. It -wil set up the manager with the backup data. Pass into it the backed up -facts, one email and phone number each. This will add the registered facts -to the backed Store. Any one of these fields may be empty, -however both fields being empty will cause an error. Any other fact that is not -an email or phone number will return an error. You may only add a fact for the -accepted types once each. If you attempt to back up a fact type that has already -been backed up, an error will be returned. Anytime an error is returned, it means -the backup was not successful. -NOTE: Do not use this as a direct store operation. This feature is intended to add facts -to a backend store that have ALREADY BEEN REGISTERED on the account. -THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. -Only call this once. It must be called after StartNetworkFollower -is called and will fail if the network has never been contacted. -This function technically has a memory leak because it causes both sides of -the bindings to think the other is in charge of the client object. -In general this is not an issue because the client object should exist -for the life of the program. -This must be called while start network follower is running. - */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUserDiscoveryFromBackup(BindingsClient* _Nullable client, NSString* _Nullable email, NSString* _Nullable phone, NSError* _Nullable* _Nullable error); - -/** - * NotificationsForMe Check if a notification received is for me -It returns a NotificationForMeReport which contains a ForMe bool stating if it is for the caller, -a Type, and a source. These are as follows: - TYPE SOURCE DESCRIPTION - "default" recipient user ID A message with no association - "request" sender user ID A channel request has been received - "reset" sender user ID A channel reset has been received - "confirm" sender user ID A channel request has been accepted - "silent" sender user ID A message which should not be notified on - "e2e" sender user ID reception of an E2E message - "group" group ID reception of a group chat message - "endFT" sender user ID Last message sent confirming end of file transfer - "groupRQ" sender user ID Request from sender to join a group chat - */ -FOUNDATION_EXPORT BindingsManyNotificationForMeReport* _Nullable BindingsNotificationsForMe(NSString* _Nullable notifCSV, NSString* _Nullable preimages, NSError* _Nullable* _Nullable error); - -/** - * RegisterLogWriter registers a callback on which logs are written. - */ -FOUNDATION_EXPORT void BindingsRegisterLogWriter(id<BindingsLogWriter> _Nullable writer); - -/** - * RestoreContactsFromBackup takes as input the jason output of the -`NewClientFromBackup` function, unmarshals it into IDs, looks up -each ID in user discovery, and initiates a session reset request. -This function will not return until every id in the list has been sent a -request. It should be called again and again until it completes. -xxDK users should not use this function. This function is used by -the mobile phone apps and are not intended to be part of the xxDK. It -should be treated as internal functions specific to the phone apps. - */ -FOUNDATION_EXPORT BindingsRestoreContactsReport* _Nullable BindingsRestoreContactsFromBackup(NSData* _Nullable backupPartnerIDs, BindingsClient* _Nullable client, BindingsUserDiscovery* _Nullable udManager, id<BindingsLookupCallback> _Nullable lookupCB, id<BindingsRestoreContactsUpdater> _Nullable updatesCb); - -/** - * ResumeBackup starts the backup processes back up with a new callback after it -has been initialized. -Call this function only when resuming a backup that has already been -initialized or to replace the callback. -To start the backup for the first time or to use a new password, use -InitializeBackup. - */ -FOUNDATION_EXPORT BindingsBackup* _Nullable BindingsResumeBackup(id<BindingsUpdateBackupFunc> _Nullable cb, BindingsClient* _Nullable c, NSError* _Nullable* _Nullable error); - -/** - * SetTimeSource sets the network time to a custom source. - */ -FOUNDATION_EXPORT void BindingsSetTimeSource(id<BindingsTimeSource> _Nullable timeNow); - -/** - * StoreSecretWithMnemonic stores the secret tied with the mnemonic to storage. -Unlike other storage operations, this does not use EKV, as that is -intrinsically tied to client operations, which the user will not have while -trying to recover their account. As such, we store the encrypted data -directly, with a specified path. Path will be a valid filepath in which the -recover file will be stored as ".recovery". - -As an example, given "home/user/xxmessenger/storagePath", -the recovery file will be stored at -"home/user/xxmessenger/storagePath/.recovery" - */ -FOUNDATION_EXPORT NSString* _Nonnull BindingsStoreSecretWithMnemonic(NSData* _Nullable secret, NSString* _Nullable path, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled contact object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsContact* _Nullable BindingsUnmarshalContact(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * Unmarshals a marshaled send report object, returns an error if it fails - */ -FOUNDATION_EXPORT BindingsSendReport* _Nullable BindingsUnmarshalSendReport(NSData* _Nullable b, NSError* _Nullable* _Nullable error); - -/** - * UpdateCommonErrors takes the passed in contents of a JSON file and updates the -errToUserErr map with the contents of the json file. The JSON's expected format -conform with the commented examples provides in errToUserErr above. -NOTE that you should not pass in a file path, but a preloaded JSON file - */ -FOUNDATION_EXPORT BOOL BindingsUpdateCommonErrors(NSString* _Nullable jsonFile, NSError* _Nullable* _Nullable error); - -// skipped function WrapAPIClient with unsupported parameter or return types - - -// skipped function WrapUserDiscovery with unsupported parameter or return types - - -@class BindingsAuthConfirmCallback; - -@class BindingsAuthRequestCallback; - -@class BindingsAuthResetNotificationCallback; - -@class BindingsClientError; - -@class BindingsEventCallbackFunctionObject; - -@class BindingsFileTransferReceiveFunc; - -@class BindingsFileTransferReceivedProgressFunc; - -@class BindingsFileTransferSentProgressFunc; - -@class BindingsGroupReceiveFunc; - -@class BindingsGroupRequestFunc; - -@class BindingsListener; - -@class BindingsLogWriter; - -@class BindingsLookupCallback; - -@class BindingsMessageDeliveryCallback; - -@class BindingsMultiLookupCallback; - -@class BindingsNetworkHealthCallback; - -@class BindingsPreimageNotification; - -@class BindingsRestoreContactsUpdater; - -@class BindingsRoundCompletionCallback; - -@class BindingsRoundEventCallback; - -@class BindingsSearchCallback; - -@class BindingsSingleSearchCallback; - -@class BindingsTimeSource; - -@class BindingsUpdateBackupFunc; - -/** - * AuthConfirmCallback notifies the register whenever they receive an auth -request confirmation - */ -@interface BindingsAuthConfirmCallback : NSObject <goSeqRefInterface, BindingsAuthConfirmCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)partner; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthRequestCallback : NSObject <goSeqRefInterface, BindingsAuthRequestCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -/** - * AuthRequestCallback notifies the register whenever they receive an auth -request - */ -@interface BindingsAuthResetNotificationCallback : NSObject <goSeqRefInterface, BindingsAuthResetNotificationCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)requestor; -@end - -@interface BindingsClientError : NSObject <goSeqRefInterface, BindingsClientError> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)report:(NSString* _Nullable)source message:(NSString* _Nullable)message trace:(NSString* _Nullable)trace; -@end - -/** - * EventCallbackFunctionObject bindings interface which contains function -that implements the EventCallbackFunction - */ -@interface BindingsEventCallbackFunctionObject : NSObject <goSeqRefInterface, BindingsEventCallbackFunctionObject> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)reportEvent:(long)priority category:(NSString* _Nullable)category evtType:(NSString* _Nullable)evtType details:(NSString* _Nullable)details; -@end - -/** - * FileTransferReceiveFunc contains a function callback that notifies the -receiver of an incoming file transfer. It is called on the reception of the -initial file transfer message. - */ -@interface BindingsFileTransferReceiveFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receiveCallback:(NSData* _Nullable)tid fileName:(NSString* _Nullable)fileName fileType:(NSString* _Nullable)fileType sender:(NSData* _Nullable)sender size:(long)size preview:(NSData* _Nullable)preview; -@end - -/** - * FileTransferReceivedProgressFunc contains a function callback that tracks the -progress of receiving a file. It is called when a file part is received, the -transfer completes, or on error. - */ -@interface BindingsFileTransferReceivedProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferReceivedProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)receivedProgressCallback:(BOOL)completed received:(long)received total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * FileTransferSentProgressFunc contains a function callback that tracks the -progress of sending a file. It is called when a file part is sent, a file -part arrives, the transfer completes, or on error. - */ -@interface BindingsFileTransferSentProgressFunc : NSObject <goSeqRefInterface, BindingsFileTransferSentProgressFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)sentProgressCallback:(BOOL)completed sent:(long)sent arrived:(long)arrived total:(long)total t:(BindingsFilePartTracker* _Nullable)t err:(NSError* _Nullable)err; -@end - -/** - * GroupReceiveFunc contains a function callback that is called when a group -message is received. - */ -@interface BindingsGroupReceiveFunc : NSObject <goSeqRefInterface, BindingsGroupReceiveFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupReceiveCallback:(BindingsGroupMessageReceive* _Nullable)msg; -@end - -/** - * GroupRequestFunc contains a function callback that is called when a group -request is received. - */ -@interface BindingsGroupRequestFunc : NSObject <goSeqRefInterface, BindingsGroupRequestFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)groupRequestCallback:(BindingsGroup* _Nullable)g; -@end - -/** - * Listener provides a callback to hear a message -An object implementing this interface can be called back when the client -gets a message of the type that the registerer specified at registration -time. - */ -@interface BindingsListener : NSObject <goSeqRefInterface, BindingsListener> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * Hear is called to receive a message in the UI - */ -- (void)hear:(BindingsMessage* _Nullable)message; -/** - * Returns a name, used for debugging - */ -- (NSString* _Nonnull)name; -@end - -@interface BindingsLogWriter : NSObject <goSeqRefInterface, BindingsLogWriter> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)log:(NSString* _Nullable)p0; -@end - -/** - * LookupCallback returns the result of a single lookup - */ -@interface BindingsLookupCallback : NSObject <goSeqRefInterface, BindingsLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -/** - * MessageDeliveryCallback gets called on the determination if all events -related to a message send were successful. - */ -@interface BindingsMessageDeliveryCallback : NSObject <goSeqRefInterface, BindingsMessageDeliveryCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(NSData* _Nullable)msgID delivered:(BOOL)delivered timedOut:(BOOL)timedOut roundResults:(NSData* _Nullable)roundResults; -@end - -/** - * MultiLookupCallback returns the result of many parallel lookups - */ -@interface BindingsMultiLookupCallback : NSObject <goSeqRefInterface, BindingsMultiLookupCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)Succeeded failed:(BindingsIdList* _Nullable)failed errors:(NSString* _Nullable)errors; -@end - -/** - * A callback when which is used to receive notification if network health -changes - */ -@interface BindingsNetworkHealthCallback : NSObject <goSeqRefInterface, BindingsNetworkHealthCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BOOL)p0; -@end - -@interface BindingsPreimageNotification : NSObject <goSeqRefInterface, BindingsPreimageNotification> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)notify:(NSData* _Nullable)identity deleted:(BOOL)deleted; -@end - -/** - * RestoreContactsUpdater interface provides a callback function -for receiving update information from RestoreContactsFromBackup. - */ -@interface BindingsRestoreContactsUpdater : NSObject <goSeqRefInterface, BindingsRestoreContactsUpdater> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -/** - * RestoreContactsCallback is called to report the current # of contacts -that have been found and how many have been restored -against the total number that need to be -processed. If an error occurs it it set on the err variable as a -plain string. - */ -- (void)restoreContactsCallback:(long)numFound numRestored:(long)numRestored total:(long)total err:(NSString* _Nullable)err; -@end - -/** - * RoundCompletionCallback is returned when the completion of a round is known. - */ -@interface BindingsRoundCompletionCallback : NSObject <goSeqRefInterface, BindingsRoundCompletionCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid success:(BOOL)success timedOut:(BOOL)timedOut; -@end - -/** - * RoundEventCallback handles waiting on the exact state of a round on -the cMix network. - */ -@interface BindingsRoundEventCallback : NSObject <goSeqRefInterface, BindingsRoundEventCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)eventCallback:(long)rid state:(long)state timedOut:(BOOL)timedOut; -@end - -/** - * SearchCallback returns the result of a search - */ -@interface BindingsSearchCallback : NSObject <goSeqRefInterface, BindingsSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContactList* _Nullable)contacts error:(NSString* _Nullable)error; -@end - -/** - * SingleSearchCallback returns the result of a single search - */ -@interface BindingsSingleSearchCallback : NSObject <goSeqRefInterface, BindingsSingleSearchCallback> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)callback:(BindingsContact* _Nullable)contact error:(NSString* _Nullable)error; -@end - -@interface BindingsTimeSource : NSObject <goSeqRefInterface, BindingsTimeSource> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (int64_t)nowMs; -@end - -/** - * UpdateBackupFunc contains a function callback that returns new backups. - */ -@interface BindingsUpdateBackupFunc : NSObject <goSeqRefInterface, BindingsUpdateBackupFunc> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)updateBackup:(NSData* _Nullable)encryptedBackup; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Universe.objc.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Universe.objc.h deleted file mode 100644 index 019e7502d581983722a15bf30799e85cbc5dd766..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/Universe.objc.h +++ /dev/null @@ -1,29 +0,0 @@ -// Objective-C API for talking to Go package. -// gobind -lang=objc -// -// File is generated by gobind. Do not edit. - -#ifndef __Universe_H__ -#define __Universe_H__ - -@import Foundation; -#include "ref.h" - -@protocol Universeerror; -@class Universeerror; - -@protocol Universeerror <NSObject> -- (NSString* _Nonnull)error; -@end - -@class Universeerror; - -@interface Universeerror : NSError <goSeqRefInterface, Universeerror> { -} -@property(strong, readonly) _Nonnull id _ref; - -- (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (NSString* _Nonnull)error; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/ref.h b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/ref.h deleted file mode 100644 index b8036a4d85c7387f3def61473a071b5d8c4c8208..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Headers/ref.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef __GO_REF_HDR__ -#define __GO_REF_HDR__ - -#include <Foundation/Foundation.h> - -// GoSeqRef is an object tagged with an integer for passing back and -// forth across the language boundary. A GoSeqRef may represent either -// an instance of a Go object, or an Objective-C object passed to Go. -// The explicit allocation of a GoSeqRef is used to pin a Go object -// when it is passed to Objective-C. The Go seq package maintains a -// reference to the Go object in a map keyed by the refnum along with -// a reference count. When the reference count reaches zero, the Go -// seq package will clear the corresponding entry in the map. -@interface GoSeqRef : NSObject { -} -@property(readonly) int32_t refnum; -@property(strong) id obj; // NULL when representing a Go object. - -// new GoSeqRef object to proxy a Go object. The refnum must be -// provided from Go side. -- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; - -- (int32_t)incNum; - -@end - -@protocol goSeqRefInterface --(GoSeqRef*) _ref; -@end - -#endif diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Modules/module.modulemap b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Modules/module.modulemap deleted file mode 100644 index 4316a5b24058edfc18ffb2dc7f7a982e8353441a..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Modules/module.modulemap +++ /dev/null @@ -1,8 +0,0 @@ -framework module "Bindings" { - header "ref.h" - header "Bindings.objc.h" - header "Universe.objc.h" - header "Bindings.h" - - export * -} \ No newline at end of file diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Resources/Info.plist b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Resources/Info.plist deleted file mode 100644 index 0d1a4b8ab9b1fc8e9357197398f73353470cb636..0000000000000000000000000000000000000000 --- a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Resources/Info.plist +++ /dev/null @@ -1,6 +0,0 @@ -<?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> - </dict> - </plist> diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2edfffa23ebd11df1784b06aa0e734df8c56269a..09979e25994dabe0b38332b1604fd2681b242cb6 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", "state" : { - "revision" : "fffc3c2729be5747390ad02d5100291a0d9ad26a", - "version" : "0.20200225.4" + "revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1", + "version" : "0.20220203.2" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864", - "version" : "5.5.0" + "revision" : "78424be314842833c04bc3bef5b72e85fff99204", + "version" : "5.6.4" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS.git", "state" : { - "revision" : "01131d68346c8ae552961c768d583c715fbe1410", - "version" : "1.4.0" + "revision" : "3d36a58a2b736f7bc499453e996a704929b25080", + "version" : "1.6.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/boringssl-SwiftPM.git", "state" : { - "revision" : "734a8247442fde37df4364c21f6a0085b6a36728", - "version" : "0.7.2" + "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", + "version" : "0.9.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ekazaev/ChatLayout", "state" : { - "revision" : "d0edb6f3ae716a26842467c540a6bee909b80360", - "version" : "1.1.14" + "revision" : "b7fe23d3ab6c174da6fad66383bfc359c212f465", + "version" : "1.2.7" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://git.xx.network/elixxir/client-ios-db.git", "state" : { - "revision" : "f8e3e0088de8301d6c4816e12f0aca1d6f02a280", - "version" : "1.1.0" + "revision" : "16480177beaa6bc80a1c6be25812abd9bcb850ea", + "version" : "1.2.0" } }, { @@ -68,26 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "4cf088c29a20f52be0f2ca54992b492c54e0076b", - "version" : "0.5.3" - } - }, - { - "identity" : "cwlcatchexception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlCatchException.git", - "state" : { - "revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea", - "version" : "2.1.1" - } - }, - { - "identity" : "cwlpreconditiontesting", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state" : { - "revision" : "fb7a26374e8570ff5c68142e5c83406d6abae0d8", - "version" : "2.0.2" + "revision" : "aa3e575929f2bcc5bad012bd2575eae716cbcdf7", + "version" : "0.8.0" } }, { @@ -95,17 +77,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ra1028/DifferenceKit", "state" : { - "revision" : "62745d7780deef4a023a792a1f8f763ec7bf9705", - "version" : "1.2.0" + "revision" : "073b9671ce2b9b5b96398611427a1f929927e428", + "version" : "1.3.0" } }, { - "identity" : "fileprovider", + "identity" : "elixxir-dapps-sdk-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/amosavian/FileProvider.git", + "location" : "https://git.xx.network/elixxir/elixxir-dapps-sdk-swift", "state" : { - "revision" : "abf68a62541a4193c8d106367ddb3648e8ab693f", - "version" : "0.26.0" + "revision" : "c433b384a1bd49627bb8e15bc1dc0ae4cb9a8ec1", + "version" : "1.0.0" } }, { @@ -113,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "08686f04881483d2bc098b2696e674c0ba135e47", - "version" : "8.10.0" + "revision" : "111d8d6ad1a1afd6c8e9561d26e55ab1e74fcb42", + "version" : "8.15.0" } }, { @@ -122,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/google-api-objectivec-client-for-rest", "state" : { - "revision" : "22e0bb02729d60db396e8b90d8189313cd86ba53", - "version" : "1.6.0" + "revision" : "0078161fcc2900ca06213a192bdfa5ece8ee58f1", + "version" : "2.0.1" } }, { @@ -131,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "9b2f6aca5b4685c45f9f5481f19bee8e7982c538", - "version" : "8.9.1" + "revision" : "ef819db8c58657a6ca367322e73f3b6322afe0a2", + "version" : "8.15.0" } }, { @@ -140,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "15ccdfd25ac55b9239b82809531ff26605e7556e", - "version" : "9.1.2" + "revision" : "5056b15c5acbb90cd214fe4d6138bdf5a740e5a8", + "version" : "9.2.0" } }, { @@ -149,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleSignIn-iOS", "state" : { - "revision" : "60ca2bfd218ccb194a746a79b41d9d50eb7e3af0", - "version" : "6.1.0" + "revision" : "9c9b36af86a4dd3da16048a36cf37351e63ccfe1", + "version" : "6.2.4" } }, { @@ -158,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "b3bb0c5551fb3f80ca939829639ab5b093edd14f", - "version" : "7.7.0" + "revision" : "68ea347bdb1a69e2d2ae2e25cd085b6ef92f64cb", + "version" : "7.9.0" } }, { @@ -167,17 +149,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "23f4254ae36fa19aecd73047c0577a9f49850d1c", - "version" : "5.26.0" + "revision" : "0ac435744a4c67c4ec23a4a671c0d53ce1fee7c6", + "version" : "6.0.0" } }, { - "identity" : "grpc-swiftpm", + "identity" : "grpc-ios", "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/grpc-SwiftPM.git", + "location" : "https://github.com/grpc/grpc-ios.git", "state" : { - "revision" : "fb405dd2c7901485f7e158b24e3a0a47e4efd8b5", - "version" : "1.28.4" + "revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6", + "version" : "1.44.3-grpc" } }, { @@ -185,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "bc6a19702ac76ac4e488b68148710eb815f9bc56", - "version" : "1.7.0" + "revision" : "4e9bbf2808b8fee444e84a48f5f3c12641987d3e", + "version" : "1.7.2" } }, { @@ -194,14 +176,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GTMAppAuth.git", "state" : { - "revision" : "40f4103fb52109032c05599a0c39ad43edbdf80a", - "version" : "1.2.2" + "revision" : "6dee0cde8a1b223737a5159e55e6b4ec16bbbdd9", + "version" : "1.3.1" } }, { "identity" : "keychainaccess", "kind" : "remoteSourceControl", - "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", "state" : { "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", "version" : "4.2.2" @@ -216,15 +198,6 @@ "version" : "1.22.2" } }, - { - "identity" : "libssh2prebuild", - "kind" : "remoteSourceControl", - "location" : "https://github.com/DimaRU/Libssh2Prebuild.git", - "state" : { - "branch" : "1.10.0+OpenSSL_1_1_1o", - "revision" : "a91bcf205a6cbc84144f840c44145656abbd266a" - } - }, { "identity" : "nanopb", "kind" : "remoteSourceControl", @@ -234,31 +207,22 @@ "version" : "2.30908.0" } }, - { - "identity" : "nimble", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Quick/Nimble", - "state" : { - "revision" : "c93f16c25af5770f0d3e6af27c9634640946b068", - "version" : "9.2.1" - } - }, { "identity" : "promises", "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "611337c330350c9c1823ad6d671e7f936af5ee13", - "version" : "2.0.0" + "revision" : "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb", + "version" : "2.1.1" } }, { - "identity" : "quick", + "identity" : "pulse", "kind" : "remoteSourceControl", - "location" : "https://github.com/Quick/Quick", + "location" : "https://github.com/kean/Pulse.git", "state" : { - "revision" : "8cce6acd38f965f5baa3167b939f86500314022b", - "version" : "3.1.2" + "revision" : "6b682c529d98a38e6fdffee2a8bfa40c8de30821", + "version" : "2.1.3" } }, { @@ -275,8 +239,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/darrarski/ScrollViewController", "state" : { - "revision" : "9a52bb056504bb4766ddb5ac518097dd48736303", - "version" : "1.2.0" + "revision" : "288999c7b9b0246aee0cfe3b7a066546038fd4b8", + "version" : "1.3.0" } }, { @@ -284,7 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/darrarski/Shout.git", "state" : { - "revision" : "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0" + "revision" : "cda197619ac395b9a2c3ddd1c6c8fad16114b830", + "version" : "0.5.5" } }, { @@ -292,8 +257,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SnapKit/SnapKit", "state" : { - "revision" : "d458564516e5676af9c70b4f4b2a9178294f1bc6", - "version" : "5.0.1" + "revision" : "f222cbdf325885926566172f6f5f06af95473158", + "version" : "5.6.0" } }, { @@ -301,8 +266,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "241301b67d8551c26d8f09bd2c0e52cc49f18007", - "version" : "0.8.0" + "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" } }, { @@ -310,8 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", - "version" : "1.0.2" + "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", + "version" : "1.0.3" } }, { @@ -319,17 +293,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "313dd217dcd1d0478118ec5d15225fd473c1564a", - "version" : "0.32.0" + "revision" : "1fcd53fc875bade47d850749ea53c324f74fd64d", + "version" : "0.45.0" } }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", + "location" : "https://github.com/pointfreeco/swift-custom-dump.git", "state" : { - "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7", - "version" : "0.5.0" + "revision" : "819d9d370cd721c9d87671e29d947279292e4541", + "version" : "0.6.0" } }, { @@ -337,8 +311,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "680bf440178a78a627b1c2c64c0855f6523ad5b9", - "version" : "0.3.2" + "revision" : "bfb0d43e75a15b6dfac770bf33479e8393884a36", + "version" : "0.4.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", + "version" : "1.4.4" } }, { @@ -346,8 +329,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf", "state" : { - "revision" : "7e2c5f3cbbeea68e004915e3a8961e20bd11d824", - "version" : "1.18.0" + "revision" : "88c7d15e1242fdb6ecbafbc7926426a19be1e98a", + "version" : "1.20.2" } }, { @@ -355,35 +338,35 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftcsv/SwiftCSV.git", "state" : { - "revision" : "048a1d3c2950b9c151ef9364b36f91baadc2c28c", - "version" : "0.8.0" + "revision" : "96fa14b92e88e0befdbc8bc31c7c2c9594a30060", + "version" : "0.8.1" } }, { - "identity" : "swiftybeaver", + "identity" : "swiftydropbox", "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver.git", + "location" : "https://github.com/dropbox/SwiftyDropbox.git", "state" : { - "revision" : "2c039501d6eeb4d4cd4aec4a8d884ad28862e044", - "version" : "1.9.5" + "revision" : "ff2da46242267837818b40ea1062a045840bc944", + "version" : "9.1.0" } }, { - "identity" : "swiftydropbox", + "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", - "location" : "https://github.com/dropbox/SwiftyDropbox.git", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git", "state" : { - "revision" : "7af87d903be1cf0af0e76e0394d992943055894e", - "version" : "8.2.1" + "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784", + "version" : "0.5.0" } }, { - "identity" : "xctest-dynamic-overlay", + "identity" : "xxm-cloud-providers", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "location" : "https://git.xx.network/elixxir/xxm-cloud-providers.git", "state" : { - "revision" : "ef8e14e7ce1c0c304c644c6ba365d06c468ded6b", - "version" : "0.3.3" + "revision" : "7e2fe50560f4c9c477d549e9c68a4c87d686d44c", + "version" : "1.0.2" } } ], diff --git a/run_swiftgen.sh b/run_swiftgen.sh index 63ba2cb744a4016ea02e72cff976714ccc44f1dc..adfb40051d165d4ae1211553b486d776c6e4bd49 100755 --- a/run_swiftgen.sh +++ b/run_swiftgen.sh @@ -1,2 +1,2 @@ #!/bin/bash -swiftgen config run --config Sources/Shared/swiftgen.yml +swiftgen config run --config Sources/AppResources/swiftgen.yml