diff --git a/Package.swift b/Package.swift index 9317c5bef0efb8db66aabe1cb1485b61fd2426f6..bcec957c2be3321cb1b9f1bcc1dd176814764b54 100644 --- a/Package.swift +++ b/Package.swift @@ -68,7 +68,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.0"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.1"), .package(url: "https://github.com/google/google-api-objectivec-client-for-rest", from: "1.6.0"), - .package(url: "https://git.xx.network/elixxir/client-ios-db.git", .upToNextMajor(from: "1.0.5")), + .package(url: "https://git.xx.network/elixxir/client-ios-db.git", .upToNextMajor(from: "1.0.8")), .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")), @@ -220,9 +220,11 @@ let package = Package( name: "SFTPFeature", dependencies: [ "HUD", + "Models", "Shared", "Keychain", "InputField", + "Presentation", "DependencyInjection", .product( name: "Shout", diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 87b8e7dd9d903fbf11f686ac46ecb730fac4eef7..14a7b7bdb482974cff2b5b2be573d3f5fb1a33fc 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -104,7 +104,7 @@ struct DependencyRegistrator { // MARK: Isolated - container.register(HUD() as HUDType) + container.register(HUD()) container.register(ThemeController() as ThemeControlling) container.register(ToastController()) container.register(StatusBarController() as StatusBarStyleControlling) @@ -137,6 +137,8 @@ struct DependencyRegistrator { container.register( SearchCoordinator( + contactsFactory: ContactListController.init, + requestsFactory: RequestsContainerController.init, contactFactory: ContactController.init(_:), countriesFactory: CountryListController.init(_:) ) as SearchCoordinating) diff --git a/Sources/BackupFeature/Controllers/BackupController.swift b/Sources/BackupFeature/Controllers/BackupController.swift index a01ff0caa91be5ffdfa55e6fe9fe8ea7ebab41bf..a9942d8c03466fe8b620d156c9eb1cc02f2ef70a 100644 --- a/Sources/BackupFeature/Controllers/BackupController.swift +++ b/Sources/BackupFeature/Controllers/BackupController.swift @@ -6,7 +6,7 @@ import Combine import DependencyInjection public final class BackupController: UIViewController { - @Dependency private var hud: HUDType + @Dependency var hud: HUD private let viewModel = BackupViewModel.live() private var cancellables = Set<AnyCancellable>() @@ -14,7 +14,7 @@ public final class BackupController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = Asset.neutralWhite.color - hud.update(with: .on(nil)) + hud.update(with: .on) setupNavigationBar() setupBindings() diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index f136ca8c15c83578636ed3ef00643817d73b8de9..0731bc4a94f5be17d6e224d5f67573ea68ea608f 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -30,7 +30,7 @@ struct BackupConfigViewModel { extension BackupConfigViewModel { static func live() -> Self { class Context { - @Dependency var hud: HUDType + @Dependency var hud: HUD @Dependency var service: BackupService @Dependency var coordinator: BackupCoordinating } @@ -40,7 +40,7 @@ extension BackupConfigViewModel { return .init( didTapBackupNow: { context.service.performBackup() - context.hud.update(with: .on(nil)) + context.hud.update(with: .on) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { context.hud.update(with: .none) } @@ -57,7 +57,7 @@ extension BackupConfigViewModel { context.service.toggle(service: service, enabling: false) }, passphraseClosure: { passphrase in context.service.passphrase = passphrase - context.hud.update(with: .on("Initializing and securing your backup file will take few seconds, please keep the app open.")) + 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) diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index b35d314725ea97149e84edb8b9525e1fc010d4f6..f34be438f7df3afd84d5ba7c6db04612bc891cc6 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -24,7 +24,7 @@ extension Message: Differentiable { } public final class SingleChatController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var logger: XXLogger @Dependency private var voxophone: Voxophone @Dependency private var coordinator: ChatCoordinating diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index cd78765910d84d7242ee96ff145c3cd612c2492d..f51a893610402c317f938f0b29445317de9bb6d4 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -107,7 +107,7 @@ final class SingleChatViewModel { func didSend(image: UIImage) { guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } - hudRelay.send(.on(nil)) + hudRelay.send(.on) session.send(imageData: imageData, to: contact) { [weak self] in switch $0 { diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index 98febff09e352ff1e0ff495c032f1e7d1ec1613a..f481b96fb7e3e14b74096919090bf1df8c96c840 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -147,7 +147,7 @@ final class ChatListViewModel { } func leave(_ group: Group) { - hudSubject.send(.on(nil)) + hudSubject.send(.on) do { try session.leave(group: group) diff --git a/Sources/ChatListFeature/Views/ChatListView.swift b/Sources/ChatListFeature/Views/ChatListView.swift index c7303c48d04db46983e2985d40745ce236d72c43..03a407980700123d9843d9239e4822b9247dac63 100644 --- a/Sources/ChatListFeature/Views/ChatListView.swift +++ b/Sources/ChatListFeature/Views/ChatListView.swift @@ -14,7 +14,7 @@ final class ChatListView: UIView { backgroundColor = Asset.neutralWhite.color listContainerView.backgroundColor = Asset.neutralWhite.color searchListContainerView.backgroundColor = Asset.neutralWhite.color - searchView.update(placeholder: "Search chats") + searchView.update(placeholder: Localized.ChatList.Search.title) addSubview(snackBar) addSubview(searchView) @@ -22,6 +22,34 @@ final class ChatListView: UIView { containerView.addSubview(searchListContainerView) containerView.addSubview(listContainerView) + setupConstraints() + } + + 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 + } + } + + private func setupConstraints() { snackBar.snp.makeConstraints { $0.left.equalToSuperview() $0.right.equalToSuperview() @@ -49,28 +77,4 @@ final class ChatListView: UIView { $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/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift index cf64268edbdd1cad2986105c38d72a74b2273499..02859bab60bf861a5615795a016352a4f4509900 100644 --- a/Sources/ContactFeature/Controllers/ContactController.swift +++ b/Sources/ContactFeature/Controllers/ContactController.swift @@ -10,7 +10,7 @@ import DependencyInjection import ScrollViewController public final class ContactController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: ContactCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/ContactFeature/ViewModels/ContactViewModel.swift b/Sources/ContactFeature/ViewModels/ContactViewModel.swift index 67005d4b9d01cbc493ead72d8f6b89e42e64535e..5e6e06698cbfeb359a79ce231dde08f37c03ee6a 100644 --- a/Sources/ContactFeature/ViewModels/ContactViewModel.swift +++ b/Sources/ContactFeature/ViewModels/ContactViewModel.swift @@ -62,7 +62,7 @@ final class ContactViewModel { } func didTapDelete() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) do { try session.deleteContact(contact) @@ -91,7 +91,7 @@ final class ContactViewModel { } func didTapResend() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } @@ -107,7 +107,7 @@ final class ContactViewModel { } func didTapRequest(with nickname: String) { - hudRelay.send(.on(nil)) + hudRelay.send(.on) contact.nickname = nickname backgroundScheduler.schedule { [weak self] in @@ -124,7 +124,7 @@ final class ContactViewModel { } func didTapAccept(_ nickname: String) { - hudRelay.send(.on(nil)) + hudRelay.send(.on) contact.nickname = nickname backgroundScheduler.schedule { [weak self] in diff --git a/Sources/ContactListFeature/Controllers/ContactListTableController.swift b/Sources/ContactListFeature/Controllers/ContactListTableController.swift index f940366ae08cfe77f9cd54e5d1008a55b7149bc6..ac7a06628045f199393f1839e2a6f700a7cb194e 100644 --- a/Sources/ContactListFeature/Controllers/ContactListTableController.swift +++ b/Sources/ContactListFeature/Controllers/ContactListTableController.swift @@ -30,7 +30,7 @@ final class ContactListTableController: UITableViewController { private func setupTableView() { tableView.separatorStyle = .none - tableView.register(SmallAvatarAndTitleCell.self) + 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) @@ -45,11 +45,11 @@ final class ContactListTableController: UITableViewController { } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: SmallAvatarAndTitleCell = tableView.dequeueReusableCell(forIndexPath: indexPath) + let cell: AvatarCell = tableView.dequeueReusableCell(forIndexPath: indexPath) let contact = sections[indexPath.section][indexPath.row] let name = (contact.nickname ?? contact.username) ?? "Fetching username..." - cell.titleLabel.text = name - cell.avatarView.setupProfile(title: name, image: contact.photo, size: .medium) + + cell.setup(title: name, image: contact.photo) return cell } diff --git a/Sources/ContactListFeature/Controllers/CreateGroupController.swift b/Sources/ContactListFeature/Controllers/CreateGroupController.swift index 9e9d039dd5e5fe2dd4adfa179fc736ab51fa6c4b..a9559f5c3c38ccdf7a90543a9963748155e6fa6b 100644 --- a/Sources/ContactListFeature/Controllers/CreateGroupController.swift +++ b/Sources/ContactListFeature/Controllers/CreateGroupController.swift @@ -7,7 +7,7 @@ import XXModels import DependencyInjection public final class CreateGroupController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: ContactListCoordinating lazy private var titleLabel = UILabel() @@ -66,7 +66,7 @@ public final class CreateGroupController: UIViewController { private func setupTableAndCollection() { screenView.tableView.rowHeight = 64.0 - screenView.tableView.register(SmallAvatarAndTitleCell.self) + screenView.tableView.register(AvatarCell.self) screenView.collectionView.register(CreateGroupCollectionCell.self) collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>( @@ -84,10 +84,10 @@ public final class CreateGroupController: UIViewController { tableDataSource = DiffEditableDataSource<SectionId, Contact>( tableView: screenView.tableView ) { [weak self] tableView, indexPath, contact in - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self) + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self) let title = (contact.nickname ?? contact.username) ?? "" - cell.titleLabel.text = title - cell.avatarView.setupProfile(title: title, image: contact.photo, size: .medium) + + cell.setup(title: title, image: contact.photo) if let selectedElements = self?.selectedElements, selectedElements.contains(contact) { tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift index 67e70645ed6c0d00f02862bdb02f199d3d735d9d..a8f94de8042f3cd7f7d0da14a9de703f217f573b 100644 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift @@ -76,7 +76,7 @@ final class CreateGroupViewModel { } func create(name: String, welcome: String?, members: [Contact]) { - hudRelay.send(.on(nil)) + hudRelay.send(.on) session.createGroup(name: name, welcome: welcome, members: members) { [weak self] in guard let self = self else { return } diff --git a/Sources/HUD/HUD.swift b/Sources/HUD/HUD.swift index 9f45b18078d75a2d11654f99a9bd2d1b2746f4ee..207f3473c9173a3a64c8636a8373d734d207807b 100644 --- a/Sources/HUD/HUD.swift +++ b/Sources/HUD/HUD.swift @@ -10,15 +10,17 @@ private enum Constants { } public enum HUDStatus: Equatable { - case on(String?) 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: + case .on, .error, .onTitle, .onAction: return true } } @@ -50,15 +52,12 @@ public struct HUDError: Equatable { } } -public protocol HUDType { - func update(with status: HUDStatus) -} - -public final class HUD: HUDType { +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 { @@ -67,16 +66,23 @@ public final class HUD: HUDType { self.errorView = nil self.animation = nil self.window = nil + self.actionButton = nil self.titleLabel = nil switch status { - case .on(let text): + 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) - if let text = text { - titleLabel = UILabel() - titleLabel!.text = text - } case .error(let error): errorView = ErrorView(with: error) case .none: @@ -88,13 +94,19 @@ public final class HUD: HUDType { if oldValue.isPresented == false && status.isPresented == true { switch status { - case .on(let text): + case .on: animation = DotAnimation() - if let text = text { - titleLabel = UILabel() - titleLabel!.text = text - } + 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: @@ -118,7 +130,7 @@ public final class HUD: HUDType { private func showWindow() { window = Window() - window?.backgroundColor = UIColor.black.withAlphaComponent(0.5) + window?.backgroundColor = UIColor.black.withAlphaComponent(0.8) window?.rootViewController = StatusBarViewController(nil) if let animation = animation { @@ -138,6 +150,15 @@ public final class HUD: HUDType { } } + 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 @@ -166,6 +187,7 @@ public final class HUD: HUDType { self.cancellables.removeAll() self.errorView = nil self.animation = nil + self.actionButton = nil self.titleLabel = nil self.window = nil } diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift index 2b488ef925bd1d5367b94585dc4959bf14b66ea6..450c674ebc5647268114eb3b251e6b56c12efca4 100644 --- a/Sources/Integration/Client.swift +++ b/Sources/Integration/Client.swift @@ -90,7 +90,7 @@ public class Client { public func addJson(_ string: String) { guard let backupManager = backupManager else { - fatalError() + fatalError("Trying to add json parameters to backup but no backup manager created yet") } print("^^^ Set params: \(string) to backup") diff --git a/Sources/Integration/Implementations/Bindings.swift b/Sources/Integration/Implementations/Bindings.swift index 7c6feb9f10688bc0d34726aec2479c8174438484..a05387dda2653644a26a3bc28b40e011c6eb5e03 100644 --- a/Sources/Integration/Implementations/Bindings.swift +++ b/Sources/Integration/Implementations/Bindings.swift @@ -154,17 +154,15 @@ extension BindingsClient: BindingsInterface { for env: NetworkEnvironment, _ completion: @escaping (Result<Data?, Error>) -> Void ) { - log(type: .crumbs) - var error: NSError? let ndf = BindingsDownloadAndVerifySignedNdfWithUrl(env.url, env.cert, &error) - if let error = error { - log(string: error.localizedDescription, type: .error) - completion(.failure(error)) - } else { - completion(.success(ndf)) + guard error == nil else { + Self.updateNDF(for: env, completion) + return } + + completion(.success(ndf)) } /// Fetches a JSON with up-to-date error descriptions diff --git a/Sources/Integration/Session/Session+Contacts.swift b/Sources/Integration/Session/Session+Contacts.swift index 3f6a27711512043b3677b0c54fcc68265a72192e..7bbe5254ab00b17b8053323586347ef819f10ff1 100644 --- a/Sources/Integration/Session/Session+Contacts.swift +++ b/Sources/Integration/Session/Session+Contacts.swift @@ -94,7 +94,7 @@ extension Session { } public func retryRequest(_ contact: Contact) throws { - log(string: "Retrying to request a contact", type: .info) + let name = (contact.nickname ?? contact.username) ?? "" client.bindings.add(contact.marshaled!, from: myQR) { [weak self, contact] in var contact = contact @@ -103,11 +103,21 @@ extension Session { do { switch $0 { case .success: - log(string: "Retrying to request a contact -- Success", type: .info) contact.authStatus = .requested - case .failure(let error): - log(string: "Retrying to request a contact -- Failed: \(error.localizedDescription)", type: .error) + + 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) @@ -170,6 +180,13 @@ extension Session { 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 diff --git a/Sources/Integration/Session/Session+UD.swift b/Sources/Integration/Session/Session+UD.swift index 630d822af475c32bb21fb09bb4d9c8bc80a5a54f..1fe3e2e09b2f7f0ae327b039602320bbc884eb2b 100644 --- a/Sources/Integration/Session/Session+UD.swift +++ b/Sources/Integration/Session/Session+UD.swift @@ -1,8 +1,35 @@ import Models import XXModels import Foundation +import Combine extension Session { + 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() @@ -69,6 +96,8 @@ extension Session { phone = confirmation.content } - updateFactsOnBackup() + if let _ = client.backupManager { + updateFactsOnBackup() + } } } diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift index e14c4901e1a22fe97869768b154ec25dd70eff93..f85a6c6f0e3350e20c2bb234e6a92935e589b01f 100644 --- a/Sources/Integration/Session/Session.swift +++ b/Sources/Integration/Session/Session.swift @@ -130,8 +130,20 @@ public final class Session: SessionType { let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8)) username = params.username - phone = params.phone - email = params.email + + if let paramsPhone = params.phone, !paramsPhone.isEmpty { + phone = paramsPhone + } + + if let paramsEmail = params.email, !paramsEmail.isEmpty { + email = paramsEmail + } + } + + print("^^^ \(report.parameters)") + + guard username!.isEmpty == false else { + fatalError("Trying to restore an account that has no username") } try continueInitialization() diff --git a/Sources/Integration/Session/SessionType.swift b/Sources/Integration/Session/SessionType.swift index e99c6f37fb45599aae26836c7a8f248ae20f02f1..b871332b216e5c7dbdc0adfebc4bac3f306913b7 100644 --- a/Sources/Integration/Session/SessionType.swift +++ b/Sources/Integration/Session/SessionType.swift @@ -66,4 +66,6 @@ public protocol SessionType { members: [Contact], _ completion: @escaping (Result<GroupInfo, Error>) -> Void ) + + func search(fact: String) -> AnyPublisher<Contact, Error> } diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index 33c2f8dad55d29d02a7e74b3d70437fbaea4aa6f..2eb373f55784bed3e9a11293acc7cca9aee2fc08 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -7,7 +7,7 @@ import DependencyInjection public final class LaunchController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: LaunchCoordinating lazy private var screenView = LaunchView() diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 4e1b25ea61eb4475c9849f1873482b8a6eec3b25..fefbe7cf6035b19d804febd7e544e22166d11bf8 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -58,7 +58,7 @@ final class LaunchViewModel { func viewDidAppear() { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - self?.hudSubject.send(.on(nil)) + self?.hudSubject.send(.on) self?.checkVersion() } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift index 75b7d0ffb081bec57a2c8cf06d0fa6915fd07092..90c210569b58b904473f5c2e2020740e2945a09d 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift @@ -9,7 +9,7 @@ import ScrollViewController import Models public final class OnboardingEmailConfirmationController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift index 68b10159a32ab4e5167d10ba98d993f453fdf24d..54fb8cf4efcf4cd29489729598ed22144b918f65 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift @@ -8,7 +8,7 @@ import DependencyInjection import ScrollViewController public final class OnboardingEmailController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift index dbef5be43b8314ffcc71a2481bb4bcc0d67a5e6d..0017798bc5fc2a4703a56cf6b4fc2a45fbcffa55 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift @@ -9,7 +9,7 @@ import ScrollViewController import Models public final class OnboardingPhoneConfirmationController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift index b935ae07434e90d94818a2ea400202924323ea32..8793508fe6ec8d94e3ee00ff5f3052031c70ee53 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift @@ -8,7 +8,7 @@ import DependencyInjection import ScrollViewController public final class OnboardingPhoneController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift index 2ed639b116b86ab36b2cdf8924b63626bd653d0c..d95169bbd9c5ce078687a77c96768d91e9d2336d 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift @@ -6,7 +6,7 @@ import Combine import DependencyInjection public final class OnboardingStartController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: OnboardingCoordinating lazy private var screenView = OnboardingStartView() diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index d25db4581f5c5dbd3083515b1828433494631e45..3bc4493ebd99899a4b9ea646964816ff0140009e 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -8,7 +8,7 @@ import DependencyInjection import ScrollViewController public final class OnboardingUsernameController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: OnboardingCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift index 1263fc26efb0f01d50f00a25e8b4b27e3b821b81..b3871d6234517f1d493ab61da1466bc754446fd7 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift @@ -58,7 +58,7 @@ final class OnboardingEmailConfirmationViewModel { } func didTapNext() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift index 618a903459267835ea47210d17ab0e5f763ac68e..c3cbbb897840964e15f908b9fa83d5d6537f7b23 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift @@ -38,7 +38,7 @@ final class OnboardingEmailViewModel { } func didTapNext() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift index 0af84d605d2c56a938e474d27fd79ad324943ee3..2bd5a7ae35fbca7bf871479f998abeef9e4ce625 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift @@ -58,7 +58,7 @@ final class OnboardingPhoneConfirmationViewModel { } func didTapNext() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift index 4698673d45cbe3b05f73fb3b1943f598f1c9655e..0aff02f402420b959d03f166ba4eb456d1098bea 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift @@ -47,7 +47,7 @@ final class OnboardingPhoneViewModel { } func didTapNext() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift index 8766aac8e7629e4bdb9079cd8a3774aa2d1989a2..ecb64035e73e259bf147ce4ee7f427723d40b3f3 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift @@ -42,7 +42,7 @@ final class OnboardingUsernameViewModel { } func didTapRegister() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift index d909662aa961fda594217d71d4dd940bd0cf80af..c005b0bf9c10daa6db1f8521799da86bbfad26a4 100644 --- a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift @@ -10,7 +10,7 @@ import ScrollViewController public typealias ControllerClosure = (UIViewController, AttributeConfirmation) -> Void public final class ProfileCodeController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD lazy private var screenView = ProfileCodeView() lazy private var scrollViewController = ScrollViewController() diff --git a/Sources/ProfileFeature/Controllers/ProfileController.swift b/Sources/ProfileFeature/Controllers/ProfileController.swift index 62a2c90ee9bfb56674f8601bb1007d62c5f3323d..e3138f275dd967d73d58548649e4156409178e57 100644 --- a/Sources/ProfileFeature/Controllers/ProfileController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileController.swift @@ -7,7 +7,7 @@ import Combine import DependencyInjection public final class ProfileController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: ProfileCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift index fb85221449bea9ecd89b1a9507db4182b7365622..97ced65c65a10b7d4ac8033a5b2c0d80cf959997 100644 --- a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift @@ -7,7 +7,7 @@ import DependencyInjection import ScrollViewController public final class ProfileEmailController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: ProfileCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift index b6ddaffb5574bdb44f70994fe2ce2c7b0e98b3c0..eb77fd14d36bce3960f30a73aa4ac7c117e453e7 100644 --- a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift +++ b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift @@ -9,7 +9,7 @@ import ScrollViewController #warning("TODO: Merge ProfilePhoneController/ProfileEmailController") public final class ProfilePhoneController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: ProfileCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift index 0cc8f60cfaea6921d99a90ccbba74a0cdfc322c2..d9763e989e23e37c9736582a41fcf1b63aef03b2 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift @@ -57,7 +57,7 @@ final class ProfileCodeViewModel { } func didTapNext() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift index df2bb28cdaa889462d1db1eb506cb14129567312..6b57bc81fe4040faaccaeefff057a275a90deaac 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift @@ -40,7 +40,7 @@ final class ProfileEmailViewModel { } func didTapNext() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift index 9bf5da6e11788124e2bba6b04ba05f2f64b7f033..1013725b419a118c4e437d3e8959c50748447d72 100644 --- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift @@ -47,7 +47,7 @@ final class ProfilePhoneViewModel { } func didTapNext() { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index 8f03379f02adde54606a70e4bd7c76f3e1fea87d..a7066eccfaada6e74960beb7a5b3b8b7e72548cb 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -83,7 +83,7 @@ final class ProfileViewModel { } func didTapDelete(isEmail: Bool) { - hudRelay.send(.on(nil)) + hudRelay.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift index c166da6b2c97b01f38e273b2a1a91ccbed475055..c0af65d5f83a4bc1528065eef1551e597a52f6c9 100644 --- a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift @@ -5,7 +5,7 @@ import Combine import DependencyInjection final class RequestsFailedController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD lazy private var screenView = RequestsFailedView() private var cancellables = Set<AnyCancellable>() diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift index 5edb77726fa6d43ba7d4e2a816192fcadd39303b..0a30d05fb32b5ec4d3eb54b1fdeb9ced674a17ac 100644 --- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift @@ -10,7 +10,7 @@ import DrawerFeature import DependencyInjection final class RequestsReceivedController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var toaster: ToastController @Dependency private var coordinator: RequestsCoordinating diff --git a/Sources/RequestsFeature/Controllers/RequestsSentController.swift b/Sources/RequestsFeature/Controllers/RequestsSentController.swift index 36c245f4476fdd8795a6c0c38612f86c1ec6953e..ceed2cc28989b51d72d0191e63e81ade2f930bdd 100644 --- a/Sources/RequestsFeature/Controllers/RequestsSentController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsSentController.swift @@ -5,7 +5,7 @@ import Combine import DependencyInjection final class RequestsSentController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD var connectionsPublisher: AnyPublisher<Void, Never> { connectionSubject.eraseToAnyPublisher() diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift index 9214c9ef3f19d337ae66f0f9a6c8368741ff1096..b7d81db3a65b791e001a897506418295b56f8b3a 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift @@ -38,7 +38,7 @@ final class RequestsFailedViewModel { func didTapStateButtonFor(request: Request) { guard case let .contact(contact) = request, request.status == .failedToRequest else { return } - hudSubject.send(.on(nil)) + hudSubject.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index 7dd9e8c1d07c9b15d9829803be59c55c0955fd12..ded18f77a4da0d956d2bdb0b1779495c818d41f5 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -150,7 +150,7 @@ final class RequestsReceivedViewModel { } func didRequestAccept(group: Group) { - hudSubject.send(.on(nil)) + hudSubject.send(.on) backgroundScheduler.schedule { [weak self] in do { @@ -208,7 +208,7 @@ final class RequestsReceivedViewModel { } func didRequestAccept(contact: Contact, nickname: String? = nil) { - hudSubject.send(.on(nil)) + hudSubject.send(.on) var contact = contact contact.nickname = nickname ?? contact.username diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index 313a3694ac8498710b4847c501f41a7889c6535a..f94ed8f5164de5c5a419a9b9271499ae7daa4952 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -53,7 +53,7 @@ final class RequestsSentViewModel { func didTapStateButtonFor(request item: RequestSent) { guard case let .contact(contact) = item.request, item.request.status == .requested else { return } - hudSubject.send(.on(nil)) + hudSubject.send(.on) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } @@ -71,8 +71,10 @@ final class RequestsSentViewModel { item.isResent = true allRequests.append(item) + let name = (contact.nickname ?? contact.username) ?? "" + self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resent(contact.nickname ?? contact.username), + title: Localized.Requests.Sent.Toast.resent(name), leftImage: Asset.requestSentToaster.image )) diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift index c73e94edec314484dc06307f84e9e528f66bcc2a..b396d1069df7008772290e5c9e2f275adb5ccf4b 100644 --- a/Sources/RestoreFeature/Controllers/RestoreListController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -6,7 +6,7 @@ import DrawerFeature import DependencyInjection public final class RestoreListController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: RestoreCoordinating lazy private var screenView = RestoreListView() diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift index a4c2402e819c0576afdacc501c4550cd3656269f..438207bb1cc5df10104d3be19029ef01cc7d3c19 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift @@ -47,7 +47,7 @@ final class RestoreListViewModel { guard let self = self else { return } controller.navigationController?.popViewController(animated: true) - self.hudSubject.send(.on(nil)) + self.hudSubject.send(.on) self.sftpService.fetchMetadata{ result in switch result { @@ -72,7 +72,7 @@ final class RestoreListViewModel { googleDriveService.authorize(presenting: controller) { authResult in switch authResult { case .success: - self.hudSubject.send(.on(nil)) + self.hudSubject.send(.on) self.googleDriveService.downloadMetadata { downloadResult in switch downloadResult { case .success(let metadata): @@ -97,7 +97,7 @@ final class RestoreListViewModel { private func didRequestICloudAuthorization() { if icloudService.isAuthorized() { - self.hudSubject.send(.on(nil)) + self.hudSubject.send(.on) icloudService.downloadMetadata { result in switch result { @@ -129,7 +129,7 @@ final class RestoreListViewModel { case .success(let bool): guard bool == true else { return } - self.hudSubject.send(.on(nil)) + self.hudSubject.send(.on) dropboxService.downloadMetadata { metadataResult in switch metadataResult { case .success(let metadata): diff --git a/Sources/SFTPFeature/SFTPController.swift b/Sources/SFTPFeature/SFTPController.swift index 0a8c8d38ed8dbd9d802d58962b468b11f4c57431..f80908b9cdf6106bacef3c2fa9f6c4eac46e8145 100644 --- a/Sources/SFTPFeature/SFTPController.swift +++ b/Sources/SFTPFeature/SFTPController.swift @@ -5,7 +5,7 @@ import DependencyInjection import ScrollViewController public final class SFTPController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD lazy private var screenView = SFTPView() lazy private var scrollViewController = ScrollViewController() diff --git a/Sources/SFTPFeature/SFTPViewModel.swift b/Sources/SFTPFeature/SFTPViewModel.swift index dcf397a1da1c474681001b87df0d148ae26b2706..e64536bfd96277642d25ddb13f5c81570836a22b 100644 --- a/Sources/SFTPFeature/SFTPViewModel.swift +++ b/Sources/SFTPFeature/SFTPViewModel.swift @@ -45,7 +45,7 @@ final class SFTPViewModel { } func didTapLogin() { - hudSubject.send(.on(nil)) + hudSubject.send(.on) let host = stateSubject.value.host let username = stateSubject.value.username diff --git a/Sources/SearchFeature/Controllers/CameraController.swift b/Sources/SearchFeature/Controllers/CameraController.swift new file mode 100644 index 0000000000000000000000000000000000000000..7473920844e51bf6b77634818ee56282cb00b5d2 --- /dev/null +++ b/Sources/SearchFeature/Controllers/CameraController.swift @@ -0,0 +1,61 @@ +import Combine +import AVFoundation + +final class CameraController: NSObject { + var dataPublisher: AnyPublisher<Data, Never> { + dataSubject + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + lazy var previewLayer: CALayer = { + let layer = AVCaptureVideoPreviewLayer(session: session) + layer.videoGravity = .resizeAspectFill + return layer + }() + + private let session = AVCaptureSession() + private let metadataOutput = AVCaptureMetadataOutput() + private let dataSubject = PassthroughSubject<Data, Never>() + + override init() { + super.init() + setupCameraDevice() + } + + func start() { + guard session.isRunning == false else { return } + session.startRunning() + } + + func stop() { + guard session.isRunning == true else { return } + session.stopRunning() + } + + private func setupCameraDevice() { + if let captureDevice = AVCaptureDevice.default(for: .video), + let input = try? AVCaptureDeviceInput(device: captureDevice) { + + if session.canAddInput(input) && session.canAddOutput(metadataOutput) { + session.addInput(input) + session.addOutput(metadataOutput) + } + + metadataOutput.setMetadataObjectsDelegate(self, queue: .main) + metadataOutput.metadataObjectTypes = [.qr] + } + } + + func metadataOutput( + _ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection + ) { + guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + let data = object.stringValue?.data(using: .nonLossyASCII), object.type == .qr else { return } + dataSubject.send(data) + } +} + +extension CameraController: AVCaptureMetadataOutputObjectsDelegate {} diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index 1baedc82b9ca8d74ddb2f6a122b4a3d9b1262351..ad85b25640a1f1a5e418bf9974b2a0656021d202 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -2,32 +2,49 @@ import UIKit import Theme import Shared import Combine +import XXModels +import DrawerFeature import DependencyInjection public final class SearchContainerController: UIViewController { - @Dependency private var statusBarController: StatusBarStyleControlling + @Dependency var coordinator: SearchCoordinating + @Dependency var statusBarController: StatusBarStyleControlling lazy private var screenView = SearchContainerView() + private var contentOffset: CGPoint? private var cancellables = Set<AnyCancellable>() - private let qrController = SearchQRController() - private let emailController = SearchEmailController() - private let phoneController = SearchPhoneController() - private let usernameController = SearchUsernameController() + private let viewModel = SearchContainerViewModel() + private let leftController = SearchLeftController() + private let rightController = SearchRightController() + private var drawerCancellables = Set<AnyCancellable>() public override func loadView() { view = screenView - screenView.scrollView.delegate = self embedControllers() } public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) 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() { @@ -55,12 +72,23 @@ public final class SearchContainerController: UIViewController { private func setupBindings() { screenView.segmentedControl .actionPublisher + .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [unowned self] in - let page = CGFloat($0.rawValue) - let point: CGPoint = CGPoint(x: screenView.frame.width * page, y: 0.0) - screenView.scrollView.setContentOffset(point, animated: true) + 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) } @objc private func didTapBack() { @@ -68,91 +96,90 @@ public final class SearchContainerController: UIViewController { } private func embedControllers() { - addChild(qrController) - addChild(emailController) - addChild(phoneController) - addChild(usernameController) + addChild(leftController) + addChild(rightController) - screenView.scrollView.addSubview(qrController.view) - screenView.scrollView.addSubview(emailController.view) - screenView.scrollView.addSubview(phoneController.view) - screenView.scrollView.addSubview(usernameController.view) + screenView.scrollView.addSubview(leftController.view) + screenView.scrollView.addSubview(rightController.view) - usernameController.view.snp.makeConstraints { + 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(emailController.view.snp.left) - } - - emailController.view.snp.makeConstraints { - $0.top.equalTo(screenView.segmentedControl.snp.bottom) - $0.width.equalTo(screenView) - $0.bottom.equalTo(screenView) - $0.right.equalTo(phoneController.view.snp.left) + $0.right.equalTo(rightController.view.snp.left) } - phoneController.view.snp.makeConstraints { + rightController.view.snp.makeConstraints { $0.top.equalTo(screenView.segmentedControl.snp.bottom) $0.width.equalTo(screenView) $0.bottom.equalTo(screenView) - $0.right.equalTo(qrController.view.snp.left) } - qrController.view.snp.makeConstraints { - $0.top.equalTo(screenView.segmentedControl.snp.bottom) - $0.width.equalTo(screenView) - $0.bottom.equalTo(screenView) - } - - qrController.didMove(toParent: self) - emailController.didMove(toParent: self) - phoneController.didMove(toParent: self) - usernameController.didMove(toParent: self) + leftController.didMove(toParent: self) + rightController.didMove(toParent: self) } } -extension SearchContainerController: UIScrollViewDelegate { - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - let pageOffset = scrollView.contentOffset.x / view.frame.width - scrollSegmentedControlTrack(using: pageOffset) - updateSegmentedControlButtonsColor(using: pageOffset) - } - - private func scrollSegmentedControlTrack(using pageOffset: CGFloat) { - let amountOfTabs = 4.0 - let tabWidth = screenView.bounds.width / amountOfTabs - - if let leftConstraint = screenView.segmentedControl.leftConstraint { - leftConstraint.update(offset: pageOffset * tabWidth) - } - } - - private func updateSegmentedControlButtonsColor(using pageOffset: CGFloat) { - let qrRate = highlightRateFor(page: 3, offset: pageOffset) - let emailRate = highlightRateFor(page: 1, offset: pageOffset) - let phoneRate = highlightRateFor(page: 2, offset: pageOffset) - let usernameRate = highlightRateFor(page: 0, offset: pageOffset) +extension SearchContainerController { + private func presentCoverTrafficDrawer() { + let enableButton = CapsuleButton() + enableButton.set( + style: .brandColored, + title: Localized.ChatList.Traffic.positive + ) - screenView.segmentedControl.qrCodeButton.updateHighlighting(rate: qrRate) - screenView.segmentedControl.emailButton.updateHighlighting(rate: emailRate) - screenView.segmentedControl.phoneButton.updateHighlighting(rate: phoneRate) - screenView.segmentedControl.usernameButton.updateHighlighting(rate: usernameRate) - } + let dismissButton = CapsuleButton() + dismissButton.set( + style: .seeThrough, + title: Localized.ChatList.Traffic.negative + ) - private func highlightRateFor(page: CGFloat, offset: CGFloat) -> CGFloat { - let lowerBound = page - 1 - let upperBound = page + 1 - - if offset > lowerBound && offset < upperBound { - if (offset - lowerBound) > 1 { - return 1 - (offset - page) - } else { - return offset - lowerBound - } - } else { - return 0 - } + 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) } } diff --git a/Sources/SearchFeature/Controllers/SearchEmailController.swift b/Sources/SearchFeature/Controllers/SearchEmailController.swift deleted file mode 100644 index 1380e7f190f76d68068dd253f0a2bd5ac35359f3..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Controllers/SearchEmailController.swift +++ /dev/null @@ -1,9 +0,0 @@ -import UIKit - -final class SearchEmailController: UIViewController { - lazy private var screenView = SearchEmailView() - - override func loadView() { - view = screenView - } -} diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift new file mode 100644 index 0000000000000000000000000000000000000000..bbfabd2836990bb00c86e01a09527bdbac8b1145 --- /dev/null +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -0,0 +1,433 @@ +import HUD +import UIKit +import Shared +import Combine +import XXModels +import Defaults +import Countries +import DrawerFeature +import DependencyInjection + +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() + + private var dataSource: SearchDiffableDataSource! + private(set) var viewModel = SearchLeftViewModel() + private var drawerCancellables = Set<AnyCancellable>() + private let adrpURLString = "https://links.xx.network/adrp" + + private var cancellables = Set<AnyCancellable>() + private var hudCancellables = Set<AnyCancellable>() + + override func loadView() { + view = screenView + } + + override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + setupBindings() + } + + 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" + } + + 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 + } + } + + 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 + .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) + } + + 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: "#") + } + + 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) + } + + 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) + } + + 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) + } + +} + +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, 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 + } + + presentRequestDrawer(forContact: contact) + } +} diff --git a/Sources/SearchFeature/Controllers/SearchPhoneController.swift b/Sources/SearchFeature/Controllers/SearchPhoneController.swift deleted file mode 100644 index 63d6f56897223f6291aa360974d8c1cf3ac98856..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Controllers/SearchPhoneController.swift +++ /dev/null @@ -1,9 +0,0 @@ -import UIKit - -final class SearchPhoneController: UIViewController { - lazy private var screenView = SearchPhoneView() - - override func loadView() { - view = screenView - } -} diff --git a/Sources/SearchFeature/Controllers/SearchQRController.swift b/Sources/SearchFeature/Controllers/SearchQRController.swift deleted file mode 100644 index 04fe440f0ab5c3b3753f0758c2e58aa7edaa1432..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Controllers/SearchQRController.swift +++ /dev/null @@ -1,9 +0,0 @@ -import UIKit - -final class SearchQRController: UIViewController { - lazy private var screenView = SearchQRView() - - override func loadView() { - view = screenView - } -} diff --git a/Sources/SearchFeature/Controllers/SearchRightController.swift b/Sources/SearchFeature/Controllers/SearchRightController.swift new file mode 100644 index 0000000000000000000000000000000000000000..35240054497992ff2b5a277618321dceffeb652a --- /dev/null +++ b/Sources/SearchFeature/Controllers/SearchRightController.swift @@ -0,0 +1,81 @@ +import UIKit +import Combine +import DependencyInjection + +final class SearchRightController: UIViewController { + @Dependency var coordinator: SearchCoordinating + + lazy private var screenView = SearchRightView() + + private var cancellables = Set<AnyCancellable>() + private let cameraController = CameraController() + private(set) var viewModel = SearchRightViewModel() + + 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 viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.viewWillDisappear() + } + + private func setupBindings() { + 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.foundPublisher + .receive(on: DispatchQueue.main) + .delay(for: 1, scheduler: DispatchQueue.main) + .sink { [unowned self] in coordinator.toContact($0, from: self) } + .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) + } +} diff --git a/Sources/SearchFeature/Controllers/SearchTableController.swift b/Sources/SearchFeature/Controllers/SearchTableController.swift deleted file mode 100644 index 67459cffbf9a7e1f9cbd05ceab5fb788f4af9f3f..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Controllers/SearchTableController.swift +++ /dev/null @@ -1,55 +0,0 @@ -import UIKit -import Models -import Combine -import XXModels - -final class SearchTableController: UITableViewController { - private let viewModel: SearchViewModel - private var cancellables = Set<AnyCancellable>() - private(set) var dataSource = [Contact]() - - init(_ viewModel: SearchViewModel) { - self.viewModel = viewModel - super.init(style: .grouped) - } - - required init?(coder: NSCoder) { nil } - - override func viewDidLoad() { - super.viewDidLoad() - tableView.backgroundColor = .clear - tableView.separatorStyle = .none - tableView.register(SearchCell.self) - - viewModel.itemsRelay - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - dataSource = $0 - tableView.reloadData() - }.store(in: &cancellables) - } - - override func tableView( - _ tableView: UITableView, - cellForRowAt indexPath: IndexPath - ) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SearchCell.self) - let username = dataSource[indexPath.row].username! - - cell.setup( - title: username, - subtitle: username, - avatarTitle: username, - avatarImage: nil, - avatarSize: .large - ) - - return cell - } - - override func tableView( - _: UITableView, - numberOfRowsInSection: Int - ) -> Int { dataSource.count } -} - diff --git a/Sources/SearchFeature/Controllers/SearchUsernameController.swift b/Sources/SearchFeature/Controllers/SearchUsernameController.swift deleted file mode 100644 index 136951c882abe313099b5ced226a0d16fa497893..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Controllers/SearchUsernameController.swift +++ /dev/null @@ -1,9 +0,0 @@ -import UIKit - -final class SearchUsernameController: UIViewController { - lazy private var screenView = SearchUsernameView() - - override func loadView() { - view = screenView - } -} diff --git a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift index 2f66a6228fd96bb156266a80c16a9ac5ba53c5d9..21a9d7b3d5eb9b1cef283a410135157fb7571ef1 100644 --- a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift +++ b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift @@ -6,6 +6,8 @@ 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) @@ -15,21 +17,38 @@ public protocol SearchCoordinating { 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) diff --git a/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift b/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift new file mode 100644 index 0000000000000000000000000000000000000000..10d7bccc8c0f100b32cd34bf7a91312796ae9f15 --- /dev/null +++ b/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift @@ -0,0 +1,23 @@ +import UIKit +import XXModels + +enum SearchSection { + case stranger + case connections +} + +enum SearchItem: Equatable, Hashable { + case stranger(Contact) + case connection(Contact) +} + +class SearchDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch snapshot().sectionIdentifiers[section] { + case .stranger: + return "" + case .connections: + return "LOCAL RESULTS" + } + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..e308f6a5b21134224ec17155b9585a9718cf1204 --- /dev/null +++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift @@ -0,0 +1,58 @@ +import UIKit +import Combine +import Defaults +import Integration +import PushFeature +import DependencyInjection + +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 + } + } + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..30f77b191cbd9115f856d6133a2df3b2fa01fb0b --- /dev/null +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -0,0 +1,137 @@ +import HUD +import UIKit +import Shared +import Combine +import XXModels +import Countries +import Integration +import DependencyInjection + +typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> + +struct SearchLeftViewState { + var input = "" + var snapshot: SearchSnapshot? + var country: Country = .fromMyPhone() + var item: SearchSegmentedControl.Item = .username +} + +final class SearchLeftViewModel { + @Dependency var session: SessionType + + var hudPublisher: AnyPublisher<HUDStatus, Never> { + hudSubject.eraseToAnyPublisher() + } + + var successPublisher: AnyPublisher<Contact, Never> { + successSubject.eraseToAnyPublisher() + } + + var statePublisher: AnyPublisher<SearchLeftViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + 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()) + + 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() + hudSubject.send(.none) + } + + func didStartSearching() { + guard stateSubject.value.input.isEmpty == false else { return } + + hudSubject.send(.onAction(Localized.Ud.Search.cancel)) + + var content = stateSubject.value.input + let prefix = stateSubject.value.item.written.first!.uppercased() + + if stateSubject.value.item == .phone { + content += stateSubject.value.country.code + } + + 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) + } + + func didTapResend(contact: Contact) { + hudSubject.send(.on) + + do { + try self.session.retryRequest(contact) + hudSubject.send(.none) + } catch { + hudSubject.send(.error(.init(with: error))) + } + } + + 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))) + } + } + + 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) + } + } + + private func appendToLocalSearch(_ user: Contact?) { + var snapshot = SearchSnapshot() + + if var user = user { + if let contact = try? session.dbManager.fetchContacts(.init(id: [user.id])).first { + user.authStatus = contact.authStatus + } + + if user.authStatus != .friend { + snapshot.appendSections([.stranger]) + snapshot.appendItems([.stranger(user)], toSection: .stranger) + } + } + + let localsQuery = Contact.Query(text: stateSubject.value.input, authStatus: [.friend]) + + if let locals = try? session.dbManager.fetchContacts(localsQuery), locals.count > 0 { + let localsWithoutMe = locals.filter { $0.id != session.myId } + snapshot.appendSections([.connections]) + snapshot.appendItems(localsWithoutMe.map(SearchItem.connection), toSection: .connections) + } + + stateSubject.value.snapshot = snapshot + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..5cfe38db68c498a15522f745174e41415281ba17 --- /dev/null +++ b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift @@ -0,0 +1,111 @@ +import Shared +import Combine +import XXModels +import Foundation +import Permissions +import Integration +import DependencyInjection + +enum ScanningStatus: Equatable { + case reading + case processing + case success + case failed(ScanningError) +} + +enum ScanningError: Equatable { + case requestOpened + case unknown(String) + case cameraPermission + case alreadyFriends(String) +} + +final class SearchRightViewModel { + @Dependency var session: SessionType + @Dependency var permissions: PermissionHandling + + var foundPublisher: AnyPublisher<Contact, Never> { + foundSubject.eraseToAnyPublisher() + } + + var cameraSemaphorePublisher: AnyPublisher<Bool, Never> { + cameraSemaphoreSubject.eraseToAnyPublisher() + } + + var statusPublisher: AnyPublisher<ScanningStatus, Never> { + statusSubject.eraseToAnyPublisher() + } + + 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)) + } + } + } + + 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 { + /// 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() + )) + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchViewModel.swift b/Sources/SearchFeature/ViewModels/SearchViewModel.swift deleted file mode 100644 index 5dae23c8883eeccafb3a66ac2c3dd339d45d1094..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/ViewModels/SearchViewModel.swift +++ /dev/null @@ -1,190 +0,0 @@ -import HUD -import UIKit -import Models -import Combine -import Defaults -import XXModels -import Countries -import Foundation -import Integration -import PushFeature -import CombineSchedulers -import DependencyInjection - -enum SelectedFilter { - case username - case email - case phone - - var prefix: String { - switch self { - case .username: - return "U" - case .phone: - return "P" - case .email: - return "E" - } - } -} - -struct SearchViewState: Equatable { - var input: String = "" - var phoneInput: String = "" - var selectedFilter: SelectedFilter = .username - var country: Country = .fromMyPhone() -} - -final class SearchViewModel { - @KeyObject(.dummyTrafficOn, defaultValue: false) var isCoverTrafficEnabled: Bool - @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications - @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic: Bool - - @Dependency private var session: SessionType - @Dependency private var pushHandler: PushHandling - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var placeholderPublisher: AnyPublisher<Bool, Never> { - placeholderSubject.eraseToAnyPublisher() - } - - var coverTrafficPublisher: AnyPublisher<Void, Never> { - coverTrafficSubject.eraseToAnyPublisher() - } - - var statePublisher: AnyPublisher<SearchViewState, Never> { - stateSubject.eraseToAnyPublisher() - } - - var successPublisher: AnyPublisher<Contact, Never> { - successSubject.eraseToAnyPublisher() - } - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> - = DispatchQueue.global().eraseToAnyScheduler() - - let itemsRelay = CurrentValueSubject<[Contact], Never>([]) - private let successSubject = PassthroughSubject<Contact, Never>() - private let coverTrafficSubject = PassthroughSubject<Void, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let placeholderSubject = CurrentValueSubject<Bool, Never>(true) - private let stateSubject = CurrentValueSubject<SearchViewState, Never>(.init()) - - func didAppear() { - verifyCoverTraffic() - verifyNotifications() - } - - func didSelect(filter: SelectedFilter) { - stateSubject.value.selectedFilter = filter - } - - func didInput(_ string: String) { - stateSubject.value.input = string.trimmingCharacters(in: .whitespacesAndNewlines) - } - - func didInputPhone(_ string: String) { - stateSubject.value.phoneInput = string.trimmingCharacters(in: .whitespacesAndNewlines) - } - - func didChooseCountry(_ country: Country) { - stateSubject.value.country = country - } - - func didEnableCoverTraffic() { - isCoverTrafficEnabled = true - session.setDummyTraffic(status: true) - } - - func didTapSearch() { - hudSubject.send(.on(nil)) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - var content = self.stateSubject.value.selectedFilter.prefix - - if self.stateSubject.value.selectedFilter == .phone { - content += self.stateSubject.value.phoneInput + self.stateSubject.value.country.code - } else { - content += self.stateSubject.value.input - } - - try self.session.search(fact: content) { result in - self.placeholderSubject.send(false) - - switch result { - case .success(let searched): - self.hudSubject.send(.none) - self.itemsRelay.send([searched]) - case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) - self.itemsRelay.send([]) - } - } - } catch { - self.hudSubject.send(.error(.init(with: error))) - } - } - } - - 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 - } - } - } - - 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: Contact) { - hudSubject.send(.on(nil)) - var contact = contact - contact.nickname = contact.username - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.session.add(contact) - self.hudSubject.send(.none) - self.successSubject.send(contact) - } catch { - self.hudSubject.send(.error(.init(with: error))) - } - } - - } -} diff --git a/Sources/SearchFeature/Views/OverlayView.swift b/Sources/SearchFeature/Views/OverlayView.swift new file mode 100644 index 0000000000000000000000000000000000000000..8242857716936fe49cf21a59bad280195c726165 --- /dev/null +++ b/Sources/SearchFeature/Views/OverlayView.swift @@ -0,0 +1,181 @@ +import UIKit +import Shared + +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) + } + } + + required init?(coder: NSCoder) { nil } + + 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 + } + + 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 + ) + } +} diff --git a/Sources/SearchFeature/Views/SearchCell.swift b/Sources/SearchFeature/Views/SearchCell.swift deleted file mode 100644 index 08efd502958c8373949e09f83cc01101150a1ddd..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Views/SearchCell.swift +++ /dev/null @@ -1,82 +0,0 @@ -import UIKit -import Shared - -final class SearchCell: UITableViewCell { - private let titleLabel = UILabel() - private let subtitleLabel = UILabel() - private let separatorView = UIView() - private let avatarView = AvatarView() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = Asset.neutralWhite.color - - titleLabel.textColor = Asset.neutralActive.color - subtitleLabel.textColor = Asset.neutralDisabled.color - separatorView.backgroundColor = Asset.neutralLine.color - - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - subtitleLabel.font = Fonts.Mulish.regular.font(size: 12.0) - - contentView.addSubview(titleLabel) - contentView.addSubview(avatarView) - contentView.addSubview(subtitleLabel) - contentView.addSubview(separatorView) - - setupConstraints() - } - - required init?(coder: NSCoder) { nil } - - override func prepareForReuse() { - super.prepareForReuse() - titleLabel.text = nil - subtitleLabel.text = nil - avatarView.prepareForReuse() - } - - func setup( - title: String, - subtitle: String, - avatarTitle: String, - avatarImage: Data?, - avatarSize: AvatarView.Size - ) { - titleLabel.text = title - subtitleLabel.text = subtitle - avatarView.setupProfile( - title: avatarTitle, - image: avatarImage, - size: avatarSize - ) - } - - private func setupConstraints() { - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.left.equalTo(avatarView.snp.right).offset(16) - $0.right.lessThanOrEqualToSuperview().offset(-20) - } - - subtitleLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(3) - $0.left.equalTo(titleLabel) - $0.bottom.equalToSuperview().offset(-22) - } - - avatarView.snp.makeConstraints { - $0.left.equalToSuperview().offset(28) - $0.width.height.equalTo(48) - $0.bottom.equalToSuperview().offset(-16) - } - - separatorView.snp.makeConstraints { - $0.height.equalTo(1) - $0.left.equalToSuperview().offset(24) - $0.right.equalToSuperview().offset(-24) - $0.bottom.equalToSuperview() - } - } -} diff --git a/Sources/SearchFeature/Views/SearchEmailView.swift b/Sources/SearchFeature/Views/SearchEmailView.swift deleted file mode 100644 index 053c62592714b4822b94e063d3747d1a693b141f..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Views/SearchEmailView.swift +++ /dev/null @@ -1,28 +0,0 @@ -import UIKit -import Shared -import InputField - -final class SearchEmailView: UIView { - let inputField = InputField() - - init() { - super.init(frame: .zero) - - inputField.setup( - style: .regular, - title: "Email", - placeholder: "Email" - ) - - addSubview(inputField) - - inputField.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(15) - $0.right.equalToSuperview().offset(-15) - $0.bottom.lessThanOrEqualToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/SearchFeature/Views/SearchLeftEmptyView.swift b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift new file mode 100644 index 0000000000000000000000000000000000000000..84c64c87a6096a16b2bbf793a9eadc1c95495324 --- /dev/null +++ b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift @@ -0,0 +1,26 @@ +import UIKit +import Shared + +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) + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift new file mode 100644 index 0000000000000000000000000000000000000000..7742ff1d64c56151a88ae9b5ae47e82ebb59dc1b --- /dev/null +++ b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift @@ -0,0 +1,74 @@ +import UIKit +import Shared +import Combine + +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() + } + + 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() + } + } +} diff --git a/Sources/SearchFeature/Views/SearchLeftView.swift b/Sources/SearchFeature/Views/SearchLeftView.swift new file mode 100644 index 0000000000000000000000000000000000000000..bbdda3f926818439e4c28d5beb020239a623048a --- /dev/null +++ b/Sources/SearchFeature/Views/SearchLeftView.swift @@ -0,0 +1,71 @@ +import UIKit +import Shared + +final class SearchLeftView: UIView { + let tableView = UITableView() + let inputStackView = UIStackView() + let inputField = SearchComponent() + let emptyView = SearchLeftEmptyView() + let countryButton = SearchCountryComponent() + let placeholderView = SearchLeftPlaceholderView() + + init() { + super.init(frame: .zero) + + emptyView.isHidden = true + backgroundColor = Asset.neutralWhite.color + tableView.backgroundColor = Asset.neutralWhite.color + + inputStackView.spacing = 5 + inputStackView.addArrangedSubview(countryButton) + inputStackView.addArrangedSubview(inputField) + + addSubview(inputStackView) + addSubview(tableView) + addSubview(emptyView) + addSubview(placeholderView) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + func updateUIForItem(item: SearchSegmentedControl.Item) { + countryButton.isHidden = item != .phone + + 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) + } + + 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() + } + + 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() + } + } +} diff --git a/Sources/SearchFeature/Views/SearchPhoneView.swift b/Sources/SearchFeature/Views/SearchPhoneView.swift deleted file mode 100644 index 1868cec6960091bcf08f9294740d8121b1a41bca..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Views/SearchPhoneView.swift +++ /dev/null @@ -1,28 +0,0 @@ -import UIKit -import Shared -import InputField - -final class SearchPhoneView: UIView { - let inputField = InputField() - - init() { - super.init(frame: .zero) - - inputField.setup( - style: .regular, - title: "Phone", - placeholder: "Phone" - ) - - addSubview(inputField) - - inputField.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(15) - $0.right.equalToSuperview().offset(-15) - $0.bottom.lessThanOrEqualToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/SearchFeature/Views/SearchPlaceholderView.swift b/Sources/SearchFeature/Views/SearchPlaceholderView.swift deleted file mode 100644 index 84ae459286df757555853e8fd4eb7369c323d476..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Views/SearchPlaceholderView.swift +++ /dev/null @@ -1,65 +0,0 @@ -import UIKit -import Shared - -final class SearchPlaceholderView: UIView { - let titleView = TextWithInfoView() - let didTapInfo: () -> Void - - init(didTapInfo: @escaping () -> Void) { - self.didTapInfo = didTapInfo - - super.init(frame: .zero) - - let paragraph = NSMutableParagraphStyle() - paragraph.lineSpacing = 5 - paragraph.lineHeightMultiple = 1.0 - paragraph.alignment = .center - - titleView.setup( - text: "Your searches are anonymous.\nSearch information is never linked to your account or personally identifiable.", - attributes: [ - .foregroundColor: Asset.neutralBody.color, - .font: Fonts.Mulish.regular.font(size: 16.0) as Any, - .paragraphStyle: paragraph - ], - didTapInfo: { didTapInfo() } - ) - - addSubview(titleView) - - titleView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(60) - make.left.equalToSuperview().offset(60) - make.right.equalToSuperview().offset(-60) - } - } - - required init?(coder: NSCoder) { nil } -} - -final class SearchEmptyView: UIView { - private let title = UILabel() - - init() { - super.init(frame: .zero) - - backgroundColor = Asset.neutralWhite.color - - title.textColor = Asset.neutralBody.color - title.font = Fonts.Mulish.regular.font(size: 12.0) - - addSubview(title) - - title.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.left.equalToSuperview().offset(30) - make.right.equalToSuperview().offset(-30) - } - } - - required init?(coder: NSCoder) { nil } - - func set(filter: String) { - title.text = Localized.Ud.noneFound(filter) - } -} diff --git a/Sources/SearchFeature/Views/SearchQRView.swift b/Sources/SearchFeature/Views/SearchQRView.swift deleted file mode 100644 index 4adae04506dd7f525df2563018aeedb3dba37b10..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Views/SearchQRView.swift +++ /dev/null @@ -1,28 +0,0 @@ -import UIKit -import Shared -import InputField - -final class SearchQRView: UIView { - let inputField = InputField() - - init() { - super.init(frame: .zero) - - inputField.setup( - style: .regular, - title: "QR", - placeholder: "QR" - ) - - addSubview(inputField) - - inputField.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(15) - $0.right.equalToSuperview().offset(-15) - $0.bottom.lessThanOrEqualToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/SearchFeature/Views/SearchRightView.swift b/Sources/SearchFeature/Views/SearchRightView.swift new file mode 100644 index 0000000000000000000000000000000000000000..363808754852969a75c3c811836faa9b40455034 --- /dev/null +++ b/Sources/SearchFeature/Views/SearchRightView.swift @@ -0,0 +1,116 @@ +import UIKit +import Shared + +final class SearchRightView: UIView { + let statusLabel = UILabel() + let imageView = UIImageView() + let stackView = UIStackView() + let overlayView = OverlayView() + let animationView = DotAnimation() + let 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) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + func update(status: ScanningStatus) { + var text: String + + switch status { + 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.friends(name) + actionButton.setTitle(Localized.Scan.contact, for: .normal) + actionButton.isHidden = false + + case .cameraPermission: + text = Localized.Scan.Error.denied + 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 + } + + private func setupConstraints() { + 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) + } + } +} diff --git a/Sources/SearchFeature/Views/SearchSegmentedButton.swift b/Sources/SearchFeature/Views/SearchSegmentedButton.swift index bc4c0f283059e19f3941731dea73f0f907c2dfcb..3b8e65fb1b778703a748626155d6f2b0b18ec94b 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedButton.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedButton.swift @@ -12,7 +12,6 @@ final class SearchSegmentedButton: UIControl { imageView.contentMode = .center titleLabel.textAlignment = .center - titleLabel.textColor = Asset.neutralWhite.color titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0) addSubview(titleLabel) @@ -23,27 +22,16 @@ final class SearchSegmentedButton: UIControl { required init?(coder: NSCoder) { nil } - func setup( - title: String, - icon: UIImage, - iconColor: UIColor = Asset.neutralDisabled.color, - titleColor: UIColor = Asset.neutralDisabled.color - ) { - self.imageView.image = icon - self.titleLabel.text = title - self.imageView.tintColor = iconColor - self.titleLabel.textColor = titleColor + func setup(title: String, icon: UIImage) { + imageView.image = icon + titleLabel.text = title + imageView.tintColor = discreteColor + titleLabel.textColor = discreteColor } - func updateHighlighting(rate: CGFloat) { - let color = UIColor.fade( - from: discreteColor, - to: highlightColor, - pcent: rate - ) - - imageView.tintColor = color - titleLabel.textColor = color + func setSelected(_ bool: Bool) { + imageView.tintColor = bool ? highlightColor : discreteColor + titleLabel.textColor = bool ? highlightColor : discreteColor } private func setupConstraints() { diff --git a/Sources/SearchFeature/Views/SearchSegmentedControl.swift b/Sources/SearchFeature/Views/SearchSegmentedControl.swift index 212c78e6900ab3a4735d86e124b2970f905ffc79..6141360378cd659e89dbdf67c4180f4a0e9b4ce6 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedControl.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedControl.swift @@ -9,39 +9,42 @@ final class SearchSegmentedControl: UIView { case email case phone case qr + + var written: String { + switch self { + case .qr: return "qr" + case .email: return "email" + case .phone: return "phone number" + case .username: return "username" + } + } } private let trackView = UIView() private let stackView = UIStackView() + private var leftConstraint: Constraint? private let trackIndicatorView = UIView() - private(set) var leftConstraint: Constraint? - private(set) var usernameButton = SearchSegmentedButton() - private(set) var emailButton = SearchSegmentedButton() - private(set) var phoneButton = SearchSegmentedButton() - private(set) var qrCodeButton = SearchSegmentedButton() + private let emailButton = SearchSegmentedButton() + private let phoneButton = SearchSegmentedButton() + private let qrCodeButton = SearchSegmentedButton() + private let usernameButton = SearchSegmentedButton() var actionPublisher: AnyPublisher<Item, Never> { actionSubject.eraseToAnyPublisher() } private var cancellables = Set<AnyCancellable>() - private let actionSubject = PassthroughSubject<Item, Never>() + private let actionSubject = CurrentValueSubject<Item, Never>(.username) init() { super.init(frame: .zero) trackView.backgroundColor = Asset.neutralLine.color trackIndicatorView.backgroundColor = Asset.brandPrimary.color - usernameButton.setup( - title: Localized.Ud.Tab.username, - icon: Asset.searchTabUsername.image, - iconColor: Asset.brandPrimary.color, - titleColor: Asset.brandPrimary.color - ) - qrCodeButton.setup(title: Localized.Ud.Tab.qr, icon: Asset.searchTabQr.image) emailButton.setup(title: Localized.Ud.Tab.email, icon: Asset.searchTabEmail.image) phoneButton.setup(title: Localized.Ud.Tab.phone, icon: Asset.searchTabPhone.image) + usernameButton.setup(title: Localized.Ud.Tab.username, icon: Asset.searchTabUsername.image) stackView.distribution = .fillEqually stackView.addArrangedSubview(usernameButton) @@ -61,25 +64,38 @@ final class SearchSegmentedControl: UIView { required init?(coder: NSCoder) { nil } private func setupBindings() { - usernameButton - .publisher(for: .touchUpInside) + usernameButton.publisher(for: .touchUpInside) .sink { [unowned self] in actionSubject.send(.username) } .store(in: &cancellables) - emailButton - .publisher(for: .touchUpInside) + emailButton.publisher(for: .touchUpInside) .sink { [unowned self] in actionSubject.send(.email) } .store(in: &cancellables) - phoneButton - .publisher(for: .touchUpInside) + phoneButton.publisher(for: .touchUpInside) .sink { [unowned self] in actionSubject.send(.phone) } .store(in: &cancellables) - qrCodeButton - .publisher(for: .touchUpInside) + qrCodeButton.publisher(for: .touchUpInside) .sink { [unowned self] in actionSubject.send(.qr) } .store(in: &cancellables) + + actionSubject + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + let tabWidth = bounds.width / 4 + if let leftConstraint = leftConstraint { + leftConstraint.update(offset: tabWidth * CGFloat($0.rawValue)) + setNeedsLayout() + UIView.animate(withDuration: 0.25) { self.layoutIfNeeded() } + } + + qrCodeButton.setSelected($0 == .qr) + emailButton.setSelected($0 == .email) + phoneButton.setSelected($0 == .phone) + usernameButton.setSelected($0 == .username) + }.store(in: &cancellables) } private func setupConstraints() { diff --git a/Sources/SearchFeature/Views/SearchUsernamePlaceholderView.swift b/Sources/SearchFeature/Views/SearchUsernamePlaceholderView.swift deleted file mode 100644 index effcf60783cbbf8dfa1a296b7171d522ad82a52a..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Views/SearchUsernamePlaceholderView.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit -import Shared - -final class SearchUsernamePlaceholderView: UIView { - let titleLabel = UILabel() - - init() { - super.init(frame: .zero) - - titleLabel.text = "[SearchUsernamePlaceholderView]" - - addSubview(titleLabel) - - titleLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/SearchFeature/Views/SearchUsernameView.swift b/Sources/SearchFeature/Views/SearchUsernameView.swift deleted file mode 100644 index 2dd66fcda8c4a775a72f5ba9dc5986c441d765fd..0000000000000000000000000000000000000000 --- a/Sources/SearchFeature/Views/SearchUsernameView.swift +++ /dev/null @@ -1,36 +0,0 @@ -import UIKit -import Shared -import InputField - -final class SearchUsernameView: UIView { - let inputField = InputField() - let placeholderView = SearchUsernamePlaceholderView() - - init() { - super.init(frame: .zero) - - inputField.setup( - style: .regular, - title: "Username", - placeholder: "Username" - ) - - addSubview(inputField) - addSubview(placeholderView) - - inputField.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) - $0.left.equalToSuperview().offset(15) - $0.right.equalToSuperview().offset(-15) - } - - placeholderView.snp.makeConstraints { - $0.top.equalTo(inputField.snp.bottom) - $0.left.equalToSuperview() - $0.right.equalToSuperview() - $0.bottom.equalToSuperview() - } - } - - required init?(coder: NSCoder) { nil } -} diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift index 7eae36405a3bf673064d10fd743b648d99d0fc28..1dc36e2ef958cc0c7fba6177e621915d96cfc4cc 100644 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift @@ -10,7 +10,7 @@ import DependencyInjection public final class AccountDeleteController: UIViewController { @KeyObject(.username, defaultValue: "") var username: String - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: SettingsCoordinating lazy private var screenView = AccountDeleteView() diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift index e12674906cde540f35bbff5741f3ec5e816b4507..52e78e17a42c3933f671917aca89b186362f349a 100644 --- a/Sources/SettingsFeature/Controllers/SettingsController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsController.swift @@ -8,7 +8,7 @@ import DependencyInjection import ScrollViewController public final class SettingsController: UIViewController { - @Dependency private var hud: HUDType + @Dependency private var hud: HUD @Dependency private var coordinator: SettingsCoordinating @Dependency private var statusBarController: StatusBarStyleControlling diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift index 0547bbbce7fd175563e525276af29f27c2a2df6c..db7e64016a317b8fe74fa496102437c771f500db 100644 --- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift @@ -17,7 +17,7 @@ final class AccountDeleteViewModel { deleting = true DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.on(nil)) + self?.hudRelay.send(.on) } do { diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index 97c146a6aa7645ef12298e8bcc2dae2edf2072e2..9b985922329599ac17010bdd2d16c43dec81a454 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -110,7 +110,7 @@ final class SettingsViewModel { } private func pushNotifications(enable: Bool) { - hudRelay.send(.on(nil)) + hudRelay.send(.on) if enable == true { pushHandler.requestAuthorization { [weak self] result in diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index 9f9fbfb27622937b8f189ffb9327c60ea9a681d0..ed861800297b7c53e92a0e83f19aa322de123ec4 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -218,7 +218,7 @@ public enum Localized { public static let login = Localized.tr("Localizable", "accountRestore.sftp.login") /// Password public static let password = Localized.tr("Localizable", "accountRestore.sftp.password") - /// Login to your server. Your credentials will be automatically and securley saved locally on your device. + /// Login to your server. Your credentials will be automatically and securely saved locally on your device. public static let subtitle = Localized.tr("Localizable", "accountRestore.sftp.subtitle") /// Login to your SFTP public static let title = Localized.tr("Localizable", "accountRestore.sftp.title") @@ -430,6 +430,10 @@ public enum Localized { /// Cancel public static let cancel = Localized.tr("Localizable", "chatList.navigationBar.cancel") } + public enum Search { + /// Search chats + public static let title = Localized.tr("Localizable", "chatList.search.title") + } public enum Traffic { /// Not now public static let negative = Localized.tr("Localizable", "chatList.traffic.negative") @@ -961,6 +965,14 @@ public enum Localized { public static func resent(_ p1: Any) -> String { return Localized.tr("Localizable", "requests.sent.toast.resent", String(describing: p1)) } + /// Request couldn't be resent to %@ + public static func resentFailed(_ p1: Any) -> String { + return Localized.tr("Localizable", "requests.sent.toast.resentFailed", String(describing: p1)) + } + /// Request successfully sent to %@ + public static func sent(_ p1: Any) -> String { + return Localized.tr("Localizable", "requests.sent.toast.sent", String(describing: p1)) + } } } } @@ -1201,8 +1213,6 @@ public enum Localized { public static func noneFound(_ p1: Any) -> String { return Localized.tr("Localizable", "ud.noneFound", String(describing: p1)) } - /// User - public static let sectionTitle = Localized.tr("Localizable", "ud.sectionTitle") /// Search public static let title = Localized.tr("Localizable", "ud.title") public enum NicknameDrawer { @@ -1214,8 +1224,6 @@ public enum Localized { public static let title = Localized.tr("Localizable", "ud.nicknameDrawer.title") } public enum Placeholder { - /// Searching is private by nature. The network cannot identify who a search request came from. - public static let title = Localized.tr("Localizable", "ud.placeholder.title") public enum Drawer { /// Got it public static let action = Localized.tr("Localizable", "ud.placeholder.drawer.action") @@ -1237,6 +1245,24 @@ public enum Localized { /// Request Contact public static let title = Localized.tr("Localizable", "ud.requestDrawer.title") } + public enum Search { + /// Cancel search + public static let cancel = Localized.tr("Localizable", "ud.search.cancel") + /// There are no users with that %@. + public static func empty(_ p1: Any) -> String { + return Localized.tr("Localizable", "ud.search.empty", String(describing: p1)) + } + /// Search by %@ + public static func input(_ p1: Any) -> String { + return Localized.tr("Localizable", "ud.search.input", String(describing: p1)) + } + public enum Placeholder { + /// Your searches are anonymous. Search information is never linked to your account or personally identifiable. + public static let subtitle = Localized.tr("Localizable", "ud.search.placeholder.subtitle") + /// Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel. + public static let title = Localized.tr("Localizable", "ud.search.placeholder.title") + } + } public enum Tab { /// Email public static let email = Localized.tr("Localizable", "ud.tab.email") diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 3aa07cedf9576e85634d4d61ec701efcee5229c5..dd95d4002c125c9caa98007bf7074c172bf5aaf8 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -25,6 +25,8 @@ // ChatListFeature +"chatList.search.title" += "Search chats"; "chatList.navigationBar.cancel" = "Cancel"; "chatList.title" @@ -351,8 +353,12 @@ = "Search for connections"; "requests.sent.empty" = "You haven't sent any requests"; +"requests.sent.toast.sent" += "Request successfully sent to %@"; "requests.sent.toast.resent" = "Request successfully resent to %@"; +"requests.sent.toast.resentFailed" += "Request couldn't be resent to %@"; // RequestsFeature - Failed @@ -948,24 +954,20 @@ "ud.title" = "Search"; -"ud.placeholder.title" -= "Searching is private by nature. The network cannot identify who a search request came from."; +"ud.tab.username" += "Username"; +"ud.tab.email" += "Email"; +"ud.tab.phone" += "Phone"; +"ud.tab.qr" += "QR Code"; "ud.placeholder.drawer.title" = "Search"; "ud.placeholder.drawer.subtitle" = "You can search for users by their username, email, or phone number using the xx network’s #Anonymous Data Retrieval protocol# which keeps a user’s identity anonymous while requesting data. All sent requests contain salted hashes of what you are searching for. Raw data on emails, usernames, and phone numbers do not leave your phone."; "ud.placeholder.drawer.action" = "Got it"; -"ud.tab.phone" -= "Phone"; -"ud.tab.email" -= "Email"; -"ud.tab.username" -= "Username"; -"ud.tab.qr" -= "QR Code"; -"ud.sectionTitle" -= "User"; "ud.noneFound" = "There are no users with that %@."; "ud.requestDrawer.title" @@ -985,6 +987,18 @@ "ud.nicknameDrawer.save" = "Save"; +"ud.search.input" += "Search by %@"; +"ud.search.empty" += "There are no users with that %@."; +"ud.search.cancel" += "Cancel search"; + +"ud.search.placeholder.title" += "Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel."; +"ud.search.placeholder.subtitle" += "Your searches are anonymous. Search information is never linked to your account or personally identifiable."; + // LaunchFeature "launch.version.failed" diff --git a/Sources/Shared/Views/AvatarCell.swift b/Sources/Shared/Views/AvatarCell.swift new file mode 100644 index 0000000000000000000000000000000000000000..43f0e3fd6686464b6d66c31770627134e9c075c0 --- /dev/null +++ b/Sources/Shared/Views/AvatarCell.swift @@ -0,0 +1,187 @@ +import UIKit +import Combine + +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() + } + } + + 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() + } + + 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 + } + + 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) + } + } + + 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/SearchComponent.swift b/Sources/Shared/Views/SearchComponent.swift index 2e87cfbf6dceddcb50009a78b41fd2c0aa598ea5..9608aad71aaa56a5da852516405d069837a703c5 100644 --- a/Sources/Shared/Views/SearchComponent.swift +++ b/Sources/Shared/Views/SearchComponent.swift @@ -15,6 +15,10 @@ public final class SearchComponent: UIView { textSubject.eraseToAnyPublisher() } + public var returnPublisher: AnyPublisher<Void, Never> { + returnSubject.eraseToAnyPublisher() + } + private var rightImage = Asset.sharedScan.image { didSet { rightButton.setImage(rightImage, for: .normal) @@ -26,13 +30,59 @@ public final class SearchComponent: UIView { } private var cancellables = Set<AnyCancellable>() - private var rightSubject = PassthroughSubject<Void, Never>() - private var textSubject = PassthroughSubject<String, Never>() - private var isEditingSubject = CurrentValueSubject<Bool, Never>(false) + 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) - setup() + + 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.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() } required init?(coder: NSCoder) { nil } @@ -81,55 +131,6 @@ public final class SearchComponent: UIView { isEditingSubject.send(false) } - private func setup() { - 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.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() - setupAccessibility() - } - private func setupConstraints() { containerView.snp.makeConstraints { $0.top.equalToSuperview() @@ -159,16 +160,17 @@ public final class SearchComponent: UIView { } } - private func setupAccessibility() { - inputField.accessibilityIdentifier = Localized.Accessibility.Shared.Search.textField - rightButton.accessibilityIdentifier = Localized.Accessibility.Shared.Search.rightButton - } - 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) diff --git a/Sources/Shared/Views/SearchCountryComponent.swift b/Sources/Shared/Views/SearchCountryComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..186c58b95981ce45a799c93e27c8a53530b16cf4 --- /dev/null +++ b/Sources/Shared/Views/SearchCountryComponent.swift @@ -0,0 +1,57 @@ +import UIKit + +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) + } + + 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) + } + + 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() + } + } +} diff --git a/Sources/Shared/Views/SmallAvatarAndTitleCell.swift b/Sources/Shared/Views/SmallAvatarAndTitleCell.swift deleted file mode 100644 index 4c37a96467729da2f4234ad646287a6127455f63..0000000000000000000000000000000000000000 --- a/Sources/Shared/Views/SmallAvatarAndTitleCell.swift +++ /dev/null @@ -1,50 +0,0 @@ -import UIKit - -public final class SmallAvatarAndTitleCell: UITableViewCell { - let separatorView = UIView() - public let titleLabel = UILabel() - public let avatarView = AvatarView() - - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectedBackgroundView = UIView() - multipleSelectionBackgroundView = UIView() - backgroundColor = Asset.neutralWhite.color - - titleLabel.textColor = Asset.neutralActive.color - titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0) - separatorView.backgroundColor = Asset.neutralLine.color - - contentView.addSubview(titleLabel) - contentView.addSubview(avatarView) - contentView.addSubview(separatorView) - - avatarView.snp.makeConstraints { - $0.width.height.equalTo(36) - $0.left.equalToSuperview().offset(27) - $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() - } - } - - required init?(coder: NSCoder) { nil } - - public override func prepareForReuse() { - super.prepareForReuse() - titleLabel.text = nil - avatarView.prepareForReuse() - } -} diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index f9de6930fb00ffc8a1befdc355e207f45741c671..0f086b282d6502bb27ba0b20989c093d0be98ab2 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://git.xx.network/elixxir/client-ios-db.git", "state" : { - "revision" : "adf3c4b906870ecbd0d1d7208f0666939fd08665", - "version" : "1.0.5" + "revision" : "785e1f653ee5eaaaf58a82c8abbcda2174fbc27a", + "version" : "1.0.8" } }, {