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/Package.swift b/Package.swift index bcec957c2be3321cb1b9f1bcc1dd176814764b54..08f6fef2fb77a9c65ba50627b93abcb367a5a6f6 100644 --- a/Package.swift +++ b/Package.swift @@ -722,6 +722,13 @@ let package = Package( dependencies: ["DependencyInjection"] ), + // MARK: - AppTests + + .testTarget( + name: "AppTests", + dependencies: ["App"] + ), + // MARK: - ProfileFeatureTests .testTarget( diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 72a7f682477967599a823601b8315c46ed429ee6..00b8875eaf650d668ae4541fb254fc52c2c50980 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -20,6 +20,7 @@ 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 @@ -142,10 +143,30 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { - dropboxService.handleOpenUrl(url) + if let username = getUsernameFromInvitationDeepLink(url) { + let router = try! DependencyInjection.Container.shared.resolve() as PushRouter + invitation = username + router.navigateTo(.search, {}) + + return true + } else { + return dropboxService.handleOpenUrl(url) + } } } +func getUsernameFromInvitationDeepLink(_ url: URL) -> String? { + if let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + components.scheme == "xxnetwork", + components.host == "messenger", + let queryItem = components.queryItems?.first(where: { $0.name == "invitation" }), + let username = queryItem.value { + return username + } + + return nil +} + // MARK: Notifications extension AppDelegate: UNUserNotificationCenterDelegate { diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 14a7b7bdb482974cff2b5b2be573d3f5fb1a33fc..05d03f78e33764ef8a3f3cd325b5fa1b9d958c18 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(_:), @@ -250,38 +251,3 @@ struct DependencyRegistrator { ) as ChatListCoordinating) } } - -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 as? RequestsContainerController) == nil { - navigationController.setViewControllers([RequestsContainerController()], 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/App/PushRouter.swift b/Sources/App/PushRouter.swift new file mode 100644 index 0000000000000000000000000000000000000000..d81f841fc2c8b28a0380da507e37208c0bf2faf9 --- /dev/null +++ b/Sources/App/PushRouter.swift @@ -0,0 +1,51 @@ +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 .search: + if !(navigationController.viewControllers.last is SearchContainerController) { + navigationController.setViewControllers([ + ChatListController(), + SearchContainerController() + ], animated: true) + } + case .requests: + if !(navigationController.viewControllers.last is RequestsContainerController) { + navigationController.setViewControllers([RequestsContainerController()], 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/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 2eb373f55784bed3e9a11293acc7cca9aee2fc08..0a6a1c651affe95c973a5eb6b5b46a968b5b5a48 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -5,7 +5,6 @@ import Combine import PushFeature import DependencyInjection - public final class LaunchController: UIViewController { @Dependency private var hud: HUD @Dependency private var coordinator: LaunchCoordinating @@ -50,9 +49,14 @@ 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 { + case .search: + coordinator.toSearch(from: self) + case .requests: coordinator.toRequests(from: self) 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/Controllers/MenuController.swift b/Sources/MenuFeature/Controllers/MenuController.swift index 682e23d450bde134916a2db8a00d13c2279c9321..93f1fcf4a328da1f73f077b8f0f8806ceee0c7f5 100644 --- a/Sources/MenuFeature/Controllers/MenuController.swift +++ b/Sources/MenuFeature/Controllers/MenuController.swift @@ -9,6 +9,7 @@ public enum MenuItem { case join case scan case chats + case share case profile case contacts case requests @@ -171,6 +172,19 @@ public final class MenuController: UIViewController { } }.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) } diff --git a/Sources/MenuFeature/Coordinator/MenuCoordinator.swift b/Sources/MenuFeature/Coordinator/MenuCoordinator.swift index fba5c8ea4f8bfb1aa155dcf4ada8fba1a58c3f39..3e7d20cc85f6a4afa7e50e2f8133bdf041f5ace6 100644 --- a/Sources/MenuFeature/Coordinator/MenuCoordinator.swift +++ b/Sources/MenuFeature/Coordinator/MenuCoordinator.swift @@ -4,9 +4,11 @@ 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() @@ -16,6 +18,8 @@ public struct MenuCoordinator: MenuCoordinating { var settingsFactory: () -> UIViewController var contactsFactory: () -> UIViewController var requestsFactory: () -> UIViewController + var activityControllerFactory: ([Any]) -> UIViewController + = { UIActivityViewController(activityItems: $0, applicationActivities: nil) } public init( scanFactory: @escaping () -> UIViewController, @@ -61,4 +65,9 @@ public extension MenuCoordinator { 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 f3e4bcbd5260af8c2e62849c7eca05b4098cffe4..98bc5d74b66c3e85e43ea5f9b7a21129db7cd55d 100644 --- a/Sources/MenuFeature/ViewModels/MenuViewModel.swift +++ b/Sources/MenuFeature/ViewModels/MenuViewModel.swift @@ -40,4 +40,8 @@ final class MenuViewModel { var version: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" } + + var referralDeeplink: String { + "xxnetwork://messenger?invitation=\(username)" + } } diff --git a/Sources/MenuFeature/Views/MenuView.swift b/Sources/MenuFeature/Views/MenuView.swift index b19ec9d1d1367f5d910dfe97d40ee9198b7e3ec0..e256782eddaa5e686d2bfc7e3ef4af237c3eb51c 100644 --- a/Sources/MenuFeature/Views/MenuView.swift +++ b/Sources/MenuFeature/Views/MenuView.swift @@ -2,31 +2,33 @@ import UIKit import Shared final class MenuView: UIView { - let headerView = MenuHeaderView() + 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 joinButton = MenuSectionButton() - let infoStackView = UIStackView() - let buildLabel = UILabel() - let versionLabel = UILabel() - let xxdkVersionLabel = UILabel() init() { super.init(frame: .zero) backgroundColor = Asset.neutralDark.color - chatsButton.set(title: Localized.Menu.chats, image: Asset.menuChats.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) - joinButton.set(title: "Join xx network", image: Asset.permissionLogo.image) stackView.addArrangedSubview(chatsButton) stackView.addArrangedSubview(contactsButton) @@ -35,6 +37,7 @@ final class MenuView: UIView { stackView.addArrangedSubview(settingsButton) stackView.addArrangedSubview(dashboardButton) stackView.addArrangedSubview(joinButton) + stackView.addArrangedSubview(shareButton) infoStackView.spacing = 10 infoStackView.axis = .vertical @@ -59,17 +62,17 @@ final class MenuView: UIView { 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 .scan: - scanButton.set(color: Asset.brandPrimary.color) case .settings: settingsButton.set(color: Asset.brandPrimary.color) - case .profile, .dashboard, .join: + case .share, .join, .profile, .dashboard: break } } diff --git a/Sources/PushFeature/PushRouter.swift b/Sources/PushFeature/PushRouter.swift index 6fc66797612acf41e56213ab64ffc9a7029d873e..51af8a80ae87cf6bbab518a666256e9925685325 100644 --- a/Sources/PushFeature/PushRouter.swift +++ b/Sources/PushFeature/PushRouter.swift @@ -4,6 +4,7 @@ public struct PushRouter { public typealias NavigateTo = (Route, @escaping () -> Void) -> Void public enum Route { + case search case requests case groupChat(id: Data) case contactChat(id: Data) diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index bbfabd2836990bb00c86e01a09527bdbac8b1145..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) @@ -403,7 +415,6 @@ final class SearchLeftController: UIViewController { coordinator.toDrawer(drawer, from: self) } - } extension SearchLeftController: UITableViewDelegate { diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index 6c0a8390f2c65ad407e77f058a0d30fd0ab6a20e..8206e136f89d0068432458ccac97d8ebe544c2da 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,29 @@ 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)) + + 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) + } + } func didEnterInput(_ string: String) { stateSubject.value.input = string diff --git a/Sources/Shared/AutoGenerated/Assets.swift b/Sources/Shared/AutoGenerated/Assets.swift index 6755de526a369e8245365d38a47984ea96a02e42..c55ff98183f5c8b9c3776a40a24bc8ddba91429b 100644 --- a/Sources/Shared/AutoGenerated/Assets.swift +++ b/Sources/Shared/AutoGenerated/Assets.swift @@ -70,6 +70,7 @@ public enum Asset { public static let menuRequests = ImageAsset(name: "menu_requests") public static let menuScan = ImageAsset(name: "menu_scan") public static let menuSettings = ImageAsset(name: "menu_settings") + public static let menuShare = ImageAsset(name: "menu_share") public static let onboardingBackground = ImageAsset(name: "onboarding_background") public static let onboardingBottomLogoStart = ImageAsset(name: "onboarding_bottom_logo_start") public static let onboardingEmail = ImageAsset(name: "onboarding_email") diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index 315babf8e14569c98f9ea0f2513962566f9bfef8..1640774e563ec20094d2d9df9fc77dd118cc3f56 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -633,6 +633,8 @@ public enum Localized { public static let contacts = Localized.tr("Localizable", "menu.contacts") /// Dashboard public static let dashboard = Localized.tr("Localizable", "menu.dashboard") + /// Join xx network + public static let join = Localized.tr("Localizable", "menu.join") /// Profile public static let profile = Localized.tr("Localizable", "menu.profile") /// Requests @@ -641,6 +643,16 @@ public enum Localized { public static let scan = Localized.tr("Localizable", "menu.scan") /// Settings public static let settings = Localized.tr("Localizable", "menu.settings") + /// Share my profile + public static let share = Localized.tr("Localizable", "menu.share") + /// Hi, I'm using xx messenger, you can download it here: + /// https://invite.xx.network + /// + /// And you can add me using this link: + /// %@ + public static func shareContent(_ p1: Any) -> String { + return Localized.tr("Localizable", "menu.shareContent", String(describing: p1)) + } /// Hello public static let title = Localized.tr("Localizable", "menu.title") /// Version %@ diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b09b053bb7c5bd064ae240eb8d69b92a81b85519 --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsMenu/menu_share.imageset/Icon.pdf @@ -0,0 +1,101 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 4.000000 cm +0.693500 0.711750 0.730000 scn +12.599999 1.800547 m +12.599999 4.501754 l +12.599999 4.999001 13.002944 5.402100 13.500000 5.402100 c +13.997056 5.402100 14.400000 4.999001 14.400000 4.501754 c +14.400000 1.800547 l +14.400000 0.807714 13.593665 0.000029 12.603939 0.000029 c +1.796060 0.000029 l +0.802609 0.000029 0.000000 0.804142 0.000000 1.800547 c +0.000000 4.501754 l +0.000000 4.999001 0.402944 5.402100 0.900000 5.402100 c +1.397056 5.402100 1.800000 4.999001 1.800000 4.501754 c +1.796060 1.800718 l +12.599999 1.800547 l +h +f +n +Q +q +-1.000000 -0.000000 0.000000 -1.000000 15.648438 20.002563 cm +0.693500 0.711750 0.730000 scn +1.539240 4.081299 m +2.545713 3.074440 l +2.545713 11.492543 l +2.545713 11.989956 2.945192 12.393188 3.445713 12.393188 c +3.942770 12.393188 4.345713 11.990088 4.345713 11.492543 c +4.345713 3.074440 l +5.352188 4.081299 l +5.705159 4.434405 6.273771 4.438073 6.627693 4.084015 c +6.979165 3.732409 6.977091 3.160265 6.624980 2.808019 c +4.084824 0.266890 l +3.906826 0.088822 3.676712 0.001439 3.446377 0.001585 c +3.215733 -0.000003 2.985826 0.087598 2.809317 0.264174 c +2.807509 0.265984 0.266448 2.808019 0.266448 2.808019 c +-0.086523 3.161125 -0.090189 3.729957 0.263733 4.084015 c +0.615205 4.435622 1.187128 4.433546 1.539240 4.081299 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1353 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001443 00000 n +0000001466 00000 n +0000001639 00000 n +0000001713 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1772 +%%EOF \ No newline at end of file diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 2b7263918c46d1868d27b5fe82ff5d21137786e9..32235ed0528a1881c6c513f4edece96ca47ce4b3 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -8,6 +8,10 @@ = "Connections"; "menu.requests" = "Requests"; +"menu.join" += "Join xx network"; +"menu.share" += "Share my profile"; "menu.viewProfile" = "View Profile"; "menu.profile" @@ -22,6 +26,8 @@ = "Build %@"; "menu.version" = "Version %@"; +"menu.shareContent" += "Hi, I'm using xx messenger, you can download it here:\nhttps://invite.xx.network\n\nAnd you can add me using this link:\n%@"; // ChatListFeature 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, diff --git a/Tests/AppTests/General/InvitationTests.swift b/Tests/AppTests/General/InvitationTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..146c885afab3c85686cc5f216c8e85ff851911b5 --- /dev/null +++ b/Tests/AppTests/General/InvitationTests.swift @@ -0,0 +1,21 @@ +import XCTest + +@testable import App + +final class AppDelegateTests: XCTestCase { + func test_invitationDeeplink() { + XCTAssertNil( + getUsernameFromInvitationDeepLink(URL(string: "http://messenger?invitation=john_doe")!) + ) + + XCTAssertNotEqual( + getUsernameFromInvitationDeepLink(URL(string: "xxnetwork://messenger?invitation=the_rock")!), + "john_doe" + ) + + XCTAssertEqual( + getUsernameFromInvitationDeepLink(URL(string: "xxnetwork://messenger?invitation=john_doe")!), + "john_doe" + ) + } +}