diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 97adde523..fadcd4594 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -53,11 +53,11 @@ final class ComposeViewController: TableNodeViewController { private let clientConfiguration: ClientConfiguration private let search = PassthroughSubject() - private let userDefaults: UserDefaults private let email: String private var cancellable = Set() + private var input: ComposeMessageInput private var contextToSend = ComposeMessageContext() @@ -73,7 +73,6 @@ final class ComposeViewController: TableNodeViewController { decorator: ComposeViewDecorator = ComposeViewDecorator(), input: ComposeMessageInput = .empty, cloudContactProvider: CloudContactsProvider? = nil, - userDefaults: UserDefaults = .standard, contactsService: ContactsServiceType? = nil, composeMessageService: ComposeMessageService? = nil, filesManager: FilesManagerType = FilesManager(), @@ -89,7 +88,6 @@ final class ComposeViewController: TableNodeViewController { self.notificationCenter = notificationCenter self.input = input self.decorator = decorator - self.userDefaults = userDefaults let clientConfiguration = appContext.clientConfigurationService.getSaved(for: email) self.contactsService = contactsService ?? ContactsService( localContactsProvider: LocalContactsProvider( @@ -136,6 +134,7 @@ final class ComposeViewController: TableNodeViewController { setupNavigationBar() observeKeyboardNotifications() observerAppStates() + observeComposeUpdates() setupQuote() } @@ -181,6 +180,32 @@ final class ComposeViewController: TableNodeViewController { self.contextToSend.recipients = [ComposeMessageRecipient(email: "tom@flowcrypt.com", state: decorator.recipientIdleState)] } + private func observeComposeUpdates() { + composeMessageService.onStateChanged { [weak self] state in + DispatchQueue.main.async { + self?.updateSpinner(with: state) + } + } + } + + private func updateSpinner(with state: ComposeMessageService.State) { + switch state { + case .progressChanged(let progress): + showProgressHUD( + progress: progress, + label: state.message ?? "\(progress)" + ) + case .messageSent: + showProgressHUDWithCustomImage( + imageName: "checkmark.circle", + label: "compose_sent".localized + ) + case .startComposing, .validatingMessage: + showIndeterminateHUD(with: state.message ?? "") + case .idle: + hideSpinner() + } + } } // MARK: - Drafts @@ -451,10 +476,7 @@ extension ComposeViewController { UIApplication.shared.isIdleTimerDisabled = true try await service.encryptAndSend( message: sendableMsg, - threadId: input.threadId, - progressHandler: { [weak self] progress in - self?.updateSpinner(progress: progress, systemImageName: "checkmark.circle") - } + threadId: input.threadId ) handleSuccessfullySentMessage() } @@ -463,7 +485,11 @@ extension ComposeViewController { UIApplication.shared.isIdleTimerDisabled = false hideSpinner() navigationItem.rightBarButtonItem?.isEnabled = true - showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + + let hideSpinnerAnimationDuration: TimeInterval = 1 + DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in + self?.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + } } private func handleSuccessfullySentMessage() { @@ -1168,7 +1194,7 @@ extension ComposeViewController: FilesManagerPresenter {} // TODO temporary solution for background execution problem private actor ServiceActor { - private let composeMessageService: ComposeMessageService + let composeMessageService: ComposeMessageService private let contactsService: ContactsServiceType private let cloudContactProvider: CloudContactsProvider @@ -1180,10 +1206,11 @@ private actor ServiceActor { self.cloudContactProvider = cloudContactProvider } - func encryptAndSend(message: SendableMsg, threadId: String?, progressHandler: ((Float) -> Void)?) async throws { - try await composeMessageService.encryptAndSend(message: message, - threadId: threadId, - progressHandler: progressHandler) + func encryptAndSend(message: SendableMsg, threadId: String?) async throws { + try await composeMessageService.encryptAndSend( + message: message, + threadId: threadId + ) } func searchContacts(query: String) async throws -> [String] { diff --git a/FlowCrypt/Extensions/UIViewController+Spinner.swift b/FlowCrypt/Extensions/UIViewController+Spinner.swift index 55096e6b1..653a0d68c 100644 --- a/FlowCrypt/Extensions/UIViewController+Spinner.swift +++ b/FlowCrypt/Extensions/UIViewController+Spinner.swift @@ -29,21 +29,21 @@ extension UIViewController { } @MainActor - func updateSpinner(label: String = "compose_uploading".localized, - progress: Float? = nil, - systemImageName: String? = nil) { + func updateSpinner( + label: String = "compose_uploading".localized, + progress: Float? = nil, + systemImageName: String? = nil + ) { if let progress = progress { if progress >= 1, let imageName = systemImageName { - self.updateSpinner(label: "compose_sent".localized, - systemImageName: imageName) + self.updateSpinner( + label: "compose_sent".localized, + systemImageName: imageName) } else { self.showProgressHUD(progress: progress, label: label) } - } else if let imageName = systemImageName { - self.showProgressHUDWithCustomImage(imageName: imageName, label: label) } else { - self.currentProgressHUD.mode = .indeterminate - self.currentProgressHUD.label.text = label + showIndeterminateHUD(with: label) } } @@ -54,17 +54,17 @@ extension UIViewController { .forEach { $0.hide(animated: true) } self.view.isUserInteractionEnabled = true } -} -extension UIViewController { - private func showProgressHUD(progress: Float, label: String) { + @MainActor + func showProgressHUD(progress: Float, label: String) { let percent = Int(progress * 100) currentProgressHUD.label.text = "\(label) \(percent)%" currentProgressHUD.progress = progress currentProgressHUD.mode = .annularDeterminate } - private func showProgressHUDWithCustomImage(imageName: String, label: String) { + @MainActor + func showProgressHUDWithCustomImage(imageName: String, label: String) { let configuration = UIImage.SymbolConfiguration(pointSize: 36) let imageView = UIImageView(image: .init(systemName: imageName, withConfiguration: configuration)) currentProgressHUD.minSize = CGSize(width: 150, height: 90) @@ -72,4 +72,10 @@ extension UIViewController { currentProgressHUD.mode = .customView currentProgressHUD.label.text = label } + + @MainActor + func showIndeterminateHUD(with title: String) { + self.currentProgressHUD.mode = .indeterminate + self.currentProgressHUD.label.text = title + } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index cc136d9b5..a5cb4ee2c 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -41,7 +41,7 @@ final class ComposeMessageService { private let contactsService: ContactsServiceType private let core: CoreComposeMessageType & KeyParser private let draftGateway: DraftGateway? - private let logger: Logger + private lazy var logger: Logger = Logger.nested(Self.self) init( clientConfiguration: ClientConfiguration, @@ -63,6 +63,11 @@ final class ComposeMessageService { self.logger = Logger.nested(in: Self.self, with: "ComposeMessageService") } + private var onStateChanged: ((State) -> Void)? + func onStateChanged(_ completion: ((State) -> Void)?) { + self.onStateChanged = completion + } + func validateAndProduceSendableMsg( input: ComposeMessageInput, contextToSend: ComposeMessageContext, @@ -70,6 +75,8 @@ final class ComposeMessageService { includeAttachments: Bool = true, signingPrv: PrvKeyInfo? ) async throws -> SendableMsg { + onStateChanged?(.validatingMessage) + let recipients = contextToSend.recipients guard recipients.isNotEmpty else { throw MessageValidationError.emptyRecipient @@ -107,6 +114,7 @@ final class ComposeMessageService { let allRecipientPubs = try await getPubKeys(for: recipients) let replyToMimeMsg = input.replyToMime .flatMap { String(data: $0, encoding: .utf8) } + return SendableMsg( text: text, to: recipients.map(\.email), @@ -163,21 +171,60 @@ final class ComposeMessageService { } // MARK: - Encrypt and Send - func encryptAndSend(message: SendableMsg, threadId: String?, progressHandler: ((Float) -> Void)?) async throws { + func encryptAndSend(message: SendableMsg, threadId: String?) async throws { do { + onStateChanged?(.startComposing) let r = try await core.composeEmail( msg: message, fmt: MsgFmt.encryptInline ) - try await messageGateway.sendMail(input: MessageGatewayInput(mime: r.mimeEncoded, threadId: threadId), - progressHandler: progressHandler) + try await messageGateway.sendMail( + input: MessageGatewayInput(mime: r.mimeEncoded, threadId: threadId), + progressHandler: { [weak self] progress in + self?.onStateChanged?(.progressChanged(progress)) + } + ) + // cleaning any draft saved/created/fetched during editing if let draftId = draft?.identifier { await draftGateway?.deleteDraft(with: draftId) } + onStateChanged?(.messageSent) } catch { throw ComposeMessageError.gatewayError(error) } } } + +extension ComposeMessageService { + enum State { + case idle + case validatingMessage + case startComposing + case progressChanged(Float) + case messageSent + + var message: String? { + switch self { + case .idle: + return nil + case .validatingMessage: + return "Validating" + case .startComposing: + return "Encrypting" + case .progressChanged: + return "Uploading" + case .messageSent: + return "Message sent" + } + } + + var progress: Float? { + guard case .progressChanged(let progress) = self else { + return nil + } + return progress + } + } +}