From 00c19ae1422d76b3b1ce6cfb115f4017eacfb1bb Mon Sep 17 00:00:00 2001
From: Bruno Muniz Azevedo Filho <bruno@elixxir.io>
Date: Mon, 18 Jul 2022 00:52:04 -0300
Subject: [PATCH] Almost done w/ username searching

---
 .../SearchContainerController.swift           | 104 ++++++-
 .../Controllers/SearchQRController.swift      |   2 +-
 .../SearchUsernameController.swift            | 286 +++++++++++++++++-
 .../ViewModels/SearchContainerViewModel.swift |  58 ++++
 .../ViewModels/SearchUsernameViewModel.swift  |  96 ++++++
 .../ViewModels/SearchViewModel.swift          |  49 +--
 .../Views/SearchUsernameView.swift            |   9 +
 Sources/Shared/Views/SearchComponent.swift    |  17 +-
 8 files changed, 568 insertions(+), 53 deletions(-)
 create mode 100644 Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift
 create mode 100644 Sources/SearchFeature/ViewModels/SearchUsernameViewModel.swift

diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift
index 1baedc82..788d47de 100644
--- a/Sources/SearchFeature/Controllers/SearchContainerController.swift
+++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift
@@ -2,17 +2,43 @@ import UIKit
 import Theme
 import Shared
 import Combine
+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 private var statusBarController: StatusBarStyleControlling
+    @Dependency var coordinator: SearchCoordinating
+    @Dependency var statusBarController: StatusBarStyleControlling
 
     lazy private var screenView = SearchContainerView()
 
-    private var cancellables = Set<AnyCancellable>()
     private let qrController = SearchQRController()
+    private var cancellables = Set<AnyCancellable>()
+    private let viewModel = SearchContainerViewModel()
     private let emailController = SearchEmailController()
     private let phoneController = SearchPhoneController()
+    private var drawerCancellables = Set<AnyCancellable>()
     private let usernameController = SearchUsernameController()
 
     public override func loadView() {
@@ -30,6 +56,11 @@ public final class SearchContainerController: UIViewController {
         )
     }
 
+    public override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        viewModel.didAppear()
+    }
+
     public override func viewDidLoad() {
         super.viewDidLoad()
         setupNavigationBar()
@@ -61,6 +92,12 @@ public final class SearchContainerController: UIViewController {
                 let point: CGPoint = CGPoint(x: screenView.frame.width * page, y: 0.0)
                 screenView.scrollView.setContentOffset(point, animated: true)
             }.store(in: &cancellables)
+
+        viewModel.coverTrafficPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in presentCoverTrafficDrawer() }
+            .store(in: &cancellables)
+
     }
 
     @objc private func didTapBack() {
@@ -156,3 +193,66 @@ extension SearchContainerController: UIScrollViewDelegate {
         }
     }
 }
+
+extension SearchContainerController {
+    private func presentCoverTrafficDrawer() {
+        let enableButton = CapsuleButton()
+        enableButton.set(
+            style: .brandColored,
+            title: Localized.ChatList.Traffic.positive
+        )
+
+        let dismissButton = CapsuleButton()
+        dismissButton.set(
+            style: .seeThrough,
+            title: Localized.ChatList.Traffic.negative
+        )
+
+        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/SearchQRController.swift b/Sources/SearchFeature/Controllers/SearchQRController.swift
index 49718728..b6941edd 100644
--- a/Sources/SearchFeature/Controllers/SearchQRController.swift
+++ b/Sources/SearchFeature/Controllers/SearchQRController.swift
@@ -65,7 +65,7 @@ final class SearchQRController: UIViewController {
             } else {
                 DispatchQueue.main.async {
                     self.status = .failed(.cameraPermission)
-                    self.screenView.update(with: .failed(.cameraPermission))
+//                    self.screenView.update(with: .failed(.cameraPermission))
                 }
             }
         }
diff --git a/Sources/SearchFeature/Controllers/SearchUsernameController.swift b/Sources/SearchFeature/Controllers/SearchUsernameController.swift
index 563cc86f..33da386f 100644
--- a/Sources/SearchFeature/Controllers/SearchUsernameController.swift
+++ b/Sources/SearchFeature/Controllers/SearchUsernameController.swift
@@ -1,16 +1,29 @@
+import HUD
 import UIKit
 import Shared
 import Combine
+import XXModels
+import Defaults
+import Countries
 import DrawerFeature
 import DependencyInjection
 
 final class SearchUsernameController: UIViewController {
+    @Dependency private var hud: HUDType
     @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 = SearchUsernameView()
 
     private var cancellables = Set<AnyCancellable>()
+    private let viewModel = SearchUsernameViewModel()
     private var drawerCancellables = Set<AnyCancellable>()
+    private let adrpURLString = "https://links.xx.network/adrp"
+    private var dataSource: SearchTableViewDiffableDataSource!
 
     override func loadView() {
         view = screenView
@@ -18,15 +31,82 @@ final class SearchUsernameController: UIViewController {
 
     override func viewDidLoad() {
         super.viewDidLoad()
+        setupTableView()
         setupBindings()
     }
 
+    private func setupTableView() {
+        screenView.tableView.separatorStyle = .none
+        screenView.tableView.tableFooterView = UIView()
+        screenView.tableView.register(SmallAvatarAndTitleCell.self)
+        screenView.tableView.dataSource = dataSource
+        screenView.tableView.delegate = self
+
+        dataSource = SearchTableViewDiffableDataSource(
+            tableView: screenView.tableView
+        ) { tableView, indexPath, item in
+            let contact: Contact
+            let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self)
+
+            switch item {
+            case .stranger(let stranger):
+                contact = stranger
+            case .connection(let connection):
+                contact = connection
+            }
+
+            let title = (contact.nickname ?? contact.username) ?? ""
+            cell.titleLabel.text = title
+            cell.avatarView.setupProfile(title: title, image: contact.photo, size: .medium)
+            return cell
+        }
+    }
+
     private func setupBindings() {
+        viewModel.hudPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [hud] in hud.update(with: $0) }
+            .store(in: &cancellables)
+
+        viewModel.statePublisher
+            .compactMap(\.snapshot)
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                screenView.placeholderView.isHidden = true
+                dataSource.apply($0, animatingDifferences: false)
+            }.store(in: &cancellables)
+
         screenView.placeholderView
             .infoPublisher
             .receive(on: DispatchQueue.main)
             .sink { [unowned self] in presentSearchDisclaimer() }
             .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.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() {
@@ -46,7 +126,7 @@ final class SearchUsernameController: UIViewController {
             ),
             DrawerLinkText(
                 text: Localized.Ud.Placeholder.Drawer.subtitle,
-                urlString: "https://links.xx.network/adrp",
+                urlString: adrpURLString,
                 spacingAfter: 37
             ),
             DrawerStack(views: [
@@ -66,4 +146,208 @@ final class SearchUsernameController: UIViewController {
 
         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 SearchUsernameController: 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)
+            }
+        }
+    }
+
+    private func didTap(contact: Contact) {
+        guard contact.authStatus == .stranger else {
+            coordinator.toContact(contact, from: self)
+            return
+        }
+
+        presentRequestDrawer(forContact: contact)
+    }
 }
diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift
new file mode 100644
index 00000000..e308f6a5
--- /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/SearchUsernameViewModel.swift b/Sources/SearchFeature/ViewModels/SearchUsernameViewModel.swift
new file mode 100644
index 00000000..f64543be
--- /dev/null
+++ b/Sources/SearchFeature/ViewModels/SearchUsernameViewModel.swift
@@ -0,0 +1,96 @@
+import HUD
+import UIKit
+import Combine
+import XXModels
+import Integration
+import DependencyInjection
+
+typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
+
+struct SearchUsernameViewState {
+    var input = ""
+    var snapshot: SearchSnapshot?
+}
+
+final class SearchUsernameViewModel {
+    @Dependency var session: SessionType
+
+    var hudPublisher: AnyPublisher<HUDStatus, Never> {
+        hudSubject.eraseToAnyPublisher()
+    }
+
+    var successPublisher: AnyPublisher<Contact, Never> {
+        successSubject.eraseToAnyPublisher()
+    }
+
+    var statePublisher: AnyPublisher<SearchUsernameViewState, Never> {
+        stateSubject.eraseToAnyPublisher()
+    }
+
+    private let successSubject = PassthroughSubject<Contact, Never>()
+    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
+    private let stateSubject = CurrentValueSubject<SearchUsernameViewState, Never>(.init())
+
+    func didEnterInput(_ string: String) {
+        stateSubject.value.input = string
+    }
+
+    func didStartSearching() {
+        hudSubject.send(.on(nil))
+
+        do {
+            try session.search(fact: "U\(stateSubject.value.input)") { [weak self] in
+                guard let self = self else { return }
+
+                switch $0 {
+                case .success(let contact):
+                    self.hudSubject.send(.none)
+                    self.appendToLocalSearch(contact)
+
+                case .failure(let error):
+                    self.appendToLocalSearch(nil)
+                    self.hudSubject.send(.error(.init(with: error)))
+                }
+            }
+        } catch {
+            hudSubject.send(.error(.init(with: error)))
+        }
+    }
+
+    func didTapRequest(contact: Contact) {
+        hudSubject.send(.on(nil))
+        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(_ contact: Contact?) {
+        var snapshot = SearchSnapshot()
+
+        if let contact = contact {
+            snapshot.appendSections([.stranger])
+            snapshot.appendItems([.stranger(contact)], toSection: .stranger)
+        }
+
+        if let locals = try? session.dbManager.fetchContacts(Contact.Query(username: stateSubject.value.input)), locals.count > 0 {
+            snapshot.appendSections([.connections])
+            snapshot.appendItems(locals.map(SearchItem.connection), toSection: .connections)
+        }
+
+        stateSubject.value.snapshot = snapshot
+    }
+}
diff --git a/Sources/SearchFeature/ViewModels/SearchViewModel.swift b/Sources/SearchFeature/ViewModels/SearchViewModel.swift
index 5dae23c8..e59b7a85 100644
--- a/Sources/SearchFeature/ViewModels/SearchViewModel.swift
+++ b/Sources/SearchFeature/ViewModels/SearchViewModel.swift
@@ -36,12 +36,9 @@ struct SearchViewState: Equatable {
 }
 
 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()
@@ -51,10 +48,6 @@ final class SearchViewModel {
         placeholderSubject.eraseToAnyPublisher()
     }
 
-    var coverTrafficPublisher: AnyPublisher<Void, Never> {
-        coverTrafficSubject.eraseToAnyPublisher()
-    }
-
     var statePublisher: AnyPublisher<SearchViewState, Never> {
         stateSubject.eraseToAnyPublisher()
     }
@@ -68,15 +61,12 @@ final class SearchViewModel {
 
     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
@@ -94,10 +84,6 @@ final class SearchViewModel {
         stateSubject.value.country = country
     }
 
-    func didEnableCoverTraffic() {
-        isCoverTrafficEnabled = true
-        session.setDummyTraffic(status: true)
-    }
 
     func didTapSearch() {
         hudSubject.send(.on(nil))
@@ -132,35 +118,6 @@ final class SearchViewModel {
         }
     }
 
-    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 {
diff --git a/Sources/SearchFeature/Views/SearchUsernameView.swift b/Sources/SearchFeature/Views/SearchUsernameView.swift
index e6de2ce0..1783ec3c 100644
--- a/Sources/SearchFeature/Views/SearchUsernameView.swift
+++ b/Sources/SearchFeature/Views/SearchUsernameView.swift
@@ -2,6 +2,7 @@ import UIKit
 import Shared
 
 final class SearchUsernameView: UIView {
+    let tableView = UITableView()
     let inputField = SearchComponent()
     let placeholderView = SearchUsernamePlaceholderView()
 
@@ -13,6 +14,7 @@ final class SearchUsernameView: UIView {
             imageAtRight: nil
         )
 
+        addSubview(tableView)
         addSubview(inputField)
         addSubview(placeholderView)
 
@@ -28,6 +30,13 @@ final class SearchUsernameView: UIView {
             $0.right.equalToSuperview().offset(-20)
         }
 
+        tableView.snp.makeConstraints {
+            $0.top.equalTo(inputField.snp.bottom)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+
         placeholderView.snp.makeConstraints {
             $0.top.equalTo(inputField.snp.bottom)
             $0.left.equalToSuperview().offset(32.5)
diff --git a/Sources/Shared/Views/SearchComponent.swift b/Sources/Shared/Views/SearchComponent.swift
index 2e87cfbf..a12c0946 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,9 +30,10 @@ 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)
@@ -169,6 +174,12 @@ public final class SearchComponent: UIView {
         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)
-- 
GitLab