diff --git a/App/client-ios/Resources/Info.plist b/App/client-ios/Resources/Info.plist index ed25d86cf62d659ee34ae0f720d9de7f8a56fefb..dd578c50c07a9aef6e5edff517adeffcbe4be8ca 100644 --- a/App/client-ios/Resources/Info.plist +++ b/App/client-ios/Resources/Info.plist @@ -34,10 +34,10 @@ </dict> <dict> <key>CFBundleURLName</key> - <string>xxmessenger</string> + <string>xxnetwork</string> <key>CFBundleURLSchemes</key> <array> - <string>xxmessenger</string> + <string>xxnetwork</string> </array> </dict> <dict> diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index f2ba599d5878e80d217675e81a049946c18bda1e..fdf6090ebec6b8062d3befee5c1f6a3176116ed1 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -20,11 +20,11 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { @Dependency private var crashReporter: CrashReporter @Dependency private var dropboxService: DropboxInterface + @KeyObject(.invitation, defaultValue: nil) var invitation: String? @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool @KeyObject(.crashReporting, defaultValue: true) var isCrashReportingEnabled: Bool - var invitation: String? var calledStopNetwork = false var forceFailedPendingMessages = false @@ -134,12 +134,6 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { } public func applicationDidBecomeActive(_ application: UIApplication) { - // TODO: - /// If an invitation is set -> navigate to - /// search screen and perform a search - /// - invitation = nil - application.applicationIconBadgeNumber = 0 coverView?.removeFromSuperview() } @@ -149,12 +143,14 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { - if let host = url.host, host.starts(with: "invitation-") { - invitation = host.replacingOccurrences(of: "invitation-", with: "") + if let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let invitation = components.queryItems?.first(where: { $0.name == "invitation" }), + let username = invitation.value { + self.invitation = username return true + } else { + return dropboxService.handleOpenUrl(url) } - - return dropboxService.handleOpenUrl(url) } } diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 14a7b7bdb482974cff2b5b2be573d3f5fb1a33fc..7c4d70a963f5bfdea10b1c8cc20f2c4a7ccc9953 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -113,6 +113,7 @@ struct DependencyRegistrator { container.register( LaunchCoordinator( + searchFactory: SearchContainerController.init, requestsFactory: RequestsContainerController.init, chatListFactory: ChatListController.init, onboardingFactory: OnboardingStartController.init(_:), diff --git a/Sources/Defaults/KeyObject.swift b/Sources/Defaults/KeyObject.swift index 7757f7ed4fa550c478736b76cd7c1036ef4fde34..5714b112586e2c54b1c6aeae19224e4a060770fc 100644 --- a/Sources/Defaults/KeyObject.swift +++ b/Sources/Defaults/KeyObject.swift @@ -21,6 +21,7 @@ public enum Key: String { // MARK: General case theme + case invitation // MARK: Requests diff --git a/Sources/Integration/Session/Session+UD.swift b/Sources/Integration/Session/Session+UD.swift index 1fe3e2e09b2f7f0ae327b039602320bbc884eb2b..27add4b13b1839482aefe29badc0f0fbe4e61e8f 100644 --- a/Sources/Integration/Session/Session+UD.swift +++ b/Sources/Integration/Session/Session+UD.swift @@ -1,9 +1,24 @@ +import Retry import Models +import Combine import XXModels import Foundation -import Combine 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 diff --git a/Sources/Integration/Session/SessionType.swift b/Sources/Integration/Session/SessionType.swift index b871332b216e5c7dbdc0adfebc4bac3f306913b7..effd6c96c3239904722b74f2856f2a953f0b986e 100644 --- a/Sources/Integration/Session/SessionType.swift +++ b/Sources/Integration/Session/SessionType.swift @@ -68,4 +68,6 @@ public protocol SessionType { ) func search(fact: String) -> AnyPublisher<Contact, Error> + + func waitForNodes(timeout: Int) -> AnyPublisher<Void, Error> } diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index 8db4bd6344c73a883176942b46e9ae22724bd609..9eeeb76322911c025810eb2246293ce446ff6af5 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -49,6 +49,8 @@ public final class LaunchController: UIViewController { .receive(on: DispatchQueue.main) .sink { [unowned self] in switch $0 { + case .search: + coordinator.toSearch(from: self) case .chats: if let pushRoute = pendingPushRoute { switch pushRoute { diff --git a/Sources/LaunchFeature/LaunchCoordinator.swift b/Sources/LaunchFeature/LaunchCoordinator.swift index 4f5a56291b19634df4c46edc2634acb55ba5812e..a9612d61ecf217dcdef7af544cef60b67509adbd 100644 --- a/Sources/LaunchFeature/LaunchCoordinator.swift +++ b/Sources/LaunchFeature/LaunchCoordinator.swift @@ -5,6 +5,7 @@ import Presentation public protocol LaunchCoordinating { func toChats(from: UIViewController) + func toSearch(from: UIViewController) func toRequests(from: UIViewController) func toOnboarding(with: String, from: UIViewController) func toSingleChat(with: Contact, from: UIViewController) @@ -14,6 +15,7 @@ public protocol LaunchCoordinating { public struct LaunchCoordinator: LaunchCoordinating { var replacePresenter: Presenting = ReplacePresenter() + var searchFactory: () -> UIViewController var requestsFactory: () -> UIViewController var chatListFactory: () -> UIViewController var onboardingFactory: (String) -> UIViewController @@ -21,12 +23,14 @@ public struct LaunchCoordinator: LaunchCoordinating { var groupChatFactory: (GroupInfo) -> UIViewController public init( + searchFactory: @escaping () -> UIViewController, requestsFactory: @escaping () -> UIViewController, chatListFactory: @escaping () -> UIViewController, onboardingFactory: @escaping (String) -> UIViewController, singleChatFactory: @escaping (Contact) -> UIViewController, groupChatFactory: @escaping (GroupInfo) -> UIViewController ) { + self.searchFactory = searchFactory self.requestsFactory = requestsFactory self.chatListFactory = chatListFactory self.groupChatFactory = groupChatFactory @@ -36,6 +40,12 @@ public struct LaunchCoordinator: LaunchCoordinating { } public extension LaunchCoordinator { + func toSearch(from parent: UIViewController) { + let screen = searchFactory() + let chatListScreen = chatListFactory() + replacePresenter.present(chatListScreen, screen, from: parent) + } + func toChats(from parent: UIViewController) { let screen = chatListFactory() replacePresenter.present(screen, from: parent) diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index fefbe7cf6035b19d804febd7e544e22166d11bf8..42b69bb7bfc8e2d45ec2fe14acdf18f01e9bd3db 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -24,6 +24,7 @@ struct Update { enum LaunchRoute { case chats + case search case update(Update) case onboarding(String) } @@ -36,6 +37,7 @@ final class LaunchViewModel { @Dependency private var permissionHandler: PermissionHandling @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.invitation, defaultValue: nil) var invitation: String? @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool var hudPublisher: AnyPublisher<HUDStatus, Never> { @@ -180,17 +182,28 @@ final class LaunchViewModel { private func checkBiometrics() { if permissionHandler.isBiometricsAvailable && isBiometricsOn { permissionHandler.requestBiometrics { [weak self] in + guard let self = self else { return } + switch $0 { case .success(let granted): guard granted else { return } - self?.routeSubject.send(.chats) + + if self.invitation != nil { + self.routeSubject.send(.search) + } else { + self.routeSubject.send(.chats) + } case .failure(let error): - self?.hudSubject.send(.error(HUDError(with: error))) + self.hudSubject.send(.error(HUDError(with: error))) } } } else { - routeSubject.send(.chats) + if self.invitation != nil { + self.routeSubject.send(.search) + } else { + self.routeSubject.send(.chats) + } } } } diff --git a/Sources/MenuFeature/ViewModels/MenuViewModel.swift b/Sources/MenuFeature/ViewModels/MenuViewModel.swift index 20f91ac91b8ab31d8c86b909529cb1a3cab2b487..98bc5d74b66c3e85e43ea5f9b7a21129db7cd55d 100644 --- a/Sources/MenuFeature/ViewModels/MenuViewModel.swift +++ b/Sources/MenuFeature/ViewModels/MenuViewModel.swift @@ -42,6 +42,6 @@ final class MenuViewModel { } var referralDeeplink: String { - "xxmessenger://invitation-\(username)" + "xxnetwork://messenger?invitation=\(username)" } } diff --git a/Sources/MenuFeature/Views/MenuView.swift b/Sources/MenuFeature/Views/MenuView.swift index 7342ced784547ec83d7f3252af4d0f8044b591ef..e256782eddaa5e686d2bfc7e3ef4af237c3eb51c 100644 --- a/Sources/MenuFeature/Views/MenuView.swift +++ b/Sources/MenuFeature/Views/MenuView.swift @@ -72,7 +72,7 @@ final class MenuView: UIView { requestsButton.set(color: Asset.brandPrimary.color) case .settings: settingsButton.set(color: Asset.brandPrimary.color) - default: + case .share, .join, .profile, .dashboard: break } } diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index 6be71bfe90dbddbf0426e7eac07b0033d189276f..b3ca0e83922c6fe4868b68e5338ff1ee7fdaa496 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -37,6 +37,11 @@ final class SearchLeftController: UIViewController { setupBindings() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.viewDidAppear() + } + func endEditing() { screenView.inputField.endEditing(true) } @@ -131,6 +136,13 @@ final class SearchLeftController: UIViewController { .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) diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index 6c0a8390f2c65ad407e77f058a0d30fd0ab6a20e..e9c6e68d19f6ce11cd798bcec3c7f584c7480617 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -3,8 +3,10 @@ import UIKit import Shared import Combine import XXModels +import Defaults import Countries import Integration +import NetworkMonitor import DependencyInjection typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> @@ -18,6 +20,9 @@ struct SearchLeftViewState { final class SearchLeftViewModel { @Dependency var session: SessionType + @Dependency var networkMonitor: NetworkMonitoring + + @KeyObject(.invitation, defaultValue: nil) var invitation: String? var hudPublisher: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() @@ -35,6 +40,31 @@ final class SearchLeftViewModel { 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>() + + func viewDidAppear() { + if let pendingInvitation = invitation { + invitation = nil + stateSubject.value.input = pendingInvitation + hudSubject.send(.onAction(Localized.Ud.Search.cancel)) + + networkMonitor.statusPublisher + .first { $0 == .available } + .map { [unowned self] _ in + session.waitForNodes(timeout: 5).first() + .sink { + if case .failure(let error) = $0 { + self.hudSubject.send(.error(.init(with: error))) + } + networkCancellable.removeAll() + } receiveValue: { _ in + self.didStartSearching() + networkCancellable.removeAll() + }.store(in: &networkCancellable) + }.sink(receiveValue: { _ in }) + .store(in: &networkCancellable) + } + } func didEnterInput(_ string: String) { stateSubject.value.input = string diff --git a/Sources/Shared/Views/SearchComponent.swift b/Sources/Shared/Views/SearchComponent.swift index 9608aad71aaa56a5da852516405d069837a703c5..b3d530991d687604af21ea311eca69c458566f32 100644 --- a/Sources/Shared/Views/SearchComponent.swift +++ b/Sources/Shared/Views/SearchComponent.swift @@ -115,6 +115,10 @@ public final class SearchComponent: UIView { } } + public func update(content: String) { + inputField.text = content + } + public func update(placeholder: String) { inputField.attributedPlaceholder = NSAttributedString( string: placeholder,