From 0acdfb91a71d92b38d8b99f87e90918a059ec8f8 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Tue, 30 Nov 2021 22:55:02 +0200 Subject: [PATCH 1/3] Show sending progress on ui --- .../Compose/ComposeViewController.swift | 38 ++++++++---- .../Extensions/UIViewController+Spinner.swift | 16 ++--- .../ComposeMessageService.swift | 62 +++++++++++++++++-- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 91e01df4a..8cde2b2e1 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -53,11 +53,12 @@ final class ComposeViewController: TableNodeViewController { private let router: GlobalRouterType private let search = PassthroughSubject() - private let userDefaults: UserDefaults private let email: String private var cancellable = Set() + private var lifeTimeCancellable = Set() + private var input: ComposeMessageInput private var contextToSend = ComposeMessageContext() @@ -73,7 +74,6 @@ final class ComposeViewController: TableNodeViewController { decorator: ComposeViewDecorator = ComposeViewDecorator(), input: ComposeMessageInput = .empty, cloudContactProvider: CloudContactsProvider = UserContactsProvider(), - userDefaults: UserDefaults = .standard, contactsService: ContactsServiceType = ContactsService(), composeMessageService: ComposeMessageService = ComposeMessageService(), filesManager: FilesManagerType = FilesManager(), @@ -87,7 +87,6 @@ final class ComposeViewController: TableNodeViewController { self.notificationCenter = notificationCenter self.input = input self.decorator = decorator - self.userDefaults = userDefaults self.contactsService = contactsService self.cloudContactProvider = cloudContactProvider self.composeMessageService = composeMessageService @@ -119,6 +118,7 @@ final class ComposeViewController: TableNodeViewController { setupNavigationBar() observeKeyboardNotifications() observerAppStates() + observeComposeUpdates() setupQuote() } @@ -164,6 +164,22 @@ final class ComposeViewController: TableNodeViewController { self.contextToSend.recipients = [ComposeMessageRecipient(email: "tom@flowcrypt.com", state: decorator.recipientIdleState)] } + private func observeComposeUpdates() { + composeMessageService.$state + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + guard let message = state.message else { + self?.hideSpinner() + return + } + self?.updateSpinner( + label: message, + progress: state.progress, + systemImageName: "checkmark.circle" + ) + } + .store(in: &lifeTimeCancellable) + } } // MARK: - Drafts @@ -434,10 +450,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() } @@ -1151,7 +1164,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 @@ -1163,10 +1176,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 32f01b5ee..4f7ec88e4 100644 --- a/FlowCrypt/Extensions/UIViewController+Spinner.swift +++ b/FlowCrypt/Extensions/UIViewController+Spinner.swift @@ -28,18 +28,20 @@ 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 diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index c01082c35..46ce2c7d9 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -38,7 +38,9 @@ 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) + + @Published private(set) var state: State = .idle init( messageGateway: MessageGateway = MailProvider.shared.messageSender, @@ -62,8 +64,11 @@ final class ComposeMessageService { includeAttachments: Bool = true, signingPrv: PrvKeyInfo? ) async throws -> SendableMsg { + state = .validatingMessage + let recipients = contextToSend.recipients guard recipients.isNotEmpty else { + state = .error throw MessageValidationError.emptyRecipient } @@ -71,24 +76,29 @@ final class ComposeMessageService { let emptyEmails = emails.filter { !$0.hasContent } guard emails.isNotEmpty, emptyEmails.isEmpty else { + state = .error throw MessageValidationError.emptyRecipient } guard emails.filter({ !$0.isValidEmail }).isEmpty else { + state = .error throw MessageValidationError.invalidEmailRecipient } guard input.isQuote || contextToSend.subject?.hasContent ?? false else { + state = .error throw MessageValidationError.emptySubject } guard let text = contextToSend.message, text.hasContent else { + state = .error throw MessageValidationError.emptyMessage } let subject = contextToSend.subject ?? "(no subject)" guard let myPubKey = self.dataService.publicKey() else { + state = .error throw MessageValidationError.missedPublicKey } @@ -99,6 +109,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), @@ -130,12 +141,15 @@ final class ComposeMessageService { logger.logDebug("validate recipients: \(recipients)") logger.logDebug("validate recipient keyStates: \(recipients.map { $0.keyState })") guard !contains(keyState: .empty) else { + state = .error throw MessageValidationError.noPubRecipients } guard !contains(keyState: .expired) else { + state = .error throw MessageValidationError.expiredKeyRecipients } guard !contains(keyState: .revoked) else { + state = .error throw MessageValidationError.revokedKeyRecipients } return recipients.flatMap(\.activePubKeys).map(\.armored) @@ -150,26 +164,66 @@ final class ComposeMessageService { ) draft = try await draftGateway?.saveDraft(input: MessageGatewayInput(mime: r.mimeEncoded, threadId: threadId), draft: draft) } catch { + state = .error throw ComposeMessageError.gatewayError(error) } } // MARK: - Encrypt and Send - func encryptAndSend(message: SendableMsg, threadId: String?, progressHandler: ((Float) -> Void)?) async throws { + func encryptAndSend(message: SendableMsg, threadId: String?) async throws { do { + state = .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?.state = .progressChanged(progress) + } + ) + // cleaning any draft saved/created/fetched during editing if let draftId = draft?.identifier { await draftGateway?.deleteDraft(with: draftId) } + state = .messageSent } catch { + state = .error throw ComposeMessageError.gatewayError(error) } } } + +extension ComposeMessageService { + enum State { + case idle + case validatingMessage + case startComposing + case progressChanged(Float) + case messageSent + case error + + var message: String? { + switch self { + case .idle, .error: + return nil + case .validatingMessage: + return "Validating" + case .startComposing, .progressChanged: + return "Composing" + case .messageSent: + return "Message sent" + } + } + + var progress: Float? { + guard case .progressChanged(let progress) = self else { + return nil + } + return progress + } + } +} From 4b5145711facbed801122a8d56b943144a6830c5 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Wed, 1 Dec 2021 22:17:01 +0200 Subject: [PATCH 2/3] Rework ComposeMessageService to use callbacks for state updates --- .../Compose/ComposeViewController.swift | 14 +++++---- .../ComposeMessageService.swift | 29 +++++++------------ 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 8cde2b2e1..ae4a55951 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -57,7 +57,6 @@ final class ComposeViewController: TableNodeViewController { private let email: String private var cancellable = Set() - private var lifeTimeCancellable = Set() private var input: ComposeMessageInput private var contextToSend = ComposeMessageContext() @@ -165,9 +164,8 @@ final class ComposeViewController: TableNodeViewController { } private func observeComposeUpdates() { - composeMessageService.$state - .receive(on: DispatchQueue.main) - .sink { [weak self] state in + composeMessageService.onStateChanged { [weak self] state in + DispatchQueue.main.async { guard let message = state.message else { self?.hideSpinner() return @@ -178,7 +176,7 @@ final class ComposeViewController: TableNodeViewController { systemImageName: "checkmark.circle" ) } - .store(in: &lifeTimeCancellable) + } } } @@ -459,7 +457,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() { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 46ce2c7d9..05fb820ea 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -40,8 +40,6 @@ final class ComposeMessageService { private let draftGateway: DraftGateway? private lazy var logger: Logger = Logger.nested(Self.self) - @Published private(set) var state: State = .idle - init( messageGateway: MessageGateway = MailProvider.shared.messageSender, draftGateway: DraftGateway? = MailProvider.shared.draftGateway, @@ -57,6 +55,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, @@ -64,11 +67,10 @@ final class ComposeMessageService { includeAttachments: Bool = true, signingPrv: PrvKeyInfo? ) async throws -> SendableMsg { - state = .validatingMessage + onStateChanged?(.validatingMessage) let recipients = contextToSend.recipients guard recipients.isNotEmpty else { - state = .error throw MessageValidationError.emptyRecipient } @@ -76,29 +78,24 @@ final class ComposeMessageService { let emptyEmails = emails.filter { !$0.hasContent } guard emails.isNotEmpty, emptyEmails.isEmpty else { - state = .error throw MessageValidationError.emptyRecipient } guard emails.filter({ !$0.isValidEmail }).isEmpty else { - state = .error throw MessageValidationError.invalidEmailRecipient } guard input.isQuote || contextToSend.subject?.hasContent ?? false else { - state = .error throw MessageValidationError.emptySubject } guard let text = contextToSend.message, text.hasContent else { - state = .error throw MessageValidationError.emptyMessage } let subject = contextToSend.subject ?? "(no subject)" guard let myPubKey = self.dataService.publicKey() else { - state = .error throw MessageValidationError.missedPublicKey } @@ -141,15 +138,12 @@ final class ComposeMessageService { logger.logDebug("validate recipients: \(recipients)") logger.logDebug("validate recipient keyStates: \(recipients.map { $0.keyState })") guard !contains(keyState: .empty) else { - state = .error throw MessageValidationError.noPubRecipients } guard !contains(keyState: .expired) else { - state = .error throw MessageValidationError.expiredKeyRecipients } guard !contains(keyState: .revoked) else { - state = .error throw MessageValidationError.revokedKeyRecipients } return recipients.flatMap(\.activePubKeys).map(\.armored) @@ -164,7 +158,6 @@ final class ComposeMessageService { ) draft = try await draftGateway?.saveDraft(input: MessageGatewayInput(mime: r.mimeEncoded, threadId: threadId), draft: draft) } catch { - state = .error throw ComposeMessageError.gatewayError(error) } } @@ -172,7 +165,7 @@ final class ComposeMessageService { // MARK: - Encrypt and Send func encryptAndSend(message: SendableMsg, threadId: String?) async throws { do { - state = .startComposing + onStateChanged?(.startComposing) let r = try await core.composeEmail( msg: message, fmt: MsgFmt.encryptInline @@ -181,7 +174,7 @@ final class ComposeMessageService { try await messageGateway.sendMail( input: MessageGatewayInput(mime: r.mimeEncoded, threadId: threadId), progressHandler: { [weak self] progress in - self?.state = .progressChanged(progress) + self?.onStateChanged?(.progressChanged(progress)) } ) @@ -189,9 +182,8 @@ final class ComposeMessageService { if let draftId = draft?.identifier { await draftGateway?.deleteDraft(with: draftId) } - state = .messageSent + onStateChanged?(.messageSent) } catch { - state = .error throw ComposeMessageError.gatewayError(error) } } @@ -204,11 +196,10 @@ extension ComposeMessageService { case startComposing case progressChanged(Float) case messageSent - case error var message: String? { switch self { - case .idle, .error: + case .idle: return nil case .validatingMessage: return "Validating" From a00a0abe49528343eabbd5a6c89663d4761f8cd8 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Fri, 3 Dec 2021 20:49:17 +0200 Subject: [PATCH 3/3] Update progress status during composing --- .../Compose/ComposeViewController.swift | 29 +++++++++++++------ .../Extensions/UIViewController+Spinner.swift | 22 ++++++++------ .../ComposeMessageService.swift | 6 ++-- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index bb279a49a..fadcd4594 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -183,18 +183,29 @@ final class ComposeViewController: TableNodeViewController { private func observeComposeUpdates() { composeMessageService.onStateChanged { [weak self] state in DispatchQueue.main.async { - guard let message = state.message else { - self?.hideSpinner() - return - } - self?.updateSpinner( - label: message, - progress: state.progress, - systemImageName: "checkmark.circle" - ) + 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 diff --git a/FlowCrypt/Extensions/UIViewController+Spinner.swift b/FlowCrypt/Extensions/UIViewController+Spinner.swift index ba85311e2..653a0d68c 100644 --- a/FlowCrypt/Extensions/UIViewController+Spinner.swift +++ b/FlowCrypt/Extensions/UIViewController+Spinner.swift @@ -33,19 +33,17 @@ extension UIViewController { 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 - ) + systemImageName: imageName) } else { self.showProgressHUD(progress: progress, label: label) } } else { - self.currentProgressHUD.mode = .indeterminate - self.currentProgressHUD.label.text = label + showIndeterminateHUD(with: label) } } @@ -56,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) @@ -74,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 7025ea92b..a5cb4ee2c 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -211,8 +211,10 @@ extension ComposeMessageService { return nil case .validatingMessage: return "Validating" - case .startComposing, .progressChanged: - return "Composing" + case .startComposing: + return "Encrypting" + case .progressChanged: + return "Uploading" case .messageSent: return "Message sent" }