Newer
Older
import Defaults
import NetworkMonitor
typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
var input = ""
var snapshot: SearchSnapshot?
var item: SearchSegmentedControl.Item = .username
@Dependency var reportingStatus: ReportingStatus
@Dependency var networkMonitor: NetworkMonitoring
@KeyObject(.username, defaultValue: nil) var username: String?
@KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool
@KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool
var hudPublisher: AnyPublisher<HUDStatus, Never> {
hudSubject.eraseToAnyPublisher()
}
var successPublisher: AnyPublisher<XXModels.Contact, Never> {
successSubject.eraseToAnyPublisher()
}
var statePublisher: AnyPublisher<SearchLeftViewState, Never> {
stateSubject.eraseToAnyPublisher()
}
var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()
private var searchCancellables = Set<AnyCancellable>()
private let successSubject = PassthroughSubject<XXModels.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))
networkMonitor.statusPublisher
.first { $0 == .available }
.eraseToAnyPublisher()
.flatMap { _ in
self.waitForNodes(timeout: 5)
}.sink(receiveCompletion: {
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)
}
guard stateSubject.value.input.isEmpty == false else { return }
if stateSubject.value.item == .phone {
content += stateSubject.value.country.code
}
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
enum NodeRegistrationError: Error {
case unhealthyNet
case belowMinimum
}
retry(max: 5, retryStrategy: .delay(seconds: 2)) { [weak self] in
guard let self = self else { return }
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
guard let self = self else { return }
if case .unhealthyNet = $0 as? NodeRegistrationError {
self.hudSubject.send(.error(.init(content: "Network is not healthy yet, try again within the next minute or so.")))
} else if case .belowMinimum = $0 as? NodeRegistrationError {
self.hudSubject.send(.error(.init(content: "Node registration ratio is still below 80%, try again within the next minute or so.")))
} else {
self.hudSubject.send(.error(.init(with: $0)))
}
return
}
if stateSubject.value.item == .phone {
factType = .phone
} else if stateSubject.value.item == .email {
factType = .email
backgroundScheduler.schedule { [weak self] in
guard let self = self else { return }
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 {
self.hudSubject.send(.none)
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,
isBlocked: false,
isBanned: false,
createdAt: Date()
)
print(">>> SearchUD error: \(error.localizedDescription)")
self.appendToLocalSearch(nil)
self.hudSubject.send(.error(.init(with: error)))
}
})
)
print(">>> UDSearch.Exception: \(error.localizedDescription)")
var contact = contact
contact.authStatus = .requesting
backgroundScheduler.schedule { [weak self] in
guard let self = self else { return }
do {
try self.database.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(
)
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)))
}
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 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(
)
contact.authStatus = .requested
contact = try self.database.saveContact(contact)
self.hudSubject.send(.none)
self.successSubject.send(contact)
self.presentSuccessToast(for: contact, resent: false)
} catch {
contact.authStatus = .requestFailed
_ = try? self.database.saveContact(contact)
self.hudSubject.send(.error(.init(with: error)))
}
func didSet(nickname: String, for contact: XXModels.Contact) {
if var contact = try? database.fetchContacts(.init(id: [contact.id])).first {
_ = try? database.saveContact(contact)
private func appendToLocalSearch(_ user: XXModels.Contact?) {
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)
}
let localsQuery = Contact.Query(
text: stateSubject.value.input,
authStatus: [.friend],
isBlocked: reportingStatus.isEnabled() ? false : nil,
isBanned: reportingStatus.isEnabled() ? false : nil
if let locals = try? database.fetchContacts(localsQuery),
let localsWithoutMe = removeMyself(from: locals),
localsWithoutMe.isEmpty == false {
snapshot.appendItems(
localsWithoutMe.map(SearchItem.connection),
toSection: .connections
)
}
stateSubject.value.snapshot = snapshot
}
private func removeMyself(from collection: [XXModels.Contact]) -> [XXModels.Contact]? {
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 ?? "")
toastController.enqueueToast(model: .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
guard let self = self else { return }
_ = try self.messenger.cMix.get()!.getNodeRegistrationStatus()
promise(.success(()))
}.finalCatch {
promise(.failure($0))
}
}
}.eraseToAnyPublisher()
}