Skip to content
Snippets Groups Projects
SearchLeftViewModel.swift 8.7 KiB
Newer Older
import HUD
import UIKit
Bruno Muniz's avatar
Bruno Muniz committed
import Shared
import Combine
import XXModels
import XXClient
import Countries
import Models
import Defaults
import CustomDump
import ReportingFeature
import CombineSchedulers
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 e2e: E2E
Bruno Muniz's avatar
Bruno Muniz committed
    @Dependency var cMix: CMix
    @Dependency var database: Database
    @Dependency var userDiscovery: UserDiscovery
    @Dependency var reportingStatus: ReportingStatus
    @Dependency var networkMonitor: NetworkMonitoring

    @KeyObject(.username, defaultValue: nil) var username: String?

    var myId: Data {
        try! GetIdFromContact.live(userDiscovery.getContact())
    }

    var hudPublisher: AnyPublisher<HUDStatus, Never> {
        hudSubject.eraseToAnyPublisher()
    }

    var successPublisher: AnyPublisher<Contact, Never> {
        successSubject.eraseToAnyPublisher()
    }

    var statePublisher: AnyPublisher<SearchLeftViewState, Never> {
        stateSubject.eraseToAnyPublisher()
    }

    var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()

    private var invitation: String?
    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())
    private var networkCancellable = Set<AnyCancellable>()

    init(_ invitation: String? = nil) {
        self.invitation = invitation
    }

    func viewDidAppear() {
        if let pendingInvitation = invitation {
            invitation = nil
            stateSubject.value.input = pendingInvitation
            hudSubject.send(.onAction(Localized.Ud.Search.cancel))

Bruno Muniz's avatar
Bruno Muniz committed
            networkCancellable.removeAll()

            networkMonitor.statusPublisher
                .first { $0 == .available }
                .eraseToAnyPublisher()
                .flatMap { _ in self.session.waitForNodes(timeout: 5) }
                .sink {
                    if case .failure(let error) = $0 {
                        self.hudSubject.send(.error(.init(with: error)))
                    }
                } receiveValue: {
                    self.didStartSearching()
                }.store(in: &networkCancellable)

    func didEnterInput(_ string: String) {
        stateSubject.value.input = string
    }

    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() {
Bruno Muniz's avatar
Bruno Muniz committed
        guard stateSubject.value.input.isEmpty == false else { return }

Bruno Muniz's avatar
Bruno Muniz committed
        hudSubject.send(.onAction(Localized.Ud.Search.cancel))
        var content = stateSubject.value.input
        if stateSubject.value.item == .phone {
            content += stateSubject.value.country.code
        }

Bruno Muniz's avatar
Bruno Muniz committed
        let nrr = try! cMix.getNodeRegistrationStatus()
        print("^^^ NRR: \(nrr.ratio)")

        backgroundScheduler.schedule { [weak self] in
            guard let self = self else { return }

            do {
                let report = try SearchUD.live(
                    e2eId: self.e2e.getId(),
                    udContact: self.userDiscovery.getContact(),
Bruno Muniz's avatar
Bruno Muniz committed
                    facts: [Fact(fact: content, type: self.stateSubject.value.item.rawValue)],
                    callback: .init(handle: {
                        switch $0 {
                        case .success(let dataArray):
Bruno Muniz's avatar
Bruno Muniz committed
                            print("^^^ searchUD success: \(dataArray.map { $0.base64EncodedString() })")
Bruno Muniz's avatar
Bruno Muniz committed
                             self.hudSubject.send(.none)
//                             self.appendToLocalSearch(contact)

                        case .failure(let error):
Bruno Muniz's avatar
Bruno Muniz committed
                            print("^^^ searchUD error: \(error.localizedDescription)")
                            self.appendToLocalSearch(nil)
                            self.hudSubject.send(.error(.init(with: error)))
                        }
                    })
                )

Bruno Muniz's avatar
Bruno Muniz committed
                print("^^^ report: \(report))")
Bruno Muniz's avatar
Bruno Muniz committed
                print("^^^ exception: \(error.localizedDescription)")
Bruno Muniz's avatar
Bruno Muniz committed
        }
Bruno Muniz's avatar
Bruno Muniz committed
    func didTapResend(contact: Contact) {
        hudSubject.send(.on)

        var contact = contact
        contact.authStatus = .requesting

        backgroundScheduler.schedule { [weak self] in
            guard let self = self else { return }

            do {
                try self.database.saveContact(contact)

                var myFacts = try self.userDiscovery.getFacts()
                myFacts.append(Fact(fact: self.username!, type: FactType.username.rawValue))

                let _ = try self.e2e.requestAuthenticatedChannel(
                    partnerContact: contact.id,
                    myFacts: myFacts
                )

                contact.authStatus = .requested
                contact = try self.database.saveContact(contact)

                self.hudSubject.send(.none)
            } catch {
                contact.authStatus = .requestFailed
                _ = try? self.database.saveContact(contact)
                self.hudSubject.send(.error(.init(with: error)))
            }
    func didTapRequest(contact: Contact) {
Bruno Muniz's avatar
Bruno Muniz committed
        hudSubject.send(.on)
        var contact = contact
        contact.nickname = contact.username
        contact.authStatus = .requesting

        backgroundScheduler.schedule { [weak self] in
            guard let self = self else { return }
            do {
                try self.database.saveContact(contact)

                var myFacts = try self.userDiscovery.getFacts()
                myFacts.append(Fact(fact: self.username!, type: FactType.username.rawValue))

                let _ = try self.e2e.requestAuthenticatedChannel(
                    partnerContact: contact.marshaled!,
                    myFacts: myFacts
                )

                contact.authStatus = .requested
                contact = try self.database.saveContact(contact)

                self.hudSubject.send(.none)
                self.successSubject.send(contact)
            } catch {
                contact.authStatus = .requestFailed
                _ = try? self.database.saveContact(contact)
                self.hudSubject.send(.error(.init(with: error)))
            }
        }
    }

    func didSet(nickname: String, for contact: Contact) {
        if var contact = try? database.fetchContacts(.init(id: [contact.id])).first {
            contact.nickname = nickname
            _ = try? database.saveContact(contact)
    private func appendToLocalSearch(_ user: Contact?) {
        var snapshot = SearchSnapshot()

        if var user = user {
            if let contact = try? database.fetchContacts(.init(id: [user.id])).first {
                user.isBanned = contact.isBanned
                user.isBlocked = contact.isBlocked
                user.authStatus = contact.authStatus
            }

            if user.authStatus != .friend, !reportingStatus.isEnabled() {
                snapshot.appendSections([.stranger])
                snapshot.appendItems([.stranger(user)], toSection: .stranger)
            } else if user.authStatus != .friend, reportingStatus.isEnabled(), !user.isBanned, !user.isBlocked {
                snapshot.appendSections([.stranger])
                snapshot.appendItems([.stranger(user)], toSection: .stranger)
            }
Bruno Muniz's avatar
Bruno Muniz committed
        let localsQuery = Contact.Query(
            text: stateSubject.value.input,
            authStatus: [.friend],
            isBlocked: reportingStatus.isEnabled() ? false : nil,
            isBanned: reportingStatus.isEnabled() ? false : nil
Bruno Muniz's avatar
Bruno Muniz committed
        )
        if let locals = try? database.fetchContacts(localsQuery),
Bruno Muniz's avatar
Bruno Muniz committed
           let localsWithoutMe = removeMyself(from: locals),
           localsWithoutMe.isEmpty == false {
            snapshot.appendSections([.connections])
Bruno Muniz's avatar
Bruno Muniz committed
            snapshot.appendItems(
                localsWithoutMe.map(SearchItem.connection),
                toSection: .connections
            )
        }

        stateSubject.value.snapshot = snapshot
    }
Bruno Muniz's avatar
Bruno Muniz committed

    private func removeMyself(from collection: [Contact]) -> [Contact]? {
        collection.filter { $0.id != myId }