diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index efc85dcfbe323423488bae475e541399d2fd300a..ea6c89d1406aac9bb22c7e5447b499e2dfd48ace 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -6,27 +6,6 @@ import XXModels import DrawerFeature import DependencyInjection -enum SearchSection { - case stranger - case connections -} - -enum SearchItem: Equatable, Hashable { - case stranger(Contact) - case connection(Contact) -} - -class SearchTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch snapshot().sectionIdentifiers[section] { - case .stranger: - return "" - case .connections: - return "CONNECTIONS" - } - } -} - public final class SearchContainerController: UIViewController { @Dependency var coordinator: SearchCoordinating @Dependency var statusBarController: StatusBarStyleControlling @@ -89,6 +68,7 @@ public final class SearchContainerController: UIViewController { screenView.scrollView.setContentOffset(point, animated: true) } else { screenView.scrollView.setContentOffset(.zero, animated: true) + leftController.viewModel.didSelectItem($0) } }.store(in: &cancellables) diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index 35014bc256329b4d6f00b562e9841540681e2304..b172eb74050a519c6aef1845d18067b3bc67e07f 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -19,11 +19,11 @@ final class SearchLeftController: UIViewController { lazy private var screenView = SearchLeftView() - private let viewModel = SearchLeftViewModel() private var cancellables = Set<AnyCancellable>() + private var dataSource: SearchDiffableDataSource! + private(set) var viewModel = SearchLeftViewModel() private var drawerCancellables = Set<AnyCancellable>() private let adrpURLString = "https://links.xx.network/adrp" - private var dataSource: SearchTableViewDiffableDataSource! override func loadView() { view = screenView @@ -42,7 +42,7 @@ final class SearchLeftController: UIViewController { screenView.tableView.dataSource = dataSource screenView.tableView.delegate = self - dataSource = SearchTableViewDiffableDataSource( + dataSource = SearchDiffableDataSource( tableView: screenView.tableView ) { tableView, indexPath, item in let contact: Contact @@ -75,6 +75,13 @@ final class SearchLeftController: UIViewController { .sink { [hud] in hud.update(with: $0) } .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 .compactMap(\.snapshot) .receive(on: DispatchQueue.main) diff --git a/Sources/SearchFeature/Controllers/SearchRightController.swift b/Sources/SearchFeature/Controllers/SearchRightController.swift index b8468f11590254ed24132a3223a53e80cd8e3868..7aa7dd6ad19821e28c19247dc419715baab9e35d 100644 --- a/Sources/SearchFeature/Controllers/SearchRightController.swift +++ b/Sources/SearchFeature/Controllers/SearchRightController.swift @@ -1,27 +1,10 @@ import UIKit -import Permissions import DependencyInjection -enum SearchQRStatus: Equatable { - case reading - case processing - case success - case failed(SearchQRError) -} - -enum SearchQRError: Equatable { - case requestOpened - case unknown(String) - case cameraPermission - case alreadyFriends(String) -} - final class SearchRightController: UIViewController { - @Dependency private var permissions: PermissionHandling - lazy private var screenView = SearchRightView() - private let camera = Camera() +// private let camera = Camera() private var status: SearchQRStatus? override func loadView() { @@ -54,111 +37,20 @@ final class SearchRightController: UIViewController { } private func startCamera() { - permissions.requestCamera { [weak self] granted in - guard let self = self else { return } - - if granted { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.camera.start() - } - } else { - DispatchQueue.main.async { - self.status = .failed(.cameraPermission) -// self.screenView.update(with: .failed(.cameraPermission)) - } - } - } - } -} - - -import Combine -import AVFoundation - -protocol CameraType { - func start() - func stop() - - var previewLayer: CALayer { get } - var dataPublisher: AnyPublisher<Data, Never> { get } -} - -final class Camera: NSObject, CameraType { - 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] - } - } -} - -extension Camera: AVCaptureMetadataOutputObjectsDelegate { - 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) - } -} - -final class MockCamera: NSObject, CameraType { - private let dataSubject = PassthroughSubject<Data, Never>() - - func start() { - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in - self?.dataSubject.send("###".data(using: .utf8)!) - } - } - - func stop() {} - - var previewLayer: CALayer { CALayer() } - - var dataPublisher: AnyPublisher<Data, Never> { - dataSubject - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() +// permissions.requestCamera { [weak self] granted in +// guard let self = self else { return } +// +// if granted { +// DispatchQueue.main.async { [weak self] in +// guard let self = self else { return } +// self.camera.start() +// } +// } else { +// DispatchQueue.main.async { +// self.status = .failed(.cameraPermission) +//// self.screenView.update(with: .failed(.cameraPermission)) +// } +// } +// } } } diff --git a/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift b/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift new file mode 100644 index 0000000000000000000000000000000000000000..4fef0dfacc14889b3074da1635bb119bde721a11 --- /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 "CONNECTIONS" + } + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index effe59c7a456d6291b458a973979364232e5ed64..1607e4ec81e6b103ef630e02ed7277a1c00b7ddf 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -10,6 +10,7 @@ typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchIte struct SearchLeftViewState { var input = "" var snapshot: SearchSnapshot? + var item: SearchSegmentedControl.Item = .username } final class SearchLeftViewModel { @@ -35,11 +36,17 @@ final class SearchLeftViewModel { stateSubject.value.input = string } + func didSelectItem(_ item: SearchSegmentedControl.Item) { + stateSubject.value.item = item + } + func didStartSearching() { hudSubject.send(.on(nil)) + let prefix = stateSubject.value.item.written.first!.uppercased() + do { - try session.search(fact: "U\(stateSubject.value.input)") { [weak self] in + try session.search(fact: "\(prefix)\(stateSubject.value.input)") { [weak self] in guard let self = self else { return } switch $0 { diff --git a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..663fb5db6d751097dba8843a7400484bcbc874db --- /dev/null +++ b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift @@ -0,0 +1,112 @@ +import Permissions +import DependencyInjection + +enum SearchQRStatus: Equatable { + case reading + case processing + case success + case failed(SearchQRError) +} + +enum SearchQRError: Equatable { + case requestOpened + case unknown(String) + case cameraPermission + case alreadyFriends(String) +} + +final class SearchRightViewModel { + @Dependency private var permissions: PermissionHandling +} + +// +// +//import Combine +//import AVFoundation +// +//protocol CameraType { +// func start() +// func stop() +// +// var previewLayer: CALayer { get } +// var dataPublisher: AnyPublisher<Data, Never> { get } +//} +// +//final class Camera: NSObject, CameraType { +// 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] +// } +// } +//} +// +//extension Camera: AVCaptureMetadataOutputObjectsDelegate { +// 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) +// } +//} +// +//final class MockCamera: NSObject, CameraType { +// private let dataSubject = PassthroughSubject<Data, Never>() +// +// func start() { +// DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in +// self?.dataSubject.send("###".data(using: .utf8)!) +// } +// } +// +// func stop() {} +// +// var previewLayer: CALayer { CALayer() } +// +// var dataPublisher: AnyPublisher<Data, Never> { +// dataSubject +// .receive(on: DispatchQueue.main) +// .eraseToAnyPublisher() +// } +//} diff --git a/Sources/SearchFeature/Views/SearchLeftEmptyView.swift b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift new file mode 100644 index 0000000000000000000000000000000000000000..f52174b7eb9ad2665317e0e7268bf0e715c28142 --- /dev/null +++ b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift @@ -0,0 +1,25 @@ +import UIKit +import Shared + +final class SearchLeftEmptyView: UIView { + let titleLabel = UILabel() + + init() { + super.init(frame: .zero) + + 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 index 5f6264be460e2923becf751692bc219f27e07210..b0fe2d60e56c0aee24849d5f225cba9ca77c427f 100644 --- a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift +++ b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift @@ -16,7 +16,7 @@ final class SearchLeftPlaceholderView: UIView { super.init(frame: .zero) let attrString = NSMutableAttributedString( - string: Localized.Ud.Search.Username.Placeholder.title, + string: Localized.Ud.Search.Placeholder.title, attributes: [ .foregroundColor: Asset.neutralDark.color, .font: Fonts.Mulish.bold.font(size: 32.0) @@ -36,7 +36,7 @@ final class SearchLeftPlaceholderView: UIView { paragraph.lineHeightMultiple = 1.3 subtitleWithInfo.setup( - text: Localized.Ud.Search.Username.Placeholder.subtitle, + text: Localized.Ud.Search.Placeholder.subtitle, attributes: [ .paragraphStyle: paragraph, .foregroundColor: Asset.neutralBody.color, diff --git a/Sources/SearchFeature/Views/SearchLeftView.swift b/Sources/SearchFeature/Views/SearchLeftView.swift index f9df64ab10c7ade03d93fe58c3e47acd6eac43cf..d002596536f519c5c3a3febca984af7727607cae 100644 --- a/Sources/SearchFeature/Views/SearchLeftView.swift +++ b/Sources/SearchFeature/Views/SearchLeftView.swift @@ -4,36 +4,11 @@ import Shared final class SearchLeftView: UIView { let tableView = UITableView() let inputField = SearchComponent() - let emptyView: UIView = { - let view = UIView() - let label = UILabel() - - label.numberOfLines = 0 - label.textAlignment = .center - label.font = Fonts.Mulish.regular.font(size: 15.0) - label.text = Localized.Ud.Search.Username.Empty.title - label.textColor = Asset.neutralSecondaryAlternative.color - - view.addSubview(label) - label.snp.makeConstraints { - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - } - - return view - }() - + let emptyView = SearchLeftEmptyView() let placeholderView = SearchLeftPlaceholderView() init() { super.init(frame: .zero) - - inputField.set( - placeholder: Localized.Ud.Search.Username.input, - imageAtRight: nil - ) - emptyView.isHidden = true addSubview(tableView) @@ -46,6 +21,14 @@ final class SearchLeftView: UIView { required init?(coder: NSCoder) { nil } + func updateUIForItem(item: SearchSegmentedControl.Item) { + 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() { inputField.snp.makeConstraints { $0.top.equalToSuperview().offset(20) diff --git a/Sources/SearchFeature/Views/SearchSegmentedControl.swift b/Sources/SearchFeature/Views/SearchSegmentedControl.swift index 05eabb36e0b8b1794d351ca150611029bb3643de..6141360378cd659e89dbdf67c4180f4a0e9b4ce6 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedControl.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedControl.swift @@ -9,6 +9,15 @@ 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() diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index ada5805cb79cece20fc7c23c6a6d435a8b0e319b..6e3ada55daa0f34b8285e1cd708d93a70f17e32f 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -1238,27 +1238,19 @@ public enum Localized { public static let title = Localized.tr("Localizable", "ud.requestDrawer.title") } public enum Search { - public enum Email { - /// Search by email - public static let input = Localized.tr("Localizable", "ud.search.email.input") + /// There are no users with that %@. + public static func empty(_ p1: Any) -> String { + return Localized.tr("Localizable", "ud.search.empty", String(describing: p1)) } - public enum Phone { - /// Search by phone number - public static let input = Localized.tr("Localizable", "ud.search.phone.input") + /// Search by %@ + public static func input(_ p1: Any) -> String { + return Localized.tr("Localizable", "ud.search.input", String(describing: p1)) } - public enum Username { - /// Search by username - public static let input = Localized.tr("Localizable", "ud.search.username.input") - public enum Empty { - /// There are no users with that username - public static let title = Localized.tr("Localizable", "ud.search.username.empty.title") - } - 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.username.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.username.placeholder.title") - } + 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 { diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 0668a396144a736f712d0c79f0986dba83a7a74e..c50b6c120e8a8cbd10b128fbbfff708683510bfc 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -982,19 +982,15 @@ = "Edit your new contact’s nickname so you know who they are."; "ud.nicknameDrawer.save" = "Save"; -"ud.search.username.input" -= "Search by username"; -"ud.search.email.input" -= "Search by email"; -"ud.search.phone.input" -= "Search by phone number"; -"ud.search.username.empty.title" -= "There are no users with that username"; +"ud.search.input" += "Search by %@"; +"ud.search.empty" += "There are no users with that %@."; -"ud.search.username.placeholder.title" +"ud.search.placeholder.title" = "Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel."; -"ud.search.username.placeholder.subtitle" +"ud.search.placeholder.subtitle" = "Your searches are anonymous. Search information is never linked to your account or personally identifiable."; // LaunchFeature