From acbb560564f7dddb2934663d2e48b1c1365178ae Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Tue, 1 Nov 2022 17:34:11 -0300 Subject: [PATCH] refactoring hud --- Package.swift | 16 - Sources/App/DependencyRegistrator.swift | 3 +- .../Controllers/BackupController.swift | 7 - .../Controllers/BackupSFTPController.swift | 9 - .../ViewModels/BackupConfigViewModel.swift | 13 +- .../ViewModels/BackupSFTPViewModel.swift | 17 +- .../Controllers/GroupChatController.swift | 7 - .../Controllers/SingleChatController.swift | 7 - .../ViewModels/GroupChatViewModel.swift | 561 +++++------ .../ViewModels/SingleChatViewModel.swift | 949 +++++++++--------- .../ViewModel/ChatListViewModel.swift | 345 ++++--- .../Controllers/ContactController.swift | 7 - .../ViewModels/ContactViewModel.swift | 392 ++++---- .../Controllers/CreateGroupController.swift | 7 - .../ViewModels/CreateGroupViewModel.swift | 240 +++-- Sources/HUD/DotAnimation.swift | 94 -- Sources/HUD/ErrorView.swift | 58 -- Sources/HUD/HUD.swift | 194 ---- Sources/LaunchFeature/LaunchController.swift | 51 +- Sources/LaunchFeature/LaunchViewModel.swift | 33 +- ...nboardingEmailConfirmationController.swift | 6 - .../OnboardingEmailController.swift | 6 - ...nboardingPhoneConfirmationController.swift | 6 - .../OnboardingPhoneController.swift | 7 - .../OnboardingStartController.swift | 7 +- .../OnboardingUsernameController.swift | 7 - ...OnboardingEmailConfirmationViewModel.swift | 149 ++- .../ViewModels/OnboardingEmailViewModel.swift | 109 +- ...OnboardingPhoneConfirmationViewModel.swift | 151 ++- .../ViewModels/OnboardingPhoneViewModel.swift | 129 ++- .../OnboardingUsernameViewModel.swift | 123 ++- .../Controllers/ProfileCodeController.swift | 8 - .../Controllers/ProfileController.swift | 7 - .../Controllers/ProfileEmailController.swift | 7 - .../Controllers/ProfilePhoneController.swift | 7 - .../ViewModels/ProfileCodeViewModel.swift | 11 +- .../ViewModels/ProfileEmailViewModel.swift | 113 +-- .../ViewModels/ProfilePhoneViewModel.swift | 105 +- .../ViewModels/ProfileViewModel.swift | 11 +- .../RequestsFailedController.swift | 9 - .../RequestsReceivedController.swift | 11 +- .../Controllers/RequestsSentController.swift | 9 - .../ViewModels/RequestsFailedViewModel.swift | 160 ++- .../RequestsReceivedViewModel.swift | 519 +++++----- .../ViewModels/RequestsSentViewModel.swift | 217 ++-- .../Controllers/RestoreListController.swift | 7 - .../Controllers/RestoreSFTPController.swift | 8 - .../ViewModels/RestoreListViewModel.swift | 21 +- .../ViewModels/RestoreSFTPViewModel.swift | 16 +- .../Controllers/SearchLeftController.swift | 39 +- .../ViewModels/SearchLeftViewModel.swift | 577 ++++++----- .../Controllers/AccountDeleteController.swift | 11 +- .../Controllers/SettingsController.swift | 7 - .../ViewModels/AccountDeleteViewModel.swift | 134 ++- .../ViewModels/SettingsViewModel.swift | 261 +++-- .../Shared/Controllers/HUDController.swift | 19 + .../Controllers/RootViewController.swift | 148 ++- Sources/Shared/Models/HUDModel.swift | 62 ++ .../{Controllers => Models}/ToastModel.swift | 0 Sources/Shared/Views/DotAnimation.swift | 140 ++- Sources/Shared/Views/ErrorView.swift | 57 ++ Sources/Shared/Views/HUDView.swift | 33 + 62 files changed, 3043 insertions(+), 3401 deletions(-) delete mode 100644 Sources/HUD/DotAnimation.swift delete mode 100644 Sources/HUD/ErrorView.swift delete mode 100644 Sources/HUD/HUD.swift create mode 100644 Sources/Shared/Controllers/HUDController.swift create mode 100644 Sources/Shared/Models/HUDModel.swift rename Sources/Shared/{Controllers => Models}/ToastModel.swift (100%) create mode 100644 Sources/Shared/Views/ErrorView.swift create mode 100644 Sources/Shared/Views/HUDView.swift diff --git a/Package.swift b/Package.swift index 5a8a8e1d..9f27f5c4 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,6 @@ let package = Package( ], products: [ .library(name: "App", targets: ["App"]), - .library(name: "HUD", targets: ["HUD"]), .library(name: "Shared", targets: ["Shared"]), .library(name: "Models", targets: ["Models"]), .library(name: "XXLogger", targets: ["XXLogger"]), @@ -266,13 +265,6 @@ let package = Package( .product(name: "ScrollViewController", package: "ScrollViewController"), ] ), - .target( - name: "HUD", - dependencies: [ - .target(name: "Shared"), - .product(name: "SnapKit", package: "SnapKit"), - ] - ), .target( name: "XXLogger", dependencies: [ @@ -318,7 +310,6 @@ let package = Package( .target( name: "RestoreFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Presentation"), .target(name: "DependencyInjection"), @@ -353,7 +344,6 @@ let package = Package( .target( name: "ChatFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Defaults"), .target(name: "Keychain"), @@ -384,7 +374,6 @@ let package = Package( .target( name: "SearchFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Countries"), .target(name: "PushFeature"), @@ -408,7 +397,6 @@ let package = Package( .target( name: "LaunchFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Defaults"), .target(name: "PushFeature"), @@ -454,7 +442,6 @@ let package = Package( .target( name: "ProfileFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Keychain"), .target(name: "Defaults"), @@ -507,7 +494,6 @@ let package = Package( .target( name: "OnboardingFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Defaults"), .target(name: "Keychain"), @@ -547,7 +533,6 @@ let package = Package( .target( name: "BackupFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Models"), .target(name: "InputField"), @@ -607,7 +592,6 @@ let package = Package( .target( name: "SettingsFeature", dependencies: [ - .target(name: "HUD"), .target(name: "Shared"), .target(name: "Defaults"), .target(name: "Keychain"), diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 824b2ef6..5f3d0418 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -7,7 +7,6 @@ import MobileCoreServices // MARK: Isolated features -import HUD import Bindings import XXLogger import Keychain @@ -110,7 +109,7 @@ struct DependencyRegistrator { // MARK: Isolated - container.register(HUD()) + container.register(HUDController()) container.register(ToastController()) container.register(StatusBarStylist()) diff --git a/Sources/BackupFeature/Controllers/BackupController.swift b/Sources/BackupFeature/Controllers/BackupController.swift index 822ebbc7..780efb8f 100644 --- a/Sources/BackupFeature/Controllers/BackupController.swift +++ b/Sources/BackupFeature/Controllers/BackupController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Models @@ -6,8 +5,6 @@ import Combine import DependencyInjection public final class BackupController: UIViewController { - @Dependency var hud: HUD - private let viewModel = BackupViewModel.live() private var cancellables = Set<AnyCancellable>() @@ -21,8 +18,6 @@ public final class BackupController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = Asset.neutralWhite.color - hud.update(with: .on) - setupNavigationBar() setupBindings() } @@ -41,8 +36,6 @@ public final class BackupController: UIViewController { .receive(on: DispatchQueue.main) .removeDuplicates() .sink { [unowned self] in - hud.update(with: .none) - switch $0 { case .setup: contentViewController = BackupSetupController(viewModel.setupViewModel()) diff --git a/Sources/BackupFeature/Controllers/BackupSFTPController.swift b/Sources/BackupFeature/Controllers/BackupSFTPController.swift index 7c5c9c7c..3fa7f9c3 100644 --- a/Sources/BackupFeature/Controllers/BackupSFTPController.swift +++ b/Sources/BackupFeature/Controllers/BackupSFTPController.swift @@ -1,12 +1,8 @@ -import HUD import UIKit import Combine -import DependencyInjection import ScrollViewController public final class BackupSFTPController: UIViewController { - @Dependency private var hud: HUD - lazy private var screenView = BackupSFTPView() lazy private var scrollViewController = ScrollViewController() @@ -44,11 +40,6 @@ public final class BackupSFTPController: UIViewController { } private func setupBindings() { - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - viewModel.authPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] params in diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index 569a65de..1057d966 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -35,8 +34,8 @@ struct BackupConfigViewModel { extension BackupConfigViewModel { static func live() -> Self { class Context { - @Dependency var hud: HUD @Dependency var service: BackupService + @Dependency var hudController: HUDController @Dependency var coordinator: BackupCoordinating } @@ -45,9 +44,9 @@ extension BackupConfigViewModel { return .init( didTapBackupNow: { context.service.didForceBackup() - context.hud.update(with: .on) + context.hudController.show() DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { - context.hud.update(with: .none) + context.hudController.dismiss() } }, didChooseWifiOnly: context.service.didSetWiFiOnly(enabled:), @@ -61,10 +60,12 @@ extension BackupConfigViewModel { context.coordinator.toPassphrase(from: controller, cancelClosure: { context.service.toggle(service: service, enabling: false) }, passphraseClosure: { passphrase in - context.hud.update(with: .onTitle("Initializing and securing your backup file will take few seconds, please keep the app open.")) + context.hudController.show(.init( + content: "Initializing and securing your backup file will take few seconds, please keep the app open." + )) context.service.toggle(service: service, enabling: enabling) context.service.initializeBackup(passphrase: passphrase) - context.hud.update(with: .none) + context.hudController.dismiss() }) }, didTapService: { service, controller in diff --git a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift index 9b113c9f..109bc6ef 100644 --- a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift @@ -1,12 +1,12 @@ import UIKit - -import HUD import Shout import Socket +import Shared import Combine import Foundation import CloudFiles import CloudFilesSFTP +import DependencyInjection struct SFTPViewState { var host: String = "" @@ -16,9 +16,7 @@ struct SFTPViewState { } final class BackupSFTPViewModel { - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } + @Dependency var hudController: HUDController var statePublisher: AnyPublisher<SFTPViewState, Never> { stateSubject.eraseToAnyPublisher() @@ -28,7 +26,6 @@ final class BackupSFTPViewModel { authSubject.eraseToAnyPublisher() } - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) private let authSubject = PassthroughSubject<(String, String, String), Never>() @@ -48,7 +45,7 @@ final class BackupSFTPViewModel { } func didTapLogin() { - hudSubject.send(.on) + hudController.show() let host = stateSubject.value.host let username = stateSubject.value.username @@ -67,7 +64,7 @@ final class BackupSFTPViewModel { ).link(anyController) { switch $0 { case .success: - self.hudSubject.send(.none) + self.hudController.dismiss() self.authSubject.send((host, username, password)) case .failure(let error): var message = "An error occurred while trying to link SFTP: " @@ -84,11 +81,11 @@ final class BackupSFTPViewModel { message.append(error.localizedDescription) } - self.hudSubject.send(.error(.init(content: message))) + self.hudController.show(.init(content: message)) } } } catch { - self.hudSubject.send(.error(.init(with: error))) + self.hudController.show(.init(error: error)) } } } diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift index 5bd1504e..9f8b464a 100644 --- a/Sources/ChatFeature/Controllers/GroupChatController.swift +++ b/Sources/ChatFeature/Controllers/GroupChatController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -20,7 +19,6 @@ typealias OutgoingFailedGroupTextCell = CollectionCell<FlexibleSpace, StackMessa typealias OutgoingFailedGroupReplyCell = CollectionCell<FlexibleSpace, ReplyStackMessageView> public final class GroupChatController: UIViewController { - @Dependency var hud: HUD @Dependency var database: Database @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: ChatCoordinating @@ -181,11 +179,6 @@ public final class GroupChatController: UIViewController { } }.store(in: &cancellables) - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - viewModel.reportPopupPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] contact in diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 5ad2e83d..a356dfa2 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -24,7 +23,6 @@ extension Message: Differentiable { } public final class SingleChatController: UIViewController { - @Dependency var hud: HUD @Dependency var logger: XXLogger @Dependency var voxophone: Voxophone @Dependency var barStylist: StatusBarStylist @@ -240,11 +238,6 @@ public final class SingleChatController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - sheet.actionPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in diff --git a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift index 2265930f..4a7c0e9e 100644 --- a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -15,317 +14,313 @@ import struct XXModels.Message import XXClient enum GroupChatNavigationRoutes: Equatable { - case waitingRound - case webview(String) + case waitingRound + case webview(String) } final class GroupChatViewModel { - @Dependency var database: Database - @Dependency var sendReport: SendReport - @Dependency var groupManager: GroupChat - @Dependency var messenger: Messenger - @Dependency var reportingStatus: ReportingStatus - @Dependency var toastController: ToastController - - @KeyObject(.username, defaultValue: nil) var username: String? - - var myId: Data { - try! messenger.e2e.get()!.getContact().getId() - } - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var reportPopupPublisher: AnyPublisher<XXModels.Contact, Never> { - reportPopupSubject.eraseToAnyPublisher() - } - - var replyPublisher: AnyPublisher<(String, String), Never> { - replySubject.eraseToAnyPublisher() - } + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var sendReport: SendReport + @Dependency var groupManager: GroupChat + @Dependency var hudController: HUDController + @Dependency var reportingStatus: ReportingStatus + @Dependency var toastController: ToastController + + @KeyObject(.username, defaultValue: nil) var username: String? + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var reportPopupPublisher: AnyPublisher<XXModels.Contact, Never> { + reportPopupSubject.eraseToAnyPublisher() + } + + var replyPublisher: AnyPublisher<(String, String), Never> { + replySubject.eraseToAnyPublisher() + } + + var routesPublisher: AnyPublisher<GroupChatNavigationRoutes, Never> { + routesSubject.eraseToAnyPublisher() + } + + let info: GroupInfo + private var stagedReply: Reply? + private var cancellables = Set<AnyCancellable>() + private let reportPopupSubject = PassthroughSubject<XXModels.Contact, Never>() + private let replySubject = PassthroughSubject<(String, String), Never>() + private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>() + + var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { + database.fetchMessagesPublisher(.init(chat: .group(info.group.id))) + .replaceError(with: []) + .map { messages -> [ArraySection<ChatSection, Message>] in + let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in + let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) + return Calendar.current.date(from: components)! + } - var routesPublisher: AnyPublisher<GroupChatNavigationRoutes, Never> { - routesSubject.eraseToAnyPublisher() + return groupedByDate + .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } + .sorted(by: { $0.model.date < $1.model.date }) + } + .map { sections -> [ArraySection<ChatSection, Message>] in + var snapshot = [ArraySection<ChatSection, Message>]() + sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } + return snapshot + }.eraseToAnyPublisher() + } + + init(_ info: GroupInfo) { + self.info = info + } + + func readAll() { + let assignment = Message.Assignments(isUnread: false) + let query = Message.Query(chat: .group(info.group.id)) + _ = try? database.bulkUpdateMessages(query, assignment) + } + + func didRequestDelete(_ messages: [Message]) { + _ = try? database.deleteMessages(.init(id: Set(messages.map(\.id)))) + } + + func didRequestReport(_ message: Message) { + if let contact = try? database.fetchContacts(.init(id: [message.senderId])).first { + reportPopupSubject.send(contact) } - - let info: GroupInfo - private var stagedReply: Reply? - private var cancellables = Set<AnyCancellable>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let reportPopupSubject = PassthroughSubject<XXModels.Contact, Never>() - private let replySubject = PassthroughSubject<(String, String), Never>() - private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>() - - var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { - database.fetchMessagesPublisher(.init(chat: .group(info.group.id))) - .replaceError(with: []) - .map { messages -> [ArraySection<ChatSection, Message>] in - let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in - let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) - return Calendar.current.date(from: components)! - } - - return groupedByDate - .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } - .sorted(by: { $0.model.date < $1.model.date }) + } + + func send(_ text: String) { + var message = Message( + senderId: myId, + recipientId: nil, + groupId: info.group.id, + date: Date(), + status: .sending, + isUnread: false, + text: text.trimmingCharacters(in: .whitespacesAndNewlines), + replyMessageId: stagedReply?.messageId + ) + + print("") + print("Outgoing GroupMessage:") + print("- groupId: \(info.group.id.base64EncodedString().prefix(10))...") + print("- senderId: \(myId.base64EncodedString().prefix(10))...") + print("- payload.text: \(message.text)") + + do { + message = try database.saveMessage(message) + + let payload = Payload( + text: text.trimmingCharacters(in: .whitespacesAndNewlines), + reply: stagedReply + ).asData() + + let report = try groupManager.send( + groupId: info.group.id, + message: payload + ) + + print("- messageId: \(report.messageId.base64EncodedString().prefix(10))...") + + if let foo = stagedReply { + print("- payload.reply.messageId: \(foo.messageId.base64EncodedString().prefix(10))...") + print("- payload.reply.senderId: \(foo.senderId.base64EncodedString().prefix(10))...") + } else { + print("- payload.reply: ∅") + } + + message.networkId = report.messageId + + try messenger.cMix.get()!.waitForRoundResult( + roundList: try report.encode(), + timeoutMS: 15_000, + callback: .init(handle: { + switch $0 { + case .delivered: + message.status = .sent + if let foo = try? self.database.saveMessage(message) { + message = foo } - .map { sections -> [ArraySection<ChatSection, Message>] in - var snapshot = [ArraySection<ChatSection, Message>]() - sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } - return snapshot - }.eraseToAnyPublisher() - } - - init(_ info: GroupInfo) { - self.info = info - } - - func readAll() { - let assignment = Message.Assignments(isUnread: false) - let query = Message.Query(chat: .group(info.group.id)) - _ = try? database.bulkUpdateMessages(query, assignment) - } - - func didRequestDelete(_ messages: [Message]) { - _ = try? database.deleteMessages(.init(id: Set(messages.map(\.id)))) - } - - func didRequestReport(_ message: Message) { - if let contact = try? database.fetchContacts(.init(id: [message.senderId])).first { - reportPopupSubject.send(contact) - } - } - - func send(_ text: String) { - var message = Message( - senderId: myId, - recipientId: nil, - groupId: info.group.id, - date: Date(), - status: .sending, - isUnread: false, - text: text.trimmingCharacters(in: .whitespacesAndNewlines), - replyMessageId: stagedReply?.messageId - ) - - print("") - print("Outgoing GroupMessage:") - print("- groupId: \(info.group.id.base64EncodedString().prefix(10))...") - print("- senderId: \(myId.base64EncodedString().prefix(10))...") - print("- payload.text: \(message.text)") - - do { - message = try database.saveMessage(message) - - let payload = Payload( - text: text.trimmingCharacters(in: .whitespacesAndNewlines), - reply: stagedReply - ).asData() - - let report = try groupManager.send( - groupId: info.group.id, - message: payload - ) - - print("- messageId: \(report.messageId.base64EncodedString().prefix(10))...") - - if let foo = stagedReply { - print("- payload.reply.messageId: \(foo.messageId.base64EncodedString().prefix(10))...") - print("- payload.reply.senderId: \(foo.senderId.base64EncodedString().prefix(10))...") + case .notDelivered(timedOut: let timedOut): + if timedOut { + message.status = .sendingTimedOut } else { - print("- payload.reply: ∅") + message.status = .sendingFailed } - message.networkId = report.messageId - - try messenger.cMix.get()!.waitForRoundResult( - roundList: try report.encode(), - timeoutMS: 15_000, - callback: .init(handle: { - switch $0 { - case .delivered: - message.status = .sent - if let foo = try? self.database.saveMessage(message) { - message = foo - } - case .notDelivered(timedOut: let timedOut): - if timedOut { - message.status = .sendingTimedOut - } else { - message.status = .sendingFailed - } - - if let foo = try? self.database.saveMessage(message) { - message = foo - } - } - }) - ) - - print("") - - message.roundURL = report.roundURL - message.date = Date.fromTimestamp(Int(report.timestamp)) - message = try database.saveMessage(message) - } catch { - message.status = .sendingFailed - if let foo = try? database.saveMessage(message) { - message = foo + if let foo = try? self.database.saveMessage(message) { + message = foo } - } - - stagedReply = nil + } + }) + ) + + print("") + + message.roundURL = report.roundURL + message.date = Date.fromTimestamp(Int(report.timestamp)) + message = try database.saveMessage(message) + } catch { + message.status = .sendingFailed + if let foo = try? database.saveMessage(message) { + message = foo + } } - func retry(_ message: Message) { - var message = message - - do { - message.status = .sending - message = try database.saveMessage(message) - - var reply: Reply? - - if let replyId = message.replyMessageId { - reply = Reply(messageId: replyId, senderId: myId) + stagedReply = nil + } + + func retry(_ message: Message) { + var message = message + + do { + message.status = .sending + message = try database.saveMessage(message) + + var reply: Reply? + + if let replyId = message.replyMessageId { + reply = Reply(messageId: replyId, senderId: myId) + } + + let report = try groupManager.send( + groupId: message.groupId!, + message: Payload( + text: message.text, + reply: reply + ).asData() + ) + + try messenger.cMix.get()!.waitForRoundResult( + roundList: try report.encode(), + timeoutMS: 15_000, + callback: .init(handle: { + switch $0 { + case .delivered: + message.status = .sent + if let foo = try? self.database.saveMessage(message) { + message = foo } - - let report = try groupManager.send( - groupId: message.groupId!, - message: Payload( - text: message.text, - reply: reply - ).asData() - ) - - try messenger.cMix.get()!.waitForRoundResult( - roundList: try report.encode(), - timeoutMS: 15_000, - callback: .init(handle: { - switch $0 { - case .delivered: - message.status = .sent - if let foo = try? self.database.saveMessage(message) { - message = foo - } - case .notDelivered(timedOut: let timedOut): - if timedOut { - message.status = .sendingTimedOut - } else { - message.status = .sendingFailed - } - - if let foo = try? self.database.saveMessage(message) { - message = foo - } - } - }) - ) - - message.networkId = report.messageId - message.date = Date.fromTimestamp(Int(report.timestamp)) - message = try database.saveMessage(message) - } catch { - message.status = .sendingFailed - if let foo = try? database.saveMessage(message) { - message = foo + case .notDelivered(timedOut: let timedOut): + if timedOut { + message.status = .sendingTimedOut + } else { + message.status = .sendingFailed } - } - } - func showRoundFrom(_ roundURL: String?) { - if let urlString = roundURL, !urlString.isEmpty { - routesSubject.send(.webview(urlString)) - } else { - routesSubject.send(.waitingRound) - } + if let foo = try? self.database.saveMessage(message) { + message = foo + } + } + }) + ) + + message.networkId = report.messageId + message.date = Date.fromTimestamp(Int(report.timestamp)) + message = try database.saveMessage(message) + } catch { + message.status = .sendingFailed + if let foo = try? database.saveMessage(message) { + message = foo + } } + } - func abortReply() { - stagedReply = nil + func showRoundFrom(_ roundURL: String?) { + if let urlString = roundURL, !urlString.isEmpty { + routesSubject.send(.webview(urlString)) + } else { + routesSubject.send(.waitingRound) } + } - func getReplyContent(for messageId: Data) -> (String, String) { - guard let message = try? database.fetchMessages(.init(networkId: messageId)).first else { - return ("[DELETED]", "[DELETED]") - } + func abortReply() { + stagedReply = nil + } - return (getName(from: message.senderId), message.text) + func getReplyContent(for messageId: Data) -> (String, String) { + guard let message = try? database.fetchMessages(.init(networkId: messageId)).first else { + return ("[DELETED]", "[DELETED]") } - func getName(from senderId: Data) -> String { - guard senderId != myId else { return "You" } + return (getName(from: message.senderId), message.text) + } - guard let contact = try? database.fetchContacts(.init(id: [senderId])).first else { - return "[DELETED]" - } - - var name = (contact.nickname ?? contact.username) ?? "Fetching username..." - - if contact.isBlocked, reportingStatus.isEnabled() { - name = "\(name) (Blocked)" - } + func getName(from senderId: Data) -> String { + guard senderId != myId else { return "You" } - return name + guard let contact = try? database.fetchContacts(.init(id: [senderId])).first else { + return "[DELETED]" } - func didRequestReply(_ message: Message) { - guard let networkId = message.networkId else { return } - stagedReply = Reply(messageId: networkId, senderId: message.senderId) - replySubject.send(getReplyContent(for: networkId)) - } + var name = (contact.nickname ?? contact.username) ?? "Fetching username..." - func report(contact: XXModels.Contact, screenshot: UIImage, completion: @escaping () -> Void) { - let report = Report( - sender: .init( - userId: contact.id.base64EncodedString(), - username: contact.username! - ), - recipient: .init( - userId: myId.base64EncodedString(), - username: username! - ), - type: .group, - screenshot: screenshot.pngData()!, - partyName: info.group.name, - partyBlob: info.group.id.base64EncodedString(), - partyMembers: info.members.map { Report.ReportUser( - userId: $0.id.base64EncodedString(), - username: $0.username ?? "") - } - ) - - hudSubject.send(.on) - sendReport(report) { result in - switch result { - case .failure(let error): - DispatchQueue.main.async { - self.hudSubject.send(.error(.init(with: error))) - } - - case .success(_): - self.blockContact(contact) - DispatchQueue.main.async { - self.hudSubject.send(.none) - self.presentReportConfirmation(contact: contact) - completion() - } - } - } + if contact.isBlocked, reportingStatus.isEnabled() { + name = "\(name) (Blocked)" } - private func blockContact(_ contact: XXModels.Contact) { - var contact = contact - contact.isBlocked = true - _ = try? database.saveContact(contact) - } + return name + } + + func didRequestReply(_ message: Message) { + guard let networkId = message.networkId else { return } + stagedReply = Reply(messageId: networkId, senderId: message.senderId) + replySubject.send(getReplyContent(for: networkId)) + } + + func report(contact: XXModels.Contact, screenshot: UIImage, completion: @escaping () -> Void) { + let report = Report( + sender: .init( + userId: contact.id.base64EncodedString(), + username: contact.username! + ), + recipient: .init( + userId: myId.base64EncodedString(), + username: username! + ), + type: .group, + screenshot: screenshot.pngData()!, + partyName: info.group.name, + partyBlob: info.group.id.base64EncodedString(), + partyMembers: info.members.map { Report.ReportUser( + userId: $0.id.base64EncodedString(), + username: $0.username ?? "") + } + ) + + hudController.show() + sendReport(report) { result in + switch result { + case .failure(let error): + DispatchQueue.main.async { + self.hudController.show(.init(error: error)) + } - private func presentReportConfirmation(contact: XXModels.Contact) { - let name = (contact.nickname ?? contact.username) ?? "the contact" - toastController.enqueueToast(model: .init( - title: "Your report has been sent and \(name) is now blocked.", - leftImage: Asset.requestSentToaster.image - )) + case .success(_): + self.blockContact(contact) + DispatchQueue.main.async { + self.hudController.dismiss() + self.presentReportConfirmation(contact: contact) + completion() + } + } } + } + + private func blockContact(_ contact: XXModels.Contact) { + var contact = contact + contact.isBlocked = true + _ = try? database.saveContact(contact) + } + + private func presentReportConfirmation(contact: XXModels.Contact) { + let name = (contact.nickname ?? contact.username) ?? "the contact" + toastController.enqueueToast(model: .init( + title: "Your report has been sent and \(name) is now blocked.", + leftImage: Asset.requestSentToaster.image + )) + } } diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 58757d2f..cd1a6f4b 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -20,503 +19,501 @@ import struct XXModels.FileTransfer import NetworkMonitor enum SingleChatNavigationRoutes: Equatable { - case none - case camera - case library - case waitingRound - case cameraPermission - case libraryPermission - case microphonePermission - case webview(String) + case none + case camera + case library + case waitingRound + case cameraPermission + case libraryPermission + case microphonePermission + case webview(String) } final class SingleChatViewModel: NSObject { - @Dependency var logger: XXLogger - @Dependency var database: Database - @Dependency var sendReport: SendReport - @Dependency var messenger: Messenger - @Dependency var permissions: PermissionHandling - @Dependency var toastController: ToastController - @Dependency var networkMonitor: NetworkMonitoring - @Dependency var transferManager: XXClient.FileTransfer - - @KeyObject(.username, defaultValue: nil) var username: String? - - var contact: XXModels.Contact { contactSubject.value } - private var stagedReply: Reply? - private var cancellables = Set<AnyCancellable>() - private let contactSubject: CurrentValueSubject<XXModels.Contact, Never> - private let replySubject = PassthroughSubject<(String, String), Never>() - private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() - private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([]) - private let reportPopupSubject = PassthroughSubject<Void, Never>() - - private var healthCancellable: XXClient.Cancellable? - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var isOnline: AnyPublisher<Bool, Never> { - networkMonitor - .statusPublisher - .map { $0 == .available } - .eraseToAnyPublisher() - } - - - var myId: Data { - try! messenger.e2e.get()!.getContact().getId() - } - - var contactPublisher: AnyPublisher<XXModels.Contact, Never> { contactSubject.eraseToAnyPublisher() } - var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() } - var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } - var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } - - var reportPopupPublisher: AnyPublisher<Void, Never> { - reportPopupSubject.eraseToAnyPublisher() - } - - var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { - sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in - var snapshot = [ArraySection<ChatSection, Message>]() - sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } - return snapshot - }.eraseToAnyPublisher() - } - - private func updateRecentState(_ contact: XXModels.Contact) { - if contact.isRecent == true { - var contact = contact - contact.isRecent = false - _ = try? database.saveContact(contact) - } - } - - func viewDidAppear() { - updateRecentState(contact) - } - - init(_ contact: XXModels.Contact) { - self.contactSubject = .init(contact) - super.init() - - updateRecentState(contact) - - database.fetchContactsPublisher(Contact.Query(id: [contact.id])) - .replaceError(with: []) - .compactMap { $0.first } - .sink { [unowned self] in contactSubject.send($0) } - .store(in: &cancellables) - - database.fetchMessagesPublisher(.init(chat: .direct(myId, contact.id))) - .replaceError(with: []) - .map { - let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in - let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) - return Calendar.current.date(from: components)! - } - - return groupedByDate - .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } - .sorted(by: { $0.model.date < $1.model.date }) - }.receive(on: DispatchQueue.main) - .sink { [unowned self] in sectionsRelay.send($0) } - .store(in: &cancellables) - - healthCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in - guard let self = self else { return } - self.networkMonitor.update($0) - })) - } - - // MARK: Public - - func getFileTransferWith(id: Data) -> FileTransfer { - guard let transfer = try? database.fetchFileTransfers(.init(id: [id])).first else { - fatalError() - } - - return transfer + @Dependency var logger: XXLogger + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var sendReport: SendReport + @Dependency var hudController: HUDController + @Dependency var permissions: PermissionHandling + @Dependency var toastController: ToastController + @Dependency var networkMonitor: NetworkMonitoring + @Dependency var transferManager: XXClient.FileTransfer + + @KeyObject(.username, defaultValue: nil) var username: String? + + var contact: XXModels.Contact { contactSubject.value } + private var stagedReply: Reply? + private var cancellables = Set<AnyCancellable>() + private let contactSubject: CurrentValueSubject<XXModels.Contact, Never> + private let replySubject = PassthroughSubject<(String, String), Never>() + private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() + private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([]) + private let reportPopupSubject = PassthroughSubject<Void, Never>() + + private var healthCancellable: XXClient.Cancellable? + + var isOnline: AnyPublisher<Bool, Never> { + networkMonitor + .statusPublisher + .map { $0 == .available } + .eraseToAnyPublisher() + } + + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var contactPublisher: AnyPublisher<XXModels.Contact, Never> { contactSubject.eraseToAnyPublisher() } + var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() } + var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } + var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } + + var reportPopupPublisher: AnyPublisher<Void, Never> { + reportPopupSubject.eraseToAnyPublisher() + } + + var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { + sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in + var snapshot = [ArraySection<ChatSection, Message>]() + sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } + return snapshot + }.eraseToAnyPublisher() + } + + private func updateRecentState(_ contact: XXModels.Contact) { + if contact.isRecent == true { + var contact = contact + contact.isRecent = false + _ = try? database.saveContact(contact) } - - func didSendAudio(url: URL) { - do { - let _ = try transferManager.send( - params: .init( - payload: .init( - name: "", - type: "", - preview: Data(), - contents: Data() - ), - recipientId: contact.id, - retry: 1, - period: 1_000 - ), - callback: .init(handle: { - switch $0 { - case .success(let progressCallback): - print(progressCallback.progress.total) - case .failure(let error): - print(error.localizedDescription) - } - }) - ) - - // transferId - } catch { - - } - } - - func didSend(image: UIImage) { - guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } - hudRelay.send(.on) - - let transferName = UUID().uuidString - - do { - let tid = try transferManager.send( - params: .init( - payload: .init( - name: transferName, - type: "jpeg", - preview: Data(), - contents: imageData - ), - recipientId: contact.id, - retry: 10, - period: 1_000 - ), - callback: .init(handle: { - switch $0 { - case .success(let progressCallback): - - if progressCallback.progress.completed { - print(">>> Outgoing transfer finished successfully") - } else { - print(">>> Outgoing transfer. (\(progressCallback.progress.transmitted)/\(progressCallback.progress.total))") - } - - /// THIS IS TOO COMPLEX, NEEDS HELP FROM DARIUSZ - - case .failure(let error): - print(">>> Transfer.error: \(error.localizedDescription)") - } - }) - ) - - let transferModel = FileTransfer( - id: tid, - contactId: contact.id, - name: transferName, - type: "jpeg", - data: imageData, - progress: 0.0, - isIncoming: false, - createdAt: Date() - ) - - let transferMessage = Message( - senderId: myId, - recipientId: contact.id, - groupId: nil, - date: Date(), - status: .sending, - isUnread: false, - text: "", - replyMessageId: nil, - roundURL: nil, - fileTransferId: tid - ) - - try database.saveFileTransfer(transferModel) - try database.saveMessage(transferMessage) - - hudRelay.send(.none) - } catch { - hudRelay.send(.error(.init(with: error))) + } + + func viewDidAppear() { + updateRecentState(contact) + } + + init(_ contact: XXModels.Contact) { + self.contactSubject = .init(contact) + super.init() + + updateRecentState(contact) + + database.fetchContactsPublisher(Contact.Query(id: [contact.id])) + .replaceError(with: []) + .compactMap { $0.first } + .sink { [unowned self] in contactSubject.send($0) } + .store(in: &cancellables) + + database.fetchMessagesPublisher(.init(chat: .direct(myId, contact.id))) + .replaceError(with: []) + .map { + let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in + let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) + return Calendar.current.date(from: components)! } + + return groupedByDate + .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } + .sorted(by: { $0.model.date < $1.model.date }) + }.receive(on: DispatchQueue.main) + .sink { [unowned self] in sectionsRelay.send($0) } + .store(in: &cancellables) + + healthCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in + guard let self = self else { return } + self.networkMonitor.update($0) + })) + } + + // MARK: Public + + func getFileTransferWith(id: Data) -> FileTransfer { + guard let transfer = try? database.fetchFileTransfers(.init(id: [id])).first else { + fatalError() } - - func readAll() { - let assignment = Message.Assignments(isUnread: false) - let query = Message.Query(chat: .direct(myId, contact.id)) - _ = try? database.bulkUpdateMessages(query, assignment) - } - - func didRequestDeleteAll() { - _ = try? database.deleteMessages(.init(chat: .direct(myId, contact.id))) - } - - func didRequestRetry(_ message: Message) { - var message = message - - do { - message.status = .sending - message = try database.saveMessage(message) - - var reply: Reply? - - if let replyId = message.replyMessageId { - reply = Reply(messageId: replyId, senderId: myId) - } - - let report = try messenger.e2e.get()!.send( - messageType: 2, - recipientId: contact.id, - payload: Payload( - text: message.text, - reply: reply - ).asData(), - e2eParams: GetE2EParams.liveDefault() - ) - - try messenger.cMix.get()!.waitForRoundResult( - roundList: try report.encode(), - timeoutMS: 15_000, - callback: .init(handle: { - switch $0 { - case .delivered: - message.status = .sent - _ = try? self.database.saveMessage(message) - - case .notDelivered(timedOut: let timedOut): - if timedOut { - message.status = .sendingTimedOut - } else { - message.status = .sendingFailed - } - - _ = try? self.database.saveMessage(message) - } - }) - ) - - message.roundURL = report.roundURL - message.networkId = report.messageId - if let timestamp = report.timestamp { - message.date = Date.fromTimestamp(Int(timestamp)) - } - - message = try database.saveMessage(message) - } catch { + + return transfer + } + + func didSendAudio(url: URL) { + do { + let _ = try transferManager.send( + params: .init( + payload: .init( + name: "", + type: "", + preview: Data(), + contents: Data() + ), + recipientId: contact.id, + retry: 1, + period: 1_000 + ), + callback: .init(handle: { + switch $0 { + case .success(let progressCallback): + print(progressCallback.progress.total) + case .failure(let error): print(error.localizedDescription) - message.status = .sendingFailed - _ = try? database.saveMessage(message) - } - } - - func didNavigateSomewhere() { - navigationRoutes.send(.none) + } + }) + ) + + // transferId + } catch { + } - - @discardableResult - func didTest(permission: PermissionType) -> Bool { - switch permission { - case .camera: - if permissions.isCameraAllowed { - navigationRoutes.send(.camera) - } else { - navigationRoutes.send(.cameraPermission) - } - case .library: - if permissions.isPhotosAllowed { - navigationRoutes.send(.library) + } + + func didSend(image: UIImage) { + guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } + hudController.show() + + let transferName = UUID().uuidString + + do { + let tid = try transferManager.send( + params: .init( + payload: .init( + name: transferName, + type: "jpeg", + preview: Data(), + contents: imageData + ), + recipientId: contact.id, + retry: 10, + period: 1_000 + ), + callback: .init(handle: { + switch $0 { + case .success(let progressCallback): + + if progressCallback.progress.completed { + print(">>> Outgoing transfer finished successfully") } else { - navigationRoutes.send(.libraryPermission) + print(">>> Outgoing transfer. (\(progressCallback.progress.transmitted)/\(progressCallback.progress.total))") } - case .microphone: - if permissions.isMicrophoneAllowed { - return true + + /// THIS IS TOO COMPLEX, NEEDS HELP FROM DARIUSZ + + case .failure(let error): + print(">>> Transfer.error: \(error.localizedDescription)") + } + }) + ) + + let transferModel = FileTransfer( + id: tid, + contactId: contact.id, + name: transferName, + type: "jpeg", + data: imageData, + progress: 0.0, + isIncoming: false, + createdAt: Date() + ) + + let transferMessage = Message( + senderId: myId, + recipientId: contact.id, + groupId: nil, + date: Date(), + status: .sending, + isUnread: false, + text: "", + replyMessageId: nil, + roundURL: nil, + fileTransferId: tid + ) + + try database.saveFileTransfer(transferModel) + try database.saveMessage(transferMessage) + + hudController.dismiss() + } catch { + hudController.show(.init(error: error)) + } + } + + func readAll() { + let assignment = Message.Assignments(isUnread: false) + let query = Message.Query(chat: .direct(myId, contact.id)) + _ = try? database.bulkUpdateMessages(query, assignment) + } + + func didRequestDeleteAll() { + _ = try? database.deleteMessages(.init(chat: .direct(myId, contact.id))) + } + + func didRequestRetry(_ message: Message) { + var message = message + + do { + message.status = .sending + message = try database.saveMessage(message) + + var reply: Reply? + + if let replyId = message.replyMessageId { + reply = Reply(messageId: replyId, senderId: myId) + } + + let report = try messenger.e2e.get()!.send( + messageType: 2, + recipientId: contact.id, + payload: Payload( + text: message.text, + reply: reply + ).asData(), + e2eParams: GetE2EParams.liveDefault() + ) + + try messenger.cMix.get()!.waitForRoundResult( + roundList: try report.encode(), + timeoutMS: 15_000, + callback: .init(handle: { + switch $0 { + case .delivered: + message.status = .sent + _ = try? self.database.saveMessage(message) + + case .notDelivered(timedOut: let timedOut): + if timedOut { + message.status = .sendingTimedOut } else { - navigationRoutes.send(.microphonePermission) + message.status = .sendingFailed } - } - - return false - } - - func didRequestCopy(_ model: Message) { - UIPasteboard.general.string = model.text - } - - func didRequestDeleteSingle(_ model: Message) { - didRequestDelete([model]) - } - - func didRequestReport(_: Message) { - reportPopupSubject.send() + + _ = try? self.database.saveMessage(message) + } + }) + ) + + message.roundURL = report.roundURL + message.networkId = report.messageId + if let timestamp = report.timestamp { + message.date = Date.fromTimestamp(Int(timestamp)) + } + + message = try database.saveMessage(message) + } catch { + print(error.localizedDescription) + message.status = .sendingFailed + _ = try? database.saveMessage(message) } - - func abortReply() { - stagedReply = nil + } + + func didNavigateSomewhere() { + navigationRoutes.send(.none) + } + + @discardableResult + func didTest(permission: PermissionType) -> Bool { + switch permission { + case .camera: + if permissions.isCameraAllowed { + navigationRoutes.send(.camera) + } else { + navigationRoutes.send(.cameraPermission) + } + case .library: + if permissions.isPhotosAllowed { + navigationRoutes.send(.library) + } else { + navigationRoutes.send(.libraryPermission) + } + case .microphone: + if permissions.isMicrophoneAllowed { + return true + } else { + navigationRoutes.send(.microphonePermission) + } } - - func send(_ string: String) { - var message: Message = .init( - senderId: myId, - recipientId: contact.id, - groupId: nil, - date: Date(), - status: .sending, - isUnread: false, - text: string.trimmingCharacters(in: .whitespacesAndNewlines), - replyMessageId: stagedReply?.messageId + + return false + } + + func didRequestCopy(_ model: Message) { + UIPasteboard.general.string = model.text + } + + func didRequestDeleteSingle(_ model: Message) { + didRequestDelete([model]) + } + + func didRequestReport(_: Message) { + reportPopupSubject.send() + } + + func abortReply() { + stagedReply = nil + } + + func send(_ string: String) { + var message: Message = .init( + senderId: myId, + recipientId: contact.id, + groupId: nil, + date: Date(), + status: .sending, + isUnread: false, + text: string.trimmingCharacters(in: .whitespacesAndNewlines), + replyMessageId: stagedReply?.messageId + ) + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + do { + message = try self.database.saveMessage(message) + + let report = try self.messenger.e2e.get()!.send( + messageType: 2, + recipientId: self.contact.id, + payload: Payload(text: message.text, reply: self.stagedReply).asData(), + e2eParams: GetE2EParams.liveDefault() ) - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - do { - message = try self.database.saveMessage(message) - - let report = try self.messenger.e2e.get()!.send( - messageType: 2, - recipientId: self.contact.id, - payload: Payload(text: message.text, reply: self.stagedReply).asData(), - e2eParams: GetE2EParams.liveDefault() - ) - - try self.messenger.cMix.get()!.waitForRoundResult( - roundList: try report.encode(), - timeoutMS: 15_000, - callback: .init(handle: { - switch $0 { - case .delivered: - message.status = .sent - _ = try? self.database.saveMessage(message) - - case .notDelivered(timedOut: let timedOut): - if timedOut { - message.status = .sendingTimedOut - } else { - message.status = .sendingFailed - } - - _ = try? self.database.saveMessage(message) - } - }) - ) - - message.roundURL = report.roundURL - message.networkId = report.messageId - if let timestamp = report.timestamp { - message.date = Date.fromTimestamp(Int(timestamp)) - } - - message = try self.database.saveMessage(message) - } catch { - print(error.localizedDescription) + + try self.messenger.cMix.get()!.waitForRoundResult( + roundList: try report.encode(), + timeoutMS: 15_000, + callback: .init(handle: { + switch $0 { + case .delivered: + message.status = .sent + _ = try? self.database.saveMessage(message) + + case .notDelivered(timedOut: let timedOut): + if timedOut { + message.status = .sendingTimedOut + } else { message.status = .sendingFailed - _ = try? self.database.saveMessage(message) + } + + _ = try? self.database.saveMessage(message) } - - self.stagedReply = nil - } - } - - func didRequestReply(_ message: Message) { - guard let networkId = message.networkId else { return } - - let senderTitle: String = { - if message.senderId == myId { - return "You" - } else { - return (contact.nickname ?? contact.username) ?? "Fetching username..." - } - }() - - replySubject.send((senderTitle, message.text)) - stagedReply = Reply(messageId: networkId, senderId: message.senderId) - } - - func getReplyContent(for messageId: Data) -> (String, String) { - guard let message = try? database.fetchMessages(.init(networkId: messageId)).first else { - return ("[DELETED]", "[DELETED]") - } - - guard let contact = try? database.fetchContacts(.init(id: [message.senderId])).first else { - fatalError() + }) + ) + + message.roundURL = report.roundURL + message.networkId = report.messageId + if let timestamp = report.timestamp { + message.date = Date.fromTimestamp(Int(timestamp)) } - - let contactTitle = (contact.nickname ?? contact.username) ?? "You" - return (contactTitle, message.text) + + message = try self.database.saveMessage(message) + } catch { + print(error.localizedDescription) + message.status = .sendingFailed + _ = try? self.database.saveMessage(message) + } + + self.stagedReply = nil } - - func showRoundFrom(_ roundURL: String?) { - if let urlString = roundURL, !urlString.isEmpty { - navigationRoutes.send(.webview(urlString)) - } else { - navigationRoutes.send(.waitingRound) - } + } + + func didRequestReply(_ message: Message) { + guard let networkId = message.networkId else { return } + + let senderTitle: String = { + if message.senderId == myId { + return "You" + } else { + return (contact.nickname ?? contact.username) ?? "Fetching username..." + } + }() + + replySubject.send((senderTitle, message.text)) + stagedReply = Reply(messageId: networkId, senderId: message.senderId) + } + + func getReplyContent(for messageId: Data) -> (String, String) { + guard let message = try? database.fetchMessages(.init(networkId: messageId)).first else { + return ("[DELETED]", "[DELETED]") } - - func didRequestDelete(_ items: [Message]) { - _ = try? database.deleteMessages(.init(id: Set(items.compactMap(\.id)))) + + guard let contact = try? database.fetchContacts(.init(id: [message.senderId])).first else { + fatalError() } - - func itemWith(id: Int64) -> Message? { - sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id }) + + let contactTitle = (contact.nickname ?? contact.username) ?? "You" + return (contactTitle, message.text) + } + + func showRoundFrom(_ roundURL: String?) { + if let urlString = roundURL, !urlString.isEmpty { + navigationRoutes.send(.webview(urlString)) + } else { + navigationRoutes.send(.waitingRound) } - - func itemAt(indexPath: IndexPath) -> Message? { - guard sectionsRelay.value.count > indexPath.section else { return nil } - - let items = sectionsRelay.value[indexPath.section].elements - return items.count > indexPath.row ? items[indexPath.row] : nil - } - - func section(at index: Int) -> ChatSection? { - sectionsRelay.value.count > 0 ? sectionsRelay.value[index].model : nil - } - - func report(screenshot: UIImage, completion: @escaping (Bool) -> Void) { - let report = Report( - sender: .init( - userId: contact.id.base64EncodedString(), - username: contact.username! - ), - recipient: .init( - userId: myId.base64EncodedString(), - username: username! - ), - type: .dm, - screenshot: screenshot.pngData()! - ) - - hudRelay.send(.on) - sendReport(report) { result in - switch result { - case .failure(let error): - DispatchQueue.main.async { - self.hudRelay.send(.error(.init(with: error))) - completion(false) - } - - case .success(_): - self.blockContact() - DispatchQueue.main.async { - self.hudRelay.send(.none) - self.presentReportConfirmation() - completion(true) - } - } + } + + func didRequestDelete(_ items: [Message]) { + _ = try? database.deleteMessages(.init(id: Set(items.compactMap(\.id)))) + } + + func itemWith(id: Int64) -> Message? { + sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id }) + } + + func itemAt(indexPath: IndexPath) -> Message? { + guard sectionsRelay.value.count > indexPath.section else { return nil } + + let items = sectionsRelay.value[indexPath.section].elements + return items.count > indexPath.row ? items[indexPath.row] : nil + } + + func section(at index: Int) -> ChatSection? { + sectionsRelay.value.count > 0 ? sectionsRelay.value[index].model : nil + } + + func report(screenshot: UIImage, completion: @escaping (Bool) -> Void) { + let report = Report( + sender: .init( + userId: contact.id.base64EncodedString(), + username: contact.username! + ), + recipient: .init( + userId: myId.base64EncodedString(), + username: username! + ), + type: .dm, + screenshot: screenshot.pngData()! + ) + + hudController.show() + sendReport(report) { result in + switch result { + case .failure(let error): + DispatchQueue.main.async { + self.hudController.show(.init(error: error)) + completion(false) } + + case .success(_): + self.blockContact() + DispatchQueue.main.async { + self.hudController.dismiss() + self.presentReportConfirmation() + completion(true) + } + } } - - private func blockContact() { - var contact = contact - contact.isBlocked = true - _ = try? database.saveContact(contact) - } - - private func presentReportConfirmation() { - let name = (contact.nickname ?? contact.username) ?? "the contact" - toastController.enqueueToast(model: .init( - title: "Your report has been sent and \(name) is now blocked.", - leftImage: Asset.requestSentToaster.image - )) - } + } + + private func blockContact() { + var contact = contact + contact.isBlocked = true + _ = try? database.saveContact(contact) + } + + private func presentReportConfirmation() { + let name = (contact.nickname ?? contact.username) ?? "the contact" + toastController.enqueueToast(model: .init( + title: "Your report has been sent and \(name) is now blocked.", + leftImage: Asset.requestSentToaster.image + )) + } } diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index 32b7e99a..f61aef1e 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Models @@ -13,195 +12,191 @@ import struct XXModels.Group import XXClient enum SearchSection { - case chats - case connections + case chats + case connections } enum SearchItem: Equatable, Hashable { - case chat(ChatInfo) - case connection(XXModels.Contact) + case chat(ChatInfo) + case connection(XXModels.Contact) } typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, XXModels.Contact> typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> final class ChatListViewModel { - @Dependency var database: Database - @Dependency var messenger: Messenger - @Dependency var groupManager: GroupChat - @Dependency var reportingStatus: ReportingStatus - - // TO REFACTOR: - var isOnline: AnyPublisher<Bool, Never> { - Just(.init(true)).eraseToAnyPublisher() - } - - var myId: Data { - try! messenger.e2e.get()!.getContact().getId() - } - - var chatsPublisher: AnyPublisher<[ChatInfo], Never> { - chatsSubject.eraseToAnyPublisher() - } - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { - let query = Contact.Query( - authStatus: [.friend], - isRecent: true, - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - return database.fetchContactsPublisher(query) + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var groupManager: GroupChat + @Dependency var hudController: HUDController + @Dependency var reportingStatus: ReportingStatus + + // TO REFACTOR: + var isOnline: AnyPublisher<Bool, Never> { + Just(.init(true)).eraseToAnyPublisher() + } + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var chatsPublisher: AnyPublisher<[ChatInfo], Never> { + chatsSubject.eraseToAnyPublisher() + } + + var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { + let query = Contact.Query( + authStatus: [.friend], + isRecent: true, + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return database.fetchContactsPublisher(query) + .replaceError(with: []) + .map { + let section = SectionId() + var snapshot = RecentsSnapshot() + snapshot.appendSections([section]) + snapshot.appendItems($0, toSection: section) + return snapshot + }.eraseToAnyPublisher() + } + + var searchPublisher: AnyPublisher<SearchSnapshot, Never> { + let contactsQuery = Contact.Query( + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return Publishers.CombineLatest3( + database.fetchContactsPublisher(contactsQuery) .replaceError(with: []) - .map { - let section = SectionId() - var snapshot = RecentsSnapshot() - snapshot.appendSections([section]) - snapshot.appendItems($0, toSection: section) - return snapshot - }.eraseToAnyPublisher() - } - - var searchPublisher: AnyPublisher<SearchSnapshot, Never> { - let contactsQuery = Contact.Query( - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - return Publishers.CombineLatest3( - database.fetchContactsPublisher(contactsQuery) - .replaceError(with: []) - .map { $0.filter { $0.id != self.myId }}, - chatsPublisher, - searchSubject - .removeDuplicates() - .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) - .eraseToAnyPublisher() - ) - .map { (contacts, chats, query) in - let connectionItems = contacts.filter { - let username = $0.username?.lowercased().contains(query.lowercased()) ?? false - let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false - return username || nickname - }.map(SearchItem.connection) - - let chatItems = chats.filter { - switch $0 { - case .group(let group): - return group.name.lowercased().contains(query.lowercased()) - - case .groupChat(let info): - let name = info.group.name.lowercased().contains(query.lowercased()) - let last = info.lastMessage.text.lowercased().contains(query.lowercased()) - return name || last - - case .contactChat(let info): - let username = info.contact.username?.lowercased().contains(query.lowercased()) ?? false - let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false - let lastMessage = info.lastMessage.text.lowercased().contains(query.lowercased()) - return username || nickname || lastMessage - - } - }.map(SearchItem.chat) - - var snapshot = SearchSnapshot() - - if connectionItems.count > 0 { - snapshot.appendSections([.connections]) - snapshot.appendItems(connectionItems, toSection: .connections) - } - - if chatItems.count > 0 { - snapshot.appendSections([.chats]) - snapshot.appendItems(chatItems, toSection: .chats) - } - - return snapshot - }.eraseToAnyPublisher() - } - - var badgeCountPublisher: AnyPublisher<Int, Never> { - let groupQuery = Group.Query(authStatus: [.pending]) - let contactsQuery = Contact.Query( - authStatus: [ - .verified, - .confirming, - .confirmationFailed, - .verificationFailed, - .verificationInProgress - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - return Publishers.CombineLatest( - database.fetchContactsPublisher(contactsQuery).replaceError(with: []), - database.fetchGroupsPublisher(groupQuery).replaceError(with: []) - ) - .map { $0.0.count + $0.1.count } + .map { $0.filter { $0.id != self.myId }}, + chatsPublisher, + searchSubject + .removeDuplicates() + .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) .eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let searchSubject = CurrentValueSubject<String, Never>("") - private let chatsSubject = CurrentValueSubject<[ChatInfo], Never>([]) - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - - init() { - database.fetchChatInfosPublisher( - ChatInfo.Query( - contactChatInfoQuery: .init( - userId: myId, - authStatus: [.friend], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ), - groupChatInfoQuery: GroupChatInfo.Query( - authStatus: [.participating], - excludeBannedContactsMessages: reportingStatus.isEnabled() - ), - groupQuery: Group.Query( - withMessages: false, - authStatus: [.participating] - ) - )) - .replaceError(with: []) - .sink { [unowned self] in chatsSubject.send($0) } - .store(in: &cancellables) - } + ) + .map { (contacts, chats, query) in + let connectionItems = contacts.filter { + let username = $0.username?.lowercased().contains(query.lowercased()) ?? false + let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false + return username || nickname + }.map(SearchItem.connection) + + let chatItems = chats.filter { + switch $0 { + case .group(let group): + return group.name.lowercased().contains(query.lowercased()) + + case .groupChat(let info): + let name = info.group.name.lowercased().contains(query.lowercased()) + let last = info.lastMessage.text.lowercased().contains(query.lowercased()) + return name || last + + case .contactChat(let info): + let username = info.contact.username?.lowercased().contains(query.lowercased()) ?? false + let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false + let lastMessage = info.lastMessage.text.lowercased().contains(query.lowercased()) + return username || nickname || lastMessage - func updateSearch(query: String) { - searchSubject.send(query) - } - - func leave(_ group: Group) { - hudSubject.send(.on) - - do { - try groupManager.leaveGroup(groupId: group.id) - try database.deleteMessages(.init(chat: .group(group.id))) - try database.deleteGroup(group) - hudSubject.send(.none) - } catch { - hudSubject.send(.error(.init(with: error))) } + }.map(SearchItem.chat) + + var snapshot = SearchSnapshot() + + if connectionItems.count > 0 { + snapshot.appendSections([.connections]) + snapshot.appendItems(connectionItems, toSection: .connections) + } + + if chatItems.count > 0 { + snapshot.appendSections([.chats]) + snapshot.appendItems(chatItems, toSection: .chats) + } + + return snapshot + }.eraseToAnyPublisher() + } + + var badgeCountPublisher: AnyPublisher<Int, Never> { + let groupQuery = Group.Query(authStatus: [.pending]) + let contactsQuery = Contact.Query( + authStatus: [ + .verified, + .confirming, + .confirmationFailed, + .verificationFailed, + .verificationInProgress + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + return Publishers.CombineLatest( + database.fetchContactsPublisher(contactsQuery).replaceError(with: []), + database.fetchGroupsPublisher(groupQuery).replaceError(with: []) + ) + .map { $0.0.count + $0.1.count } + .eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let searchSubject = CurrentValueSubject<String, Never>("") + private let chatsSubject = CurrentValueSubject<[ChatInfo], Never>([]) + + init() { + database.fetchChatInfosPublisher( + ChatInfo.Query( + contactChatInfoQuery: .init( + userId: myId, + authStatus: [.friend], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ), + groupChatInfoQuery: GroupChatInfo.Query( + authStatus: [.participating], + excludeBannedContactsMessages: reportingStatus.isEnabled() + ), + groupQuery: Group.Query( + withMessages: false, + authStatus: [.participating] + ) + )) + .replaceError(with: []) + .sink { [unowned self] in chatsSubject.send($0) } + .store(in: &cancellables) + } + + func updateSearch(query: String) { + searchSubject.send(query) + } + + func leave(_ group: Group) { + hudController.show() + + do { + try groupManager.leaveGroup(groupId: group.id) + try database.deleteMessages(.init(chat: .group(group.id))) + try database.deleteGroup(group) + hudController.dismiss() + } catch { + hudController.show(.init(error: error)) } + } - func clear(_ contact: XXModels.Contact) { - _ = try? database.deleteMessages(.init(chat: .direct(myId, contact.id))) - } - - func groupInfo(from group: Group) -> GroupInfo? { - let query = GroupInfo.Query(groupId: group.id) - guard let info = try? database.fetchGroupInfos(query).first else { - return nil - } + func clear(_ contact: XXModels.Contact) { + _ = try? database.deleteMessages(.init(chat: .direct(myId, contact.id))) + } - return info + func groupInfo(from group: Group) -> GroupInfo? { + let query = GroupInfo.Query(groupId: group.id) + guard let info = try? database.fetchGroupInfos(query).first else { + return nil } + + return info + } } diff --git a/Sources/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift index d9c7f68f..1fa130ce 100644 --- a/Sources/ContactFeature/Controllers/ContactController.swift +++ b/Sources/ContactFeature/Controllers/ContactController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Models @@ -9,7 +8,6 @@ import DependencyInjection import ScrollViewController public final class ContactController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: ContactCoordinating @@ -75,11 +73,6 @@ public final class ContactController: UIViewController { } private func setupBindings() { - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.cardComponent.avatarView.editButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) diff --git a/Sources/ContactFeature/ViewModels/ContactViewModel.swift b/Sources/ContactFeature/ViewModels/ContactViewModel.swift index 5e4b3d71..ee0e2676 100644 --- a/Sources/ContactFeature/ViewModels/ContactViewModel.swift +++ b/Sources/ContactFeature/ViewModels/ContactViewModel.swift @@ -1,218 +1,216 @@ -import HUD import UIKit +import Shared import Models import Combine import XXModels import Defaults +import XXClient import CombineSchedulers -import DependencyInjection import XXMessengerClient - -import XXClient +import DependencyInjection struct ContactViewState: Equatable { - var title: String? - var email: String? - var phone: String? - var photo: UIImage? - var username: String? - var nickname: String? + var title: String? + var email: String? + var phone: String? + var photo: UIImage? + var username: String? + var nickname: String? } final class ContactViewModel { - @Dependency var database: Database - @Dependency var messenger: Messenger - @Dependency var getFactsFromContact: GetFactsFromContact - - @KeyObject(.username, defaultValue: nil) var username: String? - @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool - @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool - - var contact: XXModels.Contact - - var popToRootPublisher: AnyPublisher<Void, Never> { popToRootRelay.eraseToAnyPublisher() } - var popPublisher: AnyPublisher<Void, Never> { popRelay.eraseToAnyPublisher() } - var hudPublisher: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - var successPublisher: AnyPublisher<Void, Never> { successRelay.eraseToAnyPublisher() } - var statePublisher: AnyPublisher<ContactViewState, Never> { stateRelay.eraseToAnyPublisher() } - - private let popRelay = PassthroughSubject<Void, Never>() - private let popToRootRelay = PassthroughSubject<Void, Never>() - private let successRelay = PassthroughSubject<Void, Never>() - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - private let stateRelay = CurrentValueSubject<ContactViewState, Never>(.init()) - - var myId: Data { - try! messenger.e2e.get()!.getContact().getId() + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + @Dependency var getFactsFromContact: GetFactsFromContact + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool + + var contact: XXModels.Contact + + var popPublisher: AnyPublisher<Void, Never> { popRelay.eraseToAnyPublisher() } + var successPublisher: AnyPublisher<Void, Never> { successRelay.eraseToAnyPublisher() } + var popToRootPublisher: AnyPublisher<Void, Never> { popToRootRelay.eraseToAnyPublisher() } + var statePublisher: AnyPublisher<ContactViewState, Never> { stateRelay.eraseToAnyPublisher() } + + private let popRelay = PassthroughSubject<Void, Never>() + private let popToRootRelay = PassthroughSubject<Void, Never>() + private let successRelay = PassthroughSubject<Void, Never>() + private let stateRelay = CurrentValueSubject<ContactViewState, Never>(.init()) + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init(_ contact: XXModels.Contact) { + self.contact = contact + + let facts = try? getFactsFromContact(contact.marshaled!) + let email = facts?.first(where: { $0.type == .email })?.value + let phone = facts?.first(where: { $0.type == .phone })?.value + + stateRelay.value = .init( + title: contact.nickname ?? contact.username, + email: email, + phone: phone, + photo: contact.photo != nil ? UIImage(data: contact.photo!) : nil, + username: contact.username, + nickname: contact.nickname + ) + } + + func didChoosePhoto(_ photo: UIImage) { + stateRelay.value.photo = photo + contact.photo = photo.jpegData(compressionQuality: 0.0) + _ = try? database.saveContact(contact) + } + + func didTapDelete() { + hudController.show() + + do { + try messenger.e2e.get()!.deleteRequest.partnerId(contact.id) + try database.deleteContact(contact) + + hudController.dismiss() + popToRootRelay.send() + } catch { + hudController.show(.init(error: error)) } - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init(_ contact: XXModels.Contact) { - self.contact = contact - - let facts = try? getFactsFromContact(contact.marshaled!) - let email = facts?.first(where: { $0.type == .email })?.value - let phone = facts?.first(where: { $0.type == .phone })?.value - - stateRelay.value = .init( - title: contact.nickname ?? contact.username, - email: email, - phone: phone, - photo: contact.photo != nil ? UIImage(data: contact.photo!) : nil, - username: contact.username, - nickname: contact.nickname + } + + func didTapReject() { + // TODO: Reject function on the API? + _ = try? database.deleteContact(contact) + popRelay.send() + } + + func didTapClear() { + _ = try? database.deleteMessages(.init(chat: .direct(myId, contact.id))) + } + + func didUpdateNickname(_ string: String) { + contact.nickname = string.isEmpty ? nil : string + stateRelay.value.title = string.isEmpty ? contact.username : string + _ = try? database.saveContact(contact) + + stateRelay.value.nickname = contact.nickname + } + + func didTapResend() { + hudController.show() + contact.authStatus = .requesting + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.database.saveContact(self.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(self.contact.marshaled!), + myFacts: includedFacts ) + + self.contact.authStatus = .requested + try self.database.saveContact(self.contact) + + self.hudController.dismiss() + self.popRelay.send() + } catch { + self.contact.authStatus = .requestFailed + _ = try? self.database.saveContact(self.contact) + self.hudController.show(.init(error: error)) + } } - - func didChoosePhoto(_ photo: UIImage) { - stateRelay.value.photo = photo - contact.photo = photo.jpegData(compressionQuality: 0.0) - _ = try? database.saveContact(contact) - } - - func didTapDelete() { - hudRelay.send(.on) - - do { - try messenger.e2e.get()!.deleteRequest.partnerId(contact.id) - try database.deleteContact(contact) - - hudRelay.send(.none) - popToRootRelay.send() - } catch { - hudRelay.send(.error(.init(with: error))) + } + + func didTapRequest(with nickname: String) { + hudController.show() + contact.nickname = nickname + contact.authStatus = .requesting + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.database.saveContact(self.contact) + + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() + + if let fact = myFacts.get(.username) { + includedFacts.append(fact) } - } - - func didTapReject() { - // TODO: Reject function on the API? - _ = try? database.deleteContact(contact) - popRelay.send() - } - - func didTapClear() { - _ = try? database.deleteMessages(.init(chat: .direct(myId, contact.id))) - } - - func didUpdateNickname(_ string: String) { - contact.nickname = string.isEmpty ? nil : string - stateRelay.value.title = string.isEmpty ? contact.username : string - _ = try? database.saveContact(contact) - - stateRelay.value.nickname = contact.nickname - } - - func didTapResend() { - hudRelay.send(.on) - contact.authStatus = .requesting - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.database.saveContact(self.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(self.contact.marshaled!), - myFacts: includedFacts - ) - - self.contact.authStatus = .requested - try self.database.saveContact(self.contact) - - self.hudRelay.send(.none) - self.popRelay.send() - } catch { - self.contact.authStatus = .requestFailed - _ = try? self.database.saveContact(self.contact) - self.hudRelay.send(.error(.init(with: error))) - } + + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) } - } - - func didTapRequest(with nickname: String) { - hudRelay.send(.on) - contact.nickname = nickname - contact.authStatus = .requesting - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.database.saveContact(self.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(self.contact.marshaled!), - myFacts: includedFacts - ) - - self.contact.authStatus = .requested - try self.database.saveContact(self.contact) - - self.hudRelay.send(.none) - self.successRelay.send() - } catch { - self.contact.authStatus = .requestFailed - _ = try? self.database.saveContact(self.contact) - self.hudRelay.send(.error(.init(with: error))) - } + + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) } + + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(self.contact.marshaled!), + myFacts: includedFacts + ) + + self.contact.authStatus = .requested + try self.database.saveContact(self.contact) + + self.hudController.dismiss() + self.successRelay.send() + } catch { + self.contact.authStatus = .requestFailed + _ = try? self.database.saveContact(self.contact) + self.hudController.show(.init(error: error)) + } } - - func didTapAccept(_ nickname: String) { - hudRelay.send(.on) - contact.nickname = nickname - contact.authStatus = .confirming - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.database.saveContact(self.contact) - - let _ = try self.messenger.e2e.get()!.confirmReceivedRequest(partner: XXClient.Contact.live(self.contact.marshaled!)) - - self.contact.authStatus = .friend - try self.database.saveContact(self.contact) - - self.hudRelay.send(.none) - self.popRelay.send() - } catch { - self.contact.authStatus = .confirmationFailed - _ = try? self.database.saveContact(self.contact) - self.hudRelay.send(.error(.init(with: error))) - } - } + } + + func didTapAccept(_ nickname: String) { + hudController.show() + contact.nickname = nickname + contact.authStatus = .confirming + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.database.saveContact(self.contact) + + let _ = try self.messenger.e2e.get()!.confirmReceivedRequest(partner: XXClient.Contact.live(self.contact.marshaled!)) + + self.contact.authStatus = .friend + try self.database.saveContact(self.contact) + + self.hudController.dismiss() + self.popRelay.send() + } catch { + self.contact.authStatus = .confirmationFailed + _ = try? self.database.saveContact(self.contact) + self.hudController.show(.init(error: error)) + } } + } } diff --git a/Sources/ContactListFeature/Controllers/CreateGroupController.swift b/Sources/ContactListFeature/Controllers/CreateGroupController.swift index bc4efdef..d6df303a 100644 --- a/Sources/ContactListFeature/Controllers/CreateGroupController.swift +++ b/Sources/ContactListFeature/Controllers/CreateGroupController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -7,7 +6,6 @@ import XXModels import DependencyInjection public final class CreateGroupController: UIViewController { - @Dependency private var hud: HUD @Dependency private var coordinator: ContactListCoordinating lazy private var titleLabel = UILabel() @@ -111,11 +109,6 @@ public final class CreateGroupController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - let selected = viewModel.selected.share() selected diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift index 08314f15..2277d915 100644 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift @@ -1,5 +1,5 @@ -import HUD import UIKit +import Shared import Models import Combine import XXModels @@ -7,146 +7,136 @@ import Defaults import XXClient import ReportingFeature import CombineSchedulers -import DependencyInjection import XXMessengerClient +import DependencyInjection final class CreateGroupViewModel { - @KeyObject(.username, defaultValue: "") var username: String - - // MARK: Injected - - @Dependency var database: Database - @Dependency var groupManager: GroupChat - @Dependency var messenger: Messenger - @Dependency var reportingStatus: ReportingStatus - - // MARK: Properties - - var myId: Data { - try! messenger.e2e.get()!.getContact().getId() - } - - var selected: AnyPublisher<[XXModels.Contact], Never> { - selectedContactsRelay.eraseToAnyPublisher() - } - - var contacts: AnyPublisher<[XXModels.Contact], Never> { - contactsRelay.eraseToAnyPublisher() + @KeyObject(.username, defaultValue: "") var username: String + + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var groupManager: GroupChat + @Dependency var hudController: HUDController + @Dependency var reportingStatus: ReportingStatus + + var myId: Data { + try! messenger.e2e.get()!.getContact().getId() + } + + var selected: AnyPublisher<[XXModels.Contact], Never> { + selectedContactsRelay.eraseToAnyPublisher() + } + + var contacts: AnyPublisher<[XXModels.Contact], Never> { + contactsRelay.eraseToAnyPublisher() + } + + var info: AnyPublisher<GroupInfo, Never> { + infoRelay.eraseToAnyPublisher() + } + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> + = DispatchQueue.global().eraseToAnyScheduler() + + private var allContacts = [XXModels.Contact]() + private var cancellables = Set<AnyCancellable>() + private let infoRelay = PassthroughSubject<GroupInfo, Never>() + private let contactsRelay = CurrentValueSubject<[XXModels.Contact], Never>([]) + private let selectedContactsRelay = CurrentValueSubject<[XXModels.Contact], Never>([]) + + init() { + let query = Contact.Query( + authStatus: [.friend], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + database.fetchContactsPublisher(query) + .replaceError(with: []) + .map { $0.filter { $0.id != self.myId }} + .map { $0.sorted(by: { $0.username! < $1.username! })} + .sink { [unowned self] in + allContacts = $0 + contactsRelay.send($0) + }.store(in: &cancellables) + } + + // MARK: Public + + func didSelect(contact: XXModels.Contact) { + if selectedContactsRelay.value.contains(contact) { + selectedContactsRelay.value.removeAll { $0.username == contact.username } + } else { + selectedContactsRelay.value.append(contact) } + } - var hud: AnyPublisher<HUDStatus, Never> { - hudRelay.eraseToAnyPublisher() + func filter(_ text: String) { + guard text.isEmpty == false else { + contactsRelay.send(allContacts) + return } - var info: AnyPublisher<GroupInfo, Never> { - infoRelay.eraseToAnyPublisher() - } - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> - = DispatchQueue.global().eraseToAnyScheduler() + contactsRelay.send( + allContacts.filter { + ($0.username ?? "").contains(text.lowercased()) + } + ) + } - private var allContacts = [XXModels.Contact]() - private var cancellables = Set<AnyCancellable>() - private let infoRelay = PassthroughSubject<GroupInfo, Never>() - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - private let contactsRelay = CurrentValueSubject<[XXModels.Contact], Never>([]) - private let selectedContactsRelay = CurrentValueSubject<[XXModels.Contact], Never>([]) + func create(name: String, welcome: String?, members: [XXModels.Contact]) { + hudController.show() - // MARK: Lifecycle + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } - init() { - let query = Contact.Query( - authStatus: [.friend], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil + do { + let report = try self.groupManager.makeGroup( + membership: members.map(\.id), + message: welcome?.data(using: .utf8), + name: name.data(using: .utf8) ) - database.fetchContactsPublisher(query) - .replaceError(with: []) - .map { $0.filter { $0.id != self.myId }} - .map { $0.sorted(by: { $0.username! < $1.username! })} - .sink { [unowned self] in - allContacts = $0 - contactsRelay.send($0) - }.store(in: &cancellables) - } - - // MARK: Public + let group = Group( + id: report.id, + name: name, + leaderId: self.myId, + createdAt: Date(), + authStatus: .participating, + serialized: try report.encode() // ? + ) - func didSelect(contact: XXModels.Contact) { - if selectedContactsRelay.value.contains(contact) { - selectedContactsRelay.value.removeAll { $0.username == contact.username } - } else { - selectedContactsRelay.value.append(contact) + _ = try self.database.saveGroup(group) + + if let welcomeMessage = welcome { + try self.database.saveMessage( + Message( + senderId: self.myId, + recipientId: nil, + groupId: group.id, + date: group.createdAt, + status: .sent, + isUnread: false, + text: welcomeMessage, + replyMessageId: nil, + roundURL: nil, + fileTransferId: nil + ) + ) } - } - func filter(_ text: String) { - guard text.isEmpty == false else { - contactsRelay.send(allContacts) - return - } + try members + .map { GroupMember(groupId: group.id, contactId: $0.id) } + .forEach { try self.database.saveGroupMember($0) } - contactsRelay.send( - allContacts.filter { - ($0.username ?? "").contains(text.lowercased()) - } - ) - } + let query = GroupInfo.Query(groupId: group.id) + let info = try self.database.fetchGroupInfos(query).first - func create(name: String, welcome: String?, members: [XXModels.Contact]) { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - let report = try self.groupManager.makeGroup( - membership: members.map(\.id), - message: welcome?.data(using: .utf8), - name: name.data(using: .utf8) - ) - - let group = Group( - id: report.id, - name: name, - leaderId: self.myId, - createdAt: Date(), - authStatus: .participating, - serialized: try report.encode() // ? - ) - - _ = try self.database.saveGroup(group) - - if let welcomeMessage = welcome { - try self.database.saveMessage( - Message( - senderId: self.myId, - recipientId: nil, - groupId: group.id, - date: group.createdAt, - status: .sent, - isUnread: false, - text: welcomeMessage, - replyMessageId: nil, - roundURL: nil, - fileTransferId: nil - ) - ) - } - - try members - .map { GroupMember(groupId: group.id, contactId: $0.id) } - .forEach { try self.database.saveGroupMember($0) } - - let query = GroupInfo.Query(groupId: group.id) - let info = try self.database.fetchGroupInfos(query).first - - self.infoRelay.send(info!) - self.hudRelay.send(.none) - } catch { - self.hudRelay.send(.error(.init(with: error))) - } - } + self.infoRelay.send(info!) + self.hudController.dismiss() + } catch { + self.hudController.show(.init(error: error)) + } } + } } diff --git a/Sources/HUD/DotAnimation.swift b/Sources/HUD/DotAnimation.swift deleted file mode 100644 index f7bfae04..00000000 --- a/Sources/HUD/DotAnimation.swift +++ /dev/null @@ -1,94 +0,0 @@ -import UIKit - -final class DotAnimation: UIView { - let leftDot = UIView() - let middleDot = UIView() - let rightDot = UIView() - - var leftInvert = false - var middleInvert = false - var rightInvert = false - - var leftValue: CGFloat = 20 - var middleValue: CGFloat = 45 - var rightValue: CGFloat = 70 - - var displayLink: CADisplayLink? - - init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - func setColor( - _ color: UIColor = UIColor( - red: 0, - green: 188/255, - blue: 206/255, - alpha: 1.0 - ) - ) { - leftDot.backgroundColor = color - middleDot.backgroundColor = color - rightDot.backgroundColor = color - } - - private func setup() { - setupCornerRadius() - setColor() - addSubviews() - setupConstraints() - - displayLink = CADisplayLink(target: self, selector: #selector(handleAnimations)) - displayLink!.add(to: RunLoop.main, forMode: .default) - } - - private func setupCornerRadius() { - leftDot.layer.cornerRadius = 7.5 - middleDot.layer.cornerRadius = 7.5 - rightDot.layer.cornerRadius = 7.5 - } - - private func addSubviews() { - addSubview(leftDot) - addSubview(middleDot) - addSubview(rightDot) - } - - private func setupConstraints() { - leftDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.right.equalTo(middleDot.snp.left).offset(-5) - make.width.height.equalTo(15) - } - - middleDot.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.height.equalTo(15) - } - - rightDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.left.equalTo(middleDot.snp.right).offset(5) - make.width.height.equalTo(15) - } - } - - @objc private func handleAnimations() { - let factor: CGFloat = 70 - - leftInvert ? (leftValue -= 1) : (leftValue += 1) - middleInvert ? (middleValue -= 1) : (middleValue += 1) - rightInvert ? (rightValue -= 1) : (rightValue += 1) - - leftDot.layer.transform = CATransform3DMakeScale(leftValue/factor, leftValue/factor, 1) - middleDot.layer.transform = CATransform3DMakeScale(middleValue/factor, middleValue/factor, 1) - rightDot.layer.transform = CATransform3DMakeScale(rightValue/factor, rightValue/factor, 1) - - if leftValue > factor || leftValue < 10 { leftInvert.toggle() } - if middleValue > factor || middleValue < 10 { middleInvert.toggle() } - if rightValue > factor || rightValue < 10 { rightInvert.toggle() } - } -} diff --git a/Sources/HUD/ErrorView.swift b/Sources/HUD/ErrorView.swift deleted file mode 100644 index 2692ab2a..00000000 --- a/Sources/HUD/ErrorView.swift +++ /dev/null @@ -1,58 +0,0 @@ -import UIKit -import Shared -import SnapKit - -final class ErrorView: UIView { - let title = UILabel() - let content = UILabel() - let stack = UIStackView() - let button = CapsuleButton() - - init(with model: HUDError) { - super.init(frame: .zero) - setup(with: model) - } - - required init?(coder: NSCoder) { nil } - - private func setup(with model: HUDError) { - layer.cornerRadius = 6 - backgroundColor = Asset.neutralWhite.color - - title.text = model.title - title.textColor = Asset.neutralBody.color - title.font = Fonts.Mulish.bold.font(size: 35.0) - title.textAlignment = .center - title.numberOfLines = 0 - - content.text = model.content - content.textColor = Asset.neutralBody.color - content.numberOfLines = 0 - content.font = Fonts.Mulish.regular.font(size: 14.0) - content.textAlignment = .center - - button.setTitle(model.buttonTitle, for: .normal) - button.setStyle(.brandColored) - - stack.axis = .vertical - - stack.addArrangedSubview(title) - stack.addArrangedSubview(content) - - if model.dismissable { - stack.addArrangedSubview(button) - } - - stack.setCustomSpacing(25, after: title) - stack.setCustomSpacing(59, after: content) - - addSubview(stack) - - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(60) - make.left.equalToSuperview().offset(57) - make.right.equalToSuperview().offset(-57) - make.bottom.equalToSuperview().offset(-35) - } - } -} diff --git a/Sources/HUD/HUD.swift b/Sources/HUD/HUD.swift deleted file mode 100644 index 8850d8a2..00000000 --- a/Sources/HUD/HUD.swift +++ /dev/null @@ -1,194 +0,0 @@ -import UIKit -import Shared -import Combine -import SnapKit - -private enum Constants { - static let title = Localized.Hud.Error.title - static let action = Localized.Hud.Error.action -} - -public enum HUDStatus: Equatable { - case none - case on - case onTitle(String) - case onAction(String) - case error(HUDError) - - var isPresented: Bool { - switch self { - case .none: - return false - case .on, .error, .onTitle, .onAction: - return true - } - } -} - -public struct HUDError: Equatable { - var title: String - var content: String - var buttonTitle: String - var dismissable: Bool - - public init( - content: String, - title: String? = nil, - buttonTitle: String? = nil, - dismissable: Bool = true - ) { - self.content = content - self.title = title ?? Constants.title - self.buttonTitle = buttonTitle ?? Constants.action - self.dismissable = dismissable - } - - public init(with error: Error) { - self.title = Constants.title - self.buttonTitle = Constants.action - self.content = error.localizedDescription - self.dismissable = true - } -} - -public final class HUD { - private(set) var window: UIWindow? - private(set) var errorView: ErrorView? - private(set) var titleLabel: UILabel? - private(set) var animation: DotAnimation? - public var actionButton: CapsuleButton? - private var cancellables = Set<AnyCancellable>() - - private var status: HUDStatus = .none { - didSet { - if oldValue.isPresented == true && status.isPresented == true { - self.errorView = nil - self.animation = nil - self.window = nil - self.actionButton = nil - self.titleLabel = nil - - switch status { - case .on: - animation = DotAnimation() - - case .onTitle(let text): - animation = DotAnimation() - titleLabel = UILabel() - titleLabel!.text = text - - case .onAction(let title): - animation = DotAnimation() - actionButton = CapsuleButton() - actionButton!.set(style: .seeThroughWhite, title: title) - - case .error(let error): - errorView = ErrorView(with: error) - case .none: - break - } - - showWindow() - } - - if oldValue.isPresented == false && status.isPresented == true { - switch status { - case .on: - animation = DotAnimation() - - case .onTitle(let text): - animation = DotAnimation() - titleLabel = UILabel() - titleLabel!.text = text - - case .onAction(let title): - animation = DotAnimation() - actionButton = CapsuleButton() - actionButton!.set(style: .seeThroughWhite, title: title) - - case .error(let error): - errorView = ErrorView(with: error) - case .none: - break - } - - showWindow() - } - - if oldValue.isPresented == true && status.isPresented == false { - hideWindow() - } - } - } - - public init() {} - - public func update(with status: HUDStatus) { - self.status = status - } - - private func showWindow() { - window = UIWindow(frame: UIScreen.main.bounds) - window?.backgroundColor = UIColor.black.withAlphaComponent(0.8) - window?.rootViewController = RootViewController(nil) - - if let animation = animation { - window?.addSubview(animation) - animation.setColor(.white) - animation.snp.makeConstraints { $0.center.equalToSuperview() } - } - - if let titleLabel = titleLabel { - window?.addSubview(titleLabel) - titleLabel.textAlignment = .center - titleLabel.numberOfLines = 0 - titleLabel.snp.makeConstraints { make in - make.left.equalToSuperview().offset(18) - make.center.equalToSuperview().offset(50) - make.right.equalToSuperview().offset(-18) - } - } - - if let actionButton = actionButton { - window?.addSubview(actionButton) - actionButton.snp.makeConstraints { - $0.left.equalToSuperview().offset(18) - $0.right.equalToSuperview().offset(-18) - $0.bottom.equalToSuperview().offset(-50) - } - } - - if let errorView = errorView { - window?.addSubview(errorView) - errorView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(18) - make.center.equalToSuperview() - make.right.equalToSuperview().offset(-18) - } - - errorView.button - .publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in hideWindow() } - .store(in: &cancellables) - } - - window?.alpha = 0.0 - window?.makeKeyAndVisible() - - UIView.animate(withDuration: 0.3) { self.window?.alpha = 1.0 } - } - - private func hideWindow() { - UIView.animate(withDuration: 0.3) { - self.window?.alpha = 0.0 - } completion: { _ in - self.cancellables.removeAll() - self.errorView = nil - self.animation = nil - self.actionButton = nil - self.titleLabel = nil - self.window = nil - } - } -} diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index fe5d10cb..4c533ee1 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -7,7 +6,6 @@ import PushFeature import DependencyInjection public final class LaunchController: UIViewController { - @Dependency var hud: HUD @Dependency var coordinator: LaunchCoordinating @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool @@ -41,12 +39,7 @@ public final class LaunchController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - + viewModel.routePublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in @@ -136,26 +129,26 @@ public final class LaunchController: UIViewController { title: model.positiveActionTitle ) - if let negativeTitle = model.negativeActionTitle { - let negativeButton = CapsuleButton() - negativeButton.set(style: .simplestColoredRed, title: negativeTitle) - - negativeButton.publisher(for: .touchUpInside) - .sink { [unowned self] in - blocker.hideWindow() - viewModel.continueWithInitialization() - }.store(in: &cancellables) - - vStack.addArrangedSubview(negativeButton) - } - - blocker.window?.addSubview(drawerView) - drawerView.snp.makeConstraints { - $0.left.equalToSuperview().offset(18) - $0.center.equalToSuperview() - $0.right.equalToSuperview().offset(-18) - } - - blocker.showWindow() +// if let negativeTitle = model.negativeActionTitle { +// let negativeButton = CapsuleButton() +// negativeButton.set(style: .simplestColoredRed, title: negativeTitle) +// +// negativeButton.publisher(for: .touchUpInside) +// .sink { [unowned self] in +// blocker.hideWindow() +// viewModel.continueWithInitialization() +// }.store(in: &cancellables) +// +// vStack.addArrangedSubview(negativeButton) +// } +// +// blocker.window?.addSubview(drawerView) +// drawerView.snp.makeConstraints { +// $0.left.equalToSuperview().offset(18) +// $0.center.equalToSuperview() +// $0.right.equalToSuperview().offset(-18) +// } +// +// blocker.showWindow() } } diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 8eda0596..d1472aa5 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -1,4 +1,3 @@ -import HUD import Shared import Models import Combine @@ -43,6 +42,7 @@ enum LaunchRoute { final class LaunchViewModel { @Dependency var database: Database + @Dependency var hudController: HUDController @Dependency var backupService: BackupService @Dependency var versionChecker: VersionChecker @Dependency var fetchBannedList: FetchBannedList @@ -57,10 +57,6 @@ final class LaunchViewModel { @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - var authCallbacksCancellable: Cancellable? var backupCallbackCancellable: Cancellable? var networkCallbacksCancellable: Cancellable? @@ -88,13 +84,12 @@ final class LaunchViewModel { private var cancellables = Set<AnyCancellable>() private let routeSubject = PassthroughSubject<LaunchRoute, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) func viewDidAppear() { backgroundScheduler.schedule(after: .init(.now() + 1)) { [weak self] in guard let self = self else { return } - self.hudSubject.send(.on) + self.hudController.show() self.versionChecker().sink { [unowned self] in switch $0 { @@ -150,16 +145,16 @@ final class LaunchViewModel { if messenger.isLoggedIn() == false { if try messenger.isRegistered() { try messenger.logIn() - hudSubject.send(.none) + hudController.dismiss() routeSubject.send(.chats) } else { try? sftpManager.unlink() try? dropboxManager.unlink() - hudSubject.send(.none) + hudController.dismiss() routeSubject.send(.onboarding) } } else { - hudSubject.send(.none) + hudController.dismiss() routeSubject.send(.chats) } @@ -171,7 +166,7 @@ final class LaunchViewModel { } catch { let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - hudSubject.send(.error(.init(content: xxError))) + hudController.show(.init(content: xxError)) } } @@ -181,7 +176,7 @@ final class LaunchViewModel { } private func presentOnboardingFlow() { - hudSubject.send(.none) + hudController.dismiss() routeSubject.send(.onboarding) } @@ -254,15 +249,15 @@ final class LaunchViewModel { } private func versionFailed(error: Error) { - let title = Localized.Launch.Version.failed - let content = error.localizedDescription - let hudError = HUDError(content: content, title: title, dismissable: false) - - hudSubject.send(.error(hudError)) + hudController.show(.init( + title: Localized.Launch.Version.failed, + content: error.localizedDescription, + isDismissable: false + )) } private func versionUpdateRequired(_ info: DappVersionInformation) { - hudSubject.send(.none) + hudController.dismiss() let model = Update( content: info.minimumMessage, @@ -276,7 +271,7 @@ final class LaunchViewModel { } private func versionUpdateRecommended(_ info: DappVersionInformation) { - hudSubject.send(.none) + hudController.dismiss() let model = Update( content: Localized.Launch.Version.Recommended.title, diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift index b45a26fc..4ae996ad 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Models @@ -8,7 +7,6 @@ import DependencyInjection import ScrollViewController public final class OnboardingEmailConfirmationController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: OnboardingCoordinating @@ -65,10 +63,6 @@ public final class OnboardingEmailConfirmationController: UIViewController { } private func setupBindings() { - viewModel.hud.receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift index b3ff6046..2f65daf7 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -7,7 +6,6 @@ import DependencyInjection import ScrollViewController public final class OnboardingEmailController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: OnboardingCoordinating @@ -50,10 +48,6 @@ public final class OnboardingEmailController: UIViewController { } private func setupBindings() { - viewModel.hud.receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift index 1fb93ad4..ab9cbca2 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Models @@ -8,7 +7,6 @@ import DependencyInjection import ScrollViewController public final class OnboardingPhoneConfirmationController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: OnboardingCoordinating @@ -65,10 +63,6 @@ public final class OnboardingPhoneConfirmationController: UIViewController { } private func setupBindings() { - viewModel.hud.receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift index e25481b4..e593fbd2 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -7,7 +6,6 @@ import DependencyInjection import ScrollViewController public final class OnboardingPhoneController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: OnboardingCoordinating @@ -50,11 +48,6 @@ public final class OnboardingPhoneController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift index a902a8d2..23ff5f77 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift @@ -1,11 +1,8 @@ -import HUD import UIKit -import Shared import Combine import DependencyInjection public final class OnboardingStartController: UIViewController { - @Dependency private var hud: HUD @Dependency private var coordinator: OnboardingCoordinating lazy private var screenView = OnboardingStartView() @@ -43,7 +40,9 @@ public final class OnboardingStartController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - screenView.startButton.publisher(for: .touchUpInside) + screenView + .startButton + .publisher(for: .touchUpInside) .sink { [unowned self] in coordinator.toTerms(from: self) } .store(in: &cancellables) } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index 2b139fad..566cc2ed 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -7,7 +6,6 @@ import DependencyInjection import ScrollViewController public final class OnboardingUsernameController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: OnboardingCoordinating @@ -50,11 +48,6 @@ public final class OnboardingUsernameController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .removeDuplicates() .compactMap { $0 } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift index 756479e2..d008997d 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -11,85 +10,83 @@ import DependencyInjection import XXMessengerClient struct OnboardingEmailConfirmationViewState: Equatable { - var input: String = "" - var status: InputField.ValidationStatus = .unknown(nil) - var resendDebouncer: Int = 0 + var input: String = "" + var status: InputField.ValidationStatus = .unknown(nil) + var resendDebouncer: Int = 0 } final class OnboardingEmailConfirmationViewModel { - @Dependency var messenger: Messenger - - @KeyObject(.email, defaultValue: nil) var email: String? - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } - private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() - - var timer: Timer? - let confirmation: AttributeConfirmation - - var state: AnyPublisher<OnboardingEmailConfirmationViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingEmailConfirmationViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init(_ confirmation: AttributeConfirmation) { - self.confirmation = confirmation - didTapResend() - } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func didTapResend() { - guard stateRelay.value.resendDebouncer == 0 else { return } - - stateRelay.value.resendDebouncer = 60 - - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in - guard let self = self, self.stateRelay.value.resendDebouncer > 0 else { - $0.invalidate() - return - } - - self.stateRelay.value.resendDebouncer -= 1 - } + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + + @KeyObject(.email, defaultValue: nil) var email: String? + + var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } + private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() + + var timer: Timer? + let confirmation: AttributeConfirmation + + var state: AnyPublisher<OnboardingEmailConfirmationViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<OnboardingEmailConfirmationViewState, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init(_ confirmation: AttributeConfirmation) { + self.confirmation = confirmation + didTapResend() + } + + func didInput(_ string: String) { + stateRelay.value.input = string + validate() + } + + func didTapResend() { + guard stateRelay.value.resendDebouncer == 0 else { return } + + stateRelay.value.resendDebouncer = 60 + + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in + guard let self = self, self.stateRelay.value.resendDebouncer > 0 else { + $0.invalidate() + return + } + + self.stateRelay.value.resendDebouncer -= 1 } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.messenger.ud.get()!.confirmFact( - confirmationId: self.confirmation.confirmationId!, - code: self.stateRelay.value.input - ) - - self.email = self.confirmation.content - - self.timer?.invalidate() - self.hudRelay.send(.none) - self.completionRelay.send(self.confirmation) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } - } + } + + func didTapNext() { + hudController.show() + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.messenger.ud.get()!.confirmFact( + confirmationId: self.confirmation.confirmationId!, + code: self.stateRelay.value.input + ) + + self.email = self.confirmation.content + + self.timer?.invalidate() + self.hudController.dismiss() + self.completionRelay.send(self.confirmation) + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } - - private func validate() { - switch Validator.code.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.code.validate(stateRelay.value.input) { + case .success: + stateRelay.value.status = .valid(nil) + case .failure(let error): + stateRelay.value.status = .invalid(error) } + } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift index 8215204d..6e1159ae 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -7,67 +6,65 @@ import Defaults import XXClient import InputField import CombineSchedulers -import DependencyInjection import XXMessengerClient +import DependencyInjection struct OnboardingEmailViewState: Equatable { - var input: String = "" - var confirmation: AttributeConfirmation? = nil - var status: InputField.ValidationStatus = .unknown(nil) + var input: String = "" + var confirmation: AttributeConfirmation? = nil + var status: InputField.ValidationStatus = .unknown(nil) } final class OnboardingEmailViewModel { - @Dependency var messenger: Messenger - - @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<OnboardingEmailViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingEmailViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - func clearUp() { - stateRelay.value.confirmation = nil + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + + @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications + + var state: AnyPublisher<OnboardingEmailViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<OnboardingEmailViewState, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + func clearUp() { + stateRelay.value.confirmation = nil + } + + func didInput(_ string: String) { + stateRelay.value.input = string + validate() + } + + func didTapNext() { + hudController.show() + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( + .init(type: .email, value: self.stateRelay.value.input) + ) + + self.hudController.dismiss() + self.stateRelay.value.confirmation = .init( + content: self.stateRelay.value.input, + isEmail: true, + confirmationId: confirmationId + ) + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( - .init(type: .email, value: self.stateRelay.value.input) - ) - - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = .init( - content: self.stateRelay.value.input, - isEmail: true, - confirmationId: confirmationId - ) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } - } - } - - private func validate() { - switch Validator.email.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.email.validate(stateRelay.value.input) { + case .success: + stateRelay.value.status = .valid(nil) + case .failure(let error): + stateRelay.value.status = .invalid(error) } + } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift index ba335ba8..387993d5 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -7,89 +6,87 @@ import Defaults import InputField import XXClient import CombineSchedulers -import DependencyInjection import XXMessengerClient +import DependencyInjection struct OnboardingPhoneConfirmationViewState: Equatable { - var input: String = "" - var status: InputField.ValidationStatus = .unknown(nil) - var resendDebouncer: Int = 0 + var input: String = "" + var status: InputField.ValidationStatus = .unknown(nil) + var resendDebouncer: Int = 0 } final class OnboardingPhoneConfirmationViewModel { - @Dependency var messenger: Messenger - - @KeyObject(.phone, defaultValue: nil) var phone: String? - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } - private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() - - var timer: Timer? - let confirmation: AttributeConfirmation - - var state: AnyPublisher<OnboardingPhoneConfirmationViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingPhoneConfirmationViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init(_ confirmation: AttributeConfirmation) { - self.confirmation = confirmation - didTapResend() - } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + + @KeyObject(.phone, defaultValue: nil) var phone: String? + + var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } + private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() + + var timer: Timer? + let confirmation: AttributeConfirmation + + var state: AnyPublisher<OnboardingPhoneConfirmationViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<OnboardingPhoneConfirmationViewState, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init(_ confirmation: AttributeConfirmation) { + self.confirmation = confirmation + didTapResend() + } + + func didInput(_ string: String) { + stateRelay.value.input = string + validate() + } + + func didTapResend() { + guard stateRelay.value.resendDebouncer == 0 else { return } + + stateRelay.value.resendDebouncer = 60 + + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in + guard let self = self, self.stateRelay.value.resendDebouncer > 0 else { + $0.invalidate() + return + } + + self.stateRelay.value.resendDebouncer -= 1 } - - func didTapResend() { - guard stateRelay.value.resendDebouncer == 0 else { return } - - stateRelay.value.resendDebouncer = 60 - - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] in - guard let self = self, self.stateRelay.value.resendDebouncer > 0 else { - $0.invalidate() - return - } - - self.stateRelay.value.resendDebouncer -= 1 - } + } + + func didTapNext() { + hudController.show() + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.messenger.ud.get()!.confirmFact( + confirmationId: self.confirmation.confirmationId!, + code: self.stateRelay.value.input + ) + + self.phone = self.confirmation.content + + self.timer?.invalidate() + self.hudController.dismiss() + self.completionRelay.send(self.confirmation) + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.messenger.ud.get()!.confirmFact( - confirmationId: self.confirmation.confirmationId!, - code: self.stateRelay.value.input - ) - - self.phone = self.confirmation.content - - self.timer?.invalidate() - self.hudRelay.send(.none) - self.completionRelay.send(self.confirmation) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } - } - } - - private func validate() { - switch Validator.code.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.code.validate(stateRelay.value.input) { + case .success: + stateRelay.value.status = .valid(nil) + case .failure(let error): + stateRelay.value.status = .invalid(error) } + } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift index 1644c795..346a7bf1 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift @@ -1,4 +1,3 @@ -import HUD import Shared import Models import Combine @@ -7,78 +6,74 @@ import Countries import InputField import Foundation import CombineSchedulers -import DependencyInjection import XXMessengerClient +import DependencyInjection struct OnboardingPhoneViewState: Equatable { - var input: String = "" - var confirmation: AttributeConfirmation? - var status: InputField.ValidationStatus = .unknown(nil) - var country: Country = .fromMyPhone() + var input: String = "" + var confirmation: AttributeConfirmation? + var status: InputField.ValidationStatus = .unknown(nil) + var country: Country = .fromMyPhone() } final class OnboardingPhoneViewModel { - @Dependency var messenger: Messenger - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<OnboardingPhoneViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingPhoneViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - // MARK: Public - - func clearUp() { - stateRelay.value.confirmation = nil - } - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func didChooseCountry(_ country: Country) { - stateRelay.value.country = country - validate() - } - - func didGoForward() { - stateRelay.value.confirmation = nil - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - let content = "\(self.stateRelay.value.input)\(self.stateRelay.value.country.code)" - - do { - let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( - .init(type: .phone, value: content) - ) - - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = .init( - content: content, - confirmationId: confirmationId - ) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } - } + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + + var state: AnyPublisher<OnboardingPhoneViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<OnboardingPhoneViewState, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + func clearUp() { + stateRelay.value.confirmation = nil + } + + func didInput(_ string: String) { + stateRelay.value.input = string + validate() + } + + func didChooseCountry(_ country: Country) { + stateRelay.value.country = country + validate() + } + + func didGoForward() { + stateRelay.value.confirmation = nil + } + + func didTapNext() { + hudController.show() + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + let content = "\(self.stateRelay.value.input)\(self.stateRelay.value.country.code)" + + do { + let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( + .init(type: .phone, value: content) + ) + + self.hudController.dismiss() + self.stateRelay.value.confirmation = .init( + content: content, + confirmationId: confirmationId + ) + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } - - private func validate() { - switch Validator.phone.validate((stateRelay.value.country.regex, stateRelay.value.input)) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.phone.validate((stateRelay.value.country.regex, stateRelay.value.input)) { + case .success: + stateRelay.value.status = .valid(nil) + case .failure(let error): + stateRelay.value.status = .invalid(error) } + } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift index b042341c..1483525b 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift @@ -1,4 +1,3 @@ -import HUD import Shared import Models import Combine @@ -12,72 +11,70 @@ import CombineSchedulers import DependencyInjection struct OnboardingUsernameViewState: Equatable { - var input: String = "" - var status: InputField.ValidationStatus = .unknown(nil) + var input: String = "" + var status: InputField.ValidationStatus = .unknown(nil) } final class OnboardingUsernameViewModel { - @Dependency var database: Database - @Dependency var messenger: Messenger + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + + @KeyObject(.username, defaultValue: "") var username: String + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> + = DispatchQueue.global().eraseToAnyScheduler() + + var greenPublisher: AnyPublisher<Void, Never> { greenRelay.eraseToAnyPublisher() } + private let greenRelay = PassthroughSubject<Void, Never>() - @KeyObject(.username, defaultValue: "") var username: String - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> - = DispatchQueue.global().eraseToAnyScheduler() - - var greenPublisher: AnyPublisher<Void, Never> { greenRelay.eraseToAnyPublisher() } - private let greenRelay = PassthroughSubject<Void, Never>() - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<OnboardingUsernameViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<OnboardingUsernameViewState, Never>(.init()) - - func didInput(_ string: String) { - stateRelay.value.input = string.trimmingCharacters(in: .whitespacesAndNewlines) - - switch Validator.username.validate(stateRelay.value.input) { - case .success(let text): - stateRelay.value.status = .valid(text) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + var state: AnyPublisher<OnboardingUsernameViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<OnboardingUsernameViewState, Never>(.init()) + + func didInput(_ string: String) { + stateRelay.value.input = string.trimmingCharacters(in: .whitespacesAndNewlines) + + switch Validator.username.validate(stateRelay.value.input) { + case .success(let text): + stateRelay.value.status = .valid(text) + case .failure(let error): + stateRelay.value.status = .invalid(error) } - - func didTapRegister() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.messenger.register( - username: self.stateRelay.value.input - ) - - try self.database.saveContact(.init( - id: self.messenger.e2e.get()!.getContact().getId(), - marshaled: self.messenger.e2e.get()!.getContact().data, - username: self.stateRelay.value.input, - email: nil, - phone: nil, - nickname: nil, - photo: nil, - authStatus: .friend, - isRecent: false, - isBlocked: false, - isBanned: false, - createdAt: Date() - )) - - self.username = self.stateRelay.value.input - self.hudRelay.send(.none) - self.greenRelay.send() - } catch { - self.hudRelay.send(.none) - self.stateRelay.value.status = .invalid(CreateUserFriendlyErrorMessage.live(error.localizedDescription)) - } - } + } + + func didTapRegister() { + hudController.show() + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.messenger.register( + username: self.stateRelay.value.input + ) + + try self.database.saveContact(.init( + id: self.messenger.e2e.get()!.getContact().getId(), + marshaled: self.messenger.e2e.get()!.getContact().data, + username: self.stateRelay.value.input, + email: nil, + phone: nil, + nickname: nil, + photo: nil, + authStatus: .friend, + isRecent: false, + isBlocked: false, + isBanned: false, + createdAt: Date() + )) + + self.username = self.stateRelay.value.input + self.hudController.dismiss() + self.greenRelay.send() + } catch { + self.hudController.dismiss() + self.stateRelay.value.status = .invalid(CreateUserFriendlyErrorMessage.live(error.localizedDescription)) + } } + } } diff --git a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift index d612b9e9..037ac041 100644 --- a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -10,8 +9,6 @@ import ScrollViewController public typealias ControllerClosure = (UIViewController, AttributeConfirmation) -> Void public final class ProfileCodeController: UIViewController { - @Dependency private var hud: HUD - lazy private var screenView = ProfileCodeView() lazy private var scrollViewController = ScrollViewController() @@ -55,11 +52,6 @@ public final class ProfileCodeController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) diff --git a/Sources/ProfileFeature/Controllers/ProfileController.swift b/Sources/ProfileFeature/Controllers/ProfileController.swift index f6b04009..15499606 100644 --- a/Sources/ProfileFeature/Controllers/ProfileController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -6,7 +5,6 @@ import DrawerFeature import DependencyInjection public final class ProfileController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: ProfileCoordinating @@ -48,11 +46,6 @@ public final class ProfileController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.emailView.actionButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) diff --git a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift index f06b1f30..39396d0a 100644 --- a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -6,7 +5,6 @@ import DependencyInjection import ScrollViewController public final class ProfileEmailController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: ProfileCoordinating @@ -40,11 +38,6 @@ public final class ProfileEmailController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) diff --git a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift index 0b126485..6b220c37 100644 --- a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift +++ b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -6,7 +5,6 @@ import DependencyInjection import ScrollViewController public final class ProfilePhoneController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: ProfileCoordinating @@ -40,11 +38,6 @@ public final class ProfilePhoneController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inputField.textPublisher .sink { [unowned self] in viewModel.didInput($0) } .store(in: &cancellables) diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift index 40c7abb5..926c6d49 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift @@ -1,4 +1,3 @@ -import HUD import Shared import Models import Combine @@ -19,6 +18,7 @@ struct ProfileCodeViewState: Equatable { final class ProfileCodeViewModel { @Dependency var messenger: Messenger + @Dependency var hudController: HUDController @Dependency var backupService: BackupService @KeyObject(.email, defaultValue: nil) var email: String? @@ -31,9 +31,6 @@ final class ProfileCodeViewModel { var completionPublisher: AnyPublisher<AttributeConfirmation, Never> { completionRelay.eraseToAnyPublisher() } private let completionRelay = PassthroughSubject<AttributeConfirmation, Never>() - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - var state: AnyPublisher<ProfileCodeViewState, Never> { stateRelay.eraseToAnyPublisher() } private let stateRelay = CurrentValueSubject<ProfileCodeViewState, Never>(.init()) @@ -65,7 +62,7 @@ final class ProfileCodeViewModel { } func didTapNext() { - hudRelay.send(.on) + hudController.show() backgroundScheduler.schedule { [weak self] in guard let self = self else { return } @@ -83,12 +80,12 @@ final class ProfileCodeViewModel { } self.timer?.invalidate() - self.hudRelay.send(.none) + self.hudController.dismiss() self.completionRelay.send(self.confirmation) self.backupService.didUpdateFacts() } catch { - self.hudRelay.send(.error(.init(with: error))) + self.hudController.show(.init(error: error)) } } } diff --git a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift index 4f7b61c3..79b36b2f 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift @@ -1,4 +1,3 @@ -import HUD import Models import Shared import Combine @@ -6,73 +5,63 @@ import XXClient import Foundation import InputField import CombineSchedulers -import DependencyInjection import XXMessengerClient +import DependencyInjection struct ProfileEmailViewState: Equatable { - var input: String = "" - var confirmation: AttributeConfirmation? = nil - var status: InputField.ValidationStatus = .unknown(nil) + var input: String = "" + var confirmation: AttributeConfirmation? = nil + var status: InputField.ValidationStatus = .unknown(nil) } final class ProfileEmailViewModel { - // MARK: Injected - - @Dependency var messenger: Messenger - - // MARK: Properties - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<ProfileEmailViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ProfileEmailViewState, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - // MARK: Public - - func didInput(_ string: String) { - stateRelay.value.input = string - validate() + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + + var state: AnyPublisher<ProfileEmailViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<ProfileEmailViewState, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + func didInput(_ string: String) { + stateRelay.value.input = string + validate() + } + + func clearUp() { + stateRelay.value.confirmation = nil + } + + func didTapNext() { + hudController.show() + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( + .init(type: .email, value: self.stateRelay.value.input) + ) + + self.hudController.dismiss() + self.stateRelay.value.confirmation = .init( + content: self.stateRelay.value.input, + isEmail: true, + confirmationId: confirmationId + ) + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } - - func clearUp() { - stateRelay.value.confirmation = nil - } - - func didTapNext() { - hudRelay.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( - .init(type: .email, value: self.stateRelay.value.input) - ) - - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = .init( - content: self.stateRelay.value.input, - isEmail: true, - confirmationId: confirmationId - ) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } - } - } - - // MARK: Private - - private func validate() { - switch Validator.email.validate(stateRelay.value.input) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.email.validate(stateRelay.value.input) { + case .success: + stateRelay.value.status = .valid(nil) + case .failure(let error): + stateRelay.value.status = .invalid(error) } + } } diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift index 560f78cc..b2b3e34d 100644 --- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift @@ -1,4 +1,3 @@ -import HUD import Shared import Models import Combine @@ -11,77 +10,67 @@ import DependencyInjection import XXMessengerClient struct ProfilePhoneViewState: Equatable { - var input: String = "" - var confirmation: AttributeConfirmation? = nil - var status: InputField.ValidationStatus = .unknown(nil) - var country: Country = .fromMyPhone() + var input: String = "" + var confirmation: AttributeConfirmation? = nil + var status: InputField.ValidationStatus = .unknown(nil) + var country: Country = .fromMyPhone() } final class ProfilePhoneViewModel { - // MARK: Injected + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController - @Dependency var messenger: Messenger + var state: AnyPublisher<ProfilePhoneViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<ProfilePhoneViewState, Never>(.init()) - // MARK: Properties + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) + func didInput(_ string: String) { + stateRelay.value.input = string + validate() + } - var state: AnyPublisher<ProfilePhoneViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ProfilePhoneViewState, Never>(.init()) + func clearUp() { + stateRelay.value.confirmation = nil + } - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + func didChooseCountry(_ country: Country) { + stateRelay.value.country = country + validate() + } - // MARK: Public + func didTapNext() { + hudController.show() - func didInput(_ string: String) { - stateRelay.value.input = string - validate() - } - - func clearUp() { - stateRelay.value.confirmation = nil - } - - func didChooseCountry(_ country: Country) { - stateRelay.value.country = country - validate() - } + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } - func didTapNext() { - hudRelay.send(.on) + let content = "\(self.stateRelay.value.input)\(self.stateRelay.value.country.code)" - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + do { + let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( + .init(type: .phone, value: content) + ) - let content = "\(self.stateRelay.value.input)\(self.stateRelay.value.country.code)" + self.hudController.dismiss() + self.stateRelay.value.confirmation = .init( + content: content, + confirmationId: confirmationId + ) - do { - let confirmationId = try self.messenger.ud.get()!.sendRegisterFact( - .init(type: .phone, value: content) - ) - - self.hudRelay.send(.none) - self.stateRelay.value.confirmation = .init( - content: content, - confirmationId: confirmationId - ) - - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } - } + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } - - // MARK: Private - - private func validate() { - switch Validator.phone.validate((stateRelay.value.country.regex, stateRelay.value.input)) { - case .success: - stateRelay.value.status = .valid(nil) - case .failure(let error): - stateRelay.value.status = .invalid(error) - } + } + + private func validate() { + switch Validator.phone.validate((stateRelay.value.country.regex, stateRelay.value.input)) { + case .success: + stateRelay.value.status = .valid(nil) + case .failure(let error): + stateRelay.value.status = .invalid(error) } + } } diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index d700516e..d8aa7752 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Models @@ -35,6 +34,7 @@ final class ProfileViewModel { @Dependency var messenger: Messenger @Dependency var backupService: BackupService + @Dependency var hudController: HUDController @Dependency var permissions: PermissionHandling var name: String { username! } @@ -42,9 +42,6 @@ final class ProfileViewModel { var state: AnyPublisher<ProfileViewState, Never> { stateRelay.eraseToAnyPublisher() } private let stateRelay = CurrentValueSubject<ProfileViewState, Never>(.init()) - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - var navigation: AnyPublisher<ProfileNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } private let navigationRoutes = PassthroughSubject<ProfileNavigationRoutes, Never>() @@ -87,7 +84,7 @@ final class ProfileViewModel { } func didTapDelete(isEmail: Bool) { - hudRelay.send(.on) + hudController.show() backgroundScheduler.schedule { [weak self] in guard let self = self else { return } @@ -109,11 +106,11 @@ final class ProfileViewModel { } self.backupService.didUpdateFacts() - self.hudRelay.send(.none) + self.hudController.dismiss() self.refresh() } catch { let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) + self.hudController.show(.init(content: xxError)) } } } diff --git a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift index c0af65d5..40d6b4cd 100644 --- a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift @@ -1,12 +1,8 @@ -import HUD import UIKit -import Shared import Combine import DependencyInjection final class RequestsFailedController: UIViewController { - @Dependency private var hud: HUD - lazy private var screenView = RequestsFailedView() private var cancellables = Set<AnyCancellable>() private let viewModel = RequestsFailedViewModel() @@ -39,10 +35,5 @@ final class RequestsFailedController: UIViewController { dataSource?.apply($0, animatingDifferences: false) screenView.collectionView.isHidden = $0.numberOfItems == 0 }.store(in: &cancellables) - - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) } } diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift index c5a91189..b6fc65c1 100644 --- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -9,9 +8,8 @@ import DrawerFeature import DependencyInjection final class RequestsReceivedController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var toaster: ToastController - @Dependency private var coordinator: RequestsCoordinating + @Dependency var toaster: ToastController + @Dependency var coordinator: RequestsCoordinating lazy private var screenView = RequestsReceivedView() private var cancellables = Set<AnyCancellable>() @@ -77,11 +75,6 @@ final class RequestsReceivedController: UIViewController { return cell } - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - viewModel.verifyingPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in presentVerifyingDrawer() } diff --git a/Sources/RequestsFeature/Controllers/RequestsSentController.swift b/Sources/RequestsFeature/Controllers/RequestsSentController.swift index ceed2cc2..b632e821 100644 --- a/Sources/RequestsFeature/Controllers/RequestsSentController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsSentController.swift @@ -1,12 +1,8 @@ -import HUD import UIKit -import Shared import Combine import DependencyInjection final class RequestsSentController: UIViewController { - @Dependency private var hud: HUD - var connectionsPublisher: AnyPublisher<Void, Never> { connectionSubject.eraseToAnyPublisher() } @@ -46,11 +42,6 @@ final class RequestsSentController: UIViewController { screenView.collectionView.isHidden = $0.numberOfItems == 0 }.store(in: &cancellables) - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.connectionsButton .publisher(for: .touchUpInside) .sink { [unowned self] in connectionSubject.send() } diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift index e570e286..97159b02 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift @@ -1,6 +1,6 @@ -import HUD import UIKit import Models +import Shared import Combine import XXModels import Defaults @@ -10,88 +10,84 @@ import DependencyInjection import XXMessengerClient final class RequestsFailedViewModel { - @Dependency var database: Database - @Dependency var messenger: Messenger - - @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 itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, Request>, Never> { - itemsSubject.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, Request>, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - database.fetchContactsPublisher(.init(authStatus: [.requestFailed, .confirmationFailed])) - .replaceError(with: []) - .map { data -> NSDiffableDataSourceSnapshot<Section, Request> in - var snapshot = NSDiffableDataSourceSnapshot<Section, Request>() - snapshot.appendSections([.appearing]) - snapshot.appendItems(data.map { Request.contact($0) }, toSection: .appearing) - return snapshot - }.sink { [unowned self] in itemsSubject.send($0) } - .store(in: &cancellables) + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool + + var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, Request>, Never> { + itemsSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, Request>, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init() { + database.fetchContactsPublisher(.init(authStatus: [.requestFailed, .confirmationFailed])) + .replaceError(with: []) + .map { data -> NSDiffableDataSourceSnapshot<Section, Request> in + var snapshot = NSDiffableDataSourceSnapshot<Section, Request>() + snapshot.appendSections([.appearing]) + snapshot.appendItems(data.map { Request.contact($0) }, toSection: .appearing) + return snapshot + }.sink { [unowned self] in itemsSubject.send($0) } + .store(in: &cancellables) + } + + func didTapStateButtonFor(request: Request) { + guard case var .contact(contact) = request, + request.status == .failedToRequest || + request.status == .failedToConfirm else { + return } - - func didTapStateButtonFor(request: Request) { - guard case var .contact(contact) = request, - request.status == .failedToRequest || - request.status == .failedToConfirm else { - return - } - - hudSubject.send(.on) - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - if request.status == .failedToRequest { - - 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.authStatus = .requested - } else { - let _ = try self.messenger.e2e.get()!.confirmReceivedRequest( - partner: XXClient.Contact.live(contact.marshaled!) - ) - - contact.authStatus = .friend - } - - try self.database.saveContact(contact) - self.hudSubject.send(.none) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudSubject.send(.error(.init(content: xxError))) - } + + hudController.show() + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + if request.status == .failedToRequest { + + 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.authStatus = .requested + } else { + let _ = try self.messenger.e2e.get()!.confirmReceivedRequest( + partner: XXClient.Contact.live(contact.marshaled!) + ) + + contact.authStatus = .friend } + + try self.database.saveContact(contact) + self.hudController.dismiss() + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } + } } diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index fd1cc9b5..c459bf79 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -15,278 +14,274 @@ import XXMessengerClient import struct XXModels.Group struct RequestReceived: Hashable, Equatable { - var request: Request? - var isHidden: Bool - var leader: String? + var request: Request? + var isHidden: Bool + var leader: String? } final class RequestsReceivedViewModel { - @Dependency var database: Database - @Dependency var groupManager: GroupChat - @Dependency var messenger: Messenger - @Dependency var reportingStatus: ReportingStatus - - @KeyObject(.isShowingHiddenRequests, defaultValue: false) var isShowingHiddenRequests: Bool - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - var verifyingPublisher: AnyPublisher<Void, Never> { - verifyingSubject.eraseToAnyPublisher() + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var groupManager: GroupChat + @Dependency var hudController: HUDController + @Dependency var reportingStatus: ReportingStatus + + @KeyObject(.isShowingHiddenRequests, defaultValue: false) var isShowingHiddenRequests: Bool + + var verifyingPublisher: AnyPublisher<Void, Never> { + verifyingSubject.eraseToAnyPublisher() + } + + var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never> { + itemsSubject.eraseToAnyPublisher() + } + + var groupConfirmationPublisher: AnyPublisher<Group, Never> { + groupConfirmationSubject.eraseToAnyPublisher() + } + + var contactConfirmationPublisher: AnyPublisher<XXModels.Contact, Never> { + contactConfirmationSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let updateSubject = CurrentValueSubject<Void, Never>(()) + private let verifyingSubject = PassthroughSubject<Void, Never>() + private let groupConfirmationSubject = PassthroughSubject<Group, Never>() + private let contactConfirmationSubject = PassthroughSubject<XXModels.Contact, Never>() + private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init() { + let groupsQuery = Group.Query( + authStatus: [ + .hidden, + .pending + ], + isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, + isLeaderBanned: reportingStatus.isEnabled() ? false : nil + ) + + let contactsQuery = Contact.Query( + authStatus: [ + .friend, + .hidden, + .verified, + .verificationFailed, + .verificationInProgress + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + let groupStream = database + .fetchGroupsPublisher(groupsQuery) + .replaceError(with: []) + + let contactsStream = database + .fetchContactsPublisher(contactsQuery) + .replaceError(with: []) + + Publishers.CombineLatest3( + groupStream, + contactsStream, + updateSubject.eraseToAnyPublisher() + ) + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.global()) + .map { [unowned self] data -> NSDiffableDataSourceSnapshot<Section, RequestReceived> in + var snapshot = NSDiffableDataSourceSnapshot<Section, RequestReceived>() + snapshot.appendSections([.appearing, .hidden]) + + let contactsFilteringFriends = data.1.filter { $0.authStatus != .friend } + let requests = data.0.map(Request.group) + contactsFilteringFriends.map(Request.contact) + let receivedRequests = requests.map { request -> RequestReceived in + switch request { + case let .group(group): + func leaderName() -> String { + if let leader = data.1.first(where: { $0.id == group.leaderId }) { + return (leader.nickname ?? leader.username) ?? "Leader is not a friend" + } else { + return "[Error retrieving leader]" + } + } + + return RequestReceived( + request: request, + isHidden: group.authStatus == .hidden, + leader: leaderName() + ) + case let .contact(contact): + return RequestReceived( + request: request, + isHidden: contact.authStatus == .hidden, + leader: nil + ) + } + } + + if self.isShowingHiddenRequests { + snapshot.appendItems(receivedRequests.filter(\.isHidden), toSection: .hidden) + } + + guard receivedRequests.filter({ $0.isHidden == false }).count > 0 else { + snapshot.appendItems([RequestReceived(isHidden: false)], toSection: .appearing) + return snapshot + } + + snapshot.appendItems(receivedRequests.filter { $0.isHidden == false }, toSection: .appearing) + return snapshot + }.sink( + receiveCompletion: { _ in }, + receiveValue: { [unowned self] in itemsSubject.send($0) } + ).store(in: &cancellables) + } + + func didToggleHiddenRequestsSwitcher() { + isShowingHiddenRequests.toggle() + updateSubject.send() + } + + func didTapStateButtonFor(request: Request) { + guard case var .contact(contact) = request else { return } + + if request.status == .failedToVerify { + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + contact.authStatus = .verificationInProgress + try self.database.saveContact(contact) + + print(">>> [messenger.verifyContact] will start") + + if try self.messenger.verifyContact(XXClient.Contact.live(contact.marshaled!)) { + print(">>> [messenger.verifyContact] verified") + + contact.authStatus = .verified + contact = try self.database.saveContact(contact) + } else { + print(">>> [messenger.verifyContact] is fake") + + try self.database.deleteContact(contact) + } + } catch { + print(">>> [messenger.verifyContact] thrown an exception: \(error.localizedDescription)") + + contact.authStatus = .verificationFailed + _ = try? self.database.saveContact(contact) + } + } + } else if request.status == .verifying { + verifyingSubject.send() } - - var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never> { - itemsSubject.eraseToAnyPublisher() + } + + func didRequestHide(group: Group) { + if var group = try? database.fetchGroups(.init(id: [group.id])).first { + group.authStatus = .hidden + _ = try? database.saveGroup(group) } - - var groupConfirmationPublisher: AnyPublisher<Group, Never> { - groupConfirmationSubject.eraseToAnyPublisher() + } + + func didRequestAccept(group: Group) { + hudController.show() + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.groupManager.joinGroup(serializedGroupData: group.serialized) + + var group = group + group.authStatus = .participating + try self.database.saveGroup(group) + + self.hudController.dismiss() + self.groupConfirmationSubject.send(group) + } catch { + self.hudController.show(.init(error: error)) + } } - - var contactConfirmationPublisher: AnyPublisher<XXModels.Contact, Never> { - contactConfirmationSubject.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let updateSubject = CurrentValueSubject<Void, Never>(()) - private let verifyingSubject = PassthroughSubject<Void, Never>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let groupConfirmationSubject = PassthroughSubject<Group, Never>() - private let contactConfirmationSubject = PassthroughSubject<XXModels.Contact, Never>() - private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - let groupsQuery = Group.Query( - authStatus: [ - .hidden, - .pending - ], - isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, - isLeaderBanned: reportingStatus.isEnabled() ? false : nil - ) - - let contactsQuery = Contact.Query( - authStatus: [ - .friend, - .hidden, - .verified, - .verificationFailed, - .verificationInProgress - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - let groupStream = database - .fetchGroupsPublisher(groupsQuery) - .replaceError(with: []) - - let contactsStream = database - .fetchContactsPublisher(contactsQuery) + } + + func fetchMembers( + _ group: Group, + _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void + ) { + if let info = try? database.fetchGroupInfos(.init(groupId: group.id)).first { + database.fetchContactsPublisher(.init(id: Set(info.members.map(\.id)))) .replaceError(with: []) - - Publishers.CombineLatest3( - groupStream, - contactsStream, - updateSubject.eraseToAnyPublisher() - ) - .subscribe(on: DispatchQueue.main) - .receive(on: DispatchQueue.global()) - .map { [unowned self] data -> NSDiffableDataSourceSnapshot<Section, RequestReceived> in - var snapshot = NSDiffableDataSourceSnapshot<Section, RequestReceived>() - snapshot.appendSections([.appearing, .hidden]) - - let contactsFilteringFriends = data.1.filter { $0.authStatus != .friend } - let requests = data.0.map(Request.group) + contactsFilteringFriends.map(Request.contact) - let receivedRequests = requests.map { request -> RequestReceived in - switch request { - case let .group(group): - func leaderName() -> String { - if let leader = data.1.first(where: { $0.id == group.leaderId }) { - return (leader.nickname ?? leader.username) ?? "Leader is not a friend" - } else { - return "[Error retrieving leader]" - } - } - - return RequestReceived( - request: request, - isHidden: group.authStatus == .hidden, - leader: leaderName() - ) - case let .contact(contact): - return RequestReceived( - request: request, - isHidden: contact.authStatus == .hidden, - leader: nil - ) - } + .sink { members in + let withUsername = members + .filter { $0.username != nil } + .map { + DrawerTableCellModel( + id: $0.id, + title: $0.nickname ?? $0.username!, + image: $0.photo, + isCreator: $0.id == group.leaderId, + isConnection: $0.authStatus == .friend + ) } - - if self.isShowingHiddenRequests { - snapshot.appendItems(receivedRequests.filter(\.isHidden), toSection: .hidden) + + let withoutUsername = members + .filter { $0.username == nil } + .map { + DrawerTableCellModel( + id: $0.id, + title: "Fetching username...", + image: $0.photo, + isCreator: $0.id == group.leaderId, + isConnection: $0.authStatus == .friend + ) } - - guard receivedRequests.filter({ $0.isHidden == false }).count > 0 else { - snapshot.appendItems([RequestReceived(isHidden: false)], toSection: .appearing) - return snapshot - } - - snapshot.appendItems(receivedRequests.filter { $0.isHidden == false }, toSection: .appearing) - return snapshot - }.sink( - receiveCompletion: { _ in }, - receiveValue: { [unowned self] in itemsSubject.send($0) } - ).store(in: &cancellables) + + completion(.success(withUsername + withoutUsername)) + }.store(in: &cancellables) } - - func didToggleHiddenRequestsSwitcher() { - isShowingHiddenRequests.toggle() - updateSubject.send() + } + + func didRequestHide(contact: XXModels.Contact) { + if var contact = try? database.fetchContacts(.init(id: [contact.id])).first { + contact.authStatus = .hidden + _ = try? database.saveContact(contact) } - - func didTapStateButtonFor(request: Request) { - guard case var .contact(contact) = request else { return } - - if request.status == .failedToVerify { - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - contact.authStatus = .verificationInProgress - try self.database.saveContact(contact) - - print(">>> [messenger.verifyContact] will start") - - if try self.messenger.verifyContact(XXClient.Contact.live(contact.marshaled!)) { - print(">>> [messenger.verifyContact] verified") - - contact.authStatus = .verified - contact = try self.database.saveContact(contact) - } else { - print(">>> [messenger.verifyContact] is fake") - - try self.database.deleteContact(contact) - } - } catch { - print(">>> [messenger.verifyContact] thrown an exception: \(error.localizedDescription)") - - contact.authStatus = .verificationFailed - _ = try? self.database.saveContact(contact) - } - } - } else if request.status == .verifying { - verifyingSubject.send() - } - } - - func didRequestHide(group: Group) { - if var group = try? database.fetchGroups(.init(id: [group.id])).first { - group.authStatus = .hidden - _ = try? database.saveGroup(group) - } + } + + func didRequestAccept(contact: XXModels.Contact, nickname: String? = nil) { + hudController.show() + + var contact = contact + contact.authStatus = .confirming + contact.nickname = nickname ?? contact.username + + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try self.database.saveContact(contact) + + let _ = try self.messenger.e2e.get()!.confirmReceivedRequest(partner: .live(contact.marshaled!)) + contact.authStatus = .friend + try self.database.saveContact(contact) + + self.hudController.dismiss() + self.contactConfirmationSubject.send(contact) + } catch { + contact.authStatus = .confirmationFailed + _ = try? self.database.saveContact(contact) + self.hudController.show(.init(error: error)) + } } - - func didRequestAccept(group: Group) { - hudSubject.send(.on) - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.groupManager.joinGroup(serializedGroupData: group.serialized) - - var group = group - group.authStatus = .participating - try self.database.saveGroup(group) - - self.hudSubject.send(.none) - self.groupConfirmationSubject.send(group) - } catch { - self.hudSubject.send(.error(.init(with: error))) - } - } - } - - func fetchMembers( - _ group: Group, - _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void - ) { - if let info = try? database.fetchGroupInfos(.init(groupId: group.id)).first { - database.fetchContactsPublisher(.init(id: Set(info.members.map(\.id)))) - .replaceError(with: []) - .sink { members in - let withUsername = members - .filter { $0.username != nil } - .map { - DrawerTableCellModel( - id: $0.id, - title: $0.nickname ?? $0.username!, - image: $0.photo, - isCreator: $0.id == group.leaderId, - isConnection: $0.authStatus == .friend - ) - } - - let withoutUsername = members - .filter { $0.username == nil } - .map { - DrawerTableCellModel( - id: $0.id, - title: "Fetching username...", - image: $0.photo, - isCreator: $0.id == group.leaderId, - isConnection: $0.authStatus == .friend - ) - } - - completion(.success(withUsername + withoutUsername)) - }.store(in: &cancellables) - } - } - - func didRequestHide(contact: XXModels.Contact) { - if var contact = try? database.fetchContacts(.init(id: [contact.id])).first { - contact.authStatus = .hidden - _ = try? database.saveContact(contact) - } - } - - func didRequestAccept(contact: XXModels.Contact, nickname: String? = nil) { - hudSubject.send(.on) - - var contact = contact - contact.authStatus = .confirming - contact.nickname = nickname ?? contact.username - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try self.database.saveContact(contact) - - let _ = try self.messenger.e2e.get()!.confirmReceivedRequest(partner: .live(contact.marshaled!)) - contact.authStatus = .friend - try self.database.saveContact(contact) - - self.hudSubject.send(.none) - self.contactConfirmationSubject.send(contact) - } catch { - contact.authStatus = .confirmationFailed - _ = try? self.database.saveContact(contact) - self.hudSubject.send(.error(.init(with: error))) - } - } - } - - func groupChatWith(group: Group) -> GroupInfo { - guard let info = try? database.fetchGroupInfos(.init(groupId: group.id)).first else { - fatalError() - } - - return info + } + + func groupChatWith(group: Group) -> GroupInfo { + guard let info = try? database.fetchGroupInfos(.init(groupId: group.id)).first else { + fatalError() } + + return info + } } diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index 86dd9591..9faa4954 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Models import Shared @@ -13,121 +12,117 @@ import DependencyInjection import XXMessengerClient struct RequestSent: Hashable, Equatable { - var request: Request - var isResent: Bool = false + var request: Request + var isResent: Bool = false } final class RequestsSentViewModel { - @Dependency var database: Database - @Dependency var messenger: Messenger - @Dependency var reportingStatus: ReportingStatus - @Dependency var toastController: ToastController - - @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 itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never> { - itemsSubject.eraseToAnyPublisher() - } - - private var cancellables = Set<AnyCancellable>() - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never>(.init()) - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - init() { - let query = Contact.Query( - authStatus: [ - .requested, - .requesting - ], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil - ) - - database.fetchContactsPublisher(query) - .replaceError(with: []) - .removeDuplicates() - .map { data -> NSDiffableDataSourceSnapshot<Section, RequestSent> in - var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() - snapshot.appendSections([.appearing]) - snapshot.appendItems(data.map { RequestSent(request: .contact($0)) }, toSection: .appearing) - return snapshot - }.sink { [unowned self] in itemsSubject.send($0) } - .store(in: &cancellables) + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + @Dependency var reportingStatus: ReportingStatus + @Dependency var toastController: ToastController + + @KeyObject(.username, defaultValue: nil) var username: String? + @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool + @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool + + var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never> { + itemsSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestSent>, Never>(.init()) + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + init() { + let query = Contact.Query( + authStatus: [ + .requested, + .requesting + ], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) + + database.fetchContactsPublisher(query) + .replaceError(with: []) + .removeDuplicates() + .map { data -> NSDiffableDataSourceSnapshot<Section, RequestSent> in + var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() + snapshot.appendSections([.appearing]) + snapshot.appendItems(data.map { RequestSent(request: .contact($0)) }, toSection: .appearing) + return snapshot + }.sink { [unowned self] in itemsSubject.send($0) } + .store(in: &cancellables) + } + + func didTapStateButtonFor(request item: RequestSent) { + guard case let .contact(contact) = item.request, + item.request.status == .requested || + item.request.status == .requesting || + item.request.status == .failedToRequest else { + return } - - func didTapStateButtonFor(request item: RequestSent) { - guard case let .contact(contact) = item.request, - item.request.status == .requested || - item.request.status == .requesting || - item.request.status == .failedToRequest else { - return + + let name = (contact.nickname ?? contact.username) ?? "" + + hudController.show() + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() + + if let fact = myFacts.get(.username) { + includedFacts.append(fact) } - - let name = (contact.nickname ?? contact.username) ?? "" - - hudSubject.send(.on) - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - 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 - ) - - self.hudSubject.send(.none) - - var item = item - var allRequests = self.itemsSubject.value.itemIdentifiers - - if let indexOfRequest = allRequests.firstIndex(of: item) { - allRequests.remove(at: indexOfRequest) - } - - item.isResent = true - allRequests.append(item) - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resent(name), - leftImage: Asset.requestSentToaster.image - )) - - var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() - snapshot.appendSections([.appearing]) - snapshot.appendItems(allRequests, toSection: .appearing) - self.itemsSubject.send(snapshot) - } catch { - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resentFailed(name), - leftImage: Asset.requestFailedToaster.image - )) - - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudSubject.send(.error(.init(content: xxError))) - } + + 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 + ) + + self.hudController.dismiss() + + var item = item + var allRequests = self.itemsSubject.value.itemIdentifiers + + if let indexOfRequest = allRequests.firstIndex(of: item) { + allRequests.remove(at: indexOfRequest) } + + item.isResent = true + allRequests.append(item) + + self.toastController.enqueueToast(model: .init( + title: Localized.Requests.Sent.Toast.resent(name), + leftImage: Asset.requestSentToaster.image + )) + + var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() + snapshot.appendSections([.appearing]) + snapshot.appendItems(allRequests, toSection: .appearing) + self.itemsSubject.send(snapshot) + } catch { + self.toastController.enqueueToast(model: .init( + title: Localized.Requests.Sent.Toast.resentFailed(name), + leftImage: Asset.requestFailedToaster.image + )) + + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) + } } + } } diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift index 15acce20..bfea5df3 100644 --- a/Sources/RestoreFeature/Controllers/RestoreListController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -6,7 +5,6 @@ import DrawerFeature import DependencyInjection public final class RestoreListController: UIViewController { - @Dependency var hud: HUD @Dependency var coordinator: RestoreCoordinating lazy private var screenView = RestoreListView() @@ -42,11 +40,6 @@ public final class RestoreListController: UIViewController { } }.store(in: &cancellables) - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - viewModel.detailsPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in diff --git a/Sources/RestoreFeature/Controllers/RestoreSFTPController.swift b/Sources/RestoreFeature/Controllers/RestoreSFTPController.swift index c60b41d9..2ddf4ab1 100644 --- a/Sources/RestoreFeature/Controllers/RestoreSFTPController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreSFTPController.swift @@ -1,12 +1,9 @@ -import HUD import UIKit import Combine import DependencyInjection import ScrollViewController public final class RestoreSFTPController: UIViewController { - @Dependency private var hud: HUD - lazy private var screenView = RestoreSFTPView() lazy private var scrollViewController = ScrollViewController() @@ -44,11 +41,6 @@ public final class RestoreSFTPController: UIViewController { } private func setupBindings() { - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - viewModel.authPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] params in diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift index 2b992439..249ab05d 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift @@ -1,5 +1,5 @@ -import HUD import UIKit +import Shared import Combine import CloudFiles import CloudFilesSFTP @@ -11,20 +11,17 @@ public struct RestorationDetails { } final class RestoreListViewModel { + @Dependency var hudController: HUDController + var sftpPublisher: AnyPublisher<Void, Never> { sftpSubject.eraseToAnyPublisher() } - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - var detailsPublisher: AnyPublisher<RestorationDetails, Never> { detailsSubject.eraseToAnyPublisher() } private let sftpSubject = PassthroughSubject<Void, Never>() - private let hudSubject = PassthroughSubject<HUDStatus, Never>() private let detailsSubject = PassthroughSubject<RestorationDetails, Never>() func setupSFTP(host: String, username: String, password: String) { @@ -54,33 +51,33 @@ final class RestoreListViewModel { case .success: onSuccess() case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) + self.hudController.show(.init(error: error)) } } } catch { - hudSubject.send(.error(.init(with: error))) + hudController.show(.init(error: error)) } } func fetch(provider: CloudService) { - hudSubject.send(.on) + hudController.show() do { try CloudFilesManager.all[provider]!.fetch { [weak self] in guard let self else { return } switch $0 { case .success(let metadata): - self.hudSubject.send(.none) + self.hudController.dismiss() self.detailsSubject.send(.init( provider: provider, metadata: metadata )) case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) + self.hudController.show(.init(error: error)) } } } catch { - hudSubject.send(.error(.init(with: error))) + hudController.show(.init(error: error)) } } } diff --git a/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift index 6c509cbb..bb81db9e 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift @@ -1,9 +1,10 @@ -import HUD import UIKit +import Shared import Combine import Foundation import CloudFiles import CloudFilesSFTP +import DependencyInjection struct SFTPViewState { var host: String = "" @@ -13,9 +14,7 @@ struct SFTPViewState { } final class RestoreSFTPViewModel { - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } + @Dependency var hudController: HUDController var statePublisher: AnyPublisher<SFTPViewState, Never> { stateSubject.eraseToAnyPublisher() @@ -25,7 +24,6 @@ final class RestoreSFTPViewModel { authSubject.eraseToAnyPublisher() } - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) private let authSubject = PassthroughSubject<(String, String, String), Never>() @@ -45,7 +43,7 @@ final class RestoreSFTPViewModel { } func didTapLogin() { - hudSubject.send(.on) + hudController.show() let host = stateSubject.value.host let username = stateSubject.value.username @@ -64,14 +62,14 @@ final class RestoreSFTPViewModel { ).link(anyController) { switch $0 { case .success: - self.hudSubject.send(.none) + self.hudController.dismiss() self.authSubject.send((host, username, password)) case .failure(let error): - self.hudSubject.send(.error(.init(with: error))) + self.hudController.show(.init(error: error)) } } } catch { - self.hudSubject.send(.error(.init(with: error))) + self.hudController.show(.init(error: error)) } } } diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index cc46ad60..e850dfde 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -9,8 +8,7 @@ import DrawerFeature import DependencyInjection final class SearchLeftController: UIViewController { - @Dependency private var hud: HUD - @Dependency private var coordinator: SearchCoordinating + @Dependency var coordinator: SearchCoordinating @KeyObject(.email, defaultValue: nil) var email: String? @KeyObject(.phone, defaultValue: nil) var phone: String? @@ -110,24 +108,23 @@ final class SearchLeftController: UIViewController { } private func setupBindings() { - viewModel.hudPublisher - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - hud.update(with: $0) - - if case .onAction = $0, let hudBtn = hud.actionButton { - hudBtn.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { [unowned self] in viewModel.didTapCancelSearch() } - .store(in: &self.hudCancellables) - } else { - hudCancellables.forEach { $0.cancel() } - hudCancellables.removeAll() - } - } - .store(in: &cancellables) - +// viewModel.hudPublisher +// .removeDuplicates() +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in +// hud.update(with: $0) +// +// if case .onAction = $0, let hudBtn = hud.actionButton { +// hudBtn.publisher(for: .touchUpInside) +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in viewModel.didTapCancelSearch() } +// .store(in: &self.hudCancellables) +// } else { +// hudCancellables.forEach { $0.cancel() } +// hudCancellables.removeAll() +// } +// } +// .store(in: &cancellables) viewModel.statePublisher .map(\.item) diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index e7a0921a..5fd6243a 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -1,4 +1,3 @@ -import HUD import Retry import UIKit import Models @@ -18,350 +17,346 @@ import DependencyInjection typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> struct SearchLeftViewState { - var input = "" - var snapshot: SearchSnapshot? - var country: Country = .fromMyPhone() - var item: SearchSegmentedControl.Item = .username + var input = "" + var snapshot: SearchSnapshot? + var country: Country = .fromMyPhone() + var item: SearchSegmentedControl.Item = .username } final class SearchLeftViewModel { - @Dependency var database: Database - @Dependency var messenger: Messenger - @Dependency var reportingStatus: ReportingStatus - @Dependency var toastController: ToastController - @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 myId: Data { - try! messenger.e2e.get()!.getContact().getId() + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var hudController: HUDController + @Dependency var reportingStatus: ReportingStatus + @Dependency var toastController: ToastController + @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 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 + hudController.show(.init(actionTitle: Localized.Ud.Search.cancel)) + + networkCancellable.removeAll() + + networkMonitor.statusPublisher + .first { $0 == .available } + .eraseToAnyPublisher() + .flatMap { _ in + self.waitForNodes(timeout: 5) + }.sink(receiveCompletion: { + if case .failure(let error) = $0 { + self.hudController.show(.init(error: error)) + } + }, receiveValue: { + self.didStartSearching() + }).store(in: &networkCancellable) } + } - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } + func didEnterInput(_ string: String) { + stateSubject.value.input = string + } - var successPublisher: AnyPublisher<XXModels.Contact, Never> { - successSubject.eraseToAnyPublisher() - } + func didPick(country: Country) { + stateSubject.value.country = country + } - var statePublisher: AnyPublisher<SearchLeftViewState, Never> { - stateSubject.eraseToAnyPublisher() - } + func didSelectItem(_ item: SearchSegmentedControl.Item) { + stateSubject.value.item = item + } - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + func didTapCancelSearch() { + searchCancellables.forEach { $0.cancel() } + searchCancellables.removeAll() + hudController.dismiss() + } - var invitation: String? - 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>() + func didStartSearching() { + guard stateSubject.value.input.isEmpty == false else { return } - 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)) - - networkCancellable.removeAll() - - 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) - } - } + hudController.show(.init(actionTitle: Localized.Ud.Search.cancel)) - func didEnterInput(_ string: String) { - stateSubject.value.input = string - } + var content = stateSubject.value.input - func didPick(country: Country) { - stateSubject.value.country = country + if stateSubject.value.item == .phone { + content += stateSubject.value.country.code } - func didSelectItem(_ item: SearchSegmentedControl.Item) { - stateSubject.value.item = item + enum NodeRegistrationError: Error { + case unhealthyNet + case belowMinimum } - func didTapCancelSearch() { - searchCancellables.forEach { $0.cancel() } - searchCancellables.removeAll() - hudSubject.send(.none) + 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.hudController.show(.init(content: "Network is not healthy yet, try again within the next minute or so.")) + } else if case .belowMinimum = $0 as? NodeRegistrationError { + self.hudController.show(.init(content:"Node registration ratio is still below 80%, try again within the next minute or so.")) + } else { + self.hudController.show(.init(error: $0)) + } + + return } - func didStartSearching() { - guard stateSubject.value.input.isEmpty == false else { return } - - hudSubject.send(.onAction(Localized.Ud.Search.cancel)) - - var content = stateSubject.value.input - - if stateSubject.value.item == .phone { - content += stateSubject.value.country.code - } - - enum NodeRegistrationError: Error { - case unhealthyNet - case belowMinimum - } + var factType: FactType = .username - 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 - } - - var factType: FactType = .username - - if stateSubject.value.item == .phone { - factType = .phone - } else if stateSubject.value.item == .email { - factType = .email - } + 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 { - case .success(let results): - 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, - isRecent: true, - isBlocked: false, - isBanned: false, - createdAt: Date() - ) - ) - case .failure(let error): - print(">>> SearchUD error: \(error.localizedDescription)") - - self.appendToLocalSearch(nil) - self.hudSubject.send(.error(.init(with: error))) - } - }) + 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 { + case .success(let results): + self.hudController.dismiss() + 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)") - print(">>> UDSearch.Report: \(report))") - } catch { - print(">>> UDSearch.Exception: \(error.localizedDescription)") + self.appendToLocalSearch(nil) + self.hudController.show(.init(error: error)) } - } - } - - func didTapResend(contact: XXModels.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) + print(">>> UDSearch.Report: \(report))") + } catch { + print(">>> UDSearch.Exception: \(error.localizedDescription)") + } + } + } - var includedFacts: [Fact] = [] - let myFacts = try self.messenger.ud.get()!.getFacts() + func didTapResend(contact: XXModels.Contact) { + hudController.show() - if let fact = myFacts.get(.username) { - includedFacts.append(fact) - } + var contact = contact + contact.authStatus = .requesting - if self.sharingEmail, let fact = myFacts.get(.email) { - includedFacts.append(fact) - } + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } - if self.sharingPhone, let fact = myFacts.get(.phone) { - includedFacts.append(fact) - } + do { + try self.database.saveContact(contact) - let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( - partner: .live(contact.marshaled!), - myFacts: includedFacts - ) + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() - contact.authStatus = .requested - contact = try self.database.saveContact(contact) - - self.hudSubject.send(.none) - self.presentSuccessToast(for: contact, resent: true) - } catch { - contact.authStatus = .requestFailed - _ = try? self.database.saveContact(contact) - self.hudSubject.send(.error(.init(with: error))) - } + if let fact = myFacts.get(.username) { + includedFacts.append(fact) } - } - func didTapRequest(contact: XXModels.Contact) { - hudSubject.send(.on) + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) + } - var contact = contact - contact.nickname = contact.username - contact.authStatus = .requesting + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) + } - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(contact.marshaled!), + myFacts: includedFacts + ) - do { - try self.database.saveContact(contact) + contact.authStatus = .requested + contact = try self.database.saveContact(contact) - var includedFacts: [Fact] = [] - let myFacts = try self.messenger.ud.get()!.getFacts() + self.hudController.dismiss() + self.presentSuccessToast(for: contact, resent: true) + } catch { + contact.authStatus = .requestFailed + _ = try? self.database.saveContact(contact) + self.hudController.show(.init(error: error)) + } + } + } - if let fact = myFacts.get(.username) { - includedFacts.append(fact) - } + func didTapRequest(contact: XXModels.Contact) { + hudController.show() - if self.sharingEmail, let fact = myFacts.get(.email) { - includedFacts.append(fact) - } + var contact = contact + contact.nickname = contact.username + contact.authStatus = .requesting - if self.sharingPhone, let fact = myFacts.get(.phone) { - includedFacts.append(fact) - } + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } - let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( - partner: .live(contact.marshaled!), - myFacts: includedFacts - ) + do { + try self.database.saveContact(contact) - contact.authStatus = .requested - contact = try self.database.saveContact(contact) + var includedFacts: [Fact] = [] + let myFacts = try self.messenger.ud.get()!.getFacts() - 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))) - } + if let fact = myFacts.get(.username) { + includedFacts.append(fact) } - } - func didSet(nickname: String, for contact: XXModels.Contact) { - if var contact = try? database.fetchContacts(.init(id: [contact.id])).first { - contact.nickname = nickname - _ = try? database.saveContact(contact) + if self.sharingEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) } - } - private func appendToLocalSearch(_ user: XXModels.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) - } + if self.sharingPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) } - let localsQuery = Contact.Query( - text: stateSubject.value.input, - authStatus: [.friend], - isBlocked: reportingStatus.isEnabled() ? false : nil, - isBanned: reportingStatus.isEnabled() ? false : nil + let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel( + partner: .live(contact.marshaled!), + myFacts: includedFacts ) - if let locals = try? database.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 + contact.authStatus = .requested + contact = try self.database.saveContact(contact) + + self.hudController.dismiss() + self.successSubject.send(contact) + self.presentSuccessToast(for: contact, resent: false) + } catch { + contact.authStatus = .requestFailed + _ = try? self.database.saveContact(contact) + self.hudController.show(.init(error: error)) + } } + } - private func removeMyself(from collection: [XXModels.Contact]) -> [XXModels.Contact]? { - collection.filter { $0.id != myId } + func didSet(nickname: String, for contact: XXModels.Contact) { + if var contact = try? database.fetchContacts(.init(id: [contact.id])).first { + contact.nickname = nickname + _ = try? database.saveContact(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 appendToLocalSearch(_ user: XXModels.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) + } } - 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() + 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.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 ?? "") + + 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() + } } diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift index 0f583b42..7b23ea80 100644 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift @@ -1,17 +1,15 @@ -import HUD import UIKit -import DrawerFeature import Shared import Combine import Defaults +import DrawerFeature import ScrollViewController import DependencyInjection public final class AccountDeleteController: UIViewController { @KeyObject(.username, defaultValue: "") var username: String - @Dependency private var hud: HUD - @Dependency private var coordinator: SettingsCoordinating + @Dependency var coordinator: SettingsCoordinating lazy private var screenView = AccountDeleteView() lazy private var scrollViewController = ScrollViewController() @@ -51,11 +49,6 @@ public final class AccountDeleteController: UIViewController { } private func setupBindings() { - viewModel.hudPublisher - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.cancelButton.publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in dismiss(animated: true) } diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift index af68cf87..de85af7c 100644 --- a/Sources/SettingsFeature/Controllers/SettingsController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsController.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -7,7 +6,6 @@ import DependencyInjection import ScrollViewController public final class SettingsController: UIViewController { - @Dependency var hud: HUD @Dependency var barStylist: StatusBarStylist @Dependency var coordinator: SettingsCoordinating @@ -92,11 +90,6 @@ public final class SettingsController: UIViewController { } private func setupBindings() { - viewModel.hud - .receive(on: DispatchQueue.main) - .sink { [hud] in hud.update(with: $0) } - .store(in: &cancellables) - screenView.inAppNotifications.switcherView .publisher(for: .valueChanged) .sink { [weak viewModel] in viewModel?.didToggleInAppNotifications() } diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift index fa7592a9..9065c744 100644 --- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift @@ -1,81 +1,77 @@ -import HUD +import Shared +import Retry import Models import Combine import Defaults import Keychain +import XXModels import XXClient import Foundation import XXMessengerClient import DependencyInjection -import Retry -import XXModels final class AccountDeleteViewModel { - @Dependency var messenger: Messenger - @Dependency var keychain: KeychainHandling - @Dependency var database: Database - - @KeyObject(.username, defaultValue: nil) var username: String? - - var hudPublisher: AnyPublisher<HUDStatus, Never> { - hudSubject.eraseToAnyPublisher() - } - - private var isCurrentlyDeleting = false - private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) - - func didTapDelete() { - guard isCurrentlyDeleting == false else { return } - isCurrentlyDeleting = true - - hudSubject.send(.on) - - do { - print(">>> try self.cleanUD()") - try cleanUD() - - print(">>> try self.messenger.destroy()") - try messenger.destroy() - - print(">>> try self.keychain.clear()") - try keychain.clear() - - print(">>> try database.drop()") - try database.drop() - - print(">>> try self.deleteDatabase()") - try deleteDatabase() - - UserDefaults.resetStandardUserDefaults() - UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) - UserDefaults.standard.synchronize() - - hudSubject.send(.error(.init( - content: "Now kill the app and re-open", - title: "Account deleted", - dismissable: false - ))) - } catch { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.hudSubject.send(.error(.init(with: error))) - } - } - } - - private func cleanUD() throws { - print(">>> Deleting my username (\(username ?? "NO_USERNAME")) from ud") - try messenger.ud.get()!.permanentDeleteAccount(username: .init(type: .username, value: username!)) - } - - private func deleteDatabase() throws { - print(">>> Deleting database...") - - let dbPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! - .appendingPathComponent("xxm_database") - .appendingPathExtension("sqlite").path - - try FileManager.default.removeItem(atPath: dbPath) + @Dependency var database: Database + @Dependency var messenger: Messenger + @Dependency var keychain: KeychainHandling + @Dependency var hudController: HUDController + + @KeyObject(.username, defaultValue: nil) var username: String? + + private var isCurrentlyDeleting = false + + func didTapDelete() { + guard isCurrentlyDeleting == false else { return } + isCurrentlyDeleting = true + + hudController.show() + + do { + print(">>> try self.cleanUD()") + try cleanUD() + + print(">>> try self.messenger.destroy()") + try messenger.destroy() + + print(">>> try self.keychain.clear()") + try keychain.clear() + + print(">>> try database.drop()") + try database.drop() + + print(">>> try self.deleteDatabase()") + try deleteDatabase() + + UserDefaults.resetStandardUserDefaults() + UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) + UserDefaults.standard.synchronize() + + hudController.show(.init( + title: "Account deleted", + content: "Now kill the app and re-open", + isDismissable: false + )) + } catch { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.hudController.show(.init(error: error)) + } } + } + + private func cleanUD() throws { + print(">>> Deleting my username (\(username ?? "NO_USERNAME")) from ud") + try messenger.ud.get()!.permanentDeleteAccount(username: .init(type: .username, value: username!)) + } + + private func deleteDatabase() throws { + print(">>> Deleting database...") + + let dbPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("xxm_database") + .appendingPathExtension("sqlite").path + + try FileManager.default.removeItem(atPath: dbPath) + } } diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index e157bce2..3bea57be 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -1,4 +1,3 @@ -import HUD import UIKit import Shared import Combine @@ -12,144 +11,140 @@ import CombineSchedulers import DependencyInjection struct SettingsViewState: Equatable { - var isHideActiveApps: Bool = false - var isPushNotification: Bool = false - var isIcognitoKeyboard: Bool = false - var isInAppNotification: Bool = false - var isBiometricsEnabled: Bool = false - var isBiometricsPossible: Bool = false - var isDummyTrafficOn = false + var isHideActiveApps: Bool = false + var isPushNotification: Bool = false + var isIcognitoKeyboard: Bool = false + var isInAppNotification: Bool = false + var isBiometricsEnabled: Bool = false + var isBiometricsPossible: Bool = false + var isDummyTrafficOn = false } final class SettingsViewModel { - @Dependency var messenger: Messenger - @Dependency var pushHandler: PushHandling - @Dependency var permissions: PermissionHandling - @Dependency var dummyTrafficManager: DummyTraffic - - @KeyObject(.biometrics, defaultValue: false) var biometrics - @KeyObject(.hideAppList, defaultValue: false) var hideAppList - @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn - @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard - @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications - @KeyObject(.inappnotifications, defaultValue: true) var inAppNotifications - - var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() - - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) - - var state: AnyPublisher<SettingsViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<SettingsViewState, Never>(.init()) - - func loadCachedSettings() { - stateRelay.value.isHideActiveApps = hideAppList - stateRelay.value.isBiometricsEnabled = biometrics - stateRelay.value.isIcognitoKeyboard = icognitoKeyboard - stateRelay.value.isPushNotification = pushNotifications - stateRelay.value.isInAppNotification = inAppNotifications - stateRelay.value.isBiometricsPossible = permissions.isBiometricsAvailable - stateRelay.value.isDummyTrafficOn = dummyTrafficManager.getStatus() - } - - func didToggleBiometrics() { - biometricAuthentication(enable: !biometrics) - } - - func didToggleInAppNotifications() { - inAppNotifications.toggle() - stateRelay.value.isInAppNotification.toggle() - } - - func didTogglePushNotifications() { - pushNotifications(enable: !pushNotifications) - } - - func didToggleDummyTraffic() { - let currently = dummyTrafficManager.getStatus() - try! dummyTrafficManager.setStatus(!currently) - stateRelay.value.isDummyTrafficOn = !currently - dummyTrafficOn = stateRelay.value.isDummyTrafficOn - } - - func didToggleHideActiveApps() { - hideAppList.toggle() - stateRelay.value.isHideActiveApps.toggle() - } - - func didToggleIcognitoKeyboard() { - icognitoKeyboard.toggle() - stateRelay.value.isIcognitoKeyboard.toggle() + @Dependency var messenger: Messenger + @Dependency var pushHandler: PushHandling + @Dependency var hudController: HUDController + @Dependency var permissions: PermissionHandling + @Dependency var dummyTrafficManager: DummyTraffic + + @KeyObject(.biometrics, defaultValue: false) var biometrics + @KeyObject(.hideAppList, defaultValue: false) var hideAppList + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn + @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard + @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications + @KeyObject(.inappnotifications, defaultValue: true) var inAppNotifications + + var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() + + var state: AnyPublisher<SettingsViewState, Never> { stateRelay.eraseToAnyPublisher() } + private let stateRelay = CurrentValueSubject<SettingsViewState, Never>(.init()) + + func loadCachedSettings() { + stateRelay.value.isHideActiveApps = hideAppList + stateRelay.value.isBiometricsEnabled = biometrics + stateRelay.value.isIcognitoKeyboard = icognitoKeyboard + stateRelay.value.isPushNotification = pushNotifications + stateRelay.value.isInAppNotification = inAppNotifications + stateRelay.value.isBiometricsPossible = permissions.isBiometricsAvailable + stateRelay.value.isDummyTrafficOn = dummyTrafficManager.getStatus() + } + + func didToggleBiometrics() { + biometricAuthentication(enable: !biometrics) + } + + func didToggleInAppNotifications() { + inAppNotifications.toggle() + stateRelay.value.isInAppNotification.toggle() + } + + func didTogglePushNotifications() { + pushNotifications(enable: !pushNotifications) + } + + func didToggleDummyTraffic() { + let currently = dummyTrafficManager.getStatus() + try! dummyTrafficManager.setStatus(!currently) + stateRelay.value.isDummyTrafficOn = !currently + dummyTrafficOn = stateRelay.value.isDummyTrafficOn + } + + func didToggleHideActiveApps() { + hideAppList.toggle() + stateRelay.value.isHideActiveApps.toggle() + } + + func didToggleIcognitoKeyboard() { + icognitoKeyboard.toggle() + stateRelay.value.isIcognitoKeyboard.toggle() + } + + private func biometricAuthentication(enable: Bool) { + stateRelay.value.isBiometricsEnabled = enable + + guard enable == true else { + biometrics = false + stateRelay.value.isBiometricsEnabled = false + return } - - // MARK: Private - - private func biometricAuthentication(enable: Bool) { - stateRelay.value.isBiometricsEnabled = enable - - guard enable == true else { - biometrics = false - stateRelay.value.isBiometricsEnabled = false - return - } - - permissions.requestBiometrics { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let granted): - if granted { - self.biometrics = true - self.stateRelay.value.isBiometricsEnabled = true - } else { - self.biometrics = false - self.stateRelay.value.isBiometricsEnabled = false - } - case .failure: - self.biometrics = false - self.stateRelay.value.isBiometricsEnabled = false - } + + permissions.requestBiometrics { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let granted): + if granted { + self.biometrics = true + self.stateRelay.value.isBiometricsEnabled = true + } else { + self.biometrics = false + self.stateRelay.value.isBiometricsEnabled = false } + case .failure: + self.biometrics = false + self.stateRelay.value.isBiometricsEnabled = false + } } - - private func pushNotifications(enable: Bool) { - hudRelay.send(.on) - - if enable == true { - pushHandler.requestAuthorization { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let granted): - self.pushNotifications = granted - self.stateRelay.value.isPushNotification = granted - if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }} - self.hudRelay.send(.none) - - case .failure(let error): - self.hudRelay.send(.error(.init(with: error))) - self.pushNotifications = false - self.stateRelay.value.isPushNotification = false - } - } - } else { - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - try UnregisterForNotifications.live( - e2eId: self.messenger.e2e.get()!.getId() - ) - - self.hudRelay.send(.none) - } catch { - let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) - self.hudRelay.send(.error(.init(content: xxError))) - } - - self.pushNotifications = false - self.stateRelay.value.isPushNotification = false - } + } + + private func pushNotifications(enable: Bool) { + hudController.show() + + if enable == true { + pushHandler.requestAuthorization { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let granted): + self.pushNotifications = granted + self.stateRelay.value.isPushNotification = granted + if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }} + self.hudController.dismiss() + + case .failure(let error): + self.hudController.show(.init(error: error)) + self.pushNotifications = false + self.stateRelay.value.isPushNotification = false + } + } + } else { + backgroundScheduler.schedule { [weak self] in + guard let self = self else { return } + + do { + try UnregisterForNotifications.live( + e2eId: self.messenger.e2e.get()!.getId() + ) + + self.hudController.dismiss() + } catch { + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudController.show(.init(content: xxError)) } + + self.pushNotifications = false + self.stateRelay.value.isPushNotification = false + } } + } } diff --git a/Sources/Shared/Controllers/HUDController.swift b/Sources/Shared/Controllers/HUDController.swift new file mode 100644 index 00000000..1d281a39 --- /dev/null +++ b/Sources/Shared/Controllers/HUDController.swift @@ -0,0 +1,19 @@ +import Combine + +public final class HUDController { + var modelPublisher: AnyPublisher<HUDModel?, Never> { + modelSubject.eraseToAnyPublisher() + } + + private let modelSubject = PassthroughSubject<HUDModel?, Never>() + + public init() {} + + public func dismiss() { + modelSubject.send(nil) + } + + public func show(_ model: HUDModel? = nil) { + modelSubject.send(model) + } +} diff --git a/Sources/Shared/Controllers/RootViewController.swift b/Sources/Shared/Controllers/RootViewController.swift index 3e1fcac1..383ef01a 100644 --- a/Sources/Shared/Controllers/RootViewController.swift +++ b/Sources/Shared/Controllers/RootViewController.swift @@ -4,12 +4,14 @@ import DependencyInjection public final class RootViewController: UIViewController { @Dependency var barStylist: StatusBarStylist + @Dependency var hudDispatcher: HUDController @Dependency var toastDispatcher: ToastController - var toastTimer: Timer? let content: UIViewController? - let toastTopPadding: CGFloat = 10 var cancellables = Set<AnyCancellable>() + + var toastTimer: Timer? + let toastTopPadding: CGFloat = 10 var topToastConstraint: NSLayoutConstraint? public init(_ content: UIViewController?) { @@ -45,15 +47,31 @@ public final class RootViewController: UIViewController { } }.store(in: &cancellables) - toastDispatcher.currentToast + toastDispatcher + .currentToast .receive(on: DispatchQueue.main) .sink { [unowned self] model in let toastView = ToastView(model: model) add(toastView: toastView) present(toastView: toastView) }.store(in: &cancellables) + + hudDispatcher + .modelPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] model in + guard model != nil else { + // REMOVE FROM SUPERVIEW + return + } + // ADD TO SUPERVIEW + }.store(in: &cancellables) } +} + +// MARK: - Toaster +extension RootViewController { @objc private func didPanToast(_ sender: UIPanGestureRecognizer) { guard let toastView = sender.view else { return } @@ -144,3 +162,127 @@ public final class RootViewController: UIViewController { } } } + +// MARK: - HUD + +extension RootViewController { + // private func showWindow() { + // if let animation = animation { + // window?.addSubview(animation) + // animation.setColor(.white) + // animation.snp.makeConstraints { $0.center.equalToSuperview() } + // } + // + // if let titleLabel = titleLabel { + // window?.addSubview(titleLabel) + // titleLabel.textAlignment = .center + // titleLabel.numberOfLines = 0 + // titleLabel.snp.makeConstraints { make in + // make.left.equalToSuperview().offset(18) + // make.center.equalToSuperview().offset(50) + // make.right.equalToSuperview().offset(-18) + // } + // } + // + // if let actionButton = actionButton { + // window?.addSubview(actionButton) + // actionButton.snp.makeConstraints { + // $0.left.equalToSuperview().offset(18) + // $0.right.equalToSuperview().offset(-18) + // $0.bottom.equalToSuperview().offset(-50) + // } + // } + // + // if let errorView = errorView { + // window?.addSubview(errorView) + // errorView.snp.makeConstraints { make in + // make.left.equalToSuperview().offset(18) + // make.center.equalToSuperview() + // make.right.equalToSuperview().offset(-18) + // } + // + // errorView.button + // .publisher(for: .touchUpInside) + // .receive(on: DispatchQueue.main) + // .sink { [unowned self] in hideWindow() } + // .store(in: &cancellables) + // } + // + // window?.alpha = 0.0 + // window?.makeKeyAndVisible() + // + // UIView.animate(withDuration: 0.3) { self.window?.alpha = 1.0 } + // } + // + // private func hideWindow() { + // UIView.animate(withDuration: 0.3) { + // self.window?.alpha = 0.0 + // } completion: { _ in + // self.cancellables.removeAll() + // self.errorView = nil + // self.animation = nil + // self.actionButton = nil + // self.titleLabel = nil + // self.window = nil + // } + // } + + + // if statusSubject.value.isPresented == true && status.isPresented == true { + // self.errorView = nil + // self.animation = nil + // self.window = nil + // self.actionButton = nil + // self.titleLabel = nil + // + // switch status { + // case .on: + // animation = DotAnimation() + // + // case .onTitle(let text): + // animation = DotAnimation() + // titleLabel = UILabel() + // titleLabel!.text = text + // + // case .onAction(let title): + // animation = DotAnimation() + // actionButton = CapsuleButton() + // actionButton!.set(style: .seeThroughWhite, title: title) + // + // case .error(let error): + // errorView = ErrorView(with: error) + // case .none: + // break + // } + // + // showWindow() + // } + + // if statusSubject.value.isPresented == false && status.isPresented == true { + // switch status { + // case .on: + // animation = DotAnimation() + // + // case .onTitle(let text): + // animation = DotAnimation() + // titleLabel = UILabel() + // titleLabel!.text = text + // + // case .onAction(let title): + // animation = DotAnimation() + // actionButton = CapsuleButton() + // actionButton!.set(style: .seeThroughWhite, title: title) + // + // case .error(let error): + // errorView = ErrorView(with: error) + // case .none: + // break + // } + // + // showWindow() + // } + + // if statusSubject.value.isPresented == true && status.isPresented == false { + // hideWindow() + // } +} diff --git a/Sources/Shared/Models/HUDModel.swift b/Sources/Shared/Models/HUDModel.swift new file mode 100644 index 00000000..d9e58e4d --- /dev/null +++ b/Sources/Shared/Models/HUDModel.swift @@ -0,0 +1,62 @@ +import UIKit + +public struct HUDModel { + var title: String? + var content: String? + var actionTitle: String? + var isDismissable: Bool + var animationColor: UIColor? + var onTapClosure: (() -> Void)? + + public init( + title: String? = nil, + content: String? = nil, + actionTitle: String? = nil, + isDismissable: Bool = true, + animationColor: UIColor? = nil, + onTapClosure: (() -> Void)? = nil + ) { + self.title = title + self.content = content + self.actionTitle = actionTitle + self.isDismissable = isDismissable + self.onTapClosure = onTapClosure + self.animationColor = animationColor + } + + public init( + error: Error, + isDismissable: Bool = true + ) { + self.isDismissable = isDismissable + self.title = Localized.Hud.Error.title + self.content = error.localizedDescription + self.actionTitle = Localized.Hud.Error.action + } +} + +//public struct HUDError: Equatable { +// var title: String +// var content: String +// var buttonTitle: String +// var dismissable: Bool +// +// public init( +// content: String, +// title: String = Localized.Hud.Error.title, +// buttonTitle: String = Localized.Hud.Error.action, +// dismissable: Bool = true +// ) { +// self.title = title +// self.content = content +// self.buttonTitle = buttonTitle +// self.dismissable = dismissable +// } +// +// public init(with error: Error) { +// self.title = Localized.Hud.Error.title +// self.buttonTitle = Localized.Hud.Error.action +// self.content = error.localizedDescription +// self.dismissable = true +// } +//} diff --git a/Sources/Shared/Controllers/ToastModel.swift b/Sources/Shared/Models/ToastModel.swift similarity index 100% rename from Sources/Shared/Controllers/ToastModel.swift rename to Sources/Shared/Models/ToastModel.swift diff --git a/Sources/Shared/Views/DotAnimation.swift b/Sources/Shared/Views/DotAnimation.swift index bfefe3c4..bcf182b9 100644 --- a/Sources/Shared/Views/DotAnimation.swift +++ b/Sources/Shared/Views/DotAnimation.swift @@ -1,100 +1,72 @@ import UIKit -import SnapKit public final class DotAnimation: UIView { - // MARK: UI - - let leftDot = UIView() - let middleDot = UIView() - let rightDot = UIView() - - // MARK: Properties - - var leftInvert = false - var middleInvert = false - var rightInvert = false - - var leftValue: CGFloat = 20 - var middleValue: CGFloat = 45 - var rightValue: CGFloat = 70 - - var displayLink: CADisplayLink? - - // MARK: Lifecycle - - public init() { - super.init(frame: .zero) - setup() - } - - required init?(coder: NSCoder) { nil } - - // MARK: Public - - func setColor(_ color: UIColor = Asset.brandPrimary.color) { - leftDot.backgroundColor = color - middleDot.backgroundColor = color - rightDot.backgroundColor = color + let leftDot = UIView() + let rightDot = UIView() + let middleDot = UIView() + var displayLink: CADisplayLink? + + var leftInvert = false + var rightInvert = false + var middleInvert = false + var leftValue: CGFloat = 20 + var rightValue: CGFloat = 70 + var middleValue: CGFloat = 45 + + public init() { + super.init(frame: .zero) + leftDot.layer.cornerRadius = 7.5 + middleDot.layer.cornerRadius = 7.5 + rightDot.layer.cornerRadius = 7.5 + + setColor() + + addSubview(leftDot) + addSubview(middleDot) + addSubview(rightDot) + + leftDot.snp.makeConstraints { + $0.centerY.equalTo(middleDot) + $0.right.equalTo(middleDot.snp.left).offset(-5) + $0.width.height.equalTo(15) } - // MARK: Private - - private func setup() { - setupCornerRadius() - setColor() - addSubviews() - setupConstraints() - - displayLink = CADisplayLink(target: self, selector: #selector(handleAnimations)) - displayLink!.add(to: RunLoop.main, forMode: .default) + middleDot.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.height.equalTo(15) } - private func setupCornerRadius() { - leftDot.layer.cornerRadius = 4.5 - middleDot.layer.cornerRadius = 4.5 - rightDot.layer.cornerRadius = 4.5 + rightDot.snp.makeConstraints { + $0.centerY.equalTo(middleDot) + $0.left.equalTo(middleDot.snp.right).offset(5) + $0.width.height.equalTo(15) } - private func addSubviews() { - addSubview(leftDot) - addSubview(middleDot) - addSubview(rightDot) - } + displayLink = CADisplayLink(target: self, selector: #selector(handleAnimations)) + displayLink!.add(to: RunLoop.main, forMode: .default) + } - private func setupConstraints() { - leftDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.right.equalTo(middleDot.snp.left).offset(-2) - make.width.height.equalTo(9) - } + required init?(coder: NSCoder) { nil } - middleDot.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.height.equalTo(9) - } + func setColor(_ color: UIColor = Asset.brandPrimary.color) { + leftDot.backgroundColor = color + middleDot.backgroundColor = color + rightDot.backgroundColor = color + } - rightDot.snp.makeConstraints { make in - make.centerY.equalTo(middleDot) - make.left.equalTo(middleDot.snp.right).offset(2) - make.width.height.equalTo(9) - } - } - - // MARK: Selectors + @objc private func handleAnimations() { + let factor: CGFloat = 70 - @objc private func handleAnimations() { - let factor: CGFloat = 70 + leftInvert ? (leftValue -= 1) : (leftValue += 1) + middleInvert ? (middleValue -= 1) : (middleValue += 1) + rightInvert ? (rightValue -= 1) : (rightValue += 1) - leftInvert ? (leftValue -= 1) : (leftValue += 1) - middleInvert ? (middleValue -= 1) : (middleValue += 1) - rightInvert ? (rightValue -= 1) : (rightValue += 1) + leftDot.layer.transform = CATransform3DMakeScale(leftValue/factor, leftValue/factor, 1) + middleDot.layer.transform = CATransform3DMakeScale(middleValue/factor, middleValue/factor, 1) + rightDot.layer.transform = CATransform3DMakeScale(rightValue/factor, rightValue/factor, 1) - leftDot.layer.transform = CATransform3DMakeScale(leftValue/factor, leftValue/factor, 1) - middleDot.layer.transform = CATransform3DMakeScale(middleValue/factor, middleValue/factor, 1) - rightDot.layer.transform = CATransform3DMakeScale(rightValue/factor, rightValue/factor, 1) - - if leftValue > factor || leftValue < 10 { leftInvert.toggle() } - if middleValue > factor || middleValue < 10 { middleInvert.toggle() } - if rightValue > factor || rightValue < 10 { rightInvert.toggle() } - } + if leftValue > factor || leftValue < 10 { leftInvert.toggle() } + if middleValue > factor || middleValue < 10 { middleInvert.toggle() } + if rightValue > factor || rightValue < 10 { rightInvert.toggle() } + } } diff --git a/Sources/Shared/Views/ErrorView.swift b/Sources/Shared/Views/ErrorView.swift new file mode 100644 index 00000000..e0206ac7 --- /dev/null +++ b/Sources/Shared/Views/ErrorView.swift @@ -0,0 +1,57 @@ +//import UIKit +//import SnapKit +// +//final class ErrorView: UIView { +// let title = UILabel() +// let content = UILabel() +// let stack = UIStackView() +// let button = CapsuleButton() +// +// init(with model: HUDError) { +// super.init(frame: .zero) +// setup(with: model) +// } +// +// required init?(coder: NSCoder) { nil } +// +// private func setup(with model: HUDError) { +// layer.cornerRadius = 6 +// backgroundColor = Asset.neutralWhite.color +// +// title.text = model.title +// title.textColor = Asset.neutralBody.color +// title.font = Fonts.Mulish.bold.font(size: 35.0) +// title.textAlignment = .center +// title.numberOfLines = 0 +// +// content.text = model.content +// content.textColor = Asset.neutralBody.color +// content.numberOfLines = 0 +// content.font = Fonts.Mulish.regular.font(size: 14.0) +// content.textAlignment = .center +// +// button.setTitle(model.buttonTitle, for: .normal) +// button.setStyle(.brandColored) +// +// stack.axis = .vertical +// +// stack.addArrangedSubview(title) +// stack.addArrangedSubview(content) +// +// if model.dismissable { +// stack.addArrangedSubview(button) +// } +// +// stack.setCustomSpacing(25, after: title) +// stack.setCustomSpacing(59, after: content) +// +// addSubview(stack) +// +// stack.snp.makeConstraints { make in +// make.top.equalToSuperview().offset(60) +// make.left.equalToSuperview().offset(57) +// make.right.equalToSuperview().offset(-57) +// make.bottom.equalToSuperview().offset(-35) +// } +// } +//} diff --git a/Sources/Shared/Views/HUDView.swift b/Sources/Shared/Views/HUDView.swift new file mode 100644 index 00000000..df54b7d5 --- /dev/null +++ b/Sources/Shared/Views/HUDView.swift @@ -0,0 +1,33 @@ +import UIKit +import Combine + +final class HUDView: UIView { + let titleLabel = UILabel() + let actionButton = CapsuleButton() + let animationView = DotAnimation() + var cancellables = Set<AnyCancellable>() + + init(model: HUDModel) { + super.init(frame: .zero) + + titleLabel.textColor = Asset.neutralWhite.color + backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.8) + + if let color = model.animationColor { + animationView.setColor(color) + } + + if let actionTitle = model.actionTitle { + actionButton.set( + style: .seeThroughWhite, + title: actionTitle + ) + actionButton + .publisher(for: .touchUpInside) + .sink { model.onTapClosure?() } + .store(in: &cancellables) + } + } + + required init?(coder: NSCoder) { nil } +} -- GitLab