Skip to content
Snippets Groups Projects
Commit c4965691 authored by Bruno Muniz's avatar Bruno Muniz :apple:
Browse files

Finished input search logic, missing phone ui component and qr

parent 4ca61015
No related branches found
No related tags found
2 merge requests!54Releasing 1.1.4,!50Search UI 2.0
Showing
with 232 additions and 206 deletions
...@@ -6,27 +6,6 @@ import XXModels ...@@ -6,27 +6,6 @@ import XXModels
import DrawerFeature import DrawerFeature
import DependencyInjection 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 { public final class SearchContainerController: UIViewController {
@Dependency var coordinator: SearchCoordinating @Dependency var coordinator: SearchCoordinating
@Dependency var statusBarController: StatusBarStyleControlling @Dependency var statusBarController: StatusBarStyleControlling
...@@ -89,6 +68,7 @@ public final class SearchContainerController: UIViewController { ...@@ -89,6 +68,7 @@ public final class SearchContainerController: UIViewController {
screenView.scrollView.setContentOffset(point, animated: true) screenView.scrollView.setContentOffset(point, animated: true)
} else { } else {
screenView.scrollView.setContentOffset(.zero, animated: true) screenView.scrollView.setContentOffset(.zero, animated: true)
leftController.viewModel.didSelectItem($0)
} }
}.store(in: &cancellables) }.store(in: &cancellables)
......
...@@ -19,11 +19,11 @@ final class SearchLeftController: UIViewController { ...@@ -19,11 +19,11 @@ final class SearchLeftController: UIViewController {
lazy private var screenView = SearchLeftView() lazy private var screenView = SearchLeftView()
private let viewModel = SearchLeftViewModel()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var dataSource: SearchDiffableDataSource!
private(set) var viewModel = SearchLeftViewModel()
private var drawerCancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>()
private let adrpURLString = "https://links.xx.network/adrp" private let adrpURLString = "https://links.xx.network/adrp"
private var dataSource: SearchTableViewDiffableDataSource!
override func loadView() { override func loadView() {
view = screenView view = screenView
...@@ -42,7 +42,7 @@ final class SearchLeftController: UIViewController { ...@@ -42,7 +42,7 @@ final class SearchLeftController: UIViewController {
screenView.tableView.dataSource = dataSource screenView.tableView.dataSource = dataSource
screenView.tableView.delegate = self screenView.tableView.delegate = self
dataSource = SearchTableViewDiffableDataSource( dataSource = SearchDiffableDataSource(
tableView: screenView.tableView tableView: screenView.tableView
) { tableView, indexPath, item in ) { tableView, indexPath, item in
let contact: Contact let contact: Contact
...@@ -75,6 +75,13 @@ final class SearchLeftController: UIViewController { ...@@ -75,6 +75,13 @@ final class SearchLeftController: UIViewController {
.sink { [hud] in hud.update(with: $0) } .sink { [hud] in hud.update(with: $0) }
.store(in: &cancellables) .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 viewModel.statePublisher
.compactMap(\.snapshot) .compactMap(\.snapshot)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
......
import UIKit import UIKit
import Permissions
import DependencyInjection 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 { final class SearchRightController: UIViewController {
@Dependency private var permissions: PermissionHandling
lazy private var screenView = SearchRightView() lazy private var screenView = SearchRightView()
private let camera = Camera() // private let camera = Camera()
private var status: SearchQRStatus? private var status: SearchQRStatus?
override func loadView() { override func loadView() {
...@@ -54,111 +37,20 @@ final class SearchRightController: UIViewController { ...@@ -54,111 +37,20 @@ final class SearchRightController: UIViewController {
} }
private func startCamera() { private func startCamera() {
permissions.requestCamera { [weak self] granted in // permissions.requestCamera { [weak self] granted in
guard let self = self else { return } // guard let self = self else { return }
//
if granted { // if granted {
DispatchQueue.main.async { [weak self] in // DispatchQueue.main.async { [weak self] in
guard let self = self else { return } // guard let self = self else { return }
self.camera.start() // self.camera.start()
} // }
} else { // } else {
DispatchQueue.main.async { // DispatchQueue.main.async {
self.status = .failed(.cameraPermission) // self.status = .failed(.cameraPermission)
// self.screenView.update(with: .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()
} }
} }
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"
}
}
}
...@@ -10,6 +10,7 @@ typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchIte ...@@ -10,6 +10,7 @@ typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchIte
struct SearchLeftViewState { struct SearchLeftViewState {
var input = "" var input = ""
var snapshot: SearchSnapshot? var snapshot: SearchSnapshot?
var item: SearchSegmentedControl.Item = .username
} }
final class SearchLeftViewModel { final class SearchLeftViewModel {
...@@ -35,11 +36,17 @@ final class SearchLeftViewModel { ...@@ -35,11 +36,17 @@ final class SearchLeftViewModel {
stateSubject.value.input = string stateSubject.value.input = string
} }
func didSelectItem(_ item: SearchSegmentedControl.Item) {
stateSubject.value.item = item
}
func didStartSearching() { func didStartSearching() {
hudSubject.send(.on(nil)) hudSubject.send(.on(nil))
let prefix = stateSubject.value.item.written.first!.uppercased()
do { 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 } guard let self = self else { return }
switch $0 { switch $0 {
......
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()
// }
//}
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 }
}
...@@ -16,7 +16,7 @@ final class SearchLeftPlaceholderView: UIView { ...@@ -16,7 +16,7 @@ final class SearchLeftPlaceholderView: UIView {
super.init(frame: .zero) super.init(frame: .zero)
let attrString = NSMutableAttributedString( let attrString = NSMutableAttributedString(
string: Localized.Ud.Search.Username.Placeholder.title, string: Localized.Ud.Search.Placeholder.title,
attributes: [ attributes: [
.foregroundColor: Asset.neutralDark.color, .foregroundColor: Asset.neutralDark.color,
.font: Fonts.Mulish.bold.font(size: 32.0) .font: Fonts.Mulish.bold.font(size: 32.0)
...@@ -36,7 +36,7 @@ final class SearchLeftPlaceholderView: UIView { ...@@ -36,7 +36,7 @@ final class SearchLeftPlaceholderView: UIView {
paragraph.lineHeightMultiple = 1.3 paragraph.lineHeightMultiple = 1.3
subtitleWithInfo.setup( subtitleWithInfo.setup(
text: Localized.Ud.Search.Username.Placeholder.subtitle, text: Localized.Ud.Search.Placeholder.subtitle,
attributes: [ attributes: [
.paragraphStyle: paragraph, .paragraphStyle: paragraph,
.foregroundColor: Asset.neutralBody.color, .foregroundColor: Asset.neutralBody.color,
......
...@@ -4,36 +4,11 @@ import Shared ...@@ -4,36 +4,11 @@ import Shared
final class SearchLeftView: UIView { final class SearchLeftView: UIView {
let tableView = UITableView() let tableView = UITableView()
let inputField = SearchComponent() let inputField = SearchComponent()
let emptyView: UIView = { let emptyView = SearchLeftEmptyView()
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 placeholderView = SearchLeftPlaceholderView() let placeholderView = SearchLeftPlaceholderView()
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
inputField.set(
placeholder: Localized.Ud.Search.Username.input,
imageAtRight: nil
)
emptyView.isHidden = true emptyView.isHidden = true
addSubview(tableView) addSubview(tableView)
...@@ -46,6 +21,14 @@ final class SearchLeftView: UIView { ...@@ -46,6 +21,14 @@ final class SearchLeftView: UIView {
required init?(coder: NSCoder) { nil } 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() { private func setupConstraints() {
inputField.snp.makeConstraints { inputField.snp.makeConstraints {
$0.top.equalToSuperview().offset(20) $0.top.equalToSuperview().offset(20)
......
...@@ -9,6 +9,15 @@ final class SearchSegmentedControl: UIView { ...@@ -9,6 +9,15 @@ final class SearchSegmentedControl: UIView {
case email case email
case phone case phone
case qr 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 trackView = UIView()
......
...@@ -1238,27 +1238,19 @@ public enum Localized { ...@@ -1238,27 +1238,19 @@ public enum Localized {
public static let title = Localized.tr("Localizable", "ud.requestDrawer.title") public static let title = Localized.tr("Localizable", "ud.requestDrawer.title")
} }
public enum Search { public enum Search {
public enum Email { /// There are no users with that %@.
/// Search by email public static func empty(_ p1: Any) -> String {
public static let input = Localized.tr("Localizable", "ud.search.email.input") return Localized.tr("Localizable", "ud.search.empty", String(describing: p1))
} }
public enum Phone { /// Search by %@
/// Search by phone number public static func input(_ p1: Any) -> String {
public static let input = Localized.tr("Localizable", "ud.search.phone.input") return Localized.tr("Localizable", "ud.search.input", String(describing: p1))
} }
public enum Username { public enum Placeholder {
/// Search by username /// Your searches are anonymous. Search information is never linked to your account or personally identifiable.
public static let input = Localized.tr("Localizable", "ud.search.username.input") public static let subtitle = Localized.tr("Localizable", "ud.search.placeholder.subtitle")
public enum Empty { /// Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel.
/// There are no users with that username public static let title = Localized.tr("Localizable", "ud.search.placeholder.title")
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 Tab { public enum Tab {
......
...@@ -982,19 +982,15 @@ ...@@ -982,19 +982,15 @@
= "Edit your new contact’s nickname so you know who they are."; = "Edit your new contact’s nickname so you know who they are.";
"ud.nicknameDrawer.save" "ud.nicknameDrawer.save"
= "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" "ud.search.input"
= "There are no users with that username"; = "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."; = "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."; = "Your searches are anonymous. Search information is never linked to your account or personally identifiable.";
// LaunchFeature // LaunchFeature
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment