Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions FlowCrypt/Controllers/Compose/ComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ final class ComposeViewController: TableNodeViewController {
private let clientConfiguration: ClientConfiguration

private let search = PassthroughSubject<String, Never>()
private let userDefaults: UserDefaults

private let email: String

private var cancellable = Set<AnyCancellable>()

private var input: ComposeMessageInput
private var contextToSend = ComposeMessageContext()

Expand All @@ -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(),
Expand All @@ -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(
Expand Down Expand Up @@ -136,6 +134,7 @@ final class ComposeViewController: TableNodeViewController {
setupNavigationBar()
observeKeyboardNotifications()
observerAppStates()
observeComposeUpdates()
setupQuote()
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand All @@ -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() {
Expand Down Expand Up @@ -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

Expand All @@ -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] {
Expand Down
32 changes: 19 additions & 13 deletions FlowCrypt/Extensions/UIViewController+Spinner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -54,22 +54,28 @@ 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)
currentProgressHUD.customView = imageView
currentProgressHUD.mode = .customView
currentProgressHUD.label.text = label
}

@MainActor
func showIndeterminateHUD(with title: String) {
self.currentProgressHUD.mode = .indeterminate
self.currentProgressHUD.label.text = title
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -63,13 +63,20 @@ 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,
email: String,
includeAttachments: Bool = true,
signingPrv: PrvKeyInfo?
) async throws -> SendableMsg {
onStateChanged?(.validatingMessage)

let recipients = contextToSend.recipients
guard recipients.isNotEmpty else {
throw MessageValidationError.emptyRecipient
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
}
}