Newer
Older
import Defaults
import Dependencies
import AppCore
typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
var input = ""
var snapshot: SearchSnapshot?
var country: Country = .fromMyPhone()
var item: SearchSegmentedControl.Item = .username
@Dependency(\.app.dbManager) var dbManager
@Dependency(\.app.messenger) var messenger
@Dependency(\.app.toastManager) var toastManager
@Dependency(\.reportingStatus) var reportingStatus
@Dependency(\.app.networkMonitor) var networkMonitor
@KeyObject(.username, defaultValue: nil) var username: String?
@KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool
@KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool
var myId: Data {
try! messenger.e2e.get()!.getContact().getId()
}
var successPublisher: AnyPublisher<XXModels.Contact, Never> {
successSubject.eraseToAnyPublisher()
}
var statePublisher: AnyPublisher<SearchLeftViewState, Never> {
stateSubject.eraseToAnyPublisher()
}
var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()
var invitation: String?
private var searchCancellables = Set<AnyCancellable>()
private let successSubject = PassthroughSubject<XXModels.Contact, Never>()
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
hudManager.show(.init(
actionTitle: Localized.Ud.Search.cancel,
hasDotAnimation: true,
onTapClosure: { [weak self] in
guard let self else { return }
self.didTapCancelSearch()
}
))
// networkMonitor
// .statusPublisher
// .first { $0 == .available }
// .eraseToAnyPublisher()
// .flatMap { _ in
// self.waitForNodes(timeout: 5)
// }.sink(receiveCompletion: {
// if case .failure(let error) = $0 {
// self.hudManager.show(.init(error: 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()
func didStartSearching() {
guard stateSubject.value.input.isEmpty == false else { return }
hudManager.show(.init(
actionTitle: Localized.Ud.Search.cancel,
hasDotAnimation: true,
onTapClosure: { [weak self] in
guard let self else { return }
self.didTapCancelSearch()
}
))
if stateSubject.value.item == .phone {
content += stateSubject.value.country.code
enum NodeRegistrationError: Error {
case unhealthyNet
case belowMinimum
retry(max: 5, retryStrategy: .delay(seconds: 2)) { [weak self] in
do {
let nrr = try self.messenger.cMix.get()!.getNodeRegistrationStatus()
if nrr.ratio < 0.8 { throw NodeRegistrationError.belowMinimum }
} catch {
throw NodeRegistrationError.unhealthyNet
}
}.finalCatch { [weak self] in
if case .unhealthyNet = $0 as? NodeRegistrationError {
self.hudManager.show(.init(content: "Network is not healthy yet, try again within the next minute or so."))
} else if case .belowMinimum = $0 as? NodeRegistrationError {
self.hudManager.show(.init(content:"Node registration ratio is still below 80%, try again within the next minute or so."))
self.hudManager.show(.init(error: $0))
if stateSubject.value.item == .phone {
factType = .phone
} else if stateSubject.value.item == .email {
factType = .email
}
do {
let report = try SearchUD.live(
params: .init(
e2eId: self.messenger.e2e.get()!.getId(),
udContact: self.messenger.ud.get()!.getContact(),
facts: [.init(type: factType, value: content)]
),
callback: .init(handle: {
switch $0 {
case .success(let results):
self.hudManager.hide()
self.appendToLocalSearch(
XXModels.Contact(
id: try! results.first!.getId(),
marshaled: results.first!.data,
username: try! results.first?.getFacts().first(where: { $0.type == .username })?.value,
email: try? results.first?.getFacts().first(where: { $0.type == .email })?.value,
phone: try? results.first?.getFacts().first(where: { $0.type == .phone })?.value,
nickname: nil,
photo: nil,
authStatus: .stranger,
isRecent: true,
isBlocked: false,
isBanned: false,
createdAt: Date()
)
case .failure(let error):
print(">>> SearchUD error: \(error.localizedDescription)")
self.hudManager.show(.init(error: error))
print(">>> UDSearch.Report: \(report))")
} catch {
print(">>> UDSearch.Exception: \(error.localizedDescription)")
}
}
}
var contact = contact
contact.authStatus = .requesting
try self.dbManager.getDB().saveContact(contact)
var includedFacts: [Fact] = []
let myFacts = try self.messenger.ud.get()!.getFacts()
if let fact = myFacts.get(.username) {
includedFacts.append(fact)
if self.sharingEmail, let fact = myFacts.get(.email) {
includedFacts.append(fact)
}
if self.sharingPhone, let fact = myFacts.get(.phone) {
includedFacts.append(fact)
}
let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel(
partner: .live(contact.marshaled!),
myFacts: includedFacts
)
contact = try self.dbManager.getDB().saveContact(contact)
self.hudManager.hide()
self.presentSuccessToast(for: contact, resent: true)
} catch {
contact.authStatus = .requestFailed
_ = try? self.dbManager.getDB().saveContact(contact)
self.hudManager.show(.init(error: error))
var contact = contact
contact.nickname = contact.username
contact.authStatus = .requesting
try self.dbManager.getDB().saveContact(contact)
var includedFacts: [Fact] = []
let myFacts = try self.messenger.ud.get()!.getFacts()
if let fact = myFacts.get(.username) {
includedFacts.append(fact)
if self.sharingEmail, let fact = myFacts.get(.email) {
includedFacts.append(fact)
if self.sharingPhone, let fact = myFacts.get(.phone) {
includedFacts.append(fact)
let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel(
partner: .live(contact.marshaled!),
myFacts: includedFacts
contact = try self.dbManager.getDB().saveContact(contact)
self.hudManager.hide()
self.successSubject.send(contact)
self.presentSuccessToast(for: contact, resent: false)
} catch {
contact.authStatus = .requestFailed
_ = try? self.dbManager.getDB().saveContact(contact)
self.hudManager.show(.init(error: error))
func didSet(nickname: String, for contact: XXModels.Contact) {
if var contact = try? dbManager.getDB().fetchContacts(.init(id: [contact.id])).first {
_ = try? dbManager.getDB().saveContact(contact)
}
private func appendToLocalSearch(_ user: XXModels.Contact?) {
var snapshot = SearchSnapshot()
if var user = user {
if let contact = try? dbManager.getDB().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)
}
let localsQuery = Contact.Query(
text: stateSubject.value.input,
authStatus: [.friend],
isBlocked: reportingStatus.isEnabled() ? false : nil,
isBanned: reportingStatus.isEnabled() ? false : nil
)
if let locals = try? dbManager.getDB().fetchContacts(localsQuery),
let localsWithoutMe = removeMyself(from: locals),
localsWithoutMe.isEmpty == false {
snapshot.appendSections([.connections])
snapshot.appendItems(
localsWithoutMe.map(SearchItem.connection),
toSection: .connections
)
stateSubject.value.snapshot = snapshot
}
private func removeMyself(from collection: [XXModels.Contact]) -> [XXModels.Contact]? {
collection.filter { $0.id != myId }
}
private func presentSuccessToast(for contact: XXModels.Contact, resent: Bool) {
let name = contact.nickname ?? contact.username
let sentTitle = Localized.Requests.Sent.Toast.sent(name ?? "")
let resentTitle = Localized.Requests.Sent.Toast.resent(name ?? "")
toastManager.enqueue(.init(
title: resent ? resentTitle : sentTitle,
leftImage: Asset.sharedSuccess.image
))
}
private func waitForNodes(timeout: Int) -> AnyPublisher<Void, Error> {
Deferred {
Future { promise in
retry(max: timeout, retryStrategy: .delay(seconds: 1)) { [weak self] in
_ = try self.messenger.cMix.get()!.getNodeRegistrationStatus()
promise(.success(()))
}.finalCatch {
promise(.failure($0))
}
}
}.eraseToAnyPublisher()
}